aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--internal/rosa/azalea/azalea_test.go13
-rw-r--r--internal/rosa/azalea/evaluate.go308
-rw-r--r--internal/rosa/azalea/evaluate_test.go358
3 files changed, 679 insertions, 0 deletions
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..431d6944
--- /dev/null
+++ b/internal/rosa/azalea/evaluate.go
@@ -0,0 +1,308 @@
+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 any, 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)
+ } else if v != nil {
+ if err = storeE(rp, v); err != nil {
+ panic(err)
+ }
+ }
+ 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..9036e782
--- /dev/null
+++ b/internal/rosa/azalea/evaluate_test.go
@@ -0,0 +1,358 @@
+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) (any, error)) []Frame {
+ return []Frame{{Func: map[unique.Handle[Ident]]F{
+ unique.Make(Ident("f")): {F: func(
+ isPackage bool,
+ args FArgs,
+ ) (v any, set bool, err error) {
+ set = true
+ v, err = check(args)
+ if isPackage {
+ err = errors.New("unexpected package")
+ }
+ return
+ }},
+ }}}
+}
+
+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 any, 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 any, 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 any, 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 any, 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,
+ ) (any, 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 [3]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 any, 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 any, 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 any, set bool, err error) {
+ v = args
+ if isPackage {
+ err = errors.New("unexpected package")
+ }
+ set = true
+ return
+ }},
+
+ unique.Make(Ident("arch")): {F: func(
+ isPackage bool,
+ args FArgs,
+ ) (v any, set bool, err error) {
+ set = false
+ if isPackage {
+ err = errors.New("unexpected package")
+ }
+ got[1] = args
+ return
+ }},
+
+ unique.Make(Ident("noop")): {F: func(
+ isPackage bool,
+ args FArgs,
+ ) (v any, set bool, err error) {
+ set = false
+ if isPackage {
+ err = errors.New("unexpected package")
+ }
+ set = true
+ got[2] = 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: FArgs{
+ {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("inputs")), V: []string{
+ "binutils",
+ "mpc",
+ "zlib",
+ "libucontext",
+ "kernel-headers",
+ }},
+ },
+ {
+ {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 any, 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 any, 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 any, set bool, err error) {
+ return
+ }},
+
+ unique.Make(Ident("arch")): {F: func(
+ bool,
+ FArgs,
+ ) (v any, set bool, err error) {
+ return
+ }},
+
+ unique.Make(Ident("noop")): {F: func(
+ bool,
+ FArgs,
+ ) (v any, 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)
+ }
+ }
+}