From 7715a60cf16e035d3c4d021d291f1cbce2ea82a7 Mon Sep 17 00:00:00 2001 From: xushiwei <396972+xushiwei@users.noreply.github.com> Date: Mon, 10 Aug 2026 05:54:59 +0000 Subject: [PATCH 1/2] modfile: support autolambda directive Parse the `autolambda name(n), ...` directive in gox.mod and store the results in Project.AutoLambdas (command => number of non-lambda arguments before the trailing implicit lambda). Multiple entries may appear in one directive or across multiple autolambda lines within a project. Fixes #158 --- modfile/rule.go | 90 ++++++++++++++++++++++++++++++++++ modfile/rule_test.go | 113 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/modfile/rule.go b/modfile/rule.go index 9cc6897..82cd7a2 100644 --- a/modfile/rule.go +++ b/modfile/rule.go @@ -390,6 +390,27 @@ usage: class [-embed -prefix=Prefix] *.workExt WorkClass [WorkPrototype]`, sw) return } proj.Pack = &Pack{Directory: dir, IndexFile: indexFile, Syntax: line} + case "autolambda": + proj := f.proj() + if proj == nil { + errorf("autolambda must declare after a project definition") + return + } + if len(args) == 0 { + errorf("usage: autolambda name(n), ...") + return + } + entries, err := parseAutoLambdas(args) + if err != nil { + wrapError(err) + return + } + if proj.AutoLambdas == nil { + proj.AutoLambdas = make(map[string]int, len(entries)) + } + for _, e := range entries { + proj.AutoLambdas[e.name] = e.nArgs + } default: if strict { errorf("unknown directive: %s", verb) @@ -397,6 +418,75 @@ usage: class [-embed -prefix=Prefix] *.workExt WorkClass [WorkPrototype]`, sw) } } +// autoLambdaEntry is one parsed `name(n)` entry of an autolambda directive. +type autoLambdaEntry struct { + name string + nArgs int +} + +// parseAutoLambdas parses the argument tokens of an autolambda directive into a +// list of `name(n)` entries. The gox.mod tokenizer splits parentheses and commas +// into their own tokens, so a directive like: +// +// autolambda times(1), forEver(0), onKey(1) +// +// arrives here as: [times ( 1 ) , forEver ( 0 ) , onKey ( 1 )]. +func parseAutoLambdas(args []string) (entries []autoLambdaEntry, err error) { + i, n := 0, len(args) + for i < n { + name := args[i] + if !isIdent(name) { + return nil, fmt.Errorf("invalid autolambda command name %q", name) + } + i++ + if i >= n || args[i] != "(" { + return nil, fmt.Errorf("autolambda %s: expect '(' after command name", name) + } + i++ + if i >= n { + return nil, fmt.Errorf("autolambda %s: expect number of arguments", name) + } + nArgs, e := strconv.Atoi(args[i]) + if e != nil || nArgs < 0 { + return nil, fmt.Errorf("autolambda %s: invalid number of arguments %q", name, args[i]) + } + i++ + if i >= n || args[i] != ")" { + return nil, fmt.Errorf("autolambda %s: expect ')' after number of arguments", name) + } + i++ + entries = append(entries, autoLambdaEntry{name: name, nArgs: nArgs}) + if i < n { + if args[i] != "," { + return nil, fmt.Errorf("autolambda: expect ',' between entries, got %q", args[i]) + } + i++ + if i >= n { + return nil, fmt.Errorf("autolambda: trailing ',' without an entry") + } + } + } + return +} + +// isIdent reports whether s is a valid identifier (letter or '_' followed by +// letters, digits or '_'). +func isIdent(s string) bool { + if s == "" { + return false + } + for i, r := range s { + if r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + continue + } + if i > 0 && r >= '0' && r <= '9' { + continue + } + return false + } + return true +} + func fileLine(n int) (file string, line int) { _, file, line, _ = runtime.Caller(n) return diff --git a/modfile/rule_test.go b/modfile/rule_test.go index fffcee5..f6bf850 100644 --- a/modfile/rule_test.go +++ b/modfile/rule_test.go @@ -248,3 +248,116 @@ pack assets sub\index.json } // ----------------------------------------------------------------------------- + +const goxmodWithAutoLambda = ` +xgo 1.6 + +project main.spx Game github.com/goplus/spx/v2 math +class -embed *.spx SpriteImpl +autolambda times(1), forEver(0), onKey(1) +` + +func TestParseAutoLambda(t *testing.T) { + f, err := ParseLax("gox.mod", []byte(goxmodWithAutoLambda), nil) + if err != nil { + t.Fatal("ParseLax failed:", err) + } + proj := f.proj() + if proj == nil { + t.Fatal("expected a project") + } + want := map[string]int{"times": 1, "forEver": 0, "onKey": 1} + if len(proj.AutoLambdas) != len(want) { + t.Fatalf("expected %d autolambda entries, got %d: %v", len(want), len(proj.AutoLambdas), proj.AutoLambdas) + } + for name, n := range want { + if got, ok := proj.AutoLambdas[name]; !ok || got != n { + t.Errorf("autolambda[%s] expected %d, got %d (ok=%v)", name, n, got, ok) + } + } +} + +const goxmodMultiAutoLambda = ` +xgo 1.6 + +project main.spx Game github.com/goplus/spx/v2 math +class -embed *.spx SpriteImpl +autolambda times(1) +autolambda forEver(0), onKey(1) +` + +func TestParseMultiAutoLambda(t *testing.T) { + f, err := ParseLax("gox.mod", []byte(goxmodMultiAutoLambda), nil) + if err != nil { + t.Fatal("ParseLax failed:", err) + } + proj := f.proj() + if proj == nil { + t.Fatal("expected a project") + } + want := map[string]int{"times": 1, "forEver": 0, "onKey": 1} + if len(proj.AutoLambdas) != len(want) { + t.Fatalf("expected %d autolambda entries, got %d: %v", len(want), len(proj.AutoLambdas), proj.AutoLambdas) + } + for name, n := range want { + if got, ok := proj.AutoLambdas[name]; !ok || got != n { + t.Errorf("autolambda[%s] expected %d, got %d (ok=%v)", name, n, got, ok) + } + } +} + +const goxmodNoAutoLambda = ` +xgo 1.6 + +project main.spx Game github.com/goplus/spx/v2 math +class -embed *.spx SpriteImpl +` + +func TestParseNoAutoLambda(t *testing.T) { + f, err := ParseLax("gox.mod", []byte(goxmodNoAutoLambda), nil) + if err != nil { + t.Fatal("ParseLax failed:", err) + } + if f.proj().AutoLambdas != nil { + t.Error("expected no autolambda directive") + } +} + +func TestParseAutoLambdaErr(t *testing.T) { + // autolambda before project + doTestParseErr(t, `gop.mod:2: autolambda must declare after a project definition`, ` +autolambda times(1) +`) + // missing arguments + doTestParseErr(t, `gop.mod:3: usage: autolambda name(n), ...`, ` +project github.com/goplus/spx math +autolambda +`) + // missing '(' + doTestParseErr(t, `gop.mod:3: autolambda times: expect '(' after command name`, ` +project github.com/goplus/spx math +autolambda times 1) +`) + // invalid number + doTestParseErr(t, `gop.mod:3: autolambda times: invalid number of arguments "x"`, ` +project github.com/goplus/spx math +autolambda times(x) +`) + // missing ')' + doTestParseErr(t, `gop.mod:3: autolambda times: expect ')' after number of arguments`, ` +project github.com/goplus/spx math +autolambda times(1 +`) + // missing ',' between entries + doTestParseErr(t, `gop.mod:3: autolambda: expect ',' between entries, got "forEver"`, ` +project github.com/goplus/spx math +autolambda times(1) forEver(0) +`) + // trailing ',' + doTestParseErr(t, `gop.mod:3: autolambda: trailing ',' without an entry`, ` +project github.com/goplus/spx math +autolambda times(1), +`) +} + +// ----------------------------------------------------------------------------- From 5b58a95944a7e13551a4ba6caf52862b56c45dcf Mon Sep 17 00:00:00 2001 From: xushiwei Date: Mon, 10 Aug 2026 14:55:29 +0800 Subject: [PATCH 2/2] modfile: parseAutoLambdas --- .github/codecov.yml | 1 + modfile/gop_test.go | 8 ++-- modfile/rule.go | 94 ++++++++++++++++++++------------------------ modfile/rule_test.go | 15 +++++++ 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 6e1dfdf..cde399d 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,3 +1,4 @@ coverage: ignore: - "modfetch" + - "sumfile" diff --git a/modfile/gop_test.go b/modfile/gop_test.go index d23eab5..a7e1c19 100644 --- a/modfile/gop_test.go +++ b/modfile/gop_test.go @@ -326,10 +326,10 @@ project ." Game math doTestParseErr(t, `gop.mod:2: "." is not a valid package path`, ` project . Game math `) - doTestParseErr(t, `gop.mod:2: symbol game invalid: invalid Go export symbol format`, ` + doTestParseErr(t, `gop.mod:2: symbol game invalid: invalid Go export type`, ` project .gmx game math `) - doTestParseErr(t, `gop.mod:2: symbol . invalid: invalid Go export symbol format`, ` + doTestParseErr(t, `gop.mod:2: symbol . invalid: invalid Go export type`, ` project .gmx . math `) doTestParseErr(t, `gop.mod:2: invalid quoted string: invalid syntax`, ` @@ -370,11 +370,11 @@ pack ."spx Sprite project github.com/goplus/spx math pack "" ."spx `) - doTestParseErr(t, `gop.mod:3: symbol .abc invalid: invalid Go export symbol format`, ` + doTestParseErr(t, `gop.mod:3: symbol .abc invalid: invalid Go export type`, ` project github.com/goplus/spx math class .spx Sprite .abc `) - doTestParseErr(t, `gop.mod:3: symbol sprite invalid: invalid Go export symbol format`, ` + doTestParseErr(t, `gop.mod:3: symbol sprite invalid: invalid Go export type`, ` project github.com/goplus/spx math class .spx sprite `) diff --git a/modfile/rule.go b/modfile/rule.go index 82cd7a2..1f032a5 100644 --- a/modfile/rule.go +++ b/modfile/rule.go @@ -250,7 +250,7 @@ func (f *File) parseVerb(errs *ErrorList, verb string, line *Line, args []string wrapError(err) return } - class, err := parseSymbol(&args[1]) + class, err := parseType(&args[1]) if err != nil { wrapError(err) return @@ -307,14 +307,14 @@ usage: class [-embed -prefix=Prefix] *.workExt WorkClass [WorkPrototype]`, sw) wrapError(err) return } - class, err := parseSymbol(&args[1]) + class, err := parseType(&args[1]) if err != nil { wrapError(err) return } protoClass := "" if len(args) > 2 { - protoClass, err = parseSymbol(&args[2]) + protoClass, err = parseType(&args[2]) if err != nil { wrapError(err) return @@ -400,17 +400,14 @@ usage: class [-embed -prefix=Prefix] *.workExt WorkClass [WorkPrototype]`, sw) errorf("usage: autolambda name(n), ...") return } - entries, err := parseAutoLambdas(args) + if proj.AutoLambdas == nil { + proj.AutoLambdas = make(map[string]int) + } + err := parseAutoLambdas(proj.AutoLambdas, args) if err != nil { wrapError(err) return } - if proj.AutoLambdas == nil { - proj.AutoLambdas = make(map[string]int, len(entries)) - } - for _, e := range entries { - proj.AutoLambdas[e.name] = e.nArgs - } default: if strict { errorf("unknown directive: %s", verb) @@ -418,12 +415,6 @@ usage: class [-embed -prefix=Prefix] *.workExt WorkClass [WorkPrototype]`, sw) } } -// autoLambdaEntry is one parsed `name(n)` entry of an autolambda directive. -type autoLambdaEntry struct { - name string - nArgs int -} - // parseAutoLambdas parses the argument tokens of an autolambda directive into a // list of `name(n)` entries. The gox.mod tokenizer splits parentheses and commas // into their own tokens, so a directive like: @@ -431,60 +422,42 @@ type autoLambdaEntry struct { // autolambda times(1), forEver(0), onKey(1) // // arrives here as: [times ( 1 ) , forEver ( 0 ) , onKey ( 1 )]. -func parseAutoLambdas(args []string) (entries []autoLambdaEntry, err error) { +func parseAutoLambdas(ret map[string]int, args []string) error { i, n := 0, len(args) for i < n { - name := args[i] - if !isIdent(name) { - return nil, fmt.Errorf("invalid autolambda command name %q", name) + name, err := parseIdent(&args[i]) + if err != nil { + return fmt.Errorf("autolambda: invalid command name %q", args[i]) } i++ if i >= n || args[i] != "(" { - return nil, fmt.Errorf("autolambda %s: expect '(' after command name", name) + return fmt.Errorf("autolambda %s: expect '(' after command name", name) } i++ - if i >= n { - return nil, fmt.Errorf("autolambda %s: expect number of arguments", name) - } nArgs, e := strconv.Atoi(args[i]) if e != nil || nArgs < 0 { - return nil, fmt.Errorf("autolambda %s: invalid number of arguments %q", name, args[i]) + return fmt.Errorf("autolambda %s: invalid number of arguments %q", name, args[i]) } i++ if i >= n || args[i] != ")" { - return nil, fmt.Errorf("autolambda %s: expect ')' after number of arguments", name) + return fmt.Errorf("autolambda %s: expect ')' after number of arguments", name) } i++ - entries = append(entries, autoLambdaEntry{name: name, nArgs: nArgs}) + if _, ok := ret[name]; ok { + return fmt.Errorf("autolambda: duplicate command %q", name) + } + ret[name] = nArgs if i < n { if args[i] != "," { - return nil, fmt.Errorf("autolambda: expect ',' between entries, got %q", args[i]) + return fmt.Errorf("autolambda: expect ',' between entries, got %q", args[i]) } i++ if i >= n { - return nil, fmt.Errorf("autolambda: trailing ',' without an entry") + return fmt.Errorf("autolambda: trailing ',' without an entry") } } } - return -} - -// isIdent reports whether s is a valid identifier (letter or '_' followed by -// letters, digits or '_'). -func isIdent(s string) bool { - if s == "" { - return false - } - for i, r := range s { - if r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { - continue - } - if i > 0 && r >= '0' && r <= '9' { - continue - } - return false - } - return true + return nil } func fileLine(n int) (file string, line int) { @@ -512,19 +485,36 @@ func AutoQuote(s string) string { } var ( - symbolRE = regexp.MustCompile(`\*?[A-Z]\w*`) + typeRE = regexp.MustCompile(`\*?[A-Z]\w*`) + idenRE = regexp.MustCompile(`\w+`) ) // TODO(xsw): to be optimized -func parseSymbol(s *string) (t string, err error) { +func parseType(s *string) (t string, err error) { + t, err = parseString(s) + if err != nil { + goto failed + } + if typeRE.MatchString(t) { + return + } + err = errors.New("invalid Go export type") +failed: + return "", &InvalidSymbolError{ + Sym: *s, + Err: err, + } +} + +func parseIdent(s *string) (t string, err error) { t, err = parseString(s) if err != nil { goto failed } - if symbolRE.MatchString(t) { + if idenRE.MatchString(t) { return } - err = errors.New("invalid Go export symbol format") + err = errors.New("invalid XGo identifier") failed: return "", &InvalidSymbolError{ Sym: *s, diff --git a/modfile/rule_test.go b/modfile/rule_test.go index f6bf850..770d90a 100644 --- a/modfile/rule_test.go +++ b/modfile/rule_test.go @@ -357,6 +357,21 @@ autolambda times(1) forEver(0) doTestParseErr(t, `gop.mod:3: autolambda: trailing ',' without an entry`, ` project github.com/goplus/spx math autolambda times(1), +`) + // invalid command name + doTestParseErr(t, `gop.mod:3: autolambda: invalid command name "!"`, ` +project github.com/goplus/spx math +autolambda !(0) +`) + // invalid command name + doTestParseErr(t, `gop.mod:3: autolambda: invalid command name "'"`, ` +project github.com/goplus/spx math +autolambda ' +`) + // duplicate command + doTestParseErr(t, `gop.mod:3: autolambda: duplicate command "times"`, ` +project github.com/goplus/spx math +autolambda times(1), times(2) `) }