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
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

View file

@ -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 {

View file

@ -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] <input.gt>")
}
@ -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")
}

View file

@ -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 {

View file

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

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
}
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:

View file

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

View file

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