From 7d86e19c07b3a76cbba08dd209dc6bcea6067029 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 10 Aug 2026 18:10:30 +0900 Subject: [PATCH] internal/rosa/azalea: implement nil identifier This also implements a nil-capable string type. Signed-off-by: Ophestra --- internal/rosa/azalea/evaluate.go | 65 ++++++++++++++++++++--- internal/rosa/azalea/evaluate_test.go | 74 +++++++++++++++++++++++++-- 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) }