diff --git a/internal/rosa/azalea/azalea_test.go b/internal/rosa/azalea/azalea_test.go index f2dec252..73330412 100644 --- a/internal/rosa/azalea/azalea_test.go +++ b/internal/rosa/azalea/azalea_test.go @@ -154,3 +154,16 @@ func TestParse(t *testing.T) { }) } } + +func BenchmarkParse(b *testing.B) { + r := strings.NewReader(sample) + for b.Loop() { + if _, err := Parse(r); err != nil { + b.Fatal(err) + } + + b.StopTimer() + r.Reset(sample) + b.StartTimer() + } +} diff --git a/internal/rosa/azalea/evaluate.go b/internal/rosa/azalea/evaluate.go new file mode 100644 index 00000000..6ec30915 --- /dev/null +++ b/internal/rosa/azalea/evaluate.go @@ -0,0 +1,305 @@ +package azalea + +import ( + "errors" + "fmt" + "maps" + "reflect" + "unique" +) + +// Value are types supported by the language. +type Value interface { + bool | int64 | string | []string | [][2]string +} + +type ( + // FArg is an argument passed to [F]. + FArg struct { + K unique.Handle[Ident] + V any + R bool + } + // FArgs are arguments passed to [F]. + FArgs []FArg + + // F is the implementation of a [Func]. + F struct { + F func(isPackage bool, args FArgs) (v string, set bool, err error) + V map[unique.Handle[Ident]]any + } +) + +// Apply applies named arguments and rejects unused arguments. +func (args FArgs) Apply(v map[unique.Handle[Ident]]any) error { + for _, arg := range args { + if arg.V == nil { + // unset + continue + } + + r, ok := v[arg.K] + if !ok { + if arg.R { + continue + } + return UndefinedError(arg.K.Value()) + } + err := storeE(r, arg.V) + if err != nil { + return err + } + } + return nil +} + +// A Frame refers to local variables and debugging information. +type Frame struct { + // Local constants. + Val map[unique.Handle[Ident]]any + // Functions. + Func map[unique.Handle[Ident]]F +} + +// UnsupportedExprError is an expression with invalid concrete type. +type UnsupportedExprError struct{ E any } + +func (e UnsupportedExprError) Error() string { + return fmt.Sprintf("unsupported expression %#v", e.E) +} + +// UndefinedError is an identifier not defined in any stack frame visible to the +// expression containing it. +type UndefinedError Ident + +func (e UndefinedError) Error() string { + return "undefined: " + string(e) +} + +// evaluate is evaluateAny with a type parameter. +func evaluate[T Value](s []Frame, expr any, rp *T) bool { + return evaluateAny(s, expr, rp) +} + +// TypeError is an unexpected type during evaluation. +type TypeError struct { + Concrete, Asserted reflect.Type +} + +func (e TypeError) Error() string { + return "expected " + e.Asserted.String() + ", got " + e.Concrete.String() +} + +func (e TypeError) Is(err error) bool { + var v TypeError + return errors.As(err, &v) && + e.Asserted == v.Asserted && + e.Concrete == v.Concrete +} + +// storeE is a convenience function to set the value of a result pointer. +func storeE(rp any, r any) error { + pv := reflect.ValueOf(rp).Elem() + v := reflect.ValueOf(r) + pt, vt := pv.Type(), v.Type() + if !vt.AssignableTo(pt) { + return TypeError{vt, pt} + } + pv.Set(v) + return nil +} + +// store is like storeE, but panics if error is non-nil. +func store[T Value](rp any, r T) { + err := storeE(rp, r) + if err != nil { + panic(err) + } +} + +// EvaluationError is an error and the expression it occurred in. +type EvaluationError struct { + Expr any + Err error +} + +// Unwrap returns the underlying error. +func (e EvaluationError) Unwrap() error { return e.Err } + +// Error returns a very long error description that should not be presented +// to the user directly. +func (e EvaluationError) Error() string { + return fmt.Sprintf("expression %#v: %v", e.Expr, e.Err) +} + +// evaluateAny implements [Evaluate]. +func evaluateAny(s []Frame, expr, rp any) bool { + defer func() { + r := recover() + if r == nil { + return + } + + err, ok := r.(error) + if !ok { + panic(r) + } + + if _, ok = err.(EvaluationError); ok { + panic(err) + } + panic(EvaluationError{expr, err}) + }() + + switch e := expr.(type) { + case Int: + store(rp, int64(e)) + return true + + case String: + store(rp, string(e)) + return true + + case Ident: + var ( + v any + ok bool + ) + for i := range s { + v, ok = s[len(s)-1-i].Val[unique.Make(e)] + if ok { + break + } + } + if !ok { + panic(UndefinedError(e)) + } + if err := storeE(rp, v); err != nil { + panic(err) + } + return true + + case Val: + if len(e) == 1 { + switch v := e[0].(type) { + case Ident: + switch v { + case "unset": + return false + case "true": + store(rp, true) + return true + case "false": + store(rp, false) + return true + default: + return evaluateAny(s, v, rp) + } + + default: + return evaluateAny(s, e[0], rp) + } + } + var v string + for i := range e { + var _r string + if evaluate(s, e[i], &_r) { + v += _r + } + } + store(rp, v) + return true + + case Array: + r := make([]string, 0, len(e)) + for i := range e { + var _r string + if evaluate(s, e[i], &_r) { + r = append(r, _r) + } + } + store(rp, r) + return true + + case []KV: + r := make([][2]string, 0, len(e)) + for i := range e { + var _r string + if e[i].V == nil || evaluate(s, e[i].V, &_r) { + r = append(r, [2]string{string(e[i].K), _r}) + } + } + store(rp, r) + return true + + case Func: + var ( + f F + ok bool + ) + for i := range s { + f, ok = s[len(s)-1-i].Func[unique.Make(e.Ident)] + if ok { + break + } + } + if !ok { + panic(UndefinedError(e.Ident)) + } + + argc := len(e.Args) + for _, arg := range e.Args { + argc += len(arg.K) - 1 + } + fargs := make([]FArg, 0, len(e.Args)) + s = append(s, Frame{Val: maps.Clone(f.V)}) + fp := &s[len(s)-1] + + for _, arg := range e.Args { + farg := FArg{R: arg.R} + if !evaluateAny(s, arg.V, &farg.V) { + farg.V = nil + } + for _, name := range arg.K { + h := unique.Make(name) + farg.K = h + fargs = append(fargs, farg) + + if arg.R && farg.V != nil { + if fp.Val == nil { + fp.Val = make(map[unique.Handle[Ident]]any) + } + (*fp).Val[h] = farg.V + } + } + } + + v, set, err := f.F(e.Package, fargs) + if err != nil { + panic(err) + } + store(rp, v) + return set + + default: + panic(UnsupportedExprError{expr}) + } +} + +// Evaluate evaluates a statement and returns its value. +func Evaluate[T Value](s []Frame, expr any) (v T, set bool, err error) { + defer func() { + r := recover() + if r == nil { + return + } + + _err, ok := r.(error) + if !ok { + panic(r) + } + err = _err + }() + set = evaluate[T](s, expr, &v) + return +} diff --git a/internal/rosa/azalea/evaluate_test.go b/internal/rosa/azalea/evaluate_test.go new file mode 100644 index 00000000..5943f34c --- /dev/null +++ b/internal/rosa/azalea/evaluate_test.go @@ -0,0 +1,357 @@ +package azalea_test + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" + "unique" + + . "hakurei.app/internal/rosa/azalea" +) + +// makeStackCheck creates a stack with a single frame with a single function "f" +// which calls the check function internally. +func makeStackCheck(check func(args FArgs) (string, error)) []Frame { + return []Frame{{Func: map[unique.Handle[Ident]]F{ + unique.Make(Ident("f")): {F: func( + isPackage bool, + args FArgs, + ) (v string, set bool, err error) { + set = true + v, err = check(args) + if isPackage { + err = errors.New("unexpected package") + } + return + }}, + }}} +} + +// makeWantArgs makes a stack for validating the arguments of a single function "f". +func makeWantArgs(want FArgs) []Frame { + return makeStackCheck(func(got FArgs) (string, error) { + if !reflect.DeepEqual(got, want) { + return "", fmt.Errorf("f: %#v, want %#v", got, want) + } + return "\xfc", nil + }) +} + +func TestEvaluate(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + s []Frame + want string + err error + }{ + {"apply unset", `f { v = unset; }`, makeStackCheck(func( + args FArgs, + ) (v string, err error) { + v = "\xfd" + err = args.Apply(map[unique.Handle[Ident]]any{ + unique.Make(Ident("v")): &v, + }) + return + }), "\xfd", nil}, + + {"apply bad type", `f { v = 9; }`, makeStackCheck(func( + args FArgs, + ) (v string, err error) { + v = "\xfd" + err = args.Apply(map[unique.Handle[Ident]]any{ + unique.Make(Ident("v")): &v, + }) + return + }), "", TypeError{ + Concrete: reflect.TypeFor[int64](), + Asserted: reflect.TypeFor[string](), + }}, + + {"apply undefined", `f { v = 9; }`, makeStackCheck(func( + args FArgs, + ) (v string, err error) { + v = "\xfd" + err = args.Apply(map[unique.Handle[Ident]]any{}) + return + }), "", EvaluationError{ + Expr: Func{ + Ident: Ident("f"), + Args: []Arg{ + {K: []Ident{"v"}, V: Val{Int(9)}}, + }, + }, + Err: UndefinedError("v"), + }}, + + {"apply bound undefined", `f { _v* = "\x00"; v = _v; }`, makeStackCheck(func( + args FArgs, + ) (v string, err error) { + v = "\xfd" + err = args.Apply(map[unique.Handle[Ident]]any{ + unique.Make(Ident("v")): &v, + }) + return + }), "\x00", nil}, + + {"undefined function", `f {}`, nil, "", EvaluationError{ + Expr: Func{Ident: "f"}, + Err: UndefinedError("f"), + }}, + + {"error wrap deep", `f { v = nil; }`, makeStackCheck(func( + FArgs, + ) (string, error) { + panic("unreachable") + }), "", EvaluationError{ + Expr: Ident("nil"), + Err: UndefinedError("nil"), + }}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var expr Func + if e, err := Parse(strings.NewReader(tc.data)); err != nil { + t.Fatal(err) + } else if len(e) != 1 { + t.Fatalf("got expression %#v", e) + } else { + expr = e[0].(Func) + } + r, set, err := Evaluate[string](tc.s, expr) + if set != (err == nil) { + t.Error("Evaluate: unexpected unset") + } + + if r != tc.want { + t.Errorf("Evaluate: %q, want %q", r, tc.want) + } + + var errEquals bool + if errors.As(err, new(TypeError)) { + errEquals = errors.Is(err, tc.err) + } else { + errEquals = reflect.DeepEqual(err, tc.err) + } + if !errEquals { + t.Errorf("Evaluate: error = %v, want %v", err, tc.err) + } + }) + } +} + +func TestEvaluateGCC(t *testing.T) { + t.Parallel() + + var gcc Func + if e, err := Parse(strings.NewReader(sample)); err != nil { + t.Fatal(err) + } else { + gcc = e[0].(Func) + } + + var got [4]FArgs + if r, set, err := Evaluate[string]([]Frame{{ + Func: map[unique.Handle[Ident]]F{ + unique.Make(Ident("gcc")): {F: func( + isPackage bool, + args FArgs, + ) (v string, set bool, err error) { + v = "\x00" + if !isPackage { + err = errors.New("not a package") + } + set = true + got[0] = args + return + }, V: map[unique.Handle[Ident]]any{ + unique.Make(Ident("binutils")): "binutils", + unique.Make(Ident("mpc")): "mpc", + unique.Make(Ident("zlib")): "zlib", + unique.Make(Ident("libucontext")): "libucontext", + unique.Make(Ident("kernel-headers")): "kernel-headers", + }}, + + unique.Make(Ident("remoteTar")): {F: func( + isPackage bool, + args FArgs, + ) (v string, set bool, err error) { + if isPackage { + err = errors.New("unexpected package") + return + } + + var url, checksum string + var compress int + if err = args.Apply(map[unique.Handle[Ident]]any{ + unique.Make(Ident("url")): &url, + unique.Make(Ident("checksum")): &checksum, + unique.Make(Ident("compress")): &compress, + }); err != nil { + return + } + + if compress != 0xcafe { + err = fmt.Errorf("unexpected compress %#v", compress) + } + set = true + v = url + "?checksum=" + checksum + return + }, V: map[unique.Handle[Ident]]any{ + unique.Make(Ident("gzip")): 0xcafe, + }}, + + unique.Make(Ident("make")): {F: func( + isPackage bool, + args FArgs, + ) (v string, set bool, err error) { + v = "\x01" + if isPackage { + err = errors.New("unexpected package") + } + set = true + got[1] = args + return + }}, + + unique.Make(Ident("arch")): {F: func( + isPackage bool, + args FArgs, + ) (v string, set bool, err error) { + set = false + if isPackage { + err = errors.New("unexpected package") + } + got[2] = args + return + }}, + + unique.Make(Ident("noop")): {F: func( + isPackage bool, + args FArgs, + ) (v string, set bool, err error) { + set = false + if isPackage { + err = errors.New("unexpected package") + } + set = true + got[3] = args + return + }, V: map[unique.Handle[Ident]]any{ + unique.Make(Ident("value")): "\xfd", + }}, + }, + }}, gcc); err != nil { + t.Fatal(err) + } else if r != "\x00" { + t.Fatalf("package: %q", r) + } else if !set { + t.Fatal("package: unset") + } + + want := [...]FArgs{ + { + {K: unique.Make(Ident("description")), V: "The GNU Compiler Collection"}, + {K: unique.Make(Ident("website")), V: "https://www.gnu.org/software/gcc"}, + {K: unique.Make(Ident("anitya")), V: int64(6502)}, + + {K: unique.Make(Ident("version")), V: "16.1.0", R: true}, + {K: unique.Make(Ident("source")), V: "https://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-16.1.0/gcc-16.1.0.tar.gz?checksum=4ASoWbxaA2FW7PAB0zzHDPC5XnNhyaAyjtDPpGzceSLeYnEIXsNYZR3PA_Zu5P0K"}, + {K: unique.Make(Ident("patches")), V: []string{"musl-off64_t-loff_t.patch", "musl-legacy-lfs.patch"}}, + + {K: unique.Make(Ident("exclusive")), V: true}, + {K: unique.Make(Ident("exec")), V: "\x01"}, + + {K: unique.Make(Ident("inputs")), V: []string{ + "binutils", + "mpc", + "zlib", + "libucontext", + "kernel-headers", + }}, + }, + { + {K: unique.Make(Ident("configure")), V: [][2]string{{"disable-multilib", ""}, {"enable-default-pie", ""}, {"disable-nls", ""}, {"with-gnu-as", ""}, {"with-gnu-ld", ""}, {"with-system-zlib", ""}, {"enable-languages", "c,c++,go"}, {"with-native-system-header-dir", "/system/include"}}}, + {K: unique.Make(Ident("make")), V: []string{"BOOT_CFLAGS='-O2 -g'", "\x00", "bootstrap"}}, + {K: unique.Make(Ident("skip-check")), V: true}, + }, + { + {K: unique.Make(Ident("amd64")), V: "''"}, + {K: unique.Make(Ident("arm64")), V: "''"}, + {K: unique.Make(Ident("default"))}, + }, + {{K: unique.Make(Ident("key")), V: "\xfd"}}, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("package: args = %#v, want %#v", got, want) + } +} + +func BenchmarkEvaluate(b *testing.B) { + var gcc Func + if e, err := Parse(strings.NewReader(sample)); err != nil { + b.Fatal(err) + } else { + gcc = e[0].(Func) + } + + s := []Frame{{ + Func: map[unique.Handle[Ident]]F{ + unique.Make(Ident("gcc")): {F: func( + bool, + FArgs, + ) (v string, set bool, err error) { + return + }, V: map[unique.Handle[Ident]]any{ + unique.Make(Ident("binutils")): "binutils", + unique.Make(Ident("mpc")): "mpc", + unique.Make(Ident("zlib")): "zlib", + unique.Make(Ident("libucontext")): "libucontext", + unique.Make(Ident("kernel-headers")): "kernel-headers", + }}, + + unique.Make(Ident("remoteTar")): {F: func( + bool, + FArgs, + ) (v string, set bool, err error) { + return + }, V: map[unique.Handle[Ident]]any{ + unique.Make(Ident("gzip")): 0xcafe, + }}, + + unique.Make(Ident("make")): {F: func( + bool, + FArgs, + ) (v string, set bool, err error) { + return + }}, + + unique.Make(Ident("arch")): {F: func( + bool, + FArgs, + ) (v string, set bool, err error) { + return + }}, + + unique.Make(Ident("noop")): {F: func( + bool, + FArgs, + ) (v string, set bool, err error) { + return + }, V: map[unique.Handle[Ident]]any{ + unique.Make(Ident("value")): "\xfd", + }}, + }, + }} + for b.Loop() { + if _, _, err := Evaluate[string](s, gcc); err != nil { + b.Fatal(err) + } + } +}