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 9cc6897..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 @@ -390,6 +390,24 @@ 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 + } + if proj.AutoLambdas == nil { + proj.AutoLambdas = make(map[string]int) + } + err := parseAutoLambdas(proj.AutoLambdas, args) + if err != nil { + wrapError(err) + return + } default: if strict { errorf("unknown directive: %s", verb) @@ -397,6 +415,51 @@ usage: class [-embed -prefix=Prefix] *.workExt WorkClass [WorkPrototype]`, sw) } } +// 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(ret map[string]int, args []string) error { + i, n := 0, len(args) + for i < n { + 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 fmt.Errorf("autolambda %s: expect '(' after command name", name) + } + i++ + nArgs, e := strconv.Atoi(args[i]) + if e != nil || nArgs < 0 { + return fmt.Errorf("autolambda %s: invalid number of arguments %q", name, args[i]) + } + i++ + if i >= n || args[i] != ")" { + return fmt.Errorf("autolambda %s: expect ')' after number of arguments", name) + } + i++ + if _, ok := ret[name]; ok { + return fmt.Errorf("autolambda: duplicate command %q", name) + } + ret[name] = nArgs + if i < n { + if args[i] != "," { + return fmt.Errorf("autolambda: expect ',' between entries, got %q", args[i]) + } + i++ + if i >= n { + return fmt.Errorf("autolambda: trailing ',' without an entry") + } + } + } + return nil +} + func fileLine(n int) (file string, line int) { _, file, line, _ = runtime.Caller(n) return @@ -422,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 fffcee5..770d90a 100644 --- a/modfile/rule_test.go +++ b/modfile/rule_test.go @@ -248,3 +248,131 @@ 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), +`) + // 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) +`) +} + +// -----------------------------------------------------------------------------