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

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