basic records support
All checks were successful
/ test (push) Successful in 50s

This commit is contained in:
Neemek 2026-08-17 17:08:24 +02:00
parent db3a3e29eb
commit 42fa039daf
11 changed files with 522 additions and 138 deletions

View file

@ -265,6 +265,30 @@ func (c *Compiler) compile(tree Node) (TypeSignature, error) {
return &TupleSignature{contents}, nil return &TupleSignature{contents}, nil
case RecordNodeType:
n := tree.(*RecordNode)
c.add(InstructionNewRecord)
contents := map[string]TypeSignature{}
for k, v := range n.entries {
t, err := c.compile(v)
if err != nil {
return nil, err
}
c.add(InstructionSetRecordItem)
c.addConstant(&StringValue{
k,
})
contents[k] = t
}
return &RecordSignature{
contents,
}, nil
case ListNodeType: case ListNodeType:
l := tree.(*ListNode) l := tree.(*ListNode)
@ -1196,6 +1220,13 @@ func (c *Compiler) getPropertySignature(source TypeSignature, property string) (
return &CompositeSignature{ return &CompositeSignature{
at, bt, at, bt,
}, nil }, nil
case TypeRecord:
prop, ok := sig.(*RecordSignature).Entries[property]
if !ok {
return nil, errors.New(fmt.Sprintf("cannot record has no property \"%s\"", property))
}
return prop, nil
default: default:
} }

View file

@ -7,7 +7,7 @@ import (
) )
type Token struct { type Token struct {
Type TokenKind Kind TokenKind
Start Pos Start Pos
End Pos End Pos
Line Pos Line Pos
@ -19,7 +19,7 @@ func (t Token) Bounds() (Pos, Pos) {
} }
func (t Token) String() string { func (t Token) String() string {
return fmt.Sprintf("token %s, '%s' %d -> %d, line %d", t.Type.String(), t.Lexeme, t.Start, t.End, t.Line) return fmt.Sprintf("token %s, '%s' %d -> %d, line %d", t.Kind.String(), t.Lexeme, t.Start, t.End, t.Line)
} }
type TokenKind uint64 type TokenKind uint64
@ -415,7 +415,7 @@ func (l *Lexer) NextToken() (Token, error) {
func NewToken(t TokenKind, start Pos, end Pos, line Pos, lexeme string) Token { func NewToken(t TokenKind, start Pos, end Pos, line Pos, lexeme string) Token {
return Token{ return Token{
Type: t, Kind: t,
Start: start, Start: start,
End: end, End: end,
Line: line, Line: line,
@ -430,7 +430,7 @@ func (l *Lexer) Tokenize() ([]Token, error) {
for ; err == nil; tok, err = l.NextToken() { for ; err == nil; tok, err = l.NextToken() {
tokens = append(tokens, tok) tokens = append(tokens, tok)
if tok.Type == TokenEOF { if tok.Kind == TokenEOF {
break break
} }
} }

View file

@ -148,8 +148,8 @@ func TestLexer_NextToken(t *testing.T) {
continue continue
} }
if tok.Type != expectedType { if tok.Kind != expectedType {
t.Errorf("Expected token type '%s' but got '%s'", expectedType, tok.Type) t.Errorf("Expected token type '%s' but got '%s'", expectedType, tok.Kind)
} else { } else {
t.Logf("Got expected token type '%s'", expectedType) t.Logf("Got expected token type '%s'", expectedType)
} }
@ -193,7 +193,7 @@ func TestLexer_NextTokenErrors(t *testing.T) {
lex := NewLexer(code) lex := NewLexer(code)
tok, err := lex.NextToken() tok, err := lex.NextToken()
for err == nil && tok.Type != TokenEOF { for err == nil && tok.Kind != TokenEOF {
tok, err = lex.NextToken() tok, err = lex.NextToken()
} }
@ -220,7 +220,7 @@ func BenchmarkLexer_NextToken(b *testing.B) {
lex := NewLexer(tc.source) lex := NewLexer(tc.source)
tok, err := lex.NextToken() tok, err := lex.NextToken()
for err == nil && tok.Type != TokenEOF { for err == nil && tok.Kind != TokenEOF {
tok, err = lex.NextToken() tok, err = lex.NextToken()
} }
} }

View file

@ -29,6 +29,7 @@ const (
NilNodeType NilNodeType
ListNodeType ListNodeType
TupleNodeType TupleNodeType
RecordNodeType
BinaryNodeType BinaryNodeType
UnaryNodeType UnaryNodeType
BlockNodeType BlockNodeType
@ -93,6 +94,8 @@ func (n NodeType) String() string {
return "Alias" return "Alias"
case IndexNodeType: case IndexNodeType:
return "Index" return "Index"
case RecordNodeType:
return "Record"
} }
return "Invalid Node Type" return "Invalid Node Type"
} }
@ -237,6 +240,36 @@ func (n TupleNode) Bounds() (Pos, Pos) {
return n.start, n.end return n.start, n.end
} }
type RecordNode struct {
entries map[string]Node
start Pos
end Pos
}
func (n RecordNode) Type() NodeType {
return RecordNodeType
}
func (n RecordNode) String() string {
sb := strings.Builder{}
sb.WriteString("(")
for name, item := range n.entries {
sb.WriteString(name)
sb.WriteString(": ")
sb.WriteString(item.String())
sb.WriteString(",")
}
sb.WriteString(")")
return sb.String()
}
func (n RecordNode) Bounds() (Pos, Pos) {
return n.start, n.end
}
type AccessNode struct { type AccessNode struct {
source Node source Node
property *Token property *Token
@ -269,6 +302,8 @@ func (n BinaryOperation) String() string {
return "multiply" return "multiply"
case BinaryDivision: case BinaryDivision:
return "divide" return "divide"
case BinaryModulo:
return "modulo"
case BinaryEquality: case BinaryEquality:
return "equality" return "equality"
case BinaryInequality: case BinaryInequality:

View file

@ -79,6 +79,10 @@ type Parser struct {
source string source string
trace []string trace []string
tokens []Token tokens []Token
state ParserState
}
type ParserState struct {
prev *Token prev *Token
curr *Token curr *Token
pos Pos pos Pos
@ -90,7 +94,9 @@ func NewParser(source string, trace []string, tokens []Token) *Parser {
source: source, source: source,
trace: trace, trace: trace,
tokens: tokens, tokens: tokens,
state: ParserState{
pos: 0, pos: 0,
},
} }
} }
@ -116,11 +122,11 @@ func (p *Parser) Parse(path string) (*Program, error) {
// initialize current // initialize current
p.advance() p.advance()
for int(p.pos) < len(p.tokens) && p.curr.Type != TokenEOF { for int(p.state.pos) < len(p.tokens) && p.state.curr.Kind != TokenEOF {
for p.accept(TokenNewLine) { for p.accept(TokenNewLine) {
} }
if p.curr.Type == TokenEOF { if p.state.curr.Kind == TokenEOF {
break break
} }
@ -139,25 +145,25 @@ func (p *Parser) Parse(path string) (*Program, error) {
&BlockNode{ &BlockNode{
statements, statements,
0, 0,
p.curr.End, p.state.curr.End,
}, },
path, path,
}, nil }, nil
} }
func (p *Parser) accept(tokenType TokenKind) bool { func (p *Parser) accept(tokenType TokenKind) bool {
if p.curr == nil { if p.state.curr == nil {
log.Fatal("unexpected current token nil") log.Fatal("unexpected current token nil")
return false return false
} }
if p.ignoreNewLine && tokenType != TokenNewLine { if p.state.ignoreNewLine && tokenType != TokenNewLine {
for p.curr.Type == TokenNewLine { for p.state.curr.Kind == TokenNewLine {
p.advance() p.advance()
} }
} }
if (*p.curr).Type == tokenType { if (*p.state.curr).Kind == tokenType {
p.advance() p.advance()
return true return true
} }
@ -165,46 +171,52 @@ func (p *Parser) accept(tokenType TokenKind) bool {
return false return false
} }
func (p *Parser) acceptAll(tokenTypes ...TokenKind) bool { func (p *Parser) getState() ParserState {
if int(p.pos)+len(tokenTypes) > len(p.tokens) { return p.state
return false }
}
for i, tokenType := range tokenTypes { func (p *Parser) restoreState(state ParserState) {
if p.tokens[int(p.pos)+i].Type != tokenType { p.state = state
}
func (p *Parser) acceptSeq(tokenTypes ...TokenKind) bool {
state := p.getState()
for _, t := range tokenTypes {
if !p.accept(t) {
p.restoreState(state)
return false return false
} }
} }
p.pos += Pos(len(tokenTypes))
return true return true
} }
func (p *Parser) expect(tokenType TokenKind, reason string) error { func (p *Parser) expect(tokenType TokenKind, reason string) error {
if !p.accept(tokenType) { if !p.accept(tokenType) {
return p.error(fmt.Sprintf("Expected token %s, got %s; %s", tokenType, p.curr.Type, reason), p.curr) return p.error(fmt.Sprintf("Expected token %s, got %s; %s", tokenType, p.state.curr.Kind, reason), p.state.curr)
} }
return nil return nil
} }
func (p *Parser) peek() (Token, error) { func (p *Parser) peek() (Token, error) {
if p.pos >= Pos(len(p.tokens)) { if p.state.pos >= Pos(len(p.tokens)) {
return Token{}, errors.New("cannot peek beyond tokens") return Token{}, errors.New("cannot peek beyond tokens")
} }
return p.tokens[p.pos], nil return p.tokens[p.state.pos], nil
} }
func (p *Parser) advance() { func (p *Parser) advance() {
p.prev = p.curr p.state.prev = p.state.curr
if p.pos < Pos(len(p.tokens)) { if p.state.pos < Pos(len(p.tokens)) {
p.curr = &p.tokens[p.pos] p.state.curr = &p.tokens[p.state.pos]
} else { } else {
p.curr = nil p.state.curr = nil
} }
p.pos++ p.state.pos++
} }
func (p *Parser) error(error string, causer *Token) error { func (p *Parser) error(error string, causer *Token) error {
@ -224,10 +236,10 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
} }
} }
oldIgnoreNewline := p.ignoreNewLine oldIgnoreNewline := p.state.ignoreNewLine
p.ignoreNewLine = false p.state.ignoreNewLine = false
start := p.prev.Start start := p.state.prev.Start
var statements []Node var statements []Node
for !p.accept(TokenCloseBrace) { for !p.accept(TokenCloseBrace) {
@ -251,22 +263,22 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
} }
} }
p.ignoreNewLine = oldIgnoreNewline p.state.ignoreNewLine = oldIgnoreNewline
return &BlockNode{statements, start, p.prev.End}, nil return &BlockNode{statements, start, p.state.prev.End}, nil
} }
t := p.curr t := p.state.curr
switch t.Type { switch t.Kind {
case TokenType: case TokenType:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
if err := p.expect(TokenName, "types must have a name"); err != nil { if err := p.expect(TokenName, "types must have a name"); err != nil {
return nil, err return nil, err
} }
name := p.prev name := p.state.prev
if err := p.expect(TokenAssign, "type aliases must be defined with an assign"); err != nil { if err := p.expect(TokenAssign, "type aliases must be defined with an assign"); err != nil {
return nil, err return nil, err
@ -282,7 +294,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
sig, sig,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenIf: case TokenIf:
@ -300,7 +312,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
var otherwise Node var otherwise Node
if p.accept(TokenElse) { if p.accept(TokenElse) {
otherwise, err = p.expression(p.curr.Type != TokenIf) otherwise, err = p.expression(p.state.curr.Kind != TokenIf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -316,7 +328,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
case TokenReturn: case TokenReturn:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
v, err := p.expression(false) v, err := p.expression(false)
if err != nil { if err != nil {
@ -326,12 +338,12 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
return &ReturnNode{ return &ReturnNode{
v, v,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenWhile: case TokenWhile:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
cond, err := p.expression(false) cond, err := p.expression(false)
if err != nil { if err != nil {
@ -347,12 +359,12 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
cond, cond,
logic, logic,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenFor: case TokenFor:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
counter, err := p.expression(false) counter, err := p.expression(false)
if err != nil { if err != nil {
@ -379,12 +391,12 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
logic, logic,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenInclude: case TokenInclude:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
if err := p.expect(TokenString, "import requires a path/name to include"); err != nil { if err := p.expect(TokenString, "import requires a path/name to include"); err != nil {
return nil, err return nil, err
@ -392,13 +404,13 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
return &IncludeNode{ return &IncludeNode{
&StringNode{ &StringNode{
p.prev.Lexeme[1 : len(p.prev.Lexeme)-1], p.state.prev.Lexeme[1 : len(p.state.prev.Lexeme)-1],
p.prev.Lexeme, p.state.prev.Lexeme,
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, },
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
default: default:
@ -408,7 +420,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) {
} }
if p.accept(TokenDeclare) || p.accept(TokenAssign) { if p.accept(TokenDeclare) || p.accept(TokenAssign) {
isDeclaration := p.prev.Type == TokenDeclare isDeclaration := p.state.prev.Kind == TokenDeclare
// possibly assign tuples; not implemented yet // possibly assign tuples; not implemented yet
v, err := p.expression(false) v, err := p.expression(false)
@ -509,7 +521,7 @@ func (p *Parser) binary() (Node, error) {
r := values.Pop() r := values.Pop()
l := values.Pop() l := values.Pop()
opToken := ops.Pop() opToken := ops.Pop()
op := tokenToBinaryOperation(opToken.Type) op := tokenToBinaryOperation(opToken.Kind)
start, _ := l.Bounds() start, _ := l.Bounds()
_, end := r.Bounds() _, end := r.Bounds()
@ -524,12 +536,12 @@ func (p *Parser) binary() (Node, error) {
}) })
} }
for isBinaryOperator(p.curr.Type) { for isBinaryOperator(p.state.curr.Kind) {
for ops.Current > 0 && binaryPrecedence(p.curr.Type) <= binaryPrecedence(ops.Peek().Type) { for ops.Current > 0 && binaryPrecedence(p.state.curr.Kind) <= binaryPrecedence(ops.Peek().Kind) {
reduce() reduce()
} }
ops.Push(p.curr) ops.Push(p.state.curr)
p.advance() p.advance()
v, err := p.chain() v, err := p.chain()
@ -558,16 +570,16 @@ func (p *Parser) chain() (Node, error) {
if err = p.expect(TokenName, "can only access properties by name"); err != nil { if err = p.expect(TokenName, "can only access properties by name"); err != nil {
return nil, err return nil, err
} }
name := p.prev name := p.state.prev
f = &AccessNode{ f = &AccessNode{
f, f,
p.prev, p.state.prev,
name.Start, name.Start,
name.End, name.End,
} }
if p.curr.Type == TokenOpenParenthesis { if p.state.curr.Kind == TokenOpenParenthesis {
args, err := p.parseArgs() args, err := p.parseArgs()
if err != nil { if err != nil {
return nil, err return nil, err
@ -577,11 +589,11 @@ func (p *Parser) chain() (Node, error) {
f, f,
args, args,
name.Start, name.Start,
p.prev.End, p.state.prev.End,
} }
} }
} else if p.curr.Type == TokenOpenParenthesis { } else if p.state.curr.Kind == TokenOpenParenthesis {
start := p.curr.Start start := p.state.curr.Start
args, err := p.parseArgs() args, err := p.parseArgs()
if err != nil { if err != nil {
return nil, err return nil, err
@ -592,10 +604,10 @@ func (p *Parser) chain() (Node, error) {
args, args,
start, start,
p.prev.End, p.state.prev.End,
} }
} else if p.accept(TokenOpenBracket) { } else if p.accept(TokenOpenBracket) {
start := p.prev.Start start := p.state.prev.Start
index, err := p.expression(false) index, err := p.expression(false)
if err != nil { if err != nil {
@ -610,7 +622,7 @@ func (p *Parser) chain() (Node, error) {
f, f,
index, index,
start, start,
p.prev.End, p.state.prev.End,
} }
} else { } else {
break break
@ -621,71 +633,71 @@ func (p *Parser) chain() (Node, error) {
} }
func (p *Parser) factor() (Node, error) { func (p *Parser) factor() (Node, error) {
switch (*p.curr).Type { switch (*p.state.curr).Kind {
case TokenString: case TokenString:
p.advance() p.advance()
return &StringNode{ return &StringNode{
(*p.prev).Lexeme[1 : len((*p.prev).Lexeme)-1], (*p.state.prev).Lexeme[1 : len((*p.state.prev).Lexeme)-1],
(*p.prev).Lexeme, (*p.state.prev).Lexeme,
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenInteger: case TokenInteger:
p.advance() p.advance()
num, success := new(big.Int).SetString(p.prev.Lexeme, 10) num, success := new(big.Int).SetString(p.state.prev.Lexeme, 10)
if !success { if !success {
return nil, p.error(fmt.Sprintf("cannot parse integer base 10: %s", p.prev.Lexeme), p.prev) return nil, p.error(fmt.Sprintf("cannot parse integer base 10: %s", p.state.prev.Lexeme), p.state.prev)
} }
return &IntegerNode{ return &IntegerNode{
num, num,
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenFloat: case TokenFloat:
p.advance() p.advance()
num, err := strconv.ParseFloat((*p.prev).Lexeme, FloatSize) num, err := strconv.ParseFloat((*p.state.prev).Lexeme, FloatSize)
if err != nil { if err != nil {
return nil, p.error(fmt.Sprintf("Error parsing number: %v", err), p.prev) return nil, p.error(fmt.Sprintf("Error parsing number: %v", err), p.state.prev)
} }
return &FloatNode{ return &FloatNode{
num, num,
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenHexadecimal: case TokenHexadecimal:
p.advance() p.advance()
start := (*p.prev).Start start := (*p.state.prev).Start
num, ok := new(big.Int).SetString(p.prev.Lexeme[2:], 16) num, ok := new(big.Int).SetString(p.state.prev.Lexeme[2:], 16)
if !ok { if !ok {
return nil, p.error(fmt.Sprintf("cannot parse hexadecimal: %v", p.prev.Lexeme), p.prev) return nil, p.error(fmt.Sprintf("cannot parse hexadecimal: %v", p.state.prev.Lexeme), p.state.prev)
} }
return &IntegerNode{ return &IntegerNode{
num, num,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenTrue: case TokenTrue:
p.advance() p.advance()
return &BooleanNode{ return &BooleanNode{
true, true,
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenFalse: case TokenFalse:
p.advance() p.advance()
return &BooleanNode{ return &BooleanNode{
false, false,
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenNil: case TokenNil:
@ -694,7 +706,7 @@ func (p *Parser) factor() (Node, error) {
case TokenOpenBracket: case TokenOpenBracket:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
// TODO: find better solution; current one is messy // TODO: find better solution; current one is messy
// Maybe perform better analysis to determine the kind of the list... // Maybe perform better analysis to determine the kind of the list...
@ -708,12 +720,12 @@ func (p *Parser) factor() (Node, error) {
[]Node{}, []Node{},
s, s,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
} }
oldIgnoreNewline := p.ignoreNewLine oldIgnoreNewline := p.state.ignoreNewLine
p.ignoreNewLine = true p.state.ignoreNewLine = true
var values []Node var values []Node
for !p.accept(TokenCloseBracket) { for !p.accept(TokenCloseBracket) {
@ -731,19 +743,19 @@ func (p *Parser) factor() (Node, error) {
values = append(values, value) values = append(values, value)
} }
p.ignoreNewLine = oldIgnoreNewline p.state.ignoreNewLine = oldIgnoreNewline
return &ListNode{ return &ListNode{
values, values,
nil, nil,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
// unary minus // unary minus
case TokenMinus: case TokenMinus:
p.advance() p.advance()
op := p.prev op := p.state.prev
f, err := p.factor() f, err := p.factor()
if err != nil { if err != nil {
@ -754,12 +766,12 @@ func (p *Parser) factor() (Node, error) {
f, f,
op, op,
op.Start, op.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenBang: case TokenBang:
p.advance() p.advance()
op := p.prev op := p.state.prev
v, err := p.factor() v, err := p.factor()
if err != nil { if err != nil {
@ -771,16 +783,16 @@ func (p *Parser) factor() (Node, error) {
v, v,
op, op,
op.Start, op.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenName: case TokenName:
p.advance() p.advance()
name := (*p.prev).Lexeme name := (*p.state.prev).Lexeme
start := p.prev.Start start := p.state.prev.Start
nameEnd := p.prev.End nameEnd := p.state.prev.End
if p.curr.Type == TokenOpenParenthesis { if p.state.curr.Kind == TokenOpenParenthesis {
args, err := p.parseArgs() args, err := p.parseArgs()
if err != nil { if err != nil {
return nil, err return nil, err
@ -794,7 +806,7 @@ func (p *Parser) factor() (Node, error) {
}, },
args, args,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
} }
@ -806,11 +818,11 @@ func (p *Parser) factor() (Node, error) {
case TokenFunc: case TokenFunc:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
var name *Token var name *Token
if p.accept(TokenName) { // can be unnamed, but accept name if it is named if p.accept(TokenName) { // can be unnamed, but accept name if it is named
name = p.prev name = p.state.prev
} }
params, err := p.parseParams() params, err := p.parseParams()
@ -842,7 +854,7 @@ func (p *Parser) factor() (Node, error) {
yield, yield,
logic, logic,
start, start,
p.prev.End, p.state.prev.End,
} }
if name != nil { if name != nil {
@ -851,7 +863,7 @@ func (p *Parser) factor() (Node, error) {
fn, fn,
true, true,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
} }
@ -859,11 +871,67 @@ func (p *Parser) factor() (Node, error) {
case TokenOpenParenthesis: case TokenOpenParenthesis:
p.advance() p.advance()
start := p.prev.Start start := p.state.prev.Start
oldCare := p.state.ignoreNewLine
p.state.ignoreNewLine = true
p.skipNewLines()
// we're inside an object // we're inside an object
if p.acceptAll(TokenName, TokenColon) { key := p.state.curr
return nil, p.error("objects are not implemented yet (TBD)", p.prev) if p.acceptSeq(TokenName, TokenColon) {
entries := map[string]Node{}
for len(entries) == 0 || !p.accept(TokenCloseParenthesis) {
if len(entries) != 0 {
p.skipNewLines()
key = p.state.curr
if !p.acceptSeq(TokenName, TokenColon) {
return nil, p.error("expected a record name", key)
}
}
name := key.Lexeme
if _, ok := entries[name]; ok {
return nil, p.error("duplicate key; already defined.", key)
}
if p.accept(TokenComma) || p.accept(TokenCloseParenthesis) {
entries[name] = &ReferenceNode{
name,
key.Start,
key.End,
}
if p.state.prev.Kind == TokenCloseParenthesis {
break
}
continue
} else {
x, err := p.expression(false)
if err != nil {
return nil, err
}
entries[name] = x
}
if !p.accept(TokenComma) {
if err := p.expect(TokenCloseParenthesis, "record must be closed"); err != nil {
return nil, err
}
break
}
}
p.state.ignoreNewLine = oldCare
return &RecordNode{
entries,
start,
p.state.prev.End,
}, nil
} }
v, err := p.expression(false) v, err := p.expression(false)
@ -876,6 +944,7 @@ func (p *Parser) factor() (Node, error) {
return nil, err return nil, err
} }
p.state.ignoreNewLine = oldCare
return v, nil return v, nil
} }
@ -897,26 +966,27 @@ func (p *Parser) factor() (Node, error) {
} }
} }
p.state.ignoreNewLine = oldCare
return &TupleNode{ return &TupleNode{
items, items,
start, start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenBreakpoint: case TokenBreakpoint:
p.advance() p.advance()
return &BreakpointNode{ return &BreakpointNode{
p.prev.Start, p.state.prev.Start,
p.prev.End, p.state.prev.End,
}, nil }, nil
case TokenOpenBrace: case TokenOpenBrace:
return p.expression(true) return p.expression(true)
default: default:
return nil, p.error(fmt.Sprintf("invalid factor %s", p.curr), p.curr) return nil, p.error(fmt.Sprintf("invalid factor %s", p.state.curr), p.state.curr)
} }
} }
@ -956,7 +1026,7 @@ func (p *Parser) parseParams() ([]FunctionParameter, error) {
params := make([]FunctionParameter, 0) params := make([]FunctionParameter, 0)
if p.accept(TokenName) { if p.accept(TokenName) {
name := (*p.prev).Lexeme name := (*p.state.prev).Lexeme
if err := p.expect(TokenColon, "parameters must have a type separated by a colon"); err != nil { if err := p.expect(TokenColon, "parameters must have a type separated by a colon"); err != nil {
return nil, err return nil, err
} }
@ -977,7 +1047,7 @@ func (p *Parser) parseParams() ([]FunctionParameter, error) {
if err := p.expect(TokenName, "parameters must have a name (cannot have trailing comma)"); err != nil { if err := p.expect(TokenName, "parameters must have a name (cannot have trailing comma)"); err != nil {
return nil, err return nil, err
} }
name = (*p.prev).Lexeme name = (*p.state.prev).Lexeme
if err := p.expect(TokenColon, "parameters must have a type separated by a colon"); err != nil { if err := p.expect(TokenColon, "parameters must have a type separated by a colon"); err != nil {
return nil, err return nil, err
} }
@ -1005,9 +1075,42 @@ func (p *Parser) parseSignature() (TypeSignature, error) {
var s TypeSignature var s TypeSignature
if p.accept(TokenOpenParenthesis) { if p.accept(TokenOpenParenthesis) {
oldCare := p.state.ignoreNewLine
p.state.ignoreNewLine = true
p.skipNewLines()
// we're inside an object // we're inside an object
if p.acceptAll(TokenName, TokenColon) { name := p.state.curr
return nil, p.error("objects are not implemented yet (TBD)", p.prev) if p.acceptSeq(TokenName, TokenColon) {
entries := map[string]TypeSignature{}
for len(entries) == 0 || !p.accept(TokenCloseParenthesis) {
if len(entries) != 0 {
p.skipNewLines()
name = p.state.curr
if !p.acceptSeq(TokenName, TokenColon) {
return nil, p.error("expected record member", p.state.curr)
}
}
sig, err := p.parseSignature()
if err != nil {
return nil, err
}
entries[name.Lexeme] = sig
if !p.accept(TokenComma) {
if err := p.expect(TokenCloseParenthesis, "record must be closed"); err != nil {
return nil, err
}
break
}
}
return &RecordSignature{
entries,
}, nil
} }
v, err := p.parseSignature() v, err := p.parseSignature()
@ -1043,6 +1146,8 @@ func (p *Parser) parseSignature() (TypeSignature, error) {
items, items,
} }
} }
p.state.ignoreNewLine = oldCare
} else if p.accept(TokenFunc) { } else if p.accept(TokenFunc) {
if err := p.expect(TokenOpenParenthesis, "func signature must have parentheses for parameters"); err != nil { if err := p.expect(TokenOpenParenthesis, "func signature must have parentheses for parameters"); err != nil {
return nil, err return nil, err
@ -1095,7 +1200,7 @@ func (p *Parser) parseSignature() (TypeSignature, error) {
if err := p.expect(TokenName, "type must be a name"); err != nil { if err := p.expect(TokenName, "type must be a name"); err != nil {
return nil, err return nil, err
} }
name := (*p.prev).Lexeme name := (*p.state.prev).Lexeme
switch name { switch name {
case "str": case "str":
@ -1130,3 +1235,8 @@ func (p *Parser) parseSignature() (TypeSignature, error) {
return s, nil return s, nil
} }
func (p *Parser) skipNewLines() {
for p.accept(TokenNewLine) {
}
}

View file

@ -15,7 +15,7 @@ func TestNewParser(t *testing.T) {
t.Fatal("parser should not be nil") t.Fatal("parser should not be nil")
} }
if p.pos != 0 { if p.state.pos != 0 {
t.Error("parser should initialize position at 0") t.Error("parser should initialize position at 0")
} }
@ -715,6 +715,95 @@ func GetTokenTestData() map[string]TokenTestData {
0, 0, 0, 0,
}, },
}, },
"record/single": {
[]Token{
NewToken(TokenOpenParenthesis, 0, 0, 0, "("),
NewToken(TokenName, 0, 0, 1, "a"),
NewToken(TokenColon, 0, 0, 0, ":"),
NewToken(TokenInteger, 0, 0, 0, "2"),
NewToken(TokenCloseParenthesis, 0, 0, 0, ")"),
NewToken(TokenEOF, 0, 0, 0, ""),
},
&BlockNode{
[]Node{
&RecordNode{
map[string]Node{
"a": &IntegerNode{
big.NewInt(2),
0, 0,
},
},
0, 0,
},
},
0, 0,
},
},
"record/multiple": {
[]Token{
NewToken(TokenOpenParenthesis, 0, 0, 0, "("),
NewToken(TokenName, 0, 0, 1, "a"),
NewToken(TokenColon, 0, 0, 0, ":"),
NewToken(TokenInteger, 0, 0, 0, "2"),
NewToken(TokenComma, 0, 0, 0, ","),
NewToken(TokenName, 0, 0, 1, "b"),
NewToken(TokenColon, 0, 0, 0, ":"),
NewToken(TokenInteger, 0, 0, 0, "4"),
NewToken(TokenCloseParenthesis, 0, 0, 0, ")"),
NewToken(TokenEOF, 0, 0, 0, ""),
},
&BlockNode{
[]Node{
&RecordNode{
map[string]Node{
"a": &IntegerNode{
big.NewInt(2),
0, 0,
},
"b": &IntegerNode{
big.NewInt(4),
0, 0,
},
},
0, 0,
},
},
0, 0,
},
},
"record/shorthand": {
[]Token{
NewToken(TokenOpenParenthesis, 0, 0, 0, "("),
NewToken(TokenName, 0, 0, 1, "a"),
NewToken(TokenColon, 0, 0, 0, ":"),
NewToken(TokenComma, 0, 0, 0, ","),
NewToken(TokenName, 0, 0, 1, "b"),
NewToken(TokenColon, 0, 0, 0, ":"),
NewToken(TokenCloseParenthesis, 0, 0, 0, ")"),
NewToken(TokenEOF, 0, 0, 0, ""),
},
&BlockNode{
[]Node{
&RecordNode{
map[string]Node{
"a": &ReferenceNode{
"a",
0, 0,
},
"b": &ReferenceNode{
"b",
0, 0,
},
},
0, 0,
},
},
0, 0,
},
},
} }
} }
@ -912,6 +1001,23 @@ func NodeEquality(t *testing.T, n1 Node, n2 Node) {
NodeEquality(t, v1, t2.items[i]) NodeEquality(t, v1, t2.items[i])
} }
case RecordNodeType:
r1 := n1.(*RecordNode)
r2 := n2.(*RecordNode)
if len(r1.entries) != len(r2.entries) {
t.Fatalf("Record node entries count does not match")
}
for i, v1 := range r1.entries {
t.Logf("Checking item %s", i)
if v2, ok := r2.entries[i]; ok {
NodeEquality(t, v1, v2)
} else {
t.Errorf("Record node entry %s from first does not exist in other", i)
}
}
default: default:
panic("unimplemented node equality") panic("unimplemented node equality")
} }
@ -939,25 +1045,55 @@ func TestParser_Parse(t *testing.T) {
} }
} }
func TestParser_AcceptAll(t *testing.T) { func TestParser_AcceptSeq(t *testing.T) {
p := NewParser("a:", []string{}, []Token{ p := NewParser("a:", []string{}, []Token{
NewToken(TokenName, 0, 1, 0, "a"), NewToken(TokenName, 0, 1, 0, "a"),
NewToken(TokenColon, 1, 2, 0, "a"), NewToken(TokenColon, 1, 2, 0, ":"),
NewToken(TokenEOF, 2, 2, 0, ""),
}) })
if !p.acceptAll(TokenName, TokenColon) { // initialize
p.advance()
if !p.acceptSeq(TokenName, TokenColon) {
t.Fatalf("tokens were not accepted") t.Fatalf("tokens were not accepted")
} }
t.Logf("tokens were accepted") t.Logf("tokens were accepted")
} }
func TestParser_AcceptAll_TooFew(t *testing.T) { func TestParser_AcceptSeqAndAfter(t *testing.T) {
p := NewParser("a", []string{}, []Token{ p := NewParser("a: 1", []string{}, []Token{
NewToken(TokenName, 0, 1, 0, "a"), NewToken(TokenName, 0, 1, 0, "a"),
NewToken(TokenColon, 1, 2, 0, ":"),
NewToken(TokenInteger, 3, 4, 0, "1"),
NewToken(TokenEOF, 4, 4, 0, ""),
}) })
if p.acceptAll(TokenName, TokenColon) { // initialize
p.advance()
if !p.acceptSeq(TokenName, TokenColon) {
t.Fatalf("seq tokens were not accepted")
}
if !p.accept(TokenInteger) {
t.Fatalf("integer was not accepted")
}
t.Logf("tokens were accepted")
}
func TestParser_AcceptSeq_TooFew(t *testing.T) {
p := NewParser("a", []string{}, []Token{
NewToken(TokenName, 0, 1, 0, "a"),
NewToken(TokenEOF, 1, 1, 0, ""),
})
// initialize
p.advance()
if p.acceptSeq(TokenName, TokenColon) {
t.Fatalf("tokens were incorrectly accepted") t.Fatalf("tokens were incorrectly accepted")
} }

View file

@ -50,9 +50,11 @@ func (t Type) String() string {
return "composite" return "composite"
case TypeInner: case TypeInner:
return "inner" return "inner"
} case TypeRecord:
return "record"
default:
panic(fmt.Sprintf("unsupported string conversion for type %v", int(t))) panic(fmt.Sprintf("unsupported string conversion for type %v", int(t)))
}
} }
func SignatureOf(v Value) TypeSignature { func SignatureOf(v Value) TypeSignature {
@ -560,7 +562,7 @@ func (s *RecordSignature) Contains(t TypeSignature) bool {
} }
func (s *RecordSignature) Equal(t TypeSignature) bool { func (s *RecordSignature) Equal(t TypeSignature) bool {
return s.Contains(t) && t.Contains(s) return s.Contains(t) && len(t.(*RecordSignature).Entries) == len(s.Entries)
} }
func (s *RecordSignature) String() string { func (s *RecordSignature) String() string {

View file

@ -777,7 +777,30 @@ func (v *RecordValue) Type() ValueType {
} }
func (v *RecordValue) String() string { func (v *RecordValue) String() string {
return "" sb := strings.Builder{}
sb.WriteString("(")
n := 0
for prop, value := range v.Entries {
if n != 0 {
sb.WriteString(", ")
}
sb.WriteString(prop)
sb.WriteString(": ")
sb.WriteString(value.DebugString())
n += 1
}
if n == 1 {
sb.WriteString(",")
}
sb.WriteString(")")
return sb.String()
} }
func (v *RecordValue) DebugString() string { func (v *RecordValue) DebugString() string {

View file

@ -126,7 +126,7 @@ const (
// InstructionAppend Append to a list. stack: (... > list > item) => (... > list) // InstructionAppend Append to a list. stack: (... > list > item) => (... > list)
InstructionAppend InstructionAppend
// InstructionFormList Form items on the stack into a list. The 2 bytes after the instructions are the amount of // InstructionFormList Form items on the stack into a list. The 2 bytes after the instructions are the amount of
// items to include) The order is reversed compared to on the stack; the top value on the stack is the last in the // items to include. The order is reversed compared to on the stack; the top value on the stack is the last in the
// list. // list.
InstructionFormList InstructionFormList
// InstructionConcatLists concatenate lists, producing a new list with the values of both lists. Pops two lists. // InstructionConcatLists concatenate lists, producing a new list with the values of both lists. Pops two lists.
@ -139,6 +139,12 @@ const (
// being the last item in the tuple. // being the last item in the tuple.
InstructionDestructureTuple InstructionDestructureTuple
// InstructionNewRecord Create a new empty record.
InstructionNewRecord
// InstructionSetRecordItem Set the value of an item in the record, and create it if it does not already exist.
// [..., record, item]; the following byte should be the index of a string constant with the name of the property.
InstructionSetRecordItem
// InstructionIndexList index into a list. The lower item is the container, and the top item // InstructionIndexList index into a list. The lower item is the container, and the top item
// is the index. [..., container, index] -> [..., item] // is the index. [..., container, index] -> [..., item]
InstructionIndexList InstructionIndexList
@ -1076,6 +1082,18 @@ func (vm *VM) Next() bool {
vm.Stack.Push(member) vm.Stack.Push(member)
case InstructionSetRecordItem:
i := vm.Stack.Pop()
prop := vm.ReadConstant().(*StringValue)
r := vm.Stack.Peek().(*RecordValue)
r.Entries[prop.Text] = i
case InstructionNewRecord:
vm.Stack.Push(&RecordValue{
map[string]Value{},
})
case InstructionIndexList: case InstructionIndexList:
i := vm.Stack.Pop().(*IntegerValue) i := vm.Stack.Pop().(*IntegerValue)
l := vm.Stack.Pop().(*ListValue) l := vm.Stack.Pop().(*ListValue)

29
records.ang Normal file
View file

@ -0,0 +1,29 @@
type User = (
id: int,
name: str,
)
user := {
user_id := 0
all_users := []User
(
get_all: fn() -> [User] { all_users },
new: fn(name: str) -> User {
data := (id: { user_id = user_id + 1 }, name:)
all_users.append(data)
data
},
fmt: fn(user: User) -> str {
user.name + "(" + str(user.id) + ")"
}
)
}
abe := user.new("abe")
lincoln := user.new("lincoln")
println(abe)
println(lincoln)

View file

@ -2,7 +2,7 @@
echo '=== Building WASM lib ===' echo '=== Building WASM lib ==='
cd wasm || exit 1 cd wasm || exit 1
if ! GOOS=js GOARCH=wasm go build .; then if ! GOOS=js GOARCH=wasm go build . "$@"; then
echo "=x= Had error building WASM lib =x=" echo "=x= Had error building WASM lib =x="
exit 1 exit 1
else else
@ -12,7 +12,7 @@ cd ..
echo '=== Building CLI ===' echo '=== Building CLI ==='
cd cli || exit 1 cd cli || exit 1
if ! go build .; then if ! go build . "$@"; then
echo "=x= Had error building CLI =x=" echo "=x= Had error building CLI =x="
exit 1 exit 1
else else
@ -22,7 +22,7 @@ cd ..
echo "=== Running go core tests ===" echo "=== Running go core tests ==="
cd core || exit 1 cd core || exit 1
if ! go test .; then if ! go test . "$@"; then
echo "=x= Core testing failed =x= " echo "=x= Core testing failed =x= "
exit 1 exit 1
else else