Preserve Gotlin semantics across packages

This commit is contained in:
pavel 2026-08-27 22:18:45 +02:00
commit acb42a5702
10 changed files with 525 additions and 34 deletions

View file

@ -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 symbols, structural semantic types, resolved expression meanings, and typed
HIR before Go emission. Class reference semantics live in `ClassType`; only 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`. 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 ## 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 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: Run directly:
```bash ```bash

View file

@ -326,27 +326,43 @@ func (s *server) buildDocumentState(uri, text string) documentState {
return state return state
} }
state.program = program state.program = program
programs, symbols := s.packageContext(uri, program.PackagePath, text) programs, symbols, metadata := s.packageContext(uri, program.PackagePath, text)
_, semanticDiagnostics := lang.AnalyzeWithContext(program, programs) _, semanticDiagnostics := lang.AnalyzeWithContextAndMetadata(program, programs, metadata)
state.symbols = indexSymbols(text, program) state.symbols = indexSymbols(text, program)
state.packageSymbols = symbols state.packageSymbols = symbols
state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics) state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics)
return state 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) path, ok := filePathFromURI(currentURI)
if !ok { 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 == "" { if root == "" {
root = filepath.Dir(path) root = filepath.Dir(path)
} }
var programs []*lang.Program var programs []*lang.Program
var symbols []symbol var symbols []symbol
var metadata []*lang.PackageMetadata
_ = filepath.WalkDir(root, func(candidate string, entry os.DirEntry, err error) error { _ = 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 return nil
} }
candidateURI := fileURI(candidate) candidateURI := fileURI(candidate)
@ -374,7 +390,20 @@ func (s *server) packageContext(currentURI, packagePath, currentText string) ([]
} }
return nil 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 { func nearestModuleRoot(directory string) string {

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@ -32,6 +33,10 @@ func runBuild(args []string) {
fs := flag.NewFlagSet("build", flag.ExitOnError) fs := flag.NewFlagSet("build", flag.ExitOnError)
srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable") srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable")
outPath := fs.String("o", "", "output file path") 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() { fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] <input.gt>") fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] <input.gt>")
} }
@ -48,7 +53,7 @@ func runBuild(args []string) {
} }
inputPath := fs.Arg(0) inputPath := fs.Arg(0)
goSrc := compileFiles(fs.Args(), !*srcOnly) goSrc := compileFilesWithMetadata(fs.Args(), !*srcOnly, metadataInputs, *metadataOutput, *metadataPackage)
if *srcOnly { if *srcOnly {
if *outPath == "" { if *outPath == "" {
@ -117,6 +122,10 @@ func compileFile(inputPath string, forceMain bool) string {
} }
func compileFiles(inputPaths []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{} program := &lang.Program{}
var firstSource string var firstSource string
for _, inputPath := range inputPaths { for _, inputPath := range inputPaths {
@ -146,12 +155,27 @@ func compileFiles(inputPaths []string, forceMain bool) string {
firstSource = string(src) 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 goSrc []byte
var err error var err error
if forceMain { if forceMain {
goSrc, err = lang.GenerateGoMain(program) goSrc, err = lang.GenerateGoMainWithMetadata(program, metadata)
} else { } else {
goSrc, err = lang.GenerateGo(program) goSrc, err = lang.GenerateGoWithMetadata(program, metadata)
} }
if err != nil { if err != nil {
fail(err) fail(err)
@ -162,6 +186,30 @@ func compileFiles(inputPaths []string, forceMain bool) string {
return string(goSrc) 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 { func buildExecutable(goSrc string, outputPath string) error {
tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go") tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go")
if err != nil { if err != nil {
@ -202,7 +250,7 @@ func normalizeBuildArgs(args []string) ([]string, error) {
switch { switch {
case arg == "-src": case arg == "-src":
flags = append(flags, arg) flags = append(flags, arg)
case arg == "-o": case arg == "-o" || arg == "-metadata" || arg == "-metadata-output" || arg == "-metadata-package":
if i+1 >= len(args) { if i+1 >= len(args) {
return nil, fmt.Errorf("missing value for -o") return nil, fmt.Errorf("missing value for -o")
} }

View file

@ -11,17 +11,25 @@ import (
) )
func GenerateGo(program *Program) ([]byte, error) { func GenerateGo(program *Program) ([]byte, error) {
return generateGo(program, "") return generateGo(program, "", nil)
} }
func GenerateGoMain(program *Program) ([]byte, error) { func GenerateGoMain(program *Program) ([]byte, error) {
return generateGo(program, "main") return generateGo(program, "main", nil)
} }
func generateGo(program *Program, packageOverride string) ([]byte, error) { func GenerateGoWithMetadata(program *Program, metadata []*PackageMetadata) ([]byte, error) {
semantic, err := Analyze(program) return generateGo(program, "", metadata)
if err != nil { }
return nil, err
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} g := goGenerator{semantic: semantic}
if err := g.program(program, packageOverride); err != nil { 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] { if ident, ok := e.Receiver.(IdentExpr); ok && g.semantic.Imports[ident.Name] {
name = exportedGoName(name) name = exportedGoName(name)
} else if class, ok := g.classForType(g.exprType(e.Receiver)); ok { } else if class, ok := g.classForType(g.exprType(e.Receiver)); ok {
for _, field := range class.Fields { resolvedType, _ := g.semantic.ResolveType(g.exprType(e.Receiver))
if field.Name == name && class.Data && !field.Private { if isImportedClassType(resolvedType) {
name = exportedGoName(name) name = exportedGoName(name)
break } 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 { } 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) { func (g *goGenerator) classForType(typ string) (ClassDecl, bool) {
typ = strings.TrimPrefix(typ, "*") resolved, err := g.semantic.ResolveType(typ)
typ = strings.TrimSuffix(typ, "?") if err != nil {
class, ok := g.semantic.Classes[typ] return ClassDecl{}, false
return class, ok }
class, _ := classInstance(resolved)
if class == nil || class.Decl == nil {
return ClassDecl{}, false
}
return *class.Decl, true
} }
func (g *goGenerator) isDefined(name string) bool { func (g *goGenerator) isDefined(name string) bool {

View file

@ -590,6 +590,11 @@ func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning,
return MappingExpression, nil return MappingExpression, nil
} }
if receiver, ok := selector.Receiver.(IdentExpr); ok { 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" { if receiver.Name == "Result" {
return EnumConstructionExpr, nil return EnumConstructionExpr, nil
} }
@ -601,7 +606,7 @@ func (resolver *semanticResolver) meaning(expr Expr, scope *Scope) (ExprMeaning,
return GoCallExpr, nil return GoCallExpr, nil
} }
if class, _ := classInstance(ResolvedType(selector.Receiver)); class != 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 return MethodCallExpr, method
} }
} }

210
internal/lang/metadata.go Normal file
View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -56,6 +56,14 @@ type ClassSymbol struct {
Methods map[string]*Symbol 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 { type SemanticProgram struct {
Syntax *Program Syntax *Program
Global *Scope Global *Scope
@ -69,6 +77,7 @@ type SemanticProgram struct {
Mappings *mappingState Mappings *mappingState
Diagnostics []SemanticDiagnostic Diagnostics []SemanticDiagnostic
FunctionEffects map[string]Effect FunctionEffects map[string]Effect
Packages map[string]*PackageSymbol
} }
func Analyze(program *Program) (*SemanticProgram, error) { func Analyze(program *Program) (*SemanticProgram, error) {
@ -80,7 +89,19 @@ func Analyze(program *Program) (*SemanticProgram, error) {
} }
func AnalyzeWithContext(program *Program, additional []*Program) (*SemanticProgram, []SemanticDiagnostic) { 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 { if err != nil {
return semantic, []SemanticDiagnostic{diagnosticForError("semantic-error", err)} return semantic, []SemanticDiagnostic{diagnosticForError("semantic-error", err)}
} }
@ -91,7 +112,7 @@ func AnalyzeWithContext(program *Program, additional []*Program) (*SemanticProgr
return semantic, nil return semantic, nil
} }
func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram, error) { func analyzeProgram(program *Program, additional []*Program, metadata []*PackageMetadata) (*SemanticProgram, error) {
semantic := &SemanticProgram{ semantic := &SemanticProgram{
Syntax: program, Syntax: program,
Global: NewScope(nil), Global: NewScope(nil),
@ -103,6 +124,7 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram,
GoPackages: map[string]*gotypes.Package{}, GoPackages: map[string]*gotypes.Package{},
Mappings: &mappingState{functions: map[string]string{}}, Mappings: &mappingState{functions: map[string]string{}},
FunctionEffects: map[string]Effect{}, FunctionEffects: map[string]Effect{},
Packages: map[string]*PackageSymbol{},
} }
programs := append([]*Program{program}, additional...) programs := append([]*Program{program}, additional...)
for _, source := range programs { for _, source := range programs {
@ -132,6 +154,9 @@ func analyzeProgram(program *Program, additional []*Program) (*SemanticProgram,
name = defaultImportAlias(imported) name = defaultImportAlias(imported)
} }
semantic.Imports[name] = true semantic.Imports[name] = true
if packageMetadata := metadataForImport(imported, metadata); packageMetadata != nil {
semantic.Packages[name] = packageSymbolFromMetadata(name, packageMetadata)
}
if importedPackage, err := importGoPackage(imported); err == nil { if importedPackage, err := importGoPackage(imported); err == nil {
semantic.GoPackages[name] = importedPackage semantic.GoPackages[name] = importedPackage
} }
@ -210,7 +235,7 @@ func (semantic *SemanticProgram) ResolveType(text string) (Type, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
resolved := resolveClassTypes(typ, semantic.ClassInfo) resolved := resolveImportedTypes(resolveClassTypes(typ, semantic.ClassInfo), semantic.Packages)
if err := validateNoClassPointer(resolved); err != nil { if err := validateNoClassPointer(resolved); err != nil {
return nil, err return nil, err
} }
@ -234,7 +259,7 @@ func (semantic *SemanticProgram) ResolveTypeRefWithParams(ref TypeRef, params []
for _, param := range params { for _, param := range params {
paramSet[param] = true 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 { if err := validateNoClassPointer(resolved); err != nil {
return nil, err return nil, err
} }
@ -244,8 +269,11 @@ func (semantic *SemanticProgram) ResolveTypeRefWithParams(ref TypeRef, params []
func validateNoClassPointer(typ Type) error { func validateNoClassPointer(typ Type) error {
switch value := typ.(type) { switch value := typ.(type) {
case GoPointerType: 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) 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) return validateNoClassPointer(value.Element)
case NullableType: case NullableType:

View file

@ -44,6 +44,13 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment)
case CallExpr: case CallExpr:
if selector, ok := value.Callee.(SelectorExpr); ok { if selector, ok := value.Callee.(SelectorExpr); ok {
if receiver, ok := selector.Receiver.(IdentExpr); 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 semantic.Imports[receiver.Name] {
if function, ok := semantic.goSelectorType(receiver.Name, selector.Name).(FunctionType); ok { if function, ok := semantic.goSelectorType(receiver.Name, selector.Name).(FunctionType); ok {
return function.Result return function.Result
@ -87,7 +94,7 @@ func (semantic *SemanticProgram) TypeOf(expr Expr, environment TypeEnvironment)
} }
} }
if class, bindings := classInstance(receiverType); class != nil { 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 { if function, ok := method.Type.(FunctionType); ok {
return substituteType(function.Result, bindings) 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 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) return substituteType(field.Type, bindings)
} }
} }
@ -233,6 +240,8 @@ func classInstance(typ Type) (*ClassSymbol, map[string]Type) {
switch value := typ.(type) { switch value := typ.(type) {
case ClassType: case ClassType:
return value.Class, map[string]Type{} return value.Class, map[string]Type{}
case ImportedClassType:
return value.Class, map[string]Type{}
case NullableType: case NullableType:
return classInstance(value.Element) return classInstance(value.Element)
case GenericType: case GenericType:
@ -245,6 +254,15 @@ func classInstance(typ Type) (*ClassSymbol, map[string]Type) {
} }
return class.Class, bindings 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 return nil, nil
} }

View file

@ -43,6 +43,14 @@ type ClassType struct{ Class *ClassSymbol }
func (ClassType) typeNode() {} func (ClassType) typeNode() {}
func (t ClassType) String() string { return t.Class.Name } 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 } type NullableType struct{ Element Type }
func (NullableType) typeNode() {} 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 { func resolveTypeParameters(typ Type, params map[string]bool) Type {
switch value := typ.(type) { switch value := typ.(type) {
case NamedType: 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 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 { func substituteType(typ Type, bindings map[string]Type) Type {
switch value := typ.(type) { switch value := typ.(type) {
case TypeParameterType: case TypeParameterType:
@ -284,10 +338,12 @@ func renderGoType(typ Type) string {
return "" return ""
case ClassType: case ClassType:
return "*" + value.Class.Name return "*" + value.Class.Name
case ImportedClassType:
return "*" + value.Package + "." + value.Class.Name
case NullableType: case NullableType:
element := renderGoType(value.Element) element := renderGoType(value.Element)
switch value.Element.(type) { switch value.Element.(type) {
case ClassType, GoPointerType: case ClassType, ImportedClassType, GoPointerType:
return element return element
} }
if element == "error" || element == "any" { if element == "error" || element == "any" {
@ -334,6 +390,9 @@ func renderGoType(typ Type) string {
if class, ok := value.Base.(ClassType); ok { if class, ok := value.Base.(ClassType); ok {
return "*" + class.Class.Name + "[" + strings.Join(args, ", ") + "]" 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, ", ") + "]" return base + "[" + strings.Join(args, ", ") + "]"
case TypeParameterType: case TypeParameterType:
return value.Name return value.Name