diff --git a/cl/compile.go b/cl/compile.go index ad277ed93..d91dbb8db 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -22,6 +22,7 @@ import ( gotoken "go/token" "go/types" "log" + "os" "reflect" "sort" "strconv" @@ -210,6 +211,7 @@ type Config struct { type nodeInterp struct { fset *token.FileSet files map[string]*ast.File + codes map[string][]byte relBaseDir string } @@ -232,9 +234,17 @@ func (p *nodeInterp) LoadExpr(node ast.Node) string { return "" } pos := p.fset.Position(start) - f := p.files[pos.Filename] n := int(node.End() - start) - return string(f.Code[pos.Offset : pos.Offset+n]) + code := p.codes[pos.Filename] + if code == nil { + src, err := os.ReadFile(pos.Filename) + if err != nil { + return "" + } + code = src + p.codes[pos.Filename] = code + } + return string(code[pos.Offset : pos.Offset+n]) } func (p *nodeInterp) ProjFile() *ast.File { @@ -360,6 +370,7 @@ type pkgCtx struct { goxMainClass string goxMain int32 // normal gox files with main func + outline bool } type pkgImp struct { @@ -577,7 +588,13 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gogen.Packag fset := conf.Fset files := pkg.Files interp := &nodeInterp{ - fset: fset, files: files, relBaseDir: relBaseDir, + fset: fset, + files: files, + codes: make(map[string][]byte, len(pkg.Files)), + relBaseDir: relBaseDir, + } + for filename, f := range files { + interp.codes[filename] = f.Code } ctx := &pkgCtx{ fset: fset, @@ -587,6 +604,7 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gogen.Packag overpos: make(map[string]token.Pos), syms: make(map[string]loader), generics: make(map[string]bool), + outline: conf.Outline, } confGox := &gogen.Config{ Types: conf.Types, diff --git a/cl/func_type_and_var.go b/cl/func_type_and_var.go index a09e7550c..8a21eba3b 100644 --- a/cl/func_type_and_var.go +++ b/cl/func_type_and_var.go @@ -557,7 +557,17 @@ func instantiate(ctx *blockCtx, exprX ast.Expr, indices ...ast.Expr) types.Type for i, index := range indices { idx[i] = toType(ctx, index) } - typ := ctx.pkg.Instantiate(x, idx, exprX) + var typ types.Type + if ctx.outline { + var err error + typ, err = types.Instantiate(nil, x, idx, false) + if err != nil { + ctx.handleErrorf(exprX.Pos(), exprX.End(), "%v", err) + return types.Typ[types.Invalid] + } + } else { + typ = ctx.pkg.Instantiate(x, idx, exprX) + } if rec := ctx.recorder(); rec != nil { rec.instantiate(exprX, x, typ) } diff --git a/cl/outline/classfile/loader.go b/cl/outline/classfile/loader.go new file mode 100644 index 000000000..20e3ccc91 --- /dev/null +++ b/cl/outline/classfile/loader.go @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package classfile + +import ( + "fmt" + goast "go/ast" + gotoken "go/token" + "go/types" + "maps" + "slices" + "strings" + + xast "github.com/goplus/xgo/ast" + "github.com/goplus/xgo/scanner" + "github.com/goplus/xgo/token" +) + +// resourceSchemaLoader builds one [ResourceSchema] from one typed framework package. +type resourceSchemaLoader struct { + fset *token.FileSet + pkg *xast.Package + types *types.Package + schema *ResourceSchema + errs scanner.ErrorList + + rawAPIScopeBindings map[*types.Func][]rawAPIScopeBinding + callableHandles map[*types.Func]*types.TypeName +} + +// rawAPIScopeBinding is one unresolved API-position scope binding directive. +type rawAPIScopeBinding struct { + pos token.Pos + target int + source ResourceAPIScopeSource +} + +// scanPackage scans all Go and XGo files of the framework package. +func (l *resourceSchemaLoader) scanPackage() { + l.rawAPIScopeBindings = make(map[*types.Func][]rawAPIScopeBinding) + l.callableHandles = make(map[*types.Func]*types.TypeName) + for _, name := range slices.Sorted(maps.Keys(l.pkg.Files)) { + l.scanXGoFile(l.pkg.Files[name]) + } + + for _, name := range slices.Sorted(maps.Keys(l.pkg.GoFiles)) { + l.scanGoFile(l.pkg.GoFiles[name]) + } +} + +// scanXGoFile scans one parsed XGo file. +func (l *resourceSchemaLoader) scanXGoFile(file *xast.File) { + for _, decl := range file.Decls { + switch d := decl.(type) { + case *xast.GenDecl: + if d.Tok == token.TYPE { + l.scanXGoTypeDecl(d) + } + case *xast.FuncDecl: + l.scanXGoFuncDecl(d) + } + } +} + +// scanGoFile scans one parsed Go file. +func (l *resourceSchemaLoader) scanGoFile(file *goast.File) { + for _, decl := range file.Decls { + switch d := decl.(type) { + case *goast.GenDecl: + if d.Tok == gotoken.TYPE { + l.scanGoTypeDecl(d) + } + case *goast.FuncDecl: + l.scanGoFuncDecl(d) + } + } +} + +// scanXGoTypeDecl scans one XGo type declaration for classfile resource directives. +func (l *resourceSchemaLoader) scanXGoTypeDecl(decl *xast.GenDecl) { + for _, rawSpec := range decl.Specs { + spec, ok := rawSpec.(*xast.TypeSpec) + if !ok || spec.TypeParams != nil || !spec.Name.IsExported() { + continue + } + l.scanXGoTypeSpec(spec.Name.Name, spec, parseDirectives(xgoDeclDoc(decl, spec))) + } +} + +// scanGoTypeDecl scans one Go type declaration for classfile resource directives. +func (l *resourceSchemaLoader) scanGoTypeDecl(decl *goast.GenDecl) { + for _, rawSpec := range decl.Specs { + spec, ok := rawSpec.(*goast.TypeSpec) + if !ok || spec.TypeParams != nil || !spec.Name.IsExported() { + continue + } + l.scanGoTypeSpec(spec.Name.Name, spec, parseDirectives(goDeclDoc(decl, spec))) + } +} + +// scanXGoTypeSpec loads classfile resource directives attached to one exported +// top-level XGo type spec. +func (l *resourceSchemaLoader) scanXGoTypeSpec(name string, spec *xast.TypeSpec, dirs directives) { + obj, kind, ok := l.scanTypeDirectives(name, dirs) + if !ok { + return + } + if iface, ok := types.Unalias(obj.Type()).Underlying().(*types.Interface); ok { + l.scanXGoInterfaceMethodSpecs(obj, kind, iface, spec) + } +} + +// scanGoTypeSpec loads classfile resource directives attached to one exported +// top-level Go type spec. +func (l *resourceSchemaLoader) scanGoTypeSpec(name string, spec *goast.TypeSpec, dirs directives) { + obj, kind, ok := l.scanTypeDirectives(name, dirs) + if !ok { + return + } + if iface, ok := types.Unalias(obj.Type()).Underlying().(*types.Interface); ok { + l.scanGoInterfaceMethodSpecs(obj, kind, iface, spec) + } +} + +// scanTypeDirectives loads classfile resource directives attached to one +// exported top-level type declaration. +func (l *resourceSchemaLoader) scanTypeDirectives(name string, dirs directives) (*types.TypeName, *ResourceKind, bool) { + l.addTypeDirectiveSyntaxErrors(dirs) + if len(dirs.resource) == 0 { + return nil, nil, false + } + obj := l.lookupType(name) + if obj == nil { + return nil, nil, false + } + if len(dirs.resource) > 1 { + l.addError(dirs.resource[1].pos, "duplicate resource directive on %s", name) + return nil, nil, false + } + kindName, ok := parseResourceKind(dirs.resource[0].arg) + if !ok { + l.addError(dirs.resource[0].pos, "invalid resource kind %q", dirs.resource[0].arg) + return nil, nil, false + } + + if isStringBased(obj) { + hasDiscovery := false + if len(dirs.discovery) > 1 { + l.addError(dirs.discovery[1].pos, "duplicate %s directive on %s", directiveResourceDiscovery, name) + } else if len(dirs.discovery) == 1 && + validDQLQuery(dirs.discovery[0].pos, dirs.discovery[0].arg, directiveResourceDiscovery) { + hasDiscovery = true + } + if hasDiscovery && len(dirs.nameDiscovery) > 1 { + l.addError(dirs.nameDiscovery[1].pos, "duplicate %s directive on %s", directiveResourceNameDiscovery, name) + } + kind := l.ensureKind(kindName, dirs.resource[0].pos) + if kind.CanonicalType != nil && kind.CanonicalType != obj { + l.addError( + dirs.resource[0].pos, + "resource kind %q has more than one canonical type (previous at %v)", + kindName, + l.fset.Position(kind.CanonicalType.Pos()), + ) + return nil, nil, false + } + kind.CanonicalType = obj + l.schema.byCanonical[obj] = kind + if hasDiscovery { + kind.DiscoveryQuery = dirs.discovery[0].arg + } + if hasDiscovery && len(dirs.nameDiscovery) == 1 && + validDQLQuery(dirs.nameDiscovery[0].pos, dirs.nameDiscovery[0].arg, directiveResourceNameDiscovery) { + kind.NameDiscoveryQuery = dirs.nameDiscovery[0].arg + } + return obj, kind, true + } + + if isHandleBearing(obj) { + if strings.Contains(kindName, ".") { + l.addError(dirs.resource[0].pos, "handle-bearing type %s must declare one top-level resource kind", name) + return nil, nil, false + } + kind := l.ensureKind(kindName, dirs.resource[0].pos) + kind.HandleTypes = append(kind.HandleTypes, obj) + l.schema.byHandle[obj] = kind + return obj, kind, true + } + + return nil, nil, false +} + +// scanXGoInterfaceMethodSpecs loads classfile scope-binding directives +// attached to method specs declared by one handle-bearing XGo interface type. +func (l *resourceSchemaLoader) scanXGoInterfaceMethodSpecs(obj *types.TypeName, kind *ResourceKind, iface *types.Interface, spec *xast.TypeSpec) { + xiface, ok := spec.Type.(*xast.InterfaceType) + if !ok || kind == nil || kind != l.schema.byHandle[obj] || xiface.Methods == nil { + return + } + for _, field := range xiface.Methods.List { + if len(field.Names) != 1 { + continue + } + dirs := parseDirectives(field.Doc) + l.addScopeBindingSyntaxErrors(dirs) + if len(dirs.apiScopeBindings) == 0 { + continue + } + fn := l.lookupInterfaceMethod(iface, field.Names[0].Name) + if fn == nil { + continue + } + l.callableHandles[fn] = obj + l.addRawAPIScopeBindings(fn, dirs.apiScopeBindings) + } +} + +// scanGoInterfaceMethodSpecs loads classfile scope-binding directives +// attached to method specs declared by one handle-bearing Go interface type. +func (l *resourceSchemaLoader) scanGoInterfaceMethodSpecs(obj *types.TypeName, kind *ResourceKind, iface *types.Interface, spec *goast.TypeSpec) { + goiface, ok := spec.Type.(*goast.InterfaceType) + if !ok || kind == nil || kind != l.schema.byHandle[obj] || goiface.Methods == nil { + return + } + for _, field := range goiface.Methods.List { + if len(field.Names) != 1 { + continue + } + dirs := parseDirectives((*xast.CommentGroup)(field.Doc)) + l.addScopeBindingSyntaxErrors(dirs) + if len(dirs.apiScopeBindings) == 0 { + continue + } + fn := l.lookupInterfaceMethod(iface, field.Names[0].Name) + if fn == nil { + continue + } + l.callableHandles[fn] = obj + l.addRawAPIScopeBindings(fn, dirs.apiScopeBindings) + } +} + +// scanXGoFuncDecl scans one XGo function or method declaration for classfile +// scope-binding directives. +func (l *resourceSchemaLoader) scanXGoFuncDecl(decl *xast.FuncDecl) { + dirs := parseDirectives(decl.Doc) + l.addScopeBindingSyntaxErrors(dirs) + if len(dirs.apiScopeBindings) == 0 { + return + } + fn := l.lookupXGoFunc(decl) + if fn == nil { + return + } + l.addRawAPIScopeBindings(fn, dirs.apiScopeBindings) +} + +// scanGoFuncDecl scans one Go function or method declaration for classfile +// scope-binding directives. +func (l *resourceSchemaLoader) scanGoFuncDecl(decl *goast.FuncDecl) { + dirs := parseDirectives(decl.Doc) + l.addScopeBindingSyntaxErrors(dirs) + if len(dirs.apiScopeBindings) == 0 { + return + } + fn := l.lookupGoFunc(decl) + if fn == nil { + return + } + l.addRawAPIScopeBindings(fn, dirs.apiScopeBindings) +} + +// addRawAPIScopeBindings records parsed scope bindings before type-driven validation. +func (l *resourceSchemaLoader) addRawAPIScopeBindings(fn *types.Func, dirs []apiScopeBindingDirective) { + seen := make(map[int]rawAPIScopeBinding) + invalid := make(map[int]bool) + for _, dir := range dirs { + if prev, ok := seen[dir.target]; ok { + l.addError(dir.pos, "duplicate %s target param.%d", directiveResourceAPIScopeBinding, dir.target) + if !invalid[dir.target] { + l.addError(prev.pos, "previous binding for target param.%d", dir.target) + } + invalid[dir.target] = true + continue + } + seen[dir.target] = rawAPIScopeBinding(dir) + } + for _, dir := range dirs { + if invalid[dir.target] { + continue + } + raw, ok := seen[dir.target] + if !ok { + continue + } + l.rawAPIScopeBindings[fn] = append(l.rawAPIScopeBindings[fn], raw) + delete(seen, dir.target) + } +} + +// validateKinds validates inter-kind constraints after scanning. +func (l *resourceSchemaLoader) validateKinds() { + for _, kind := range l.schema.Kinds { + if strings.Contains(kind.Name, ".") { + parent := kind.Name[:strings.LastIndexByte(kind.Name, '.')] + if _, ok := l.schema.Kind(parent); !ok { + l.addError(kind.pos, "resource kind %q declares undeclared direct parent kind %q", kind.Name, parent) + } + } + } +} + +// validateAPIScopeBindings validates raw API-position bindings and commits the ones +// with standardized meaning. +func (l *resourceSchemaLoader) validateAPIScopeBindings() { + for fn, raws := range l.rawAPIScopeBindings { + if len(raws) == 0 { + continue + } + sig := fn.Type().(*types.Signature) + var out []ResourceAPIScopeBinding + var valid []rawAPIScopeBinding + for _, raw := range raws { + targetKind, ok := l.kindOfTarget(sig, raw) + if !ok { + l.addError(raw.pos, "invalid resource-api-scope-binding target param.%d", raw.target) + continue + } + if !l.validSource(fn, sig, targetKind, raw) { + if raw.source.Receiver { + l.addError(raw.pos, "invalid resource-api-scope-binding source receiver for target param.%d", raw.target) + } else { + l.addError(raw.pos, "invalid resource-api-scope-binding source param.%d for target param.%d", raw.source.Param, raw.target) + } + continue + } + valid = append(valid, raw) + out = append(out, ResourceAPIScopeBinding{TargetParam: raw.target, Source: raw.source}) + } + if hasAPIScopeBindingCycle(valid) { + l.addError(fn.Pos(), "resource-api-scope-binding on %s induces a cycle", fn.FullName()) + continue + } + if len(out) != 0 { + l.schema.apiScopeBindings[fn] = out + } + } +} + +// kindOfTarget reports the scoped resource kind bound at the target parameter. +func (l *resourceSchemaLoader) kindOfTarget(sig *types.Signature, raw rawAPIScopeBinding) (*ResourceKind, bool) { + params := sig.Params() + if raw.target < 0 || raw.target >= params.Len() { + return nil, false + } + if sig.Variadic() && raw.target == params.Len()-1 { + return nil, false + } + kind, ok := l.schema.CanonicalKindOfType(params.At(raw.target).Type()) + if !ok || !strings.Contains(kind.Name, ".") { + return nil, false + } + return kind, true +} + +// validSource reports whether one scope-binding source is valid for the target +// scoped kind. +func (l *resourceSchemaLoader) validSource(fn *types.Func, sig *types.Signature, targetKind *ResourceKind, raw rawAPIScopeBinding) bool { + parentKind, ok := l.schema.Kind(targetKind.Name[:strings.LastIndexByte(targetKind.Name, '.')]) + if !ok { + return false + } + if raw.source.Receiver { + if recv := sig.Recv(); recv != nil && l.hasParentKind(recv.Type(), parentKind) { + return true + } + handle := l.callableHandles[fn] + return handle != nil && l.hasParentKind(handle.Type(), parentKind) + } + + params := sig.Params() + if raw.source.Param < 0 || raw.source.Param >= params.Len() { + return false + } + if sig.Variadic() && raw.source.Param == params.Len()-1 { + return false + } + return l.hasParentKind(params.At(raw.source.Param).Type(), parentKind) +} + +// lookupInterfaceMethod looks up one explicit method of one interface type. +func (l *resourceSchemaLoader) lookupInterfaceMethod(iface *types.Interface, methodName string) *types.Func { + for i := range iface.NumExplicitMethods() { + fn := iface.ExplicitMethod(i) + if fn.Name() == methodName { + return fn + } + } + return nil +} + +// hasParentKind reports whether typ determines parentKind as either a canonical +// resource type or a handle-bearing type. +func (l *resourceSchemaLoader) hasParentKind(typ types.Type, parentKind *ResourceKind) bool { + if kind, ok := l.schema.CanonicalKindOfType(typ); ok && kind == parentKind { + return true + } + if kind, ok := l.schema.HandleKindOfType(typ); ok && kind == parentKind { + return true + } + return false +} + +// lookupType looks up one top-level type name in the typed package scope. +func (l *resourceSchemaLoader) lookupType(name string) *types.TypeName { + obj, ok := l.types.Scope().Lookup(name).(*types.TypeName) + if !ok { + return nil + } + return obj +} + +// lookupXGoFunc looks up one typed XGo function or method declaration. +func (l *resourceSchemaLoader) lookupXGoFunc(decl *xast.FuncDecl) *types.Func { + if decl.Recv == nil { + obj, ok := l.types.Scope().Lookup(decl.Name.Name).(*types.Func) + if !ok { + return nil + } + return obj + } + name := xgoRecvBaseName(decl.Recv) + if name == "" { + return nil + } + return l.lookupMethod(name, decl.Name.Name) +} + +// lookupGoFunc looks up one typed Go function or method declaration. +func (l *resourceSchemaLoader) lookupGoFunc(decl *goast.FuncDecl) *types.Func { + if decl.Recv == nil { + obj, ok := l.types.Scope().Lookup(decl.Name.Name).(*types.Func) + if !ok { + return nil + } + return obj + } + name := goRecvBaseName(decl.Recv) + if name == "" { + return nil + } + return l.lookupMethod(name, decl.Name.Name) +} + +// lookupMethod looks up one method declared on the named receiver type. +func (l *resourceSchemaLoader) lookupMethod(recvName, methodName string) *types.Func { + obj, ok := l.types.Scope().Lookup(recvName).(*types.TypeName) + if !ok { + return nil + } + named, ok := types.Unalias(obj.Type()).(*types.Named) + if !ok { + return nil + } + for fn := range named.Methods() { + if fn.Name() == methodName { + return fn + } + } + return nil +} + +// ensureKind returns the resource kind record for name, creating it if needed. +func (l *resourceSchemaLoader) ensureKind(name string, pos token.Pos) *ResourceKind { + if ret, ok := l.schema.byKind[name]; ok { + return ret + } + ret := &ResourceKind{Name: name, pos: pos} + l.schema.byKind[name] = ret + l.schema.Kinds = append(l.schema.Kinds, ret) + return ret +} + +// addError records one schema-loading error at pos. +func (l *resourceSchemaLoader) addError(pos token.Pos, format string, args ...any) { + l.errs.Add(l.fset.Position(pos), fmt.Sprintf(format, args...)) +} + +// addTypeDirectiveSyntaxErrors reports malformed type-level classfile directives. +func (l *resourceSchemaLoader) addTypeDirectiveSyntaxErrors(dirs directives) { + for _, dir := range dirs.invalid { + switch dir.arg { + case directiveResource, directiveResourceDiscovery, directiveResourceNameDiscovery: + l.addError(dir.pos, "invalid %s directive syntax", dir.arg) + } + } +} + +// addScopeBindingSyntaxErrors reports malformed scope-binding directives. +func (l *resourceSchemaLoader) addScopeBindingSyntaxErrors(dirs directives) { + for _, dir := range dirs.invalid { + if dir.arg == directiveResourceAPIScopeBinding { + l.addError(dir.pos, "invalid %s directive syntax", directiveResourceAPIScopeBinding) + } + } +} + +// validDQLQuery reports whether arg may be accepted as one standard DQL query +// by this resource schema loader. +func validDQLQuery(pos token.Pos, arg, kind string) bool { + // TODO: Replace this placeholder with shared DQL validation once the + // standardized runtime DQL capability is available to classfile tooling. + return true +} + +// isStringBased reports whether obj is one exported string-based type. +func isStringBased(obj *types.TypeName) bool { + if !obj.Exported() { + return false + } + basic, ok := types.Unalias(obj.Type()).Underlying().(*types.Basic) + return ok && basic.Kind() == types.String +} + +// isHandleBearing reports whether obj is one exported defined interface or +// struct type. +func isHandleBearing(obj *types.TypeName) bool { + if !obj.Exported() || obj.IsAlias() { + return false + } + named, ok := types.Unalias(obj.Type()).(*types.Named) + if !ok { + return false + } + switch named.Underlying().(type) { + case *types.Interface, *types.Struct: + return true + default: + return false + } +} + +// hasAPIScopeBindingCycle reports whether parameter-to-parameter scope +// bindings induce a cycle. +func hasAPIScopeBindingCycle(apiScopeBindings []rawAPIScopeBinding) bool { + next := make(map[int]int) + for _, binding := range apiScopeBindings { + if !binding.source.Receiver { + next[binding.target] = binding.source.Param + } + } + seen := make(map[int]uint8) + var visit func(int) bool + visit = func(v int) bool { + switch seen[v] { + case 1: + return true + case 2: + return false + } + seen[v] = 1 + if to, ok := next[v]; ok && visit(to) { + return true + } + seen[v] = 2 + return false + } + for v := range next { + if visit(v) { + return true + } + } + return false +} diff --git a/cl/outline/classfile/loader_test.go b/cl/outline/classfile/loader_test.go new file mode 100644 index 000000000..94da7cc4c --- /dev/null +++ b/cl/outline/classfile/loader_test.go @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package classfile + +import ( + "go/types" + "testing" + + "github.com/goplus/xgo/token" +) + +func TestResourceSchemaLoaderAddRawAPIScopeBindings(t *testing.T) { + pkg := types.NewPackage("example.com/spx", "spx") + sig := types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(), false) + fn := types.NewFunc(0, pkg, "SetCostume", sig) + l := resourceSchemaLoader{schema: &ResourceSchema{}, fset: token.NewFileSet()} + + l.addRawAPIScopeBindings(fn, []apiScopeBindingDirective{ + {target: 0, source: ResourceAPIScopeSource{Receiver: true}}, + {target: 0, source: ResourceAPIScopeSource{Param: 1}}, + }) + + if got := l.rawAPIScopeBindings[fn]; len(got) != 0 { + t.Fatalf("unexpected raw bindings: %#v", got) + } + if len(l.errs) != 2 { + t.Fatalf("unexpected error count: %d", len(l.errs)) + } +} + +func TestResourceSchemaLoaderValidateKinds(t *testing.T) { + fset := token.NewFileSet() + file := fset.AddFile("spx.go", fset.Base(), 64) + kindPos := file.Pos(12) + l := resourceSchemaLoader{ + fset: fset, + schema: &ResourceSchema{ + Kinds: []*ResourceKind{{Name: "sprite.costume", pos: kindPos}}, + byKind: map[string]*ResourceKind{ + "sprite.costume": {Name: "sprite.costume", pos: kindPos}, + }, + }, + } + + l.validateKinds() + + if len(l.errs) != 1 { + t.Fatalf("unexpected error count: %d", len(l.errs)) + } + if l.errs[0].Pos.Filename != "spx.go" || l.errs[0].Pos.Offset != 12 { + t.Fatalf("unexpected error position: %#v", l.errs[0].Pos) + } +} + +func TestResourceSchemaLoaderKindOfTarget(t *testing.T) { + l, _, costumeKind, spriteName, costumeName, _ := testResourceSchemaLoaderKinds() + + t.Run("ValidScopedCanonical", func(t *testing.T) { + sig := types.NewSignatureType( + nil, + nil, + nil, + types.NewTuple(types.NewVar(0, nil, "costume", costumeName)), + types.NewTuple(), + false, + ) + + got, ok := l.kindOfTarget(sig, rawAPIScopeBinding{target: 0}) + if !ok || got != costumeKind { + t.Fatalf("unexpected target kind: %#v, %v", got, ok) + } + }) + + t.Run("TopLevelCanonical", func(t *testing.T) { + sig := types.NewSignatureType( + nil, + nil, + nil, + types.NewTuple(types.NewVar(0, nil, "sprite", spriteName)), + types.NewTuple(), + false, + ) + + got, ok := l.kindOfTarget(sig, rawAPIScopeBinding{target: 0}) + if ok || got != nil { + t.Fatalf("unexpected target kind: %#v, %v", got, ok) + } + }) + + t.Run("VariadicLastParam", func(t *testing.T) { + sig := types.NewSignatureType( + nil, + nil, + nil, + types.NewTuple( + types.NewVar(0, nil, "costume", costumeName), + types.NewVar(0, nil, "rest", types.NewSlice(types.Typ[types.String])), + ), + types.NewTuple(), + true, + ) + + got, ok := l.kindOfTarget(sig, rawAPIScopeBinding{target: 1}) + if ok || got != nil { + t.Fatalf("unexpected variadic target kind: %#v, %v", got, ok) + } + }) +} + +func TestResourceSchemaLoaderValidSource(t *testing.T) { + l, _, costumeKind, spriteName, costumeName, spriteImpl := testResourceSchemaLoaderKinds() + + t.Run("ReceiverHandleBearing", func(t *testing.T) { + sig := types.NewSignatureType( + types.NewVar(0, nil, "recv", types.NewPointer(spriteImpl)), + nil, + nil, + types.NewTuple(types.NewVar(0, nil, "costume", costumeName)), + types.NewTuple(), + false, + ) + fn := types.NewFunc(0, nil, "SetCostume", sig) + + if !l.validSource(fn, sig, costumeKind, rawAPIScopeBinding{target: 0, source: ResourceAPIScopeSource{Receiver: true}}) { + t.Fatal("expected valid receiver source") + } + }) + + t.Run("CanonicalParam", func(t *testing.T) { + sig := types.NewSignatureType( + nil, + nil, + nil, + types.NewTuple( + types.NewVar(0, nil, "sprite", spriteName), + types.NewVar(0, nil, "costume", costumeName), + ), + types.NewTuple(), + false, + ) + fn := types.NewFunc(0, nil, "SetCostume", sig) + + if !l.validSource(fn, sig, costumeKind, rawAPIScopeBinding{target: 1, source: ResourceAPIScopeSource{Param: 0}}) { + t.Fatal("expected valid canonical param source") + } + }) + + t.Run("WrongKind", func(t *testing.T) { + sig := types.NewSignatureType( + nil, + nil, + nil, + types.NewTuple( + types.NewVar(0, nil, "costume", costumeName), + types.NewVar(0, nil, "sprite", spriteName), + ), + types.NewTuple(), + false, + ) + fn := types.NewFunc(0, nil, "SetCostume", sig) + + if l.validSource(fn, sig, costumeKind, rawAPIScopeBinding{target: 1, source: ResourceAPIScopeSource{Param: 0}}) { + t.Fatal("unexpected valid source") + } + }) + + t.Run("InterfaceReceiverHandleBearing", func(t *testing.T) { + pkg := types.NewPackage("example.com/spx", "spx") + sig := types.NewSignatureType( + nil, + nil, + nil, + types.NewTuple(types.NewVar(0, nil, "costume", costumeName)), + types.NewTuple(), + false, + ) + fn := types.NewFunc(0, pkg, "SetCostume", sig) + iface := types.NewInterfaceType([]*types.Func{fn}, nil) + iface.Complete() + spriteObj := types.NewTypeName(0, pkg, "Sprite", nil) + types.NewNamed(spriteObj, iface, nil) + interfaceMethod := iface.ExplicitMethod(0) + l.schema.byHandle[spriteObj] = l.schema.byKind["sprite"] + l.callableHandles = map[*types.Func]*types.TypeName{interfaceMethod: spriteObj} + + if !l.validSource(interfaceMethod, interfaceMethod.Type().(*types.Signature), costumeKind, rawAPIScopeBinding{target: 0, source: ResourceAPIScopeSource{Receiver: true}}) { + t.Fatal("expected valid interface receiver source") + } + }) +} + +func TestIsStringBased(t *testing.T) { + t.Run("StringAlias", func(t *testing.T) { + obj := types.NewTypeName(0, nil, "SpriteName", types.Typ[types.String]) + if !isStringBased(obj) { + t.Fatal("expected exported string-based type") + } + }) + + t.Run("Unexported", func(t *testing.T) { + obj := types.NewTypeName(0, nil, "spriteName", types.Typ[types.String]) + if isStringBased(obj) { + t.Fatal("unexpected unexported string-based type") + } + }) + + t.Run("NonString", func(t *testing.T) { + obj := types.NewTypeName(0, nil, "SpriteID", types.Typ[types.Int]) + if isStringBased(obj) { + t.Fatal("unexpected non-string type") + } + }) +} + +func TestIsHandleBearing(t *testing.T) { + t.Run("Struct", func(t *testing.T) { + obj := types.NewTypeName(0, nil, "SpriteImpl", nil) + named := types.NewNamed(obj, types.NewStruct(nil, nil), nil) + if !isHandleBearing(obj) { + t.Fatal("expected exported struct handle-bearing type") + } + if named.Obj() != obj { + t.Fatal("expected named type object identity") + } + }) + + t.Run("Interface", func(t *testing.T) { + obj := types.NewTypeName(0, nil, "Sprite", nil) + empty := types.NewInterfaceType(nil, nil) + empty.Complete() + types.NewNamed(obj, empty, nil) + if !isHandleBearing(obj) { + t.Fatal("expected exported interface handle-bearing type") + } + }) + + t.Run("Alias", func(t *testing.T) { + obj := types.NewTypeName(0, nil, "SpriteAlias", nil) + alias := types.NewAlias(obj, types.Typ[types.String]) + if isHandleBearing(alias.Obj()) { + t.Fatal("unexpected alias handle-bearing type") + } + }) +} + +func TestHasAPIScopeBindingCycle(t *testing.T) { + t.Run("Cyclic", func(t *testing.T) { + if !hasAPIScopeBindingCycle([]rawAPIScopeBinding{ + {target: 0, source: ResourceAPIScopeSource{Param: 1}}, + {target: 1, source: ResourceAPIScopeSource{Param: 0}}, + }) { + t.Fatal("expected cycle") + } + }) + + t.Run("Acyclic", func(t *testing.T) { + if hasAPIScopeBindingCycle([]rawAPIScopeBinding{ + {target: 0, source: ResourceAPIScopeSource{Receiver: true}}, + {target: 1, source: ResourceAPIScopeSource{Param: 0}}, + }) { + t.Fatal("unexpected cycle") + } + }) +} + +func testResourceSchemaLoaderKinds() (resourceSchemaLoader, *ResourceKind, *ResourceKind, *types.Alias, *types.Alias, *types.Named) { + pkg := types.NewPackage("example.com/spx", "spx") + + spriteNameObj := types.NewTypeName(0, pkg, "SpriteName", nil) + spriteName := types.NewAlias(spriteNameObj, types.Typ[types.String]) + + costumeNameObj := types.NewTypeName(0, pkg, "SpriteCostumeName", nil) + costumeName := types.NewAlias(costumeNameObj, types.Typ[types.String]) + + spriteImplObj := types.NewTypeName(0, pkg, "SpriteImpl", nil) + spriteImpl := types.NewNamed(spriteImplObj, types.NewStruct(nil, nil), nil) + + spriteKind := &ResourceKind{ + Name: "sprite", + CanonicalType: spriteNameObj, + HandleTypes: []*types.TypeName{spriteImplObj}, + } + costumeKind := &ResourceKind{ + Name: "sprite.costume", + CanonicalType: costumeNameObj, + } + + schema := &ResourceSchema{ + Package: pkg, + Kinds: []*ResourceKind{spriteKind, costumeKind}, + byKind: map[string]*ResourceKind{ + "sprite": spriteKind, + "sprite.costume": costumeKind, + }, + byCanonical: map[*types.TypeName]*ResourceKind{ + spriteNameObj: spriteKind, + costumeNameObj: costumeKind, + }, + byHandle: map[*types.TypeName]*ResourceKind{ + spriteImplObj: spriteKind, + }, + } + + return resourceSchemaLoader{fset: token.NewFileSet(), schema: schema}, spriteKind, costumeKind, spriteName, costumeName, spriteImpl +} diff --git a/cl/outline/classfile/parse.go b/cl/outline/classfile/parse.go new file mode 100644 index 000000000..0cfd3c28c --- /dev/null +++ b/cl/outline/classfile/parse.go @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package classfile + +import ( + goast "go/ast" + "strconv" + "strings" + + xast "github.com/goplus/xgo/ast" + "github.com/goplus/xgo/token" +) + +const directivePrefix = "//xgo:class:" + +const ( + directiveResource = "resource" + directiveResourceDiscovery = "resource-discovery" + directiveResourceNameDiscovery = "resource-name-discovery" + directiveResourceAPIScopeBinding = "resource-api-scope-binding" +) + +// directives groups parsed classfile directives attached to one declaration. +type directives struct { + resource []directive + discovery []directive + nameDiscovery []directive + apiScopeBindings []apiScopeBindingDirective + invalid []directive +} + +// directive is one parsed directive with its source position and argument. +type directive struct { + pos token.Pos + arg string +} + +// apiScopeBindingDirective is one parsed resource-api-scope-binding directive. +type apiScopeBindingDirective struct { + pos token.Pos + target int + source ResourceAPIScopeSource +} + +// parseDirectives parses classfile directives from one comment group. +func parseDirectives(doc *xast.CommentGroup) directives { + var ret directives + if doc == nil { + return ret + } + for _, comment := range doc.List { + if !strings.HasPrefix(comment.Text, directivePrefix) { + continue + } + body := strings.TrimSpace(strings.TrimPrefix(comment.Text, directivePrefix)) + switch { + case strings.HasPrefix(body, directiveResourceDiscovery+" "): + ret.discovery = append(ret.discovery, directive{ + pos: token.Pos(comment.Pos()), + arg: strings.TrimSpace(strings.TrimPrefix(body, directiveResourceDiscovery+" ")), + }) + case strings.HasPrefix(body, directiveResourceNameDiscovery+" "): + ret.nameDiscovery = append(ret.nameDiscovery, directive{ + pos: token.Pos(comment.Pos()), + arg: strings.TrimSpace(strings.TrimPrefix(body, directiveResourceNameDiscovery+" ")), + }) + case strings.HasPrefix(body, directiveResourceAPIScopeBinding+" "): + target, source, ok := parseScopeBinding( + strings.TrimSpace(strings.TrimPrefix(body, directiveResourceAPIScopeBinding+" ")), + ) + if ok { + ret.apiScopeBindings = append(ret.apiScopeBindings, apiScopeBindingDirective{ + pos: token.Pos(comment.Pos()), + target: target, + source: source, + }) + continue + } + ret.invalid = append(ret.invalid, directive{ + pos: token.Pos(comment.Pos()), + arg: directiveResourceAPIScopeBinding, + }) + case strings.HasPrefix(body, directiveResource+" "): + ret.resource = append(ret.resource, directive{ + pos: token.Pos(comment.Pos()), + arg: strings.TrimSpace(strings.TrimPrefix(body, directiveResource+" ")), + }) + case body == directiveResource, + body == directiveResourceDiscovery, + body == directiveResourceNameDiscovery, + body == directiveResourceAPIScopeBinding: + ret.invalid = append(ret.invalid, directive{pos: token.Pos(comment.Pos()), arg: body}) + } + } + return ret +} + +// parseResourceKind validates one canonical resource kind spelling. +func parseResourceKind(kind string) (string, bool) { + if kind == "" { + return "", false + } + for seg := range strings.SplitSeq(kind, ".") { + if seg == "" || seg[0] < 'a' || seg[0] > 'z' { + return "", false + } + for i := 1; i < len(seg); i++ { + ch := seg[i] + if ch == '_' || ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'z' { + continue + } + return "", false + } + } + return kind, true +} + +// parseScopeBinding parses one target-source API-position binding directive. +func parseScopeBinding(arg string) (int, ResourceAPIScopeSource, bool) { + const receiver = "receiver" + + fields := strings.Fields(arg) + if len(fields) != 2 { + return 0, ResourceAPIScopeSource{}, false + } + target, ok := parseParam(fields[0]) + if !ok { + return 0, ResourceAPIScopeSource{}, false + } + if fields[1] == receiver { + return target, ResourceAPIScopeSource{Receiver: true}, true + } + sourceParam, ok := parseParam(fields[1]) + if !ok { + return 0, ResourceAPIScopeSource{}, false + } + return target, ResourceAPIScopeSource{Param: sourceParam}, true +} + +// parseParam parses one param.N API-position operand. +func parseParam(v string) (int, bool) { + const paramPrefix = "param." + + if !strings.HasPrefix(v, paramPrefix) { + return 0, false + } + n := strings.TrimPrefix(v, paramPrefix) + if n == "" || len(n) > 1 && n[0] == '0' { + return 0, false + } + ret, err := strconv.Atoi(n) + if err != nil || ret < 0 { + return 0, false + } + return ret, true +} + +// xgoDeclDoc reports the effective doc group for one XGo type declaration. +func xgoDeclDoc(decl *xast.GenDecl, spec *xast.TypeSpec) *xast.CommentGroup { + if !decl.Lparen.IsValid() && decl.Doc != nil { + return decl.Doc + } + return spec.Doc +} + +// goDeclDoc reports the effective doc group for one Go type declaration. +func goDeclDoc(decl *goast.GenDecl, spec *goast.TypeSpec) *xast.CommentGroup { + if !decl.Lparen.IsValid() && decl.Doc != nil { + return (*xast.CommentGroup)(decl.Doc) + } + return (*xast.CommentGroup)(spec.Doc) +} + +// xgoRecvBaseName reports the receiver base type name for one XGo method. +func xgoRecvBaseName(recv *xast.FieldList) string { + if recv == nil || len(recv.List) == 0 { + return "" + } + return xgoExprBaseName(recv.List[0].Type) +} + +// xgoExprBaseName reports the base identifier of one XGo receiver type expression. +func xgoExprBaseName(expr xast.Expr) string { + switch v := expr.(type) { + case *xast.Ident: + return v.Name + case *xast.StarExpr: + return xgoExprBaseName(v.X) + case *xast.ParenExpr: + return xgoExprBaseName(v.X) + default: + return "" + } +} + +// goRecvBaseName reports the receiver base type name for one Go method. +func goRecvBaseName(recv *goast.FieldList) string { + if recv == nil || len(recv.List) == 0 { + return "" + } + return goExprBaseName(recv.List[0].Type) +} + +// goExprBaseName reports the base identifier of one Go receiver type expression. +func goExprBaseName(expr goast.Expr) string { + switch v := expr.(type) { + case *goast.Ident: + return v.Name + case *goast.StarExpr: + return goExprBaseName(v.X) + case *goast.ParenExpr: + return goExprBaseName(v.X) + default: + return "" + } +} diff --git a/cl/outline/classfile/parse_test.go b/cl/outline/classfile/parse_test.go new file mode 100644 index 000000000..f22acb3a7 --- /dev/null +++ b/cl/outline/classfile/parse_test.go @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package classfile + +import ( + goast "go/ast" + "testing" + + xast "github.com/goplus/xgo/ast" + "github.com/goplus/xgo/token" +) + +func TestParseDirectives(t *testing.T) { + t.Run("ResourceDirectives", func(t *testing.T) { + doc := &xast.CommentGroup{ + List: []*xast.Comment{ + {Text: "//xgo:class:resource sprite"}, + {Text: "//xgo:class:resource-discovery sprites.*"}, + {Text: "//xgo:class:resource-name-discovery id"}, + }, + } + + dirs := parseDirectives(doc) + if len(dirs.resource) != 1 || dirs.resource[0].arg != "sprite" { + t.Fatalf("unexpected resource directives: %#v", dirs.resource) + } + if len(dirs.discovery) != 1 || dirs.discovery[0].arg != "sprites.*" { + t.Fatalf("unexpected discovery directives: %#v", dirs.discovery) + } + if len(dirs.nameDiscovery) != 1 || dirs.nameDiscovery[0].arg != "id" { + t.Fatalf("unexpected name-discovery directives: %#v", dirs.nameDiscovery) + } + }) + + t.Run("InvalidDirectiveSyntax", func(t *testing.T) { + doc := &xast.CommentGroup{ + List: []*xast.Comment{ + {Text: "//xgo:class:resource"}, + {Text: "//xgo:class:resource-api-scope-binding param.x receiver"}, + }, + } + + dirs := parseDirectives(doc) + if len(dirs.invalid) != 2 { + t.Fatalf("unexpected invalid directives: %#v", dirs.invalid) + } + }) +} + +func TestParseResourceKind(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + ret, ok := parseResourceKind("sprite.costume_frame2") + if !ok || ret != "sprite.costume_frame2" { + t.Fatalf("unexpected parse result: %q, %v", ret, ok) + } + }) + + t.Run("Invalid", func(t *testing.T) { + for _, kind := range []string{"", "Sprite", "sprite..costume", "sprite.Costume"} { + if _, ok := parseResourceKind(kind); ok { + t.Fatalf("expected invalid kind: %q", kind) + } + } + }) +} + +func TestParseScopeBinding(t *testing.T) { + t.Run("ReceiverSource", func(t *testing.T) { + target, source, ok := parseScopeBinding("param.0 receiver") + if !ok || target != 0 || !source.Receiver || source.Param != 0 { + t.Fatalf("unexpected scope binding: %d, %#v, %v", target, source, ok) + } + }) + + t.Run("ParamSource", func(t *testing.T) { + target, source, ok := parseScopeBinding("param.1 param.0") + if !ok || target != 1 || source.Receiver || source.Param != 0 { + t.Fatalf("unexpected scope binding: %d, %#v, %v", target, source, ok) + } + }) + + t.Run("Invalid", func(t *testing.T) { + for _, arg := range []string{"receiver param.0", "param.x receiver", "param.0"} { + if _, _, ok := parseScopeBinding(arg); ok { + t.Fatalf("expected invalid scope binding: %q", arg) + } + } + }) +} + +func TestParseParam(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + ret, ok := parseParam("param.12") + if !ok || ret != 12 { + t.Fatalf("unexpected param parse result: %d, %v", ret, ok) + } + }) + + t.Run("Invalid", func(t *testing.T) { + for _, v := range []string{"param.", "param.01", "param.-1", "arg.0"} { + if _, ok := parseParam(v); ok { + t.Fatalf("expected invalid param: %q", v) + } + } + }) +} + +func TestDeclDoc(t *testing.T) { + t.Run("XGoDeclDoc", func(t *testing.T) { + declDoc := &xast.CommentGroup{List: []*xast.Comment{{Text: "// decl", Slash: token.Pos(1)}}} + specDoc := &xast.CommentGroup{List: []*xast.Comment{{Text: "// spec", Slash: token.Pos(2)}}} + decl := &xast.GenDecl{Doc: declDoc} + spec := &xast.TypeSpec{Doc: specDoc} + if got := xgoDeclDoc(decl, spec); got != declDoc { + t.Fatal("expected declaration doc to take precedence") + } + }) + + t.Run("GoDeclDoc", func(t *testing.T) { + declDoc := &goast.CommentGroup{List: []*goast.Comment{{Text: "// decl", Slash: 1}}} + specDoc := &goast.CommentGroup{List: []*goast.Comment{{Text: "// spec", Slash: 2}}} + decl := &goast.GenDecl{Doc: declDoc} + spec := &goast.TypeSpec{Doc: specDoc} + if got := goDeclDoc(decl, spec); got != (*xast.CommentGroup)(declDoc) { + t.Fatal("expected declaration doc to take precedence") + } + }) +} + +func TestRecvBaseName(t *testing.T) { + t.Run("XGo", func(t *testing.T) { + recv := &xast.FieldList{List: []*xast.Field{{ + Type: &xast.StarExpr{X: &xast.ParenExpr{X: &xast.Ident{Name: "SpriteImpl"}}}, + }}} + if got := xgoRecvBaseName(recv); got != "SpriteImpl" { + t.Fatalf("unexpected XGo receiver base name: %q", got) + } + }) + + t.Run("Go", func(t *testing.T) { + recv := &goast.FieldList{List: []*goast.Field{{ + Type: &goast.StarExpr{X: &goast.ParenExpr{X: &goast.Ident{Name: "SpriteImpl"}}}, + }}} + if got := goRecvBaseName(recv); got != "SpriteImpl" { + t.Fatalf("unexpected Go receiver base name: %q", got) + } + }) +} diff --git a/cl/outline/classfile/schema.go b/cl/outline/classfile/schema.go new file mode 100644 index 000000000..123dfb6d9 --- /dev/null +++ b/cl/outline/classfile/schema.go @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package classfile loads classfile-specific resource schema from one framework +// package. +package classfile + +import ( + "fmt" + "go/types" + "slices" + + "github.com/goplus/mod/modfile" + xast "github.com/goplus/xgo/ast" + "github.com/goplus/xgo/cl/outline" + "github.com/goplus/xgo/token" +) + +// ResourceSchema is the classfile resource schema loaded from one framework package. +type ResourceSchema struct { + Package *types.Package + Kinds []*ResourceKind + + byKind map[string]*ResourceKind + byCanonical map[*types.TypeName]*ResourceKind + byHandle map[*types.TypeName]*ResourceKind + apiScopeBindings map[*types.Func][]ResourceAPIScopeBinding +} + +// LoadResourceSchema loads one classfile resource schema from the framework +// package named by the first package path of proj. +func LoadResourceSchema(pkg *xast.Package, proj *modfile.Project, conf *outline.Config) (*ResourceSchema, error) { + if len(proj.PkgPaths) == 0 { + return nil, fmt.Errorf("project has no framework package path") + } + pkgPath := proj.PkgPaths[0] + + out, err := outline.NewPackage(pkgPath, pkg, conf) + if err != nil { + return nil, err + } + + loader := resourceSchemaLoader{ + fset: conf.Fset, + pkg: pkg, + types: out.Pkg(), + schema: &ResourceSchema{ + Package: out.Pkg(), + byKind: make(map[string]*ResourceKind), + byCanonical: make(map[*types.TypeName]*ResourceKind), + byHandle: make(map[*types.TypeName]*ResourceKind), + apiScopeBindings: make(map[*types.Func][]ResourceAPIScopeBinding), + }, + } + loader.scanPackage() + loader.validateKinds() + loader.validateAPIScopeBindings() + if len(loader.errs) != 0 { + return nil, loader.errs + } + return loader.schema, nil +} + +// Kind reports the resource kind by its canonical spelling. +func (s *ResourceSchema) Kind(name string) (*ResourceKind, bool) { + ret, ok := s.byKind[name] + return ret, ok +} + +// KindOfCanonical reports the resource kind declared by one canonical resource +// reference type declaration. +func (s *ResourceSchema) KindOfCanonical(obj *types.TypeName) (*ResourceKind, bool) { + ret, ok := s.byCanonical[obj] + return ret, ok +} + +// CanonicalKindOfType reports the canonical resource kind determined by typ by +// following alias declarations only. +func (s *ResourceSchema) CanonicalKindOfType(typ types.Type) (*ResourceKind, bool) { + for typ != nil { + switch t := typ.(type) { + case *types.Named: + return s.KindOfCanonical(t.Obj()) + case *types.Alias: + if kind, ok := s.KindOfCanonical(t.Obj()); ok { + return kind, true + } + typ = t.Rhs() + default: + return nil, false + } + } + return nil, false +} + +// KindOfHandle reports the resource kind declared by one handle-bearing type declaration. +func (s *ResourceSchema) KindOfHandle(obj *types.TypeName) (*ResourceKind, bool) { + ret, ok := s.byHandle[obj] + return ret, ok +} + +// HandleKindOfType reports the handle-bearing resource kind determined by typ. +func (s *ResourceSchema) HandleKindOfType(typ types.Type) (*ResourceKind, bool) { + for { + ptr, ok := typ.(*types.Pointer) + if !ok { + break + } + typ = ptr.Elem() + } + named, ok := typ.(*types.Named) + if !ok { + return nil, false + } + return s.KindOfHandle(named.Obj()) +} + +// APIScopeBindings reports the standardized API-position scope bindings +// declared on fn. +func (s *ResourceSchema) APIScopeBindings(fn *types.Func) []ResourceAPIScopeBinding { + ret := s.apiScopeBindings[fn] + if len(ret) == 0 { + return nil + } + return slices.Clone(ret) +} + +// ResourceKind is one resource kind declared in framework source. +type ResourceKind struct { + Name string + CanonicalType *types.TypeName + HandleTypes []*types.TypeName + DiscoveryQuery string + NameDiscoveryQuery string + + pos token.Pos +} + +// ResourceAPIScopeBinding is one standardized resource-api-scope-binding. +type ResourceAPIScopeBinding struct { + TargetParam int + Source ResourceAPIScopeSource +} + +// ResourceAPIScopeSource is one direct scope source of one API-position binding. +type ResourceAPIScopeSource struct { + Receiver bool + Param int +} diff --git a/cl/outline/classfile/schema_test.go b/cl/outline/classfile/schema_test.go new file mode 100644 index 000000000..0a6dda8d3 --- /dev/null +++ b/cl/outline/classfile/schema_test.go @@ -0,0 +1,558 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package classfile + +import ( + "go/types" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/goplus/mod/modfile" + xast "github.com/goplus/xgo/ast" + "github.com/goplus/xgo/cl/outline" + "github.com/goplus/xgo/parser" + "github.com/goplus/xgo/scanner" + "github.com/goplus/xgo/token" +) + +func TestLoadResourceSchema(t *testing.T) { + t.Run("ValidGoPackage", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +// SpriteName identifies a sprite by name. +// +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +type SpriteAlias = SpriteName + +// SpriteCostumeName identifies a sprite costume by name. +// +//xgo:class:resource sprite.costume +//xgo:class:resource-discovery costumes.* +type SpriteCostumeName = string + +// SpriteCostumeFrameName identifies one sprite costume frame by name. +// +//xgo:class:resource sprite.costume.frame +//xgo:class:resource-discovery frames.* +type SpriteCostumeFrameName = string + +// WidgetName identifies a widget by name. +// +//xgo:class:resource widget +//xgo:class:resource-discovery widgets.* +//xgo:class:resource-name-discovery id +type WidgetName = string + +// SpriteImpl is a handle-bearing type of the sprite resource kind. +// +//xgo:class:resource sprite +type SpriteImpl struct{} + +// SetCostume__0 sets the current costume. +// +//xgo:class:resource-api-scope-binding param.0 receiver +func (s *SpriteImpl) SetCostume__0(costume SpriteCostumeName) {} + +// SetCostumeAndFrame sets the current costume and frame. +// +//xgo:class:resource-api-scope-binding param.0 receiver +//xgo:class:resource-api-scope-binding param.1 param.0 +func (s *SpriteImpl) SetCostumeAndFrame(costume SpriteCostumeName, frame SpriteCostumeFrameName) {} +`, + }) + + schema, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err != nil { + t.Fatalf("LoadResourceSchema failed: %v", err) + } + + sprite, ok := schema.Kind("sprite") + if !ok { + t.Fatal("sprite kind not found") + } + if sprite.CanonicalType == nil || sprite.CanonicalType.Name() != "SpriteName" { + t.Fatalf("unexpected sprite canonical type: %#v", sprite.CanonicalType) + } + if sprite.DiscoveryQuery != "sprites.*" { + t.Fatalf("unexpected sprite discovery query: %q", sprite.DiscoveryQuery) + } + if len(sprite.HandleTypes) != 1 || sprite.HandleTypes[0].Name() != "SpriteImpl" { + t.Fatalf("unexpected sprite handle types: %#v", sprite.HandleTypes) + } + + widget, ok := schema.Kind("widget") + if !ok { + t.Fatal("widget kind not found") + } + if widget.NameDiscoveryQuery != "id" { + t.Fatalf("unexpected widget name-discovery query: %q", widget.NameDiscoveryQuery) + } + + if got, ok := schema.KindOfCanonical(sprite.CanonicalType); !ok || got != sprite { + t.Fatal("KindOfCanonical did not resolve sprite canonical type") + } + spriteAlias, ok := schema.Package.Scope().Lookup("SpriteAlias").(*types.TypeName) + if !ok { + t.Fatal("SpriteAlias type not found") + } + if got, ok := schema.CanonicalKindOfType(spriteAlias.Type()); !ok || got != sprite { + t.Fatal("CanonicalKindOfType did not follow alias chain to sprite") + } + if got, ok := schema.KindOfHandle(sprite.HandleTypes[0]); !ok || got != sprite { + t.Fatal("KindOfHandle did not resolve sprite handle type") + } + + handleNamed, ok := types.Unalias(sprite.HandleTypes[0].Type()).(*types.Named) + if !ok { + t.Fatalf("unexpected handle type: %T", sprite.HandleTypes[0].Type()) + } + if got, ok := schema.HandleKindOfType(types.NewPointer(handleNamed)); !ok || got != sprite { + t.Fatal("HandleKindOfType did not resolve sprite handle type") + } + + setCostume := lookupMethod(t, handleNamed, "SetCostume__0") + apiScopeBindings := schema.APIScopeBindings(setCostume) + if len(apiScopeBindings) != 1 || apiScopeBindings[0].TargetParam != 0 || !apiScopeBindings[0].Source.Receiver { + t.Fatalf("unexpected bindings for SetCostume__0: %#v", apiScopeBindings) + } + + setCostumeAndFrame := lookupMethod(t, handleNamed, "SetCostumeAndFrame") + apiScopeBindings = schema.APIScopeBindings(setCostumeAndFrame) + if len(apiScopeBindings) != 2 { + t.Fatalf("unexpected binding count for SetCostumeAndFrame: %#v", apiScopeBindings) + } + if apiScopeBindings[0].TargetParam != 0 || !apiScopeBindings[0].Source.Receiver { + t.Fatalf("unexpected first binding for SetCostumeAndFrame: %#v", apiScopeBindings[0]) + } + if apiScopeBindings[1].TargetParam != 1 || apiScopeBindings[1].Source.Receiver || apiScopeBindings[1].Source.Param != 0 { + t.Fatalf("unexpected second binding for SetCostumeAndFrame: %#v", apiScopeBindings[1]) + } + }) + + t.Run("ValidGoInterfaceMethodSpec", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +// SpriteName identifies a sprite by name. +// +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +// SpriteCostumeName identifies a sprite costume by name. +// +//xgo:class:resource sprite.costume +//xgo:class:resource-discovery costumes.* +type SpriteCostumeName = string + +// Sprite is a handle-bearing type of the sprite resource kind. +// +//xgo:class:resource sprite +type Sprite interface { + //xgo:class:resource-api-scope-binding param.0 receiver + SetCostume__0(costume SpriteCostumeName) +} +`, + }) + + schema, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err != nil { + t.Fatalf("LoadResourceSchema failed: %v", err) + } + + sprite, ok := schema.Kind("sprite") + if !ok { + t.Fatal("sprite kind not found") + } + if len(sprite.HandleTypes) != 1 || sprite.HandleTypes[0].Name() != "Sprite" { + t.Fatalf("unexpected sprite handle types: %#v", sprite.HandleTypes) + } + + handleNamed, ok := types.Unalias(sprite.HandleTypes[0].Type()).(*types.Named) + if !ok { + t.Fatalf("unexpected handle type: %T", sprite.HandleTypes[0].Type()) + } + iface, ok := handleNamed.Underlying().(*types.Interface) + if !ok { + t.Fatalf("unexpected handle underlying type: %T", handleNamed.Underlying()) + } + if iface.NumExplicitMethods() != 1 { + t.Fatalf("unexpected explicit method count: %d", iface.NumExplicitMethods()) + } + apiScopeBindings := schema.APIScopeBindings(iface.ExplicitMethod(0)) + if len(apiScopeBindings) != 1 || apiScopeBindings[0].TargetParam != 0 || !apiScopeBindings[0].Source.Receiver { + t.Fatalf("unexpected bindings for Sprite.SetCostume__0: %#v", apiScopeBindings) + } + }) + + t.Run("ValidXGoPackage", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.xgo": `package spx + +// SpriteName identifies a sprite by name. +// +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +// SpriteCostumeName identifies a sprite costume by name. +// +//xgo:class:resource sprite.costume +//xgo:class:resource-discovery costumes.* +type SpriteCostumeName = string + +// SpriteImpl is a handle-bearing type of the sprite resource kind. +// +//xgo:class:resource sprite +type SpriteImpl struct{} + +// SetCostume__0 sets the current costume. +// +//xgo:class:resource-api-scope-binding param.0 receiver +func (s *SpriteImpl) SetCostume__0(costume SpriteCostumeName) {} +`, + }) + + schema, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err != nil { + t.Fatalf("LoadResourceSchema failed: %v", err) + } + + sprite, ok := schema.Kind("sprite") + if !ok { + t.Fatal("sprite kind not found") + } + if sprite.CanonicalType == nil || sprite.CanonicalType.Name() != "SpriteName" { + t.Fatalf("unexpected sprite canonical type: %#v", sprite.CanonicalType) + } + if sprite.DiscoveryQuery != "sprites.*" { + t.Fatalf("unexpected sprite discovery query: %q", sprite.DiscoveryQuery) + } + if len(sprite.HandleTypes) != 1 || sprite.HandleTypes[0].Name() != "SpriteImpl" { + t.Fatalf("unexpected sprite handle types: %#v", sprite.HandleTypes) + } + + handleNamed, ok := types.Unalias(sprite.HandleTypes[0].Type()).(*types.Named) + if !ok { + t.Fatalf("unexpected handle type: %T", sprite.HandleTypes[0].Type()) + } + setCostume := lookupMethod(t, handleNamed, "SetCostume__0") + apiScopeBindings := schema.APIScopeBindings(setCostume) + if len(apiScopeBindings) != 1 || apiScopeBindings[0].TargetParam != 0 || !apiScopeBindings[0].Source.Receiver { + t.Fatalf("unexpected bindings for SetCostume__0: %#v", apiScopeBindings) + } + }) + + t.Run("GroupedTypeSpec", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +type ( + // SpriteName identifies a sprite by name. + // + //xgo:class:resource sprite + //xgo:class:resource-discovery sprites.* + SpriteName = string + + // SoundName identifies a sound by name. + // + //xgo:class:resource sound + //xgo:class:resource-discovery sounds.* + SoundName = string +) +`, + }) + + schema, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err != nil { + t.Fatalf("LoadResourceSchema failed: %v", err) + } + + sprite, ok := schema.Kind("sprite") + if !ok { + t.Fatal("sprite kind not found") + } + if sprite.CanonicalType == nil || sprite.CanonicalType.Name() != "SpriteName" { + t.Fatalf("unexpected sprite canonical type: %#v", sprite.CanonicalType) + } + if sprite.DiscoveryQuery != "sprites.*" { + t.Fatalf("unexpected sprite discovery query: %q", sprite.DiscoveryQuery) + } + + sound, ok := schema.Kind("sound") + if !ok { + t.Fatal("sound kind not found") + } + if sound.CanonicalType == nil || sound.CanonicalType.Name() != "SoundName" { + t.Fatalf("unexpected sound canonical type: %#v", sound.CanonicalType) + } + if sound.DiscoveryQuery != "sounds.*" { + t.Fatalf("unexpected sound discovery query: %q", sound.DiscoveryQuery) + } + }) + + t.Run("GroupedDeclarationDocIsIgnored", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type ( + SpriteName = string + SoundName = string +) +`, + }) + + schema, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err != nil { + t.Fatalf("LoadResourceSchema failed: %v", err) + } + if len(schema.Kinds) != 0 { + t.Fatalf("unexpected kinds from grouped declaration doc: %#v", schema.Kinds) + } + }) + + t.Run("DuplicateCanonicalType", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName2 = string +`, + }) + + _, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err == nil || !strings.Contains(err.Error(), `resource kind "sprite" has more than one canonical type`) { + t.Fatalf("expected duplicate canonical type error, got %v", err) + } + if !strings.Contains(err.Error(), "previous at") { + t.Fatalf("expected duplicate canonical type error to include previous position, got %v", err) + } + }) + + t.Run("ScopedHandleBearingType", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource sprite.costume +type SpriteCostumeImpl struct{} +`, + }) + + _, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err == nil || !strings.Contains(err.Error(), "must declare one top-level resource kind") { + t.Fatalf("expected scoped handle-bearing type error, got %v", err) + } + }) + + t.Run("InvalidBindingsDoNotTriggerCycle", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +//xgo:class:resource sprite.costume +//xgo:class:resource-discovery costumes.* +type SpriteCostumeName = string + +//xgo:class:resource sprite +type SpriteImpl struct{} + +//xgo:class:resource-api-scope-binding param.0 param.1 +//xgo:class:resource-api-scope-binding param.1 param.0 +func (s *SpriteImpl) SetCostume(costume SpriteCostumeName, unrelated string) {} +`, + }) + + _, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err == nil { + t.Fatal("expected invalid binding error") + } + if strings.Contains(err.Error(), "induces a cycle") { + t.Fatalf("unexpected cycle error for invalid bindings: %v", err) + } + if !strings.Contains(err.Error(), "invalid resource-api-scope-binding source param.1 for target param.0") { + t.Fatalf("expected invalid binding source error, got %v", err) + } + }) + + t.Run("DuplicateBindingTargetIsRejectedAsWhole", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +//xgo:class:resource sprite.costume +//xgo:class:resource-discovery costumes.* +type SpriteCostumeName = string + +//xgo:class:resource sprite +type SpriteImpl struct{} + +//xgo:class:resource-api-scope-binding param.0 receiver +//xgo:class:resource-api-scope-binding param.0 receiver +func (s *SpriteImpl) SetCostume(costume SpriteCostumeName) {} +`, + }) + + _, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err == nil { + t.Fatal("expected duplicate binding target error") + } + if !strings.Contains(err.Error(), "duplicate resource-api-scope-binding target param.0") { + t.Fatalf("expected duplicate target error, got %v", err) + } + }) + + t.Run("InvalidDirectiveSyntax", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource sprite +//xgo:class:resource-discovery +type SpriteName = string + +//xgo:class:resource sprite +type SpriteImpl struct{} + +//xgo:class:resource-api-scope-binding param.x receiver +func (s *SpriteImpl) SetCostume__0(costume SpriteName) {} +`, + }) + + _, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err == nil { + t.Fatal("expected invalid directive syntax error") + } + errs, ok := err.(scanner.ErrorList) + if !ok { + t.Fatalf("expected scanner.ErrorList, got %T", err) + } + var foundDiscovery, foundBinding bool + for _, scanErr := range errs { + switch { + case strings.Contains(scanErr.Msg, "invalid resource-discovery directive syntax"): + foundDiscovery = true + case strings.Contains(scanErr.Msg, "invalid resource-api-scope-binding directive syntax"): + foundBinding = true + } + } + if !foundDiscovery { + t.Fatalf("expected invalid resource-discovery syntax error, got %v", err) + } + if !foundBinding { + t.Fatalf("expected invalid scope-binding syntax error, got %v", err) + } + }) + + t.Run("NameDiscoveryWithoutDiscoveryIsIgnored", func(t *testing.T) { + pkgPath := "example.com/spx" + pkg, conf := loadTestPackage(t, map[string]string{ + "spx.go": `package spx + +//xgo:class:resource widget +//xgo:class:resource-name-discovery id +//xgo:class:resource-name-discovery name +type WidgetName = string +`, + }) + + schema, err := LoadResourceSchema(pkg, &modfile.Project{PkgPaths: []string{pkgPath}}, conf) + if err != nil { + t.Fatalf("expected name-discovery directives without discovery to be ignored, got %v", err) + } + widget, ok := schema.Kind("widget") + if !ok { + t.Fatal("widget kind not found") + } + if widget.NameDiscoveryQuery != "" { + t.Fatalf("unexpected widget name-discovery query: %q", widget.NameDiscoveryQuery) + } + }) +} + +// loadTestPackage parses one temporary package and returns its XGo package +// representation together with one outline config. +func loadTestPackage(t *testing.T, files map[string]string) (*xast.Package, *outline.Config) { + t.Helper() + + dir := t.TempDir() + for name, content := range files { + path := filepath.Join(dir, name) + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%q) failed: %v", path, err) + } + } + + fset := token.NewFileSet() + pkgs, err := parser.ParseDirEx(fset, dir, parser.Config{ + Mode: parser.ParseComments, + }) + if err != nil { + t.Fatalf("ParseDirEx(%q) failed: %v", dir, err) + } + if len(pkgs) != 1 { + t.Fatalf("expected exactly one package, got %d", len(pkgs)) + } + for _, pkg := range pkgs { + return pkg, &outline.Config{Fset: fset} + } + t.Fatal("no package returned") + return nil, nil +} + +// lookupMethod looks up one method of the named handle-bearing type. +func lookupMethod(t *testing.T, named *types.Named, name string) *types.Func { + t.Helper() + + methods := types.NewMethodSet(types.NewPointer(named)) + sel := methods.Lookup(named.Obj().Pkg(), name) + if sel == nil { + t.Fatalf("method %s not found on %s", name, named.Obj().Name()) + } + fn, ok := sel.Obj().(*types.Func) + if !ok { + t.Fatalf("selection %s is not one method", name) + } + return fn +} diff --git a/cl/outline/outline_test.go b/cl/outline/outline_test.go new file mode 100644 index 000000000..36a49747a --- /dev/null +++ b/cl/outline/outline_test.go @@ -0,0 +1,100 @@ +package outline + +import ( + "os" + "path/filepath" + "testing" + + "github.com/goplus/xgo/cl" + "github.com/goplus/xgo/parser" + "github.com/goplus/xgo/token" +) + +func TestNewPackage(t *testing.T) { + t.Run("GoConstExpressions", func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "flags.go") + err := os.WriteFile(path, []byte(`package flags + +type dbgFlags int + +const ( + DbgFlagLoad dbgFlags = 1 << iota + DbgFlagInstr + DbgFlagAll = DbgFlagLoad | DbgFlagInstr +) +`), 0o644) + if err != nil { + t.Fatalf("WriteFile(%q) failed: %v", path, err) + } + + fset := token.NewFileSet() + pkgs, err := parser.ParseDirEx(fset, dir, parser.Config{Mode: parser.ParseComments}) + if err != nil { + t.Fatalf("ParseDirEx(%q) failed: %v", dir, err) + } + pkg := pkgs["flags"] + if pkg == nil { + t.Fatalf("flags package not found in %q", dir) + } + + out, err := NewPackage("example.com/flags", pkg, &Config{Fset: fset}) + if err != nil { + t.Fatalf("NewPackage failed: %v", err) + } + + if out.Pkg().Scope().Lookup("DbgFlagAll") == nil { + t.Fatal("DbgFlagAll not found in package scope") + } + }) + + t.Run("GoGenericTypeInstantiations", func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "project.go") + err := os.WriteFile(path, []byte(`package project + +type StageShape = map[string]any + +type StageItemHandlers[T any] struct { + Sprite func(StageShape) (T, error) +} + +type SpriteConfig struct { + Handlers StageItemHandlers[*SpriteConfig] +} + +func AppendStageItems[T any](items []T, shape StageShape, handlers StageItemHandlers[T]) ([]T, error) { + return items, nil +} + +func (c *SpriteConfig) CloneHandlers() StageItemHandlers[*SpriteConfig] { + return c.Handlers +} +`), 0o644) + if err != nil { + t.Fatalf("WriteFile(%q) failed: %v", path, err) + } + + fset := token.NewFileSet() + pkgs, err := parser.ParseDirEx(fset, dir, parser.Config{Mode: parser.ParseComments}) + if err != nil { + t.Fatalf("ParseDirEx(%q) failed: %v", dir, err) + } + pkg := pkgs["project"] + if pkg == nil { + t.Fatalf("project package not found in %q", dir) + } + + cl.SetDisableRecover(true) + defer cl.SetDisableRecover(false) + + out, err := NewPackage("example.com/project", pkg, &Config{Fset: fset}) + if err != nil { + t.Fatalf("NewPackage failed: %v", err) + } + + if out.Pkg().Scope().Lookup("AppendStageItems") == nil { + t.Fatal("AppendStageItems not found in package scope") + } + }) +} diff --git a/doc/classfile-resource-spec.md b/doc/classfile-resource-spec.md new file mode 100644 index 000000000..b25e3fe49 --- /dev/null +++ b/doc/classfile-resource-spec.md @@ -0,0 +1,500 @@ +# The XGo classfile resource specification + +This document defines the syntax and semantics of resources in XGo classfile frameworks. + +A resource is a framework-defined named entity that is visible to static analysis. + +In particular, this document defines: +- how a framework declares resource kinds and type-level resource bindings +- how a framework discovers concrete resource instances through DQL over pack documents +- how a framework declares API-position scope bindings for scoped resource references +- how work classfiles may imply top-level resource identities +- how source-level resource references participate in standardized static semantics + +## Terms + +The following terms are used throughout this document: +- Resource kind: a framework-defined resource category such as `sprite`, `sound`, or `sprite.costume` +- Top-level kind: a resource kind with no direct parent kind +- Scoped kind: a resource kind whose instances exist within the scope of another resource kind +- Framework package: the package named by the first package path of one project group +- String-based type: an exported defined type or exported alias whose type has underlying type `string` +- Canonical resource reference type: a string-based type bound to one resource kind +- Handle-bearing type: an exported defined interface type or exported defined struct type whose values may carry one + top-level resource identity of one resource kind +- Local resource name: the name of one resource instance within its direct parent scope, or at top level if its + resource kind is top-level +- Scope chain: the ordered chain of ancestor resource identities from the outermost ancestor to the direct parent +- Scope-unknown reference: a scoped resource reference whose complete scope chain is not statically available +- Framework registration identity: the identity of one project group in the active classfile registry +- Resource identity: the stable logical identity of a resource instance, independent from storage path, manifest path, + URI syntax, and runtime object identity +- Resource instance: one concrete resource available to static analysis in one classfile project under one framework + registration +- Resource-discovery comment: a framework source directive comment that declares one concrete resource discovery query + for one canonical resource reference type +- Resource-name-discovery comment: a framework source directive comment that declares one local resource name discovery + query for one canonical resource reference type +- Pack document: the pack document defined by the XGo classfile specification +- Discovery origin node: one node matched by a resource-discovery comment query and associated with one discovered + resource instance +- Callable site: one framework function declaration, one framework method declaration, or one interface method spec + declared by one top-level handle-bearing interface type declaration +- API position: the receiver or one numbered parameter position of one callable site +- Resource-api-scope-binding comment: a framework source directive comment that declares one direct API-position scope + binding for one scoped canonical resource reference type parameter position +- Resource-bearing work file kind: a work file kind whose work classfiles each imply one top-level resource instance +- Resource set: the set of concrete resource instances available to one implementation for one classfile project and one + framework registration +- Resource reference: a source-level reference to a resource + +The logical identity of a resource instance is the tuple: +- framework registration identity +- resource kind +- optional scope chain +- local resource name + +In this specification, the resource kind in a resource identity is the instance's own kind. It does not encode any +concrete ancestor identities, even when the kind spelling uses dotted segments such as `sprite.costume.frame`. + +## Notation + +The syntax in this document is specified using EBNF. It uses the same EBNF conventions as the XGo classfile +specification and the Go specification. + +```ebnf +ResourceComment = "//xgo:class:resource" ResourceKind . +ResourceDiscoveryComment = "//xgo:class:resource-discovery" StandardDQLQuery . +ResourceNameDiscoveryComment = "//xgo:class:resource-name-discovery" StandardDQLQuery . +ResourceAPIScopeBindingComment = "//xgo:class:resource-api-scope-binding" ScopeBindingTarget ScopeBindingSource . + +ResourceKind = ResourceSegment { "." ResourceSegment } . +ResourceSegment = lower_letter { lower_letter | decimal_digit | "_" } . +ScopeBindingTarget = ParameterPosition . +ScopeBindingSource = "receiver" | ParameterPosition . +ParameterPosition = "param." decimal_digit { decimal_digit } . +lower_letter = "a" ... "z" . +``` + +The lexical production `decimal_digit` is as in the Go specification. The directive comment form is as in Go directive +comment conventions. + +`StandardDQLQuery` is the remaining text of one resource-discovery or resource-name-discovery comment line and must be +one standard DQL query. + +`ResourceComment` attaches to one immediately following top-level type spec that declares exactly one exported type name +and no type parameters. + +`ParameterPosition` uses zero-based decimal indexing in ordinary parameter source order and must not use leading zeros +other than `0` itself. + +In one dotted `ResourceKind`, each `.` separates one nested resource-kind segment from its direct parent prefix. A +single-segment kind is top-level. A multi-segment kind is scoped. The direct parent kind of one multi-segment kind is +the prefix that remains after removing its final `.` segment. + +## Conformance + +An implementation conforms to this specification if it implements the syntax and semantics defined by this specification +and satisfies every rule stated using the terms "must" and "must not". + +Rules stated using "may" describe permitted behavior. Rules stated using "should" describe recommended behavior. + +Capabilities such as hover, completion, diagnostics, rename, and references are optional tool capabilities. If an +implementation provides one of those capabilities for standardized resource semantics, the capability must satisfy all +applicable "must" and "must not" rules in this specification. + +## Framework source metadata + +Framework source metadata declares which resource kinds exist, how source code refers to them, which canonical resource +reference types carry discovery queries and optional local-name discovery queries, and how framework callable sites may +provide explicit direct scope to scoped resource positions. + +### Resource comments + +A resource comment belongs to the immediately following top-level type spec that declares exactly one exported type name +and no type parameters in the framework package of the containing project group. + +A resource comment on any other declaration, on one grouped type declaration as a whole rather than on one contained +type spec, or in any other package, has no standardized meaning in this specification. + +A declaration must not bear more than one resource comment with standardized meaning. + +A resource kind is declared when a resource comment with standardized meaning names it. + +If the declaration bearing a resource comment is a string-based type: +- the comment declares the canonical resource reference type of the named resource kind +- the declaration is the canonical resource reference type declaration of that kind + +If the declaration bearing a resource comment is an exported defined interface type or exported defined struct type: +- the comment declares a handle-bearing type of the named resource kind +- the named resource kind must be top-level + +If the declaration bearing a resource comment is neither a string-based type nor an exported defined interface type nor +an exported defined struct type, the comment has no standardized meaning in this specification. + +Within one project group: +- the direct parent kind of each scoped kind must be declared in the same project group +- a resource kind must not have more than one canonical resource reference type +- a declaration that bears a resource comment with standardized meaning must belong to the framework package of the + containing project group + +A top-level resource kind may have zero, one, or more handle-bearing types. + +A handle-bearing type declaration does not by itself imply any resource instance and does not by itself define any +source-level resource-binding rule. + +Additional user-facing spellings may be expressed through ordinary Go aliases that reduce to the canonical resource +reference type of a kind and do not bear their own resource comments. + +### Resource-discovery comments + +A resource-discovery comment belongs to the immediately following declaration only if all of the following hold: +- the declaration is the canonical resource reference type declaration of one resource kind +- the declaration is in the framework package of the containing project group + +A resource-discovery comment on any other declaration, in any other package, or on the same declaration as another +resource-discovery comment, has no standardized meaning in this specification. + +Each canonical resource reference type declaration may bear at most one resource-discovery comment with standardized +meaning. + +A resource-discovery comment declares one DQL query for discovering concrete resource instances of that resource kind. + +### Resource-name-discovery comments + +A resource-name-discovery comment belongs to the immediately following declaration only if all of the following hold: +- the declaration is the canonical resource reference type declaration of one resource kind +- the declaration is in the framework package of the containing project group +- the same declaration bears one resource-discovery comment with standardized meaning + +A resource-name-discovery comment on any other declaration, in any other package, or on the same declaration as another +resource-name-discovery comment, has no standardized meaning in this specification. + +Each canonical resource reference type declaration may bear at most one resource-name-discovery comment with +standardized meaning. + +A resource-name-discovery comment declares one DQL query for discovering local resource names of that resource kind +relative to discovery origin nodes. + +### Resource-api-scope-binding comments + +A resource-api-scope-binding comment belongs to the immediately following callable site only if all of the following +hold: +- the callable site is one top-level function declaration, one top-level method declaration, or one method spec declared + by one top-level handle-bearing interface type declaration +- the callable site is in the framework package of the containing project group + +A resource-api-scope-binding comment on any other callable site, in any other package, or on the same callable site as +another resource-api-scope-binding comment with the same target parameter position, has no standardized meaning in this +specification. + +Each callable site may bear zero or more resource-api-scope-binding comments with standardized meaning, but at most one +such comment may target one parameter position. + +A set of resource-api-scope-binding comments with standardized meaning on one callable site must induce an acyclic +directed relation over API positions. + +The meaning of one resource-api-scope-binding comment is independent of its source order relative to other such comments +on the same callable site. + +A resource-api-scope-binding comment declares one direct scope source for its target parameter position. + +## Concrete resource introduction + +Concrete resource introduction in this specification occurs either through resource-discovery comments and optional +resource-name-discovery comments over pack documents or through work classfile implication. + +### Discovery-based introduction + +Resource-discovery comments and optional resource-name-discovery comments introduce project-derived resource instances +over pack documents. + +#### Discovery execution + +For a top-level resource kind, the resource-discovery comment query is evaluated on the root node of the pack document, +if any, derived from the active project group identified by the current framework registration identity. + +For a scoped resource kind whose direct parent kind is ``, the resource-discovery comment query is evaluated +relative to each discovery origin node of each discovered direct parent resource instance of kind ``. + +Relative evaluation means that the discovery origin node is used as the DQL query root for that evaluation. + +If a direct parent resource identity is available only from work-classfile implication and not from any discovery origin +node, that implied identity alone does not create a relative discovery root. + +An implementation must preserve at least one discovery origin node for each discovered resource instance. + +If one discovered resource identity is obtained from more than one discovery origin node, relative child discovery is +evaluated for each such origin node. Child identities obtained from those evaluations are merged by resource identity. + +If a canonical resource reference type declaration bears a resource-name-discovery comment, its query is evaluated +relative to each discovery origin node produced by the resource-discovery comment on that declaration. + +Relative evaluation of a resource-name-discovery comment does not change the discovery origin node associated with the +discovered resource instance and does not create any relative discovery root for child discovery. + +#### Discovery result interpretation + +A successful resource-discovery comment query match contributes one discovered resource instance candidate. + +For the rules below: +- a string scalar value is one pack-document string scalar value +- a node key name is the key by which one matched node appears as one member of its containing pack-document object, if + any +- a string member named `name` is one object member named `name` whose value is a string scalar value + +The candidate's local resource name is determined as follows: +- if a resource-name-discovery comment is present: + - if its relative evaluation for that candidate does not produce exactly one matched node, the candidate is invalid + for discovery and does not contribute a resource instance + - if that matched node denotes a string scalar value, that string value is the local resource name + - otherwise, if the matched node has a non-empty node key name, that key name is the local resource name + - otherwise, if the matched node has a string member named `name`, the value of that member is the local resource name + - otherwise, the candidate is invalid for discovery and does not contribute a resource instance +- otherwise: + - if the discovery origin node has a non-empty node key name, that key name is the local resource name + - otherwise, if the discovery origin node has a string member named `name`, the value of that member is the local + resource name + - otherwise, the candidate is invalid for discovery and does not contribute a resource instance + +For a top-level kind, the discovered identity is `(resource kind, local resource name)`. + +For a scoped kind, the discovered identity is `(resource kind, scope chain, local resource name)`, where the direct +parent identity is inherited from the current relative discovery execution context. + +### Work classfile implication + +If a handle-bearing type declaration bearing a resource comment is the registered work base class declaration of one or +more work file kinds, each such work file kind is resource-bearing. + +A resource-bearing work file kind implies one top-level resource instance for each work classfile of that kind in the +analyzed classfile project. + +The implied local resource name is the class file stem of the work classfile, before any class type naming normalization +or `-prefix=` application. + +The implied resource identity is independent from generated Go type naming, including `-prefix=` adjustments. + +This rule standardizes one classfile-native top-level resource identity. Framework-specific runtime lookup keys, +reflection binding keys, and generated Go identifiers remain framework-defined. + +This rule affects only the static resource model. Compilation and runtime behavior remain unchanged. + +Only work classfiles may imply resources through this rule. + +A work file kind must not imply more than one top-level resource kind through this rule. + +If two or more work classfiles imply the same resource identity in one analyzed classfile project: +- the identities collide +- an implementation may report a duplicate-resource diagnostic +- the colliding inputs must not be treated as distinct resource instances + +Implied resources from project classfiles and scoped classfile-implied resources are outside the scope of this +specification. + +### Identity merging + +If more than one source contributes the same resource identity, including resource-discovery comments and work-classfile +implication: +- they refer to the same logical resource instance +- metadata may merge +- the contributing sources must not change the resource kind, scope chain, or local resource name of that identity + +An implementation may preserve one origin, many origins, or provenance in a different internal form, subject to the +relative-discovery requirements above. + +### Example + +```go +// SpriteName identifies a sprite by name. +// +//xgo:class:resource sprite +//xgo:class:resource-discovery sprites.* +type SpriteName = string + +// SpriteCostumeName identifies a sprite costume by name. +// +//xgo:class:resource sprite.costume +//xgo:class:resource-discovery costumes.* +type SpriteCostumeName = string + +// WidgetName identifies a widget by name. +// +//xgo:class:resource widget +//xgo:class:resource-discovery widgets.* +//xgo:class:resource-name-discovery id +type WidgetName = string + +// SpriteImpl is a handle-bearing type of the sprite resource kind. +// +//xgo:class:resource sprite +type SpriteImpl struct{} +``` + +If one pack document contains `sprites.Hero`, the `sprite` query may discover `sprite(Hero)` and preserve `sprites.Hero` +as one discovery origin node of that resource. The `sprite.costume` query is then evaluated relative to that origin +node, so it may discover costumes of `Hero` without embedding `Hero` into the query text. + +If one `widget` origin node stores its local resource name in a child member `id` rather than in its node key or a child +member `name`, the `resource-name-discovery` query `id` is evaluated relative to that origin node and yields the local +resource name. + +If the containing project group declares `class .spx SpriteImpl`, the resource comment on `SpriteImpl` makes `.spx` +resource-bearing, and each `.spx` work classfile implies one top-level `sprite` resource instance whose local resource +name is the class file stem. + +## Static semantics + +### Resource references + +A source position participates in standardized resource semantics only if ordinary Go typing determines one canonical +resource reference type declaration of one resource kind for that position. + +For this purpose, a canonical resource reference type declaration is determined for a source expression in one of the +following ways: +- the expression's static type is that canonical resource reference type declaration, or can be reduced to it by + following Go alias declarations only +- the surrounding typed position requires that canonical resource reference type, and the expression is assignable to it + under ordinary Go typing rules + +A distinct defined type does not participate in standardized resource semantics solely because both it and a canonical +resource reference type have underlying type `string`. + +A Go alias declaration denotes the same resource kind as that canonical resource reference type if its denoted type can +be reduced, recursively through Go alias declarations only, to the same canonical resource reference type declaration. + +An expression is canonically resource-typed if one canonical resource reference type declaration is determined for it in +that way. + +For one canonically resource-typed expression: +- if the expression is a string literal or a statically evaluable string constant, it is a resource reference candidate +- if the expression cannot be statically evaluated, it remains resource-typed but is not a resolvable resource reference +- for a top-level kind, the lookup key is `(resource kind, local resource name)` +- for a scoped kind, the lookup key is `(resource kind, scope chain, local resource name)` +- if a scoped kind does not have a statically available complete scope chain, the reference is one `scope-unknown` + reference + +### API-position scope bindings + +In this subsection, the callable site is the function declaration, method declaration, or interface method spec to which +one resource-api-scope-binding comment with standardized meaning belongs. + +One resource-api-scope-binding comment has standardized meaning only if all of the following hold: +- the target parameter position exists on the callable site +- the target parameter position is not the variadic parameter position of the callable site +- the target parameter type determines one canonical resource reference type declaration of one scoped kind +- the source API position exists on the callable site +- if the source API position is one parameter position, it is not the variadic parameter position of the callable site +- the source API position determines either: + - one canonical resource reference type declaration of the direct parent kind of the target kind, or + - one handle-bearing type of that direct parent kind + +At one call that resolves to that callable site, the source API position is interpreted as follows: +- if the source API position is `receiver`, the source expression is the call's receiver expression, whether explicit or + implicit +- if the source API position is one parameter position, the source expression is the corresponding argument expression + +One resource-api-scope-binding comment contributes one explicit direct parent identity to its target argument position +only if the source expression yields one exact resource identity of the target kind's direct parent kind under the +resource semantics otherwise available at that source position. + +For this purpose, a source position may itself use explicit scope contributed by other resource-api-scope-binding +comments on the same callable site, subject to the acyclicity rule above. + +If the target kind is multiply scoped, a resource-api-scope-binding comment contributes at most the direct parent +identity. Any additional ancestor levels come from that direct parent identity's own scope chain, if available. + +If a target position receives one explicit direct parent identity from a resource-api-scope-binding comment, that +identity is explicit scope for that target position. + +Example: + +```go +//xgo:class:resource-api-scope-binding param.0 receiver +func (p *SpriteImpl) SetCostume__0(costume SpriteCostumeName) + +//xgo:class:resource-api-scope-binding param.0 receiver +//xgo:class:resource-api-scope-binding param.1 param.0 +func (p *SpriteImpl) SetCostumeAndFrame(costume SpriteCostumeName, frame SpriteCostumeFrameName) +``` + +If one call to `SetCostume__0` yields one exact `sprite` identity at its receiver position, the bound +`SpriteCostumeName` argument position may use that identity as explicit scope. + +If one call to `SetCostumeAndFrame` yields one exact `sprite` identity at its receiver position, `param.0` may use that +identity as explicit scope. If `param.0` then yields one exact `sprite.costume` identity, the bound +`SpriteCostumeFrameName` argument position may use that identity as explicit scope. + +### Scoped owner inference + +If all of the following hold: +- the current source position is inside a work classfile +- the registered work base class declaration of that work file kind bears a resource comment for one top-level kind + `` +- the referenced scoped kind has direct parent kind `` +- no explicit scope is otherwise statically available + +then the statically available direct parent identity is the implied top-level resource of the containing work classfile. + +Example: +- if the declaration of `SpriteImpl` bears `//xgo:class:resource sprite` +- and `class .spx SpriteImpl` is active +- and `SpriteCostumeName` bears `//xgo:class:resource sprite.costume` +- then a scoped `SpriteCostumeName` reference inside `Hero.spx` may use scope `(sprite, Hero)` + +If a resource-api-scope-binding comment contributes explicit scope to one target position, that explicit scope takes +precedence over the narrow rule above for that target position. + +Receiver-bound owner inference, project auto-binding owner inference, and other framework-specific context rules remain +framework-specific. + +If the narrow rule above does not apply, the scoped reference remains `scope-unknown`. + +The narrow rule above contributes at most one statically available parent identity. It does not by itself infer any +additional ancestor levels for multiply scoped kinds. + +If a framework uses plain `string` parameters or resource-like values that do not reduce to canonical resource reference +types, those positions are outside the standardized resource semantics of this specification. + +### Diagnostics + +A conforming implementation: +- may emit a "resource not found" diagnostic only when its framework-specific discovery semantics are exact for the + analyzed project state +- must not emit a "resource not found" diagnostic for dynamic expressions +- must not emit a "resource not found" diagnostic for `scope-unknown` references +- may emit a diagnostic for an empty resource name + +## Tool semantics + +### Hover + +When a source position resolves to a resource reference whose identity is present in the implementation's resource set, +an implementation that provides hover must return information about the corresponding resource instance. + +### Completion + +When the target type at an input position resolves to a resource kind, an implementation that provides completion may +base resource completion candidates on the implementation's resource set. + +For scoped kinds: +- if the scope chain is known, completion must be filtered to that scope chain +- if the scope chain is unknown, an implementation may suppress completion or provide degraded completion annotated with + scope information + +### Rename and references + +If an implementation supports rename or reference lookup for resources, resource identity must be based on +`(framework registration identity, resource kind, optional scope chain, local resource name)` rather than raw string +text. + +## Excluded semantics + +This specification intentionally does not standardize: +- framework-specific API-position scope rules beyond explicit resource-api-scope-binding comments +- API-position kind binding rules for positions that do not already depend on canonical resource reference types +- implied resources from project classfiles +- scoped classfile-implied resources beyond the work-classfile implication rule defined by this specification +- how runtime parameters such as `run(...)` are mapped back into static analysis +- the concrete encoding of preview URIs or editor-specific payloads +- one standardized completeness or exactness model for resource discovery diff --git a/doc/classfile-spec.md b/doc/classfile-spec.md index c8a26b7aa..d8a4a1513 100644 --- a/doc/classfile-spec.md +++ b/doc/classfile-spec.md @@ -13,13 +13,20 @@ There are two kinds of classfiles: The following terms are used throughout this document: - Class extension: the normalized classfile suffix used for framework lookup, e.g., `_app.gox` and `.gsh` - Class file stem: the filename without the class extension +- Classfile project: the source tree rooted at the analyzed package directory and interpreted by the classfile mechanism +- Project root directory: the root directory of one classfile project +- Pack root: the directory named by one `pack` directive and resolved relative to one project root directory +- Pack document: the logical merged configuration object derived from one pack root and its declared index filename +- Project file kind: a framework file kind declared by one `project` directive and used for project classfiles +- Work file kind: a framework file kind declared by one `class` directive and used for work classfiles +- Project classfile: the framework file that represents the project-level class +- Work classfile: a framework file that represents a non-project class within the same framework - Class type: the generated named type for a classfile +- Base class: an embedded framework type declared by classfile metadata +- Work prototype type: an exported type declared by classfile metadata and associated with one work file kind - Field declaration block: the unique top-level `var` declaration that is interpreted as class fields rather than package variables - Shadow entry: the synthetic function created from top-level statements -- Project classfile: the framework file that represents the project-level class -- Work classfile: a framework file that represents a non-project class within the same framework -- Base class: an embedded framework type declared by classfile metadata ## File classification @@ -172,9 +179,9 @@ Examples: - `get_p_#id_app.gox` has class file stem `get_p_#id` and normalized type name `get_p_id` Framework metadata may further transform the type name: -- A project file whose class file stem is `main`, or a framework that has no explicit project file, uses the project - base-class name as its default class type name. A leading `*` on the base-class name is removed -- A non-empty work-class `-prefix=` is prepended to the normalized work-file stem +- A project classfile whose class file stem is `main`, or a framework that has no explicit project classfile, uses the + name of the base type named by the project base class as its default class type name +- A non-empty `-prefix=` on a work file kind is prepended to the normalized class file stem of a work classfile - If neither of the previous rules applies and the class type name would otherwise equal one of the reserved names `init`, `main`, `go`, `goto`, `type`, `var`, `import`, `package`, `interface`, `struct`, `const`, `func`, `map`, `chan`, `for`, `if`, `else`, `switch`, `case`, `select`, `defer`, `range`, `return`, `break`, `continue`, @@ -194,7 +201,7 @@ For a framework classfile, framework-added fields precede user fields. Framework-added fields are inserted in the following order: - For a project classfile, the embedded project base class -- For a project classfile, each embedded work-class field requested by the `-embed` flag, in work-class declaration +- For a project classfile, each embedded work class field requested by the `-embed` flag, in work file kind declaration order and lexicographic source-file path order - For a work classfile, the embedded work base class - For a work classfile, an embedded pointer to the project class type, if a project class type exists and its field name @@ -359,18 +366,26 @@ The classfile loader recognizes the following module directives: ProjectDirective = "project" [ ProjectExt ExportedName ] PackagePath { PackagePath } . ClassDirective = "class" { ClassDirectiveFlag } WorkExt ExportedName [ ExportedName ] . ImportDirective = "import" [ ImportName ] PackagePath . +PackDirective = "pack" RelativeDirectoryPath PackIndexFile . ClassDirectiveFlag = "-embed" | "-prefix=" string_without_space . +RelativeDirectoryPath = string_without_space . +PackIndexFile = string_without_space . ``` -Every `class` or `import` directive belongs to the most recent preceding `project` directive. +`RelativeDirectoryPath` is one relative directory path token with no spaces. + +`PackIndexFile` is one plain file name token with no spaces. + +Every `class`, `import`, or `pack` directive belongs to the most recent preceding `project` directive. -A project group consists of one `project` directive together with all `class` and `import` directives that belong to it. +A project group consists of one `project` directive together with all `class`, `import`, and `pack` directives that +belong to it. -The first package path of a `project` directive is the framework package used to resolve any base-class symbols named by -that project group. +The first package path of a `project` directive is the framework package used to resolve the project base class, each +work base class, and each work prototype type named by that project group. -Any additional package paths participate in implicit framework-package export lookup but are not searched for -base-class symbols. +Any additional package paths participate in implicit framework-package export lookup but are not searched for those +symbols. ### Extension forms and normalization @@ -382,10 +397,9 @@ Both forms are part of the classfile mechanism. Neither form is a compatibility For newly defined framework registrations, `_[class].gox` is the recommended form. -This recommendation does not constrain the built-in registrations defined above. +The built-in registrations defined above remain valid as written. -This specification defines file classification and compilation semantics for both forms. It does not require auxiliary -tools to recognize arbitrary non-`.gox` class extensions automatically. +Auxiliary tool behavior for arbitrary non-`.gox` class extensions is implementation-defined. The textual extension token accepted by `project` and `class` directives may be written with a leading `*` and, for projects, may also be written with a leading `main`. @@ -403,18 +417,22 @@ For each `project` directive: - If `ProjectExt` and `ExportedName` are present, the directive defines a project file kind and names the project base class - If `ProjectExt` and `ExportedName` are omitted, the directive defines no project file kind or project base class and - still defines the package lookup set and the project group to which subsequent `class` and `import` directives belong + still defines the package lookup set and the project group to which subsequent `class`, `import`, and `pack` + directives belong - When a project base class is named, the first package path is the package from which that symbol is resolved - A project base-class name may be written with a leading `*`, in which case the generated project class embeds a pointer to the named base type rather than the base type itself For each `class` directive: -- The exported symbol names the work base class +- The exported symbol names the work base class and is resolved from the framework package of the containing project + group - The optional final exported symbol is the work prototype type -- If a project group declares more than one work class kind, every work class in that group must declare a prototype - type +- If a project group declares more than one work file kind, every work file kind in that group must declare a work + prototype type +- When a work prototype type is named, the work prototype type symbol is resolved from the framework package of the + containing project group +- `-embed` causes the project class type to embed a field for each generated work class instance of that work file kind - `-prefix=` prepends the given string to every generated work class type name -- `-embed` causes the project class type to embed a field for each generated work class instance of that work kind For each `import` directive: - The imported package becomes available to classfiles as an auto-imported package name @@ -422,7 +440,16 @@ For each `import` directive: - If multiple `import` directives in the same project group resolve to the same auto-import name, the last directive wins -## Project and work-class assembly +For each `pack` directive: +- The project group may declare at most one `pack` directive +- The directory path is resolved relative to the project root directory of the analyzed classfile project +- The directory path must not contain any `..` path component +- The index filename determines the root configuration filename of the pack root and the child configuration filename + expected at each descendant directory level +- The index filename must not contain `/` or `\` +- The index filename must end in one of `.json`, `.yml`, or `.yaml` + +## Project and work class assembly A test framework registration is a framework registration whose project extension has the suffix `test.gox`. @@ -434,35 +461,35 @@ For each framework registration, a package may contain at most one explicit proj It is an error for a package to contain more than one explicit project classfile for the same framework registration. -If a framework registration provides a project base class but the package contains no explicit project file for that -framework, the compiler still synthesizes a default project class type. +If a framework registration provides a project base class but the package contains no explicit project classfile for +that framework, the compiler still synthesizes a default project class type. The synthesized project class has no source file of its own. Its type name is derived by the project type-naming rules described earlier. -### Work-instance assembly for project `Main` +### Work instance assembly for project `Main` For every non-test framework registration that provides a project base class, the compiler generates a project method named `Main` on the project class type. The project base class is therefore required to provide a method named `Main`. -The generated project method constructs work-class instances and forwards them to the embedded project base-class method +The generated project method constructs work class instances and forwards them to the embedded project base-class method `Main`. The grouping rule is: -- If the framework has exactly one work class kind and that `Main` parameter is variadic, all work files of that kind - are passed as variadic arguments -- Otherwise, work files are grouped by their declared prototype type and passed as slices in `Main` parameter order +- If the framework has exactly one work file kind and that `Main` parameter is variadic, all work files of that kind are + passed as variadic arguments +- Otherwise, work files are grouped by their declared work prototype type and passed as slices in `Main` parameter order -The project `Main` method constructs one fresh work-class instance for each work file in the package. When `-embed` is -present on a work class declaration, the freshly created work instance is also assigned into the corresponding embedded -field on the project instance before the project `Main` call. +The project `Main` method constructs one fresh work class instance for each work file in the package. When `-embed` is +present on the corresponding `class` directive, the freshly created work instance is also assigned into the +corresponding embedded field on the project instance before the project `Main` call. ## Synthesized helper methods -The compiler may synthesize additional work-class methods when the declared work prototype requires them. +The compiler may synthesize additional work class methods when the declared work prototype type requires them. ### `Classfname` -If the work prototype contains a method named `Classfname`, the compiler generates: +If the work prototype type contains a method named `Classfname`, the compiler generates: ```xgo func (this *T) Classfname() string @@ -476,9 +503,9 @@ Examples: ### `Classclone` -If the work prototype contains a method named `Classclone`, the compiler generates a shallow-clone method named -`Classclone` with no parameters other than the receiver. Its result list is adopted from the prototype's `Classclone` -declaration. +If the work prototype type contains a method named `Classclone`, the compiler generates a shallow-clone method named +`Classclone` with no parameters other than the receiver. Its result list is adopted from the work prototype type's +`Classclone` declaration. The generated implementation copies `*this` by value into a temporary variable and returns the address of that temporary value. @@ -492,7 +519,7 @@ If one exists, no class-based package `main` is synthesized. If none exists, the compiler selects a class entrypoint as follows: 1. It considers only non-test framework registrations -2. Among framework project groups, it prefers a unique project group whose explicit project file has a shadow entry +2. Among framework project groups, it prefers a unique project group whose explicit project classfile has a shadow entry 3. If no such group exists, it prefers a unique remaining project group, including one that is represented only by a synthesized default project class 4. If no project group is selected, it selects the unique normal classfile that has a shadow entry, if exactly one @@ -507,6 +534,40 @@ func main() { new(T).Main() } If no class type is selected, the compiler generates an empty `main` function unless automatic main generation is disabled in compiler configuration. +## Pack roots and pack documents + +One active project group may derive zero or one pack document from one classfile project. + +If one active project group declares one `pack` directive, its pack root is the resolved directory named by that +directive. + +Active project groups do not share pack roots or pack documents. Each active project group derives its own pack +document independently from its own `pack` directive, if any. + +For one active project group whose `pack` directive resolves to one pack root `R` and one index filename `F`, the +corresponding pack document is one logical object tree derived as follows: +- `R/F` is parsed as one object value +- each descendant directory of `R` that contains `F` contributes one child object +- each such child object is parsed from that descendant `F` and merged into the root object at the relative + directory path from `R`, creating intermediate objects as needed +- directories that do not contain `F` contribute no object of their own +- files other than contributing `F` files are outside the standardized pack-document model + +If the project group declares no `pack` directive, no standardized pack document is derived for that project group. + +If `R/F` does not exist, does not parse as one object value, or if one merge would overwrite an existing key at the +same object level, no standardized pack document is derived for that project group. + +The project files remain the source of truth. Each pack document is derived from one analyzed project state and one +active project group. + +An implementation may materialize one pack document as one generated sibling file of its root configuration file named: +- `index_pack.json` if `F` ends in `.json` +- `index_pack.yml` if `F` ends in `.yml` +- `index_pack.yaml` if `F` ends in `.yaml` + +Its enablement mechanism is implementation-defined. + ## Compatibility The following compatibility aliases are also accepted: @@ -524,5 +585,5 @@ Accordingly: package semantics - Package initialization order for ordinary package variables is unchanged -The classfile mechanism therefore adds a source-level lowering rule. It does not add a new runtime object model beyond -what is produced by the lowered Go code. +The classfile mechanism therefore adds a source-level lowering rule and preserves the ordinary runtime object model +produced by the lowered Go code.