aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/rosa
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-08-10 18:10:30 +0900
committerOphestra <cat@gensokyo.uk>2026-08-10 21:36:21 +0900
commit7d86e19c07b3a76cbba08dd209dc6bcea6067029 (patch)
treea9bedca1c33f8aaabcb70daa1099ef409e6c521b /internal/rosa
parentf1ffb180bc34e16648fa2e83378244dabc60af7d (diff)
internal/rosa/azalea: implement nil identifier
This also implements a nil-capable string type. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/rosa')
-rw-r--r--internal/rosa/azalea/evaluate.go65
-rw-r--r--internal/rosa/azalea/evaluate_test.go74
2 files changed, 128 insertions, 11 deletions
diff --git a/internal/rosa/azalea/evaluate.go b/internal/rosa/azalea/evaluate.go
index 044e6826..b35a056a 100644
--- a/internal/rosa/azalea/evaluate.go
+++ b/internal/rosa/azalea/evaluate.go
@@ -12,7 +12,7 @@ import (
// Value are types supported by the language.
type Value interface {
- bool | int64 | string | []string | []int64 | [][2]string
+ bool | int64 | string | []string | []int64 | [][2]string | Nil
}
type (
@@ -33,6 +33,29 @@ type (
F func(args FArgs) (v any, set bool, err error)
V map[unique.Handle[Ident]]any
}
+
+ // Nil represents an untyped nil.
+ Nil struct{}
+
+ // AString represents an argument string that may be nil.
+ AString struct {
+ V string
+ Nil bool
+ }
+)
+
+// NilError is returned for an invalid nil assignment.
+type NilError struct{ reflect.Type }
+
+func (e NilError) Error() string {
+ return fmt.Sprintf("attempting to assign nil to %s", e.Type)
+}
+
+var (
+ // nilStringV is the [reflect.Value] of an Azalea nil assigned to an [AString].
+ nilStringV = reflect.ValueOf(AString{Nil: true})
+ // nilStringT is the [reflect.Type] of nilStringV.
+ nilStringT = nilStringV.Type()
)
// Apply applies named arguments and rejects unused arguments.
@@ -50,8 +73,8 @@ func (args FArgs) Apply(v map[unique.Handle[Ident]]any) error {
}
return UndefinedError(arg.K.Value())
}
- err := storeE(r, arg.V)
- if err != nil {
+
+ if err := storeE(r, arg.V); err != nil {
return err
}
}
@@ -115,11 +138,38 @@ func (e TypeError) Is(err error) bool {
}
// storeE is a convenience function to set the value of a result pointer.
-func storeE(rp any, r any) error {
+func storeE(rp, r any) error {
pv := reflect.ValueOf(rp).Elem()
+ pt := pv.Type()
+ as := nilStringT.AssignableTo(pt)
+
+ if r == (Nil{}) {
+ switch kind := pt.Kind(); kind {
+ case reflect.Bool, reflect.String:
+ return NilError{pt}
+ default:
+ if rv, ok := rp.(*any); ok && !reflect.ValueOf(*rv).IsValid() {
+ *rv = Nil{}
+ } else if as {
+ pv.Set(nilStringV)
+ } else {
+ pv.SetZero()
+ }
+ return nil
+ }
+ }
+
v := reflect.ValueOf(r)
- pt, vt := pv.Type(), v.Type()
- if !vt.AssignableTo(pt) {
+ if vt := v.Type(); !vt.AssignableTo(pt) {
+ if as {
+ var asv AString
+ if err := storeE(&asv.V, r); err != nil {
+ return err
+ }
+ pv.Set(reflect.ValueOf(asv))
+ return nil
+ }
+
return TypeError{vt, pt}
}
pv.Set(v)
@@ -226,6 +276,9 @@ func evaluateAny(d PF, s []Frame, expr, rp any) bool {
case "false":
store(rp, false)
return true
+ case "nil":
+ store(rp, Nil{})
+ return true
default:
return evaluateAny(d, s, v, rp)
}
diff --git a/internal/rosa/azalea/evaluate_test.go b/internal/rosa/azalea/evaluate_test.go
index 4a6c2f56..049fcb52 100644
--- a/internal/rosa/azalea/evaluate_test.go
+++ b/internal/rosa/azalea/evaluate_test.go
@@ -100,15 +100,79 @@ func TestEvaluate(t *testing.T) {
Err: UndefinedError("f"),
}},
- {"error wrap deep", `f { v = nil; }`, makeStackCheck(func(
+ {"error wrap deep", `f { v = nonexistent; }`, makeStackCheck(func(
FArgs,
) (any, error) {
panic("unreachable")
}), "", EvaluationError{
- Expr: Ident("nil"),
- Err: UndefinedError("nil"),
+ Expr: Ident("nonexistent"),
+ Err: UndefinedError("nonexistent"),
}},
+ {"nil", `f { v = nil; }`, checkArgs(FArgs{
+ {K: unique.Make(Ident("v")), V: Nil{}},
+ }), "\xfd", nil},
+
+ {"apply nil string", `f { v = nil; }`, makeStackCheck(func(
+ args FArgs,
+ ) (v any, err error) {
+ var s string
+ err = args.Apply(map[unique.Handle[Ident]]any{
+ unique.Make(Ident("v")): &s,
+ })
+ v = s
+ return
+ }), "", EvaluationError{
+ Expr: Func{
+ Ident: Ident("f"),
+ Args: []Arg{
+ {K: []Ident{"v"}, V: Val{Ident("nil")}},
+ },
+ },
+ Err: NilError{Type: reflect.TypeFor[string]()},
+ }},
+
+ {"apply nil AString", `f { v = nil; }`, makeStackCheck(func(
+ args FArgs,
+ ) (v any, err error) {
+ s := AString{}
+ err = args.Apply(map[unique.Handle[Ident]]any{
+ unique.Make(Ident("v")): &s,
+ })
+ v = s.V
+ if !s.Nil {
+ err = fmt.Errorf("got non-nil string %q", s.V)
+ }
+ return
+ }), "", nil},
+
+ {"apply AString", `f { v = "\xfd"; }`, makeStackCheck(func(
+ args FArgs,
+ ) (v any, err error) {
+ s := AString{}
+ err = args.Apply(map[unique.Handle[Ident]]any{
+ unique.Make(Ident("v")): &s,
+ })
+ v = s.V
+ if s.Nil {
+ err = fmt.Errorf("unexpected nil %q", s.V)
+ }
+ return
+ }), "\xfd", nil},
+
+ {"apply nil", `f { v = nil; }`, makeStackCheck(func(
+ args FArgs,
+ ) (v any, err error) {
+ s := make([]struct{}, 9)
+ err = args.Apply(map[unique.Handle[Ident]]any{
+ unique.Make(Ident("v")): &s,
+ })
+ if s != nil {
+ err = fmt.Errorf("got non-nil value %#v", s)
+ }
+ return
+ }), "", nil},
+
{"common inputs", `package name { inputs, v = []; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
@@ -214,8 +278,8 @@ func TestEvaluate(t *testing.T) {
}
var errEquals bool
- if _, ok := errors.AsType[TypeError](err); ok {
- errEquals = errors.Is(err, tc.err)
+ if e, ok := errors.AsType[TypeError](err); ok {
+ errEquals = e.Is(tc.err)
} else {
errEquals = reflect.DeepEqual(err, tc.err)
}