From acb42a5702bc549ad315aa73f36bb6f08eb8d5b4 Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 27 Aug 2026 22:18:45 +0200 Subject: [PATCH] Preserve Gotlin semantics across packages --- README.md | 12 ++ cmd/gotlin-lsp/main.go | 43 +++++-- cmd/gotlinc/main.go | 56 ++++++++- internal/lang/generate_go.go | 46 +++++--- internal/lang/hir.go | 7 +- internal/lang/metadata.go | 210 +++++++++++++++++++++++++++++++++ internal/lang/metadata_test.go | 64 ++++++++++ internal/lang/symbols.go | 38 +++++- internal/lang/type_checker.go | 22 +++- internal/lang/types.go | 61 +++++++++- 10 files changed, 525 insertions(+), 34 deletions(-) create mode 100644 internal/lang/metadata.go create mode 100644 internal/lang/metadata_test.go diff --git a/README.md b/README.md index 16d8faf..d180355 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ The compiler keeps source spelling in its syntax AST, then builds lexical symbols, structural semantic types, resolved expression meanings, and typed HIR before Go emission. Class reference semantics live in `ClassType`; only the semantic Type-to-Go mapping turns a class such as `User` into `*User`. +Separately compiled Gotlin packages publish a versioned `.gti.json` interface; +imports load that interface before falling back to `go/types`, preserving class +reference semantics, generics, enums, function signatures, and inferred effects +across package boundaries. ## Supported language slice @@ -268,6 +272,14 @@ go run ./cmd/gotlinc build -src ./examples/hello.gt -o /tmp/hello.go go run /tmp/hello.go ``` +Emit or consume package interfaces with repeatable metadata flags: + +```bash +gotlinc build -src -metadata-output platform.gti.json \ + -metadata-package example/platform platform.gt -o platform.go +gotlinc build -src -metadata platform.gti.json service.gt -o service.go +``` + Run directly: ```bash diff --git a/cmd/gotlin-lsp/main.go b/cmd/gotlin-lsp/main.go index bddf14b..6fcd78a 100644 --- a/cmd/gotlin-lsp/main.go +++ b/cmd/gotlin-lsp/main.go @@ -326,27 +326,43 @@ func (s *server) buildDocumentState(uri, text string) documentState { return state } state.program = program - programs, symbols := s.packageContext(uri, program.PackagePath, text) - _, semanticDiagnostics := lang.AnalyzeWithContext(program, programs) + programs, symbols, metadata := s.packageContext(uri, program.PackagePath, text) + _, semanticDiagnostics := lang.AnalyzeWithContextAndMetadata(program, programs, metadata) state.symbols = indexSymbols(text, program) state.packageSymbols = symbols state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics) return state } -func (s *server) packageContext(currentURI, packagePath, currentText string) ([]*lang.Program, []symbol) { +func (s *server) packageContext(currentURI, packagePath, currentText string) ([]*lang.Program, []symbol, []*lang.PackageMetadata) { path, ok := filePathFromURI(currentURI) if !ok { - return nil, nil + return nil, nil, nil + } + root := nearestWorkspaceRoot(filepath.Dir(path)) + if root == "" { + root = nearestModuleRoot(filepath.Dir(path)) } - root := nearestModuleRoot(filepath.Dir(path)) if root == "" { root = filepath.Dir(path) } var programs []*lang.Program var symbols []symbol + var metadata []*lang.PackageMetadata _ = filepath.WalkDir(root, func(candidate string, entry os.DirEntry, err error) error { - if err != nil || entry.IsDir() || filepath.Ext(candidate) != ".gt" { + if err != nil || entry.IsDir() { + return nil + } + if strings.HasSuffix(candidate, ".gti.json") { + if body, readErr := os.ReadFile(candidate); readErr == nil { + var item lang.PackageMetadata + if json.Unmarshal(body, &item) == nil && item.Version == lang.PackageMetadataVersion { + metadata = append(metadata, &item) + } + } + return nil + } + if filepath.Ext(candidate) != ".gt" { return nil } candidateURI := fileURI(candidate) @@ -374,7 +390,20 @@ func (s *server) packageContext(currentURI, packagePath, currentText string) ([] } return nil }) - return programs, symbols + return programs, symbols, metadata +} + +func nearestWorkspaceRoot(directory string) string { + for { + if _, err := os.Stat(filepath.Join(directory, "go.work")); err == nil { + return directory + } + parent := filepath.Dir(directory) + if parent == directory { + return "" + } + directory = parent + } } func nearestModuleRoot(directory string) string { diff --git a/cmd/gotlinc/main.go b/cmd/gotlinc/main.go index e444783..6c71602 100644 --- a/cmd/gotlinc/main.go +++ b/cmd/gotlinc/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "flag" "fmt" "os" @@ -32,6 +33,10 @@ func runBuild(args []string) { fs := flag.NewFlagSet("build", flag.ExitOnError) srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable") outPath := fs.String("o", "", "output file path") + metadataOutput := fs.String("metadata-output", "", "write Gotlin package metadata") + metadataPackage := fs.String("metadata-package", "", "import path recorded in package metadata") + var metadataInputs stringListFlag + fs.Var(&metadataInputs, "metadata", "Gotlin package metadata dependency (repeatable)") fs.Usage = func() { fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] ") } @@ -48,7 +53,7 @@ func runBuild(args []string) { } inputPath := fs.Arg(0) - goSrc := compileFiles(fs.Args(), !*srcOnly) + goSrc := compileFilesWithMetadata(fs.Args(), !*srcOnly, metadataInputs, *metadataOutput, *metadataPackage) if *srcOnly { if *outPath == "" { @@ -117,6 +122,10 @@ func compileFile(inputPath string, forceMain bool) string { } func compileFiles(inputPaths []string, forceMain bool) string { + return compileFilesWithMetadata(inputPaths, forceMain, nil, "", "") +} + +func compileFilesWithMetadata(inputPaths []string, forceMain bool, metadataPaths []string, metadataOutput, metadataPackage string) string { program := &lang.Program{} var firstSource string for _, inputPath := range inputPaths { @@ -146,12 +155,27 @@ func compileFiles(inputPaths []string, forceMain bool) string { firstSource = string(src) } } + metadata := loadMetadata(metadataPaths) + if metadataOutput != "" { + packageMetadata, err := lang.BuildPackageMetadata(program, metadataPackage) + if err != nil { + fail(err) + } + encoded, err := json.MarshalIndent(packageMetadata, "", " ") + if err != nil { + fail(err) + } + encoded = append(encoded, '\n') + if err := os.WriteFile(metadataOutput, encoded, 0o644); err != nil { + fail(err) + } + } var goSrc []byte var err error if forceMain { - goSrc, err = lang.GenerateGoMain(program) + goSrc, err = lang.GenerateGoMainWithMetadata(program, metadata) } else { - goSrc, err = lang.GenerateGo(program) + goSrc, err = lang.GenerateGoWithMetadata(program, metadata) } if err != nil { fail(err) @@ -162,6 +186,30 @@ func compileFiles(inputPaths []string, forceMain bool) string { return string(goSrc) } +type stringListFlag []string + +func (values *stringListFlag) String() string { return strings.Join(*values, ",") } +func (values *stringListFlag) Set(value string) error { *values = append(*values, value); return nil } + +func loadMetadata(paths []string) []*lang.PackageMetadata { + metadata := make([]*lang.PackageMetadata, 0, len(paths)) + for _, path := range paths { + body, err := os.ReadFile(path) + if err != nil { + fail(err) + } + var item lang.PackageMetadata + if err := json.Unmarshal(body, &item); err != nil { + fail(fmt.Errorf("load metadata %s: %w", path, err)) + } + if item.Version != lang.PackageMetadataVersion { + fail(fmt.Errorf("metadata %s uses unsupported version %d", path, item.Version)) + } + metadata = append(metadata, &item) + } + return metadata +} + func buildExecutable(goSrc string, outputPath string) error { tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go") if err != nil { @@ -202,7 +250,7 @@ func normalizeBuildArgs(args []string) ([]string, error) { switch { case arg == "-src": flags = append(flags, arg) - case arg == "-o": + case arg == "-o" || arg == "-metadata" || arg == "-metadata-output" || arg == "-metadata-package": if i+1 >= len(args) { return nil, fmt.Errorf("missing value for -o") } diff --git a/internal/lang/generate_go.go b/internal/lang/generate_go.go index e3c72e6..afb23cf 100644 --- a/internal/lang/generate_go.go +++ b/internal/lang/generate_go.go @@ -11,17 +11,25 @@ import ( ) func GenerateGo(program *Program) ([]byte, error) { - return generateGo(program, "") + return generateGo(program, "", nil) } func GenerateGoMain(program *Program) ([]byte, error) { - return generateGo(program, "main") + return generateGo(program, "main", nil) } -func generateGo(program *Program, packageOverride string) ([]byte, error) { - semantic, err := Analyze(program) - if err != nil { - return nil, err +func GenerateGoWithMetadata(program *Program, metadata []*PackageMetadata) ([]byte, error) { + return generateGo(program, "", metadata) +} + +func GenerateGoMainWithMetadata(program *Program, metadata []*PackageMetadata) ([]byte, error) { + return generateGo(program, "main", metadata) +} + +func generateGo(program *Program, packageOverride string, metadata []*PackageMetadata) ([]byte, error) { + semantic, diagnostics := AnalyzeWithMetadata(program, metadata) + if len(diagnostics) > 0 { + return nil, diagnostics[0] } g := goGenerator{semantic: semantic} if err := g.program(program, packageOverride); err != nil { @@ -1260,10 +1268,15 @@ func (g *goGenerator) expr(expr Expr, expectedType string) (string, error) { if ident, ok := e.Receiver.(IdentExpr); ok && g.semantic.Imports[ident.Name] { name = exportedGoName(name) } else if class, ok := g.classForType(g.exprType(e.Receiver)); ok { - for _, field := range class.Fields { - if field.Name == name && class.Data && !field.Private { - name = exportedGoName(name) - break + resolvedType, _ := g.semantic.ResolveType(g.exprType(e.Receiver)) + if isImportedClassType(resolvedType) { + name = exportedGoName(name) + } else { + for _, field := range class.Fields { + if field.Name == name && class.Data && !field.Private { + name = exportedGoName(name) + break + } } } } else if ident, ok := e.Receiver.(IdentExpr); ok && (ident.Name == "this" || ident.Name == "self") && g.currentClass != nil { @@ -2445,10 +2458,15 @@ func (g *goGenerator) isExternalGoCall(callee Expr) bool { } func (g *goGenerator) classForType(typ string) (ClassDecl, bool) { - typ = strings.TrimPrefix(typ, "*") - typ = strings.TrimSuffix(typ, "?") - class, ok := g.semantic.Classes[typ] - return class, ok + resolved, err := g.semantic.ResolveType(typ) + if err != nil { + return ClassDecl{}, false + } + class, _ := classInstance(resolved) + if class == nil || class.Decl == nil { + return ClassDecl{}, false + } + return *class.Decl, true } func (g *goGenerator) isDefined(name string) bool { diff --git a/internal/lang/hir.go b/internal/lang/hir.go index 0c8a288..2505e74 100644 --- a/internal/lang/hir.go +++ b/internal/lang/hir.go @@ -590,6 +590,11 @@ func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning, return MappingExpression, nil } if receiver, ok := selector.Receiver.(IdentExpr); ok { + if pack := resolver.program.Packages[receiver.Name]; pack != nil { + if function := pack.Function(selector.Name); function != nil { + return GotlinCallExpr, function + } + } if receiver.Name == "Result" { return EnumConstructionExpr, nil } @@ -601,7 +606,7 @@ func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning, return GoCallExpr, nil } if class, _ := classInstance(ResolvedType(selector.Receiver)); class != nil { - if method, ok := class.Methods[selector.Name]; ok { + if method := class.Method(selector.Name); method != nil { return MethodCallExpr, method } } diff --git a/internal/lang/metadata.go b/internal/lang/metadata.go new file mode 100644 index 0000000..5bb8cfc --- /dev/null +++ b/internal/lang/metadata.go @@ -0,0 +1,210 @@ +package lang + +import "strings" + +const PackageMetadataVersion = 1 + +type PackageMetadata struct { + Version int `json:"version"` + ImportPath string `json:"importPath"` + PackageName string `json:"packageName"` + Classes []ClassMetadata `json:"classes,omitempty"` + Enums []EnumMetadata `json:"enums,omitempty"` + Functions []FunctionMetadata `json:"functions,omitempty"` +} + +type ClassMetadata struct { + Name string `json:"name"` + TypeParams []string `json:"typeParams,omitempty"` + Data bool `json:"data,omitempty"` + Fields []FieldMetadata `json:"fields,omitempty"` + Methods []FunctionMetadata `json:"methods,omitempty"` +} + +type FieldMetadata struct { + Name string `json:"name"` + Type string `json:"type"` + Mutable bool `json:"mutable,omitempty"` +} + +type FunctionMetadata struct { + Name string `json:"name"` + TypeParams []string `json:"typeParams,omitempty"` + Params []ParamMetadata `json:"params,omitempty"` + Result string `json:"result"` + Effects Effect `json:"effects,omitempty"` +} + +type ParamMetadata struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type EnumMetadata struct { + Name string `json:"name"` + Variants []EnumVariantMetadata `json:"variants"` +} + +type EnumVariantMetadata struct { + Name string `json:"name"` + PayloadTypes []string `json:"payloadTypes,omitempty"` + StringValue string `json:"stringValue,omitempty"` +} + +func BuildPackageMetadata(program *Program, importPath string) (*PackageMetadata, error) { + semantic, err := Analyze(program) + if err != nil { + return nil, err + } + metadata := &PackageMetadata{Version: PackageMetadataVersion, ImportPath: importPath, PackageName: goPackageName(program.PackagePath)} + for index := range program.Classes { + class := &program.Classes[index] + item := ClassMetadata{Name: class.Name, TypeParams: class.TypeParams, Data: class.Data} + for _, field := range class.Fields { + item.Fields = append(item.Fields, FieldMetadata{Name: field.Name, Type: field.Type, Mutable: field.Mutable}) + } + for methodIndex := range class.Methods { + method := &class.Methods[methodIndex] + item.Methods = append(item.Methods, metadataFunction(method, semantic.FunctionEffects[class.Name+"."+method.Name])) + } + metadata.Classes = append(metadata.Classes, item) + } + for _, enum := range program.Enums { + item := EnumMetadata{Name: enum.Name} + for _, variant := range enum.Variants { + item.Variants = append(item.Variants, EnumVariantMetadata{Name: variant.Name, PayloadTypes: variant.PayloadTypes, StringValue: variant.StringValue}) + } + metadata.Enums = append(metadata.Enums, item) + } + for index := range program.Functions { + function := &program.Functions[index] + metadata.Functions = append(metadata.Functions, metadataFunction(function, semantic.FunctionEffects[function.Name])) + } + return metadata, nil +} + +func metadataFunction(function *FunctionDecl, effects Effect) FunctionMetadata { + item := FunctionMetadata{Name: function.Name, TypeParams: function.TypeParams, Result: function.ReturnType, Effects: effects} + for _, param := range function.Params { + item.Params = append(item.Params, ParamMetadata{Name: param.Name, Type: param.Type}) + } + return item +} + +func metadataForImport(imported ImportDecl, metadata []*PackageMetadata) *PackageMetadata { + path := strings.Trim(imported.Path, `"`) + if !strings.Contains(path, "/") { + path = importPathToGoPath(path) + } + for _, candidate := range metadata { + if candidate != nil && candidate.ImportPath == path { + return candidate + } + } + return nil +} + +func packageSymbolFromMetadata(alias string, metadata *PackageMetadata) *PackageSymbol { + pack := &PackageSymbol{Alias: alias, Metadata: metadata, Classes: map[string]*ClassSymbol{}, Functions: map[string]*Symbol{}, Enums: map[string]EnumDecl{}} + for _, item := range metadata.Classes { + decl := &ClassDecl{Name: item.Name, TypeParams: item.TypeParams, Data: item.Data} + class := &ClassSymbol{Name: item.Name, TypeParams: item.TypeParams, Decl: decl, Fields: map[string]*Symbol{}, Methods: map[string]*Symbol{}} + pack.Classes[item.Name] = class + } + for _, item := range metadata.Classes { + class := pack.Classes[item.Name] + for _, field := range item.Fields { + typ := resolvePackageType(field.Type, alias, pack, item.TypeParams) + decl := FieldDecl{Name: field.Name, Type: field.Type, Mutable: field.Mutable} + class.Decl.Fields = append(class.Decl.Fields, decl) + class.Fields[field.Name] = &Symbol{Name: field.Name, Kind: VariableSymbol, Type: typ, Mutable: field.Mutable, Decl: &decl} + } + for _, method := range item.Methods { + typ := metadataFunctionType(method, alias, pack, item.TypeParams) + class.Methods[method.Name] = &Symbol{Name: method.Name, Kind: FunctionSymbolKind, Type: typ, Effects: method.Effects} + } + } + for _, item := range metadata.Functions { + typ := metadataFunctionType(item, alias, pack, nil) + pack.Functions[item.Name] = &Symbol{Name: item.Name, Kind: FunctionSymbolKind, Type: typ, Effects: item.Effects} + } + for _, item := range metadata.Enums { + decl := EnumDecl{Name: item.Name} + for _, variant := range item.Variants { + decl.Variants = append(decl.Variants, EnumVariant{Name: variant.Name, PayloadTypes: variant.PayloadTypes, StringValue: variant.StringValue}) + } + pack.Enums[item.Name] = decl + } + return pack +} + +func (pack *PackageSymbol) Function(name string) *Symbol { + if function := pack.Functions[name]; function != nil { + return function + } + return pack.Functions[exportedGoName(name)] +} + +func (class *ClassSymbol) Method(name string) *Symbol { + if method := class.Methods[name]; method != nil { + return method + } + return class.Methods[exportedGoName(name)] +} + +func (class *ClassSymbol) Field(name string) *Symbol { + if field := class.Fields[name]; field != nil { + return field + } + return class.Fields[exportedGoName(name)] +} + +func metadataFunctionType(function FunctionMetadata, alias string, pack *PackageSymbol, enclosing []string) FunctionType { + params := append(append([]string{}, enclosing...), function.TypeParams...) + values := make([]Type, len(function.Params)) + for index, param := range function.Params { + values[index] = resolvePackageType(param.Type, alias, pack, params) + } + return FunctionType{TypeParams: function.TypeParams, Params: values, Result: resolvePackageType(function.Result, alias, pack, params), Effects: function.Effects} +} + +func resolvePackageType(text, alias string, pack *PackageSymbol, params []string) Type { + typ, err := ParseType(text) + if err != nil { + return UnknownType{} + } + paramSet := map[string]bool{} + for _, param := range params { + paramSet[param] = true + } + typ = resolveTypeParameters(typ, paramSet) + var resolve func(Type) Type + resolve = func(value Type) Type { + switch item := value.(type) { + case NamedType: + if class := pack.Classes[item.Name]; class != nil { + return ImportedClassType{Package: alias, Class: class} + } + return item + case NullableType: + return NullableType{Element: resolve(item.Element)} + case GoPointerType: + return GoPointerType{Element: resolve(item.Element)} + case GenericType: + args := make([]Type, len(item.Args)) + for index, arg := range item.Args { + args[index] = resolve(arg) + } + return GenericType{Base: resolve(item.Base), Args: args} + case FunctionType: + args := make([]Type, len(item.Params)) + for index, arg := range item.Params { + args[index] = resolve(arg) + } + return FunctionType{TypeParams: item.TypeParams, Params: args, Result: resolve(item.Result), Effects: item.Effects} + default: + return value + } + } + return resolve(typ) +} diff --git a/internal/lang/metadata_test.go b/internal/lang/metadata_test.go new file mode 100644 index 0000000..f55ac1b --- /dev/null +++ b/internal/lang/metadata_test.go @@ -0,0 +1,64 @@ +package lang + +import ( + "strings" + "testing" +) + +func TestImportedGotlinClassesPreserveReferenceSemantics(t *testing.T) { + library, err := Parse(`package platform +class Lifecycle { fun stopping(): Boolean { return false } } +fun createLifecycle(): Lifecycle { return Lifecycle() }`) + if err != nil { + t.Fatal(err) + } + metadata, err := BuildPackageMetadata(library, "example/platform") + if err != nil { + t.Fatal(err) + } + consumer, err := Parse(`package service +import platform "example/platform" +class Worker(val lifecycle: platform.Lifecycle) { + fun stopped(): Boolean { return lifecycle.stopping() } +} +fun create(): platform.Lifecycle { return platform.createLifecycle() }`) + if err != nil { + t.Fatal(err) + } + semantic, diagnostics := AnalyzeWithMetadata(consumer, []*PackageMetadata{metadata}) + if len(diagnostics) != 0 { + t.Fatalf("diagnostics: %#v", diagnostics) + } + fieldType := semantic.ClassInfo["Worker"].Fields["lifecycle"].Type + if _, ok := fieldType.(ImportedClassType); !ok { + t.Fatalf("type = %#v", fieldType) + } + output, err := GenerateGoWithMetadata(consumer, []*PackageMetadata{metadata}) + if err != nil { + t.Fatal(err) + } + for _, expected := range []string{"lifecycle *platform.Lifecycle", "func create() *platform.Lifecycle", "platform.CreateLifecycle()"} { + if !strings.Contains(string(output), expected) { + t.Fatalf("missing %q:\n%s", expected, output) + } + } +} + +func TestImportedGotlinClassRejectsExplicitPointer(t *testing.T) { + library, err := Parse(`package platform class Lifecycle`) + if err != nil { + t.Fatal(err) + } + metadata, err := BuildPackageMetadata(library, "example/platform") + if err != nil { + t.Fatal(err) + } + consumer, err := Parse(`package service import platform "example/platform" fun use(value: *platform.Lifecycle) {}`) + if err != nil { + t.Fatal(err) + } + _, diagnostics := AnalyzeWithMetadata(consumer, []*PackageMetadata{metadata}) + if len(diagnostics) == 0 || !strings.Contains(diagnostics[0].Message, "already reference-valued") { + t.Fatalf("diagnostics: %#v", diagnostics) + } +} diff --git a/internal/lang/symbols.go b/internal/lang/symbols.go index be312cf..a69a8a9 100644 --- a/internal/lang/symbols.go +++ b/internal/lang/symbols.go @@ -56,6 +56,14 @@ type ClassSymbol struct { Methods map[string]*Symbol } +type PackageSymbol struct { + Alias string + Metadata *PackageMetadata + Classes map[string]*ClassSymbol + Functions map[string]*Symbol + Enums map[string]EnumDecl +} + type SemanticProgram struct { Syntax *Program Global *Scope @@ -69,6 +77,7 @@ type SemanticProgram struct { Mappings *mappingState Diagnostics []SemanticDiagnostic FunctionEffects map[string]Effect + Packages map[string]*PackageSymbol } func Analyze(program *Program) (*SemanticProgram, error) { @@ -80,7 +89,19 @@ func Analyze(program *Program) (*SemanticProgram, error) { } func AnalyzeWithContext(program *Program, additional []*Program) (*SemanticProgram, []SemanticDiagnostic) { - semantic, err := analyzeProgram(program, additional) + return analyzeWithMetadata(program, additional, nil) +} + +func AnalyzeWithContextAndMetadata(program *Program, additional []*Program, metadata []*PackageMetadata) (*SemanticProgram, []SemanticDiagnostic) { + return analyzeWithMetadata(program, additional, metadata) +} + +func AnalyzeWithMetadata(program *Program, metadata []*PackageMetadata) (*SemanticProgram, []SemanticDiagnostic) { + return analyzeWithMetadata(program, nil, metadata) +} + +func analyzeWithMetadata(program *Program, additional []*Program, metadata []*PackageMetadata) (*SemanticProgram, []SemanticDiagnostic) { + semantic, err := analyzeProgram(program, additional, metadata) if err != nil { return semantic, []SemanticDiagnostic{diagnosticForError("semantic-error", err)} } @@ -91,7 +112,7 @@ func AnalyzeWithContext(program *Program, additional []*Program) (*SemanticProgr return semantic, nil } -func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, error) { +func analyzeProgram(program *Program, additional []*Program, metadata []*PackageMetadata) (*SemanticProgram, error) { semantic := &SemanticProgram{ Syntax: program, Global: NewScope(nil), @@ -103,6 +124,7 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, GoPackages: map[string]*gotypes.Package{}, Mappings: &mappingState{functions: map[string]string{}}, FunctionEffects: map[string]Effect{}, + Packages: map[string]*PackageSymbol{}, } programs := append([]*Program{program}, additional...) for _, source := range programs { @@ -132,6 +154,9 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, name = defaultImportAlias(imported) } semantic.Imports[name] = true + if packageMetadata := metadataForImport(imported, metadata); packageMetadata != nil { + semantic.Packages[name] = packageSymbolFromMetadata(name, packageMetadata) + } if importedPackage, err := importGoPackage(imported); err == nil { semantic.GoPackages[name] = importedPackage } @@ -210,7 +235,7 @@ func (semantic *SemanticProgram) ResolveType(text string) (Type, error) { if err != nil { return nil, err } - resolved := resolveClassTypes(typ, semantic.ClassInfo) + resolved := resolveImportedTypes(resolveClassTypes(typ, semantic.ClassInfo), semantic.Packages) if err := validateNoClassPointer(resolved); err != nil { return nil, err } @@ -234,7 +259,7 @@ func (semantic *SemanticProgram) ResolveTypeRefWithParams(ref TypeRef, params [] for _, param := range params { paramSet[param] = true } - resolved := resolveClassTypes(resolveTypeParameters(typ, paramSet), semantic.ClassInfo) + resolved := resolveImportedTypes(resolveClassTypes(resolveTypeParameters(typ, paramSet), semantic.ClassInfo), semantic.Packages) if err := validateNoClassPointer(resolved); err != nil { return nil, err } @@ -244,8 +269,11 @@ func (semantic *SemanticProgram) ResolveTypeRefWithParams(ref TypeRef, params [] func validateNoClassPointer(typ Type) error { switch value := typ.(type) { case GoPointerType: - if class, ok := value.Element.(ClassType); ok { + switch class := value.Element.(type) { + case ClassType: return fmt.Errorf("Gotlin class %s is already reference-valued; remove '*'", class.Class.Name) + case ImportedClassType: + return fmt.Errorf("Gotlin class %s is already reference-valued; remove '*'", class.String()) } return validateNoClassPointer(value.Element) case NullableType: diff --git a/internal/lang/type_checker.go b/internal/lang/type_checker.go index 6110263..f25922e 100644 --- a/internal/lang/type_checker.go +++ b/internal/lang/type_checker.go @@ -44,6 +44,13 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) case CallExpr: if selector, ok := value.Callee.(SelectorExpr); ok { if receiver, ok := selector.Receiver.(IdentExpr); ok { + if pack := semantic.Packages[receiver.Name]; pack != nil { + if function := pack.Function(selector.Name); function != nil { + if signature, ok := function.Type.(FunctionType); ok { + return signature.Result + } + } + } if semantic.Imports[receiver.Name] { if function, ok := semantic.goSelectorType(receiver.Name, selector.Name).(FunctionType); ok { return function.Result @@ -87,7 +94,7 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) } } if class, bindings := classInstance(receiverType); class != nil { - if method, ok := class.Methods[selector.Name]; ok { + if method := class.Method(selector.Name); method != nil { if function, ok := method.Type.(FunctionType); ok { return substituteType(function.Result, bindings) } @@ -178,7 +185,7 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment) } } if class, bindings := classInstance(semantic.TypeOf(value.Receiver, environment)); class != nil { - if field, ok := class.Fields[value.Name]; ok { + if field := class.Field(value.Name); field != nil { return substituteType(field.Type, bindings) } } @@ -233,6 +240,8 @@ func classInstance(typ Type) (*ClassSymbol, map[string]Type) { switch value := typ.(type) { case ClassType: return value.Class, map[string]Type{} + case ImportedClassType: + return value.Class, map[string]Type{} case NullableType: return classInstance(value.Element) case GenericType: @@ -245,6 +254,15 @@ func classInstance(typ Type) (*ClassSymbol, map[string]Type) { } return class.Class, bindings } + if class, ok := value.Base.(ImportedClassType); ok { + bindings := map[string]Type{} + for index, name := range class.Class.TypeParams { + if index < len(value.Args) { + bindings[name] = value.Args[index] + } + } + return class.Class, bindings + } } return nil, nil } diff --git a/internal/lang/types.go b/internal/lang/types.go index 2118241..4998ff8 100644 --- a/internal/lang/types.go +++ b/internal/lang/types.go @@ -43,6 +43,14 @@ type ClassType struct{ Class *ClassSymbol } func (ClassType) typeNode() {} func (t ClassType) String() string { return t.Class.Name } +type ImportedClassType struct { + Package string + Class *ClassSymbol +} + +func (ImportedClassType) typeNode() {} +func (t ImportedClassType) String() string { return t.Package + "." + t.Class.Name } + type NullableType struct{ Element Type } func (NullableType) typeNode() {} @@ -185,6 +193,39 @@ func resolveClassTypes(typ Type, classes map[string]*ClassSymbol) Type { } } +func resolveImportedTypes(typ Type, packages map[string]*PackageSymbol) Type { + switch value := typ.(type) { + case NamedType: + parts := strings.Split(value.Name, ".") + if len(parts) == 2 { + if pack := packages[parts[0]]; pack != nil { + if class := pack.Classes[parts[1]]; class != nil { + return ImportedClassType{Package: parts[0], Class: class} + } + } + } + return value + case NullableType: + return NullableType{Element: resolveImportedTypes(value.Element, packages)} + case GoPointerType: + return GoPointerType{Element: resolveImportedTypes(value.Element, packages)} + case GenericType: + args := make([]Type, len(value.Args)) + for index, arg := range value.Args { + args[index] = resolveImportedTypes(arg, packages) + } + return GenericType{Base: resolveImportedTypes(value.Base, packages), Args: args} + case FunctionType: + params := make([]Type, len(value.Params)) + for index, param := range value.Params { + params[index] = resolveImportedTypes(param, packages) + } + return FunctionType{TypeParams: value.TypeParams, Params: params, Result: resolveImportedTypes(value.Result, packages), Effects: value.Effects} + default: + return typ + } +} + func resolveTypeParameters(typ Type, params map[string]bool) Type { switch value := typ.(type) { case NamedType: @@ -221,6 +262,19 @@ func resolveTypeParameters(typ Type, params map[string]bool) Type { func typeEqual(left, right Type) bool { return left.String() == right.String() } +func isImportedClassType(typ Type) bool { + switch value := typ.(type) { + case ImportedClassType: + return true + case NullableType: + return isImportedClassType(value.Element) + case GenericType: + _, ok := value.Base.(ImportedClassType) + return ok + } + return false +} + func substituteType(typ Type, bindings map[string]Type) Type { switch value := typ.(type) { case TypeParameterType: @@ -284,10 +338,12 @@ func renderGoType(typ Type) string { return "" case ClassType: return "*" + value.Class.Name + case ImportedClassType: + return "*" + value.Package + "." + value.Class.Name case NullableType: element := renderGoType(value.Element) switch value.Element.(type) { - case ClassType, GoPointerType: + case ClassType, ImportedClassType, GoPointerType: return element } if element == "error" || element == "any" { @@ -334,6 +390,9 @@ func renderGoType(typ Type) string { if class, ok := value.Base.(ClassType); ok { return "*" + class.Class.Name + "[" + strings.Join(args, ", ") + "]" } + if class, ok := value.Base.(ImportedClassType); ok { + return "*" + class.Package + "." + class.Class.Name + "[" + strings.Join(args, ", ") + "]" + } return base + "[" + strings.Join(args, ", ") + "]" case TypeParameterType: return value.Name