Preserve Gotlin semantics across packages
This commit is contained in:
parent
5b30e49486
commit
acb42a5702
10 changed files with 525 additions and 34 deletions
12
README.md
12
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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
210
internal/lang/metadata.go
Normal 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)
|
||||
}
|
||||
64
internal/lang/metadata_test.go
Normal file
64
internal/lang/metadata_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue