Expand Gotlin language and tooling

This commit is contained in:
pavel 2026-08-27 01:46:57 +02:00
commit de1262b4cf
41 changed files with 6059 additions and 379 deletions

View file

@ -2,6 +2,7 @@ package lang
import (
"fmt"
"strconv"
"strings"
)
@ -44,12 +45,92 @@ func (p *parser) parseProgram() (*Program, error) {
return nil, err
}
prog.Interfaces = append(prog.Interfaces, decl)
case p.check(tokenClass):
case p.check(tokenEnum):
decl, err := p.parseEnum()
if err != nil {
return nil, err
}
prog.Enums = append(prog.Enums, decl)
case p.check(tokenClass) || p.check(tokenData):
decl, err := p.parseClass()
if err != nil {
return nil, err
}
prog.Classes = append(prog.Classes, decl)
case p.match(tokenAt):
annotation, err := p.expect(tokenIdent, "expected annotation name")
if err != nil {
return nil, err
}
if annotation.lexeme == "embed" {
if _, err := p.expect(tokenLParen, "expected '(' after embed"); err != nil {
return nil, err
}
path, err := p.expect(tokenString, "expected embed path")
if err != nil {
return nil, err
}
if _, err := p.expect(tokenRParen, "expected ')' after embed path"); err != nil {
return nil, err
}
if _, err := p.expect(tokenVal, "expected 'val' after embed annotation"); err != nil {
return nil, err
}
name, err := p.expect(tokenIdent, "expected embedded value name")
if err != nil {
return nil, err
}
if _, err := p.expect(tokenColon, "expected ':' after embedded value name"); err != nil {
return nil, err
}
typ, err := p.parseTypeRef()
if err != nil {
return nil, err
}
prog.Embeds = append(prog.Embeds, EmbedDecl{Path: strings.Trim(path.lexeme, "\""), Name: name.lexeme, Type: typ})
continue
}
switch annotation.lexeme {
case "jsonNaming":
if _, err := p.expect(tokenLParen, "expected '(' after jsonNaming"); err != nil {
return nil, err
}
policy, err := p.expect(tokenIdent, "expected JSON naming policy")
if err != nil {
return nil, err
}
if _, err := p.expect(tokenRParen, "expected ')' after JSON naming policy"); err != nil {
return nil, err
}
if !p.check(tokenData) {
return nil, fmt.Errorf("jsonNaming is only valid on data classes")
}
decl, err := p.parseClass()
if err != nil {
return nil, err
}
decl.JSONNaming = policy.lexeme
prog.Classes = append(prog.Classes, decl)
case "table":
table, err := p.parseStringAnnotationArgument("table")
if err != nil {
return nil, err
}
if !validSQLName(table) {
return nil, fmt.Errorf("invalid SQL table name %q", table)
}
if !p.check(tokenData) {
return nil, fmt.Errorf("table is only valid on data classes")
}
decl, err := p.parseClass()
if err != nil {
return nil, err
}
decl.Table = table
prog.Classes = append(prog.Classes, decl)
default:
return nil, fmt.Errorf("unsupported annotation %q", annotation.lexeme)
}
case p.check(tokenWorker):
decl, err := p.parseWorker()
if err != nil {
@ -70,6 +151,64 @@ func (p *parser) parseProgram() (*Program, error) {
return prog, nil
}
func (p *parser) parseEnum() (EnumDecl, error) {
if _, err := p.expect(tokenEnum, "expected 'enum'"); err != nil {
return EnumDecl{}, err
}
name, err := p.expect(tokenIdent, "expected enum name")
if err != nil {
return EnumDecl{}, err
}
if _, err := p.expect(tokenLBrace, "expected '{' after enum name"); err != nil {
return EnumDecl{}, err
}
var variants []EnumVariant
seen := map[string]bool{}
for !p.check(tokenRBrace) && !p.check(tokenEOF) {
variant, err := p.expect(tokenIdent, "expected enum variant")
if err != nil {
return EnumDecl{}, err
}
if seen[variant.lexeme] {
return EnumDecl{}, fmt.Errorf("duplicate enum variant %s", variant.lexeme)
}
seen[variant.lexeme] = true
var payload []string
if p.match(tokenLParen) {
if !p.check(tokenRParen) {
for {
typ, err := p.parseTypeRef()
if err != nil {
return EnumDecl{}, err
}
payload = append(payload, typ)
if !p.match(tokenComma) {
break
}
}
}
if _, err := p.expect(tokenRParen, "expected ')' after enum payload"); err != nil {
return EnumDecl{}, err
}
}
stringValue := ""
if p.match(tokenAssign) {
value, err := p.expect(tokenString, "expected string enum value")
if err != nil {
return EnumDecl{}, err
}
stringValue = strings.Trim(value.lexeme, "\"")
}
variants = append(variants, EnumVariant{Name: variant.lexeme, PayloadTypes: payload, StringValue: stringValue})
p.match(tokenComma)
p.match(tokenSemicolon)
}
if _, err := p.expect(tokenRBrace, "expected '}' after enum"); err != nil {
return EnumDecl{}, err
}
return EnumDecl{Name: name.lexeme, Variants: variants}, nil
}
func (p *parser) parsePackageDecl() (string, error) {
path, err := p.parseImportPath()
if err != nil {
@ -151,6 +290,7 @@ func (p *parser) parseInterface() (InterfaceDecl, error) {
}
func (p *parser) parseClass() (ClassDecl, error) {
data := p.match(tokenData)
if _, err := p.expect(tokenClass, "expected 'class'"); err != nil {
return ClassDecl{}, err
}
@ -174,7 +314,7 @@ func (p *parser) parseClass() (ClassDecl, error) {
return ClassDecl{}, err
}
if !p.match(tokenLBrace) {
return ClassDecl{Name: name.lexeme, Fields: fields, Parents: parents, Methods: nil}, nil
return ClassDecl{Name: name.lexeme, Data: data, Fields: fields, Parents: parents, Methods: nil}, nil
}
var methods []FunctionDecl
@ -195,7 +335,7 @@ func (p *parser) parseClass() (ClassDecl, error) {
return ClassDecl{}, err
}
return ClassDecl{Name: name.lexeme, Fields: fields, Parents: parents, Methods: methods}, nil
return ClassDecl{Name: name.lexeme, Data: data, Fields: fields, Parents: parents, Methods: methods}, nil
}
func (p *parser) parseWorker() (WorkerDecl, error) {
@ -284,6 +424,41 @@ func (p *parser) parseClassFields() ([]FieldDecl, error) {
return fields, nil
}
for {
column := ""
id := false
generated := false
for p.match(tokenAt) {
annotation, err := p.expect(tokenIdent, "expected field annotation name")
if err != nil {
return nil, err
}
switch annotation.lexeme {
case "id":
if id {
return nil, fmt.Errorf("duplicate id annotation")
}
id = true
case "generated":
if generated {
return nil, fmt.Errorf("duplicate generated annotation")
}
generated = true
case "column":
if column != "" {
return nil, fmt.Errorf("duplicate column annotation")
}
column, err = p.parseStringAnnotationArgument("column")
if err != nil {
return nil, err
}
if !validSQLName(column) {
return nil, fmt.Errorf("invalid SQL column name %q", column)
}
default:
return nil, fmt.Errorf("unsupported field annotation %q", annotation.lexeme)
}
}
private := p.match(tokenPrivate)
mutable := false
switch {
case p.match(tokenVal):
@ -305,13 +480,49 @@ func (p *parser) parseClassFields() ([]FieldDecl, error) {
if err != nil {
return nil, err
}
fields = append(fields, FieldDecl{Mutable: mutable, Name: name.lexeme, Type: typ})
fields = append(fields, FieldDecl{Mutable: mutable, Private: private, Name: name.lexeme, Type: typ, Column: column, ID: id, Generated: generated})
if !p.match(tokenComma) {
return fields, nil
}
}
}
func (p *parser) parseStringAnnotationArgument(name string) (string, error) {
if _, err := p.expect(tokenLParen, "expected '(' after "+name); err != nil {
return "", err
}
value, err := p.expect(tokenString, "expected string argument for "+name)
if err != nil {
return "", err
}
if _, err := p.expect(tokenRParen, "expected ')' after "+name+" argument"); err != nil {
return "", err
}
decoded, err := strconv.Unquote(value.lexeme)
if err != nil {
return "", fmt.Errorf("invalid string argument for %s: %w", name, err)
}
return decoded, nil
}
func validSQLName(name string) bool {
if name == "" {
return false
}
for i, r := range name {
if i == 0 {
if !isIdentStart(r) {
return false
}
continue
}
if !isIdentPart(r) {
return false
}
}
return true
}
func (p *parser) parseFunction() (FunctionDecl, error) {
signature, err := p.parseFunctionSignature()
if err != nil {
@ -448,12 +659,25 @@ func (p *parser) parseStmt() (Stmt, error) {
return nil, fmt.Errorf("'go' expects a function call expression")
}
return GoStmt{Value: expr}, nil
case p.match(tokenDefer):
expr, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, ok := expr.(CallExpr); !ok {
return nil, fmt.Errorf("'defer' expects a function call expression")
}
return DeferStmt{Value: expr}, nil
case p.match(tokenIf):
return p.parseIf()
case p.match(tokenWhile):
return p.parseWhile()
case p.match(tokenFor):
return p.parseForEach()
case p.match(tokenSelect):
return p.parseSelect()
case p.match(tokenMatch):
return p.parseMatch()
case p.match(tokenTry):
return p.parseTryCatch()
case p.check(tokenIdent) && (p.peekN(1).kind == tokenAssign || p.peekN(1).kind == tokenComma || p.peekN(1).kind == tokenPlusAssign):
@ -466,7 +690,7 @@ func (p *parser) parseStmt() (Stmt, error) {
if err != nil {
return nil, err
}
return AddAssignStmt{Name: name.lexeme, Value: value}, nil
return AddAssignStmt{Name: name.lexeme, Pos: name.pos, Value: value}, nil
}
names, err := p.parseNameList()
if err != nil {
@ -480,9 +704,18 @@ func (p *parser) parseStmt() (Stmt, error) {
return nil, err
}
if len(names) == 1 {
return AssignStmt{Name: names[0], Value: value}, nil
return AssignStmt{Name: names[0].lexeme, Pos: names[0].pos, Value: value}, nil
}
return MultiAssignStmt{Names: names, Value: value}, nil
assign := MultiAssignStmt{
Names: make([]string, 0, len(names)),
Positions: make([]int, 0, len(names)),
Value: value,
}
for _, name := range names {
assign.Names = append(assign.Names, name.lexeme)
assign.Positions = append(assign.Positions, name.pos)
}
return assign, nil
default:
expr, err := p.parseExpr(0)
if err != nil {
@ -492,6 +725,68 @@ func (p *parser) parseStmt() (Stmt, error) {
}
}
func (p *parser) parseMatch() (Stmt, error) {
if _, err := p.expect(tokenLParen, "expected '(' after match"); err != nil {
return nil, err
}
value, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, err := p.expect(tokenRParen, "expected ')' after match value"); err != nil {
return nil, err
}
if _, err := p.expect(tokenLBrace, "expected '{' after match value"); err != nil {
return nil, err
}
var cases []MatchCase
for !p.check(tokenRBrace) && !p.check(tokenEOF) {
enumName, err := p.expect(tokenIdent, "expected enum name in match case")
if err != nil {
return nil, err
}
if _, err := p.expect(tokenDoubleColon, "expected '::' in match case"); err != nil {
return nil, err
}
variant, err := p.expect(tokenIdent, "expected variant name")
if err != nil {
return nil, err
}
var bindings []string
if p.match(tokenLParen) {
if !p.check(tokenRParen) {
for {
binding, err := p.expect(tokenIdent, "expected variant binding")
if err != nil {
return nil, err
}
bindings = append(bindings, binding.lexeme)
if !p.match(tokenComma) {
break
}
}
}
if _, err := p.expect(tokenRParen, "expected ')' after variant bindings"); err != nil {
return nil, err
}
}
if _, err := p.expect(tokenArrow, "expected '->' after match pattern"); err != nil {
return nil, err
}
body, err := p.parseBlock()
if err != nil {
return nil, err
}
cases = append(cases, MatchCase{EnumName: enumName.lexeme, VariantName: variant.lexeme, Bindings: bindings, Body: body})
p.match(tokenComma)
p.match(tokenSemicolon)
}
if _, err := p.expect(tokenRBrace, "expected '}' after match"); err != nil {
return nil, err
}
return MatchStmt{Value: value, Cases: cases}, nil
}
func (p *parser) parseSelect() (Stmt, error) {
if _, err := p.expect(tokenLBrace, "expected '{' after select"); err != nil {
return nil, err
@ -570,7 +865,7 @@ func (p *parser) parseVarDecl(mutable bool) (Stmt, error) {
if err != nil {
return nil, err
}
name := names[0]
name := names[0].lexeme
var typ string
if p.match(tokenColon) {
if len(names) > 1 {
@ -592,7 +887,11 @@ func (p *parser) parseVarDecl(mutable bool) (Stmt, error) {
if len(names) == 1 {
return VarDecl{Mutable: mutable, Name: name, Type: typ, Value: value}, nil
}
return MultiVarDecl{Mutable: mutable, Names: names, Value: value}, nil
decl := MultiVarDecl{Mutable: mutable, Names: make([]string, 0, len(names)), Value: value}
for _, name := range names {
decl.Names = append(decl.Names, name.lexeme)
}
return decl, nil
}
func (p *parser) parseIf() (Stmt, error) {
@ -662,9 +961,36 @@ func (p *parser) parsePrefix() (Expr, error) {
tok := p.advance()
switch tok.kind {
case tokenIdent:
if p.match(tokenDoubleColon) {
variant, err := p.expect(tokenIdent, "expected enum variant")
if err != nil {
return nil, err
}
var values []Expr
if p.match(tokenLParen) {
if !p.check(tokenRParen) {
for {
value, err := p.parseExpr(0)
if err != nil {
return nil, err
}
values = append(values, value)
if !p.match(tokenComma) {
break
}
}
}
if _, err := p.expect(tokenRParen, "expected ')' after variant values"); err != nil {
return nil, err
}
}
return p.parsePostfix(EnumVariantExpr{EnumName: tok.lexeme, VariantName: variant.lexeme, Values: values})
}
return p.parsePostfix(IdentExpr{Name: tok.lexeme})
case tokenInt:
return IntExpr{Value: tok.lexeme}, nil
case tokenFloat:
return FloatExpr{Value: tok.lexeme}, nil
case tokenString:
return StringExpr{Value: tok.lexeme}, nil
case tokenTrue:
@ -684,7 +1010,7 @@ func (p *parser) parsePrefix() (Expr, error) {
return p.parsePostfix(expr)
case tokenLBrace:
return p.parseLambdaExpr()
case tokenBang, tokenMinus:
case tokenBang, tokenMinus, tokenAmp, tokenStar:
value, err := p.parseExpr(7)
if err != nil {
return nil, err
@ -695,18 +1021,18 @@ func (p *parser) parsePrefix() (Expr, error) {
}
}
func (p *parser) parseNameList() ([]string, error) {
func (p *parser) parseNameList() ([]token, error) {
name, err := p.expect(tokenIdent, "expected variable name")
if err != nil {
return nil, err
}
names := []string{name.lexeme}
names := []token{name}
for p.match(tokenComma) {
next, err := p.expect(tokenIdent, "expected variable name")
if err != nil {
return nil, err
}
names = append(names, next.lexeme)
names = append(names, next)
}
return names, nil
}
@ -774,11 +1100,20 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) {
for {
switch {
case p.match(tokenDot):
name, err := p.expect(tokenIdent, "expected selector name")
name := p.advance()
if !selectorName(name.lexeme) {
return nil, fmt.Errorf("expected selector name at %d, found %q", name.pos, name.lexeme)
}
expr = SelectorExpr{Receiver: expr, Name: name.lexeme}
case p.match(tokenLBracket):
index, err := p.parseExpr(0)
if err != nil {
return nil, err
}
expr = SelectorExpr{Receiver: expr, Name: name.lexeme}
if _, err := p.expect(tokenRBracket, "expected ']' after index"); err != nil {
return nil, err
}
expr = IndexExpr{Receiver: expr, Index: index}
case p.check(tokenLt):
typeArgs, hasTypeArgs, err := p.tryParseCallTypeArgs()
if err != nil {
@ -790,44 +1125,31 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) {
if _, err := p.expect(tokenLParen, "expected '(' after generic type arguments"); err != nil {
return nil, err
}
var args []Expr
if !p.check(tokenRParen) {
for {
arg, err := p.parseExpr(0)
if err != nil {
return nil, err
}
args = append(args, arg)
if !p.match(tokenComma) {
break
}
}
args, namedArgs, err := p.parseCallArguments()
if err != nil {
return nil, err
}
if _, err := p.expect(tokenRParen, "expected ')' after arguments"); err != nil {
return nil, err
}
expr = CallExpr{Callee: expr, Args: args, TypeArgs: typeArgs}
expr = CallExpr{Callee: expr, Args: args, NamedArgs: namedArgs, TypeArgs: typeArgs}
case p.match(tokenLParen):
var args []Expr
if !p.check(tokenRParen) {
for {
arg, err := p.parseExpr(0)
if err != nil {
return nil, err
}
args = append(args, arg)
if !p.match(tokenComma) {
break
}
}
args, namedArgs, err := p.parseCallArguments()
if err != nil {
return nil, err
}
if _, err := p.expect(tokenRParen, "expected ')' after arguments"); err != nil {
return nil, err
}
expr = CallExpr{Callee: expr, Args: args}
expr = CallExpr{Callee: expr, Args: args, NamedArgs: namedArgs}
case p.check(tokenLBrace):
call, ok := expr.(CallExpr)
if !ok {
var call CallExpr
switch current := expr.(type) {
case CallExpr:
call = current
case SelectorExpr:
call = CallExpr{Callee: current}
default:
return expr, nil
}
lambda, err := p.parsePrefix()
@ -842,6 +1164,54 @@ func (p *parser) parsePostfix(expr Expr) (Expr, error) {
}
}
func selectorName(value string) bool {
runes := []rune(value)
if len(runes) == 0 || !isIdentStart(runes[0]) {
return false
}
for _, r := range runes[1:] {
if !isIdentPart(r) {
return false
}
}
return true
}
func (p *parser) parseCallArguments() ([]Expr, []NamedArg, error) {
var args []Expr
var named []NamedArg
if p.check(tokenRParen) {
return args, named, nil
}
for {
if p.check(tokenIdent) && p.peekN(1).kind == tokenAssign {
if len(args) > 0 {
return nil, nil, fmt.Errorf("cannot mix positional and named arguments")
}
name := p.advance().lexeme
p.advance()
value, err := p.parseExpr(0)
if err != nil {
return nil, nil, err
}
named = append(named, NamedArg{Name: name, Value: value})
} else {
if len(named) > 0 {
return nil, nil, fmt.Errorf("cannot mix named and positional arguments")
}
value, err := p.parseExpr(0)
if err != nil {
return nil, nil, err
}
args = append(args, value)
}
if !p.match(tokenComma) {
break
}
}
return args, named, nil
}
func (p *parser) tryParseCallTypeArgs() ([]string, bool, error) {
if !p.check(tokenLt) {
return nil, false, nil
@ -937,6 +1307,9 @@ func (p *parser) parseTypeRef() (string, error) {
}
b += "<" + strings.Join(args, ", ") + ">"
}
if p.match(tokenQuestion) {
b += "?"
}
return b, nil
}