From ff11d7e0ed04d60cf933f968f347d3df2f2eba8b Mon Sep 17 00:00:00 2001 From: neemek Date: Mon, 17 Aug 2026 17:08:24 +0200 Subject: [PATCH] basic records support --- core/compiler.go | 31 ++++ core/lexer.go | 8 +- core/lexer_test.go | 8 +- core/nodes.go | 35 +++++ core/parser.go | 340 +++++++++++++++++++++++++++++--------------- core/parser_test.go | 150 ++++++++++++++++++- core/types.go | 8 +- core/values.go | 25 +++- core/vm.go | 20 ++- records.ang | 29 ++++ test_all.sh | 6 +- 11 files changed, 522 insertions(+), 138 deletions(-) create mode 100644 records.ang diff --git a/core/compiler.go b/core/compiler.go index 617871e..36f0f7a 100644 --- a/core/compiler.go +++ b/core/compiler.go @@ -265,6 +265,30 @@ func (c *Compiler) compile(tree Node) (TypeSignature, error) { 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: l := tree.(*ListNode) @@ -1196,6 +1220,13 @@ func (c *Compiler) getPropertySignature(source TypeSignature, property string) ( return &CompositeSignature{ at, bt, }, 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: } diff --git a/core/lexer.go b/core/lexer.go index ba504b8..da8aa70 100644 --- a/core/lexer.go +++ b/core/lexer.go @@ -7,7 +7,7 @@ import ( ) type Token struct { - Type TokenKind + Kind TokenKind Start Pos End Pos Line Pos @@ -19,7 +19,7 @@ func (t Token) Bounds() (Pos, Pos) { } 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 @@ -415,7 +415,7 @@ func (l *Lexer) NextToken() (Token, error) { func NewToken(t TokenKind, start Pos, end Pos, line Pos, lexeme string) Token { return Token{ - Type: t, + Kind: t, Start: start, End: end, Line: line, @@ -430,7 +430,7 @@ func (l *Lexer) Tokenize() ([]Token, error) { for ; err == nil; tok, err = l.NextToken() { tokens = append(tokens, tok) - if tok.Type == TokenEOF { + if tok.Kind == TokenEOF { break } } diff --git a/core/lexer_test.go b/core/lexer_test.go index c8158b3..a20a0c1 100644 --- a/core/lexer_test.go +++ b/core/lexer_test.go @@ -148,8 +148,8 @@ func TestLexer_NextToken(t *testing.T) { continue } - if tok.Type != expectedType { - t.Errorf("Expected token type '%s' but got '%s'", expectedType, tok.Type) + if tok.Kind != expectedType { + t.Errorf("Expected token type '%s' but got '%s'", expectedType, tok.Kind) } else { t.Logf("Got expected token type '%s'", expectedType) } @@ -193,7 +193,7 @@ func TestLexer_NextTokenErrors(t *testing.T) { lex := NewLexer(code) tok, err := lex.NextToken() - for err == nil && tok.Type != TokenEOF { + for err == nil && tok.Kind != TokenEOF { tok, err = lex.NextToken() } @@ -220,7 +220,7 @@ func BenchmarkLexer_NextToken(b *testing.B) { lex := NewLexer(tc.source) tok, err := lex.NextToken() - for err == nil && tok.Type != TokenEOF { + for err == nil && tok.Kind != TokenEOF { tok, err = lex.NextToken() } } diff --git a/core/nodes.go b/core/nodes.go index 056b206..54ddcdc 100644 --- a/core/nodes.go +++ b/core/nodes.go @@ -29,6 +29,7 @@ const ( NilNodeType ListNodeType TupleNodeType + RecordNodeType BinaryNodeType UnaryNodeType BlockNodeType @@ -93,6 +94,8 @@ func (n NodeType) String() string { return "Alias" case IndexNodeType: return "Index" + case RecordNodeType: + return "Record" } return "Invalid Node Type" } @@ -237,6 +240,36 @@ func (n TupleNode) Bounds() (Pos, Pos) { 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 { source Node property *Token @@ -269,6 +302,8 @@ func (n BinaryOperation) String() string { return "multiply" case BinaryDivision: return "divide" + case BinaryModulo: + return "modulo" case BinaryEquality: return "equality" case BinaryInequality: diff --git a/core/parser.go b/core/parser.go index 6462b7a..ca360af 100644 --- a/core/parser.go +++ b/core/parser.go @@ -76,9 +76,13 @@ func (p ParsingError) Format() string { } type Parser struct { - source string - trace []string - tokens []Token + source string + trace []string + tokens []Token + state ParserState +} + +type ParserState struct { prev *Token curr *Token pos Pos @@ -90,7 +94,9 @@ func NewParser(source string, trace []string, tokens []Token) *Parser { source: source, trace: trace, tokens: tokens, - pos: 0, + state: ParserState{ + pos: 0, + }, } } @@ -116,11 +122,11 @@ func (p *Parser) Parse(path string) (*Program, error) { // initialize current 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) { } - if p.curr.Type == TokenEOF { + if p.state.curr.Kind == TokenEOF { break } @@ -139,25 +145,25 @@ func (p *Parser) Parse(path string) (*Program, error) { &BlockNode{ statements, 0, - p.curr.End, + p.state.curr.End, }, path, }, nil } func (p *Parser) accept(tokenType TokenKind) bool { - if p.curr == nil { + if p.state.curr == nil { log.Fatal("unexpected current token nil") return false } - if p.ignoreNewLine && tokenType != TokenNewLine { - for p.curr.Type == TokenNewLine { + if p.state.ignoreNewLine && tokenType != TokenNewLine { + for p.state.curr.Kind == TokenNewLine { p.advance() } } - if (*p.curr).Type == tokenType { + if (*p.state.curr).Kind == tokenType { p.advance() return true } @@ -165,46 +171,52 @@ func (p *Parser) accept(tokenType TokenKind) bool { return false } -func (p *Parser) acceptAll(tokenTypes ...TokenKind) bool { - if int(p.pos)+len(tokenTypes) > len(p.tokens) { - return false - } +func (p *Parser) getState() ParserState { + return p.state +} - for i, tokenType := range tokenTypes { - if p.tokens[int(p.pos)+i].Type != tokenType { +func (p *Parser) restoreState(state ParserState) { + 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 } } - p.pos += Pos(len(tokenTypes)) return true } func (p *Parser) expect(tokenType TokenKind, reason string) error { 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 } 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 p.tokens[p.pos], nil + return p.tokens[p.state.pos], nil } func (p *Parser) advance() { - p.prev = p.curr + p.state.prev = p.state.curr - if p.pos < Pos(len(p.tokens)) { - p.curr = &p.tokens[p.pos] + if p.state.pos < Pos(len(p.tokens)) { + p.state.curr = &p.tokens[p.state.pos] } else { - p.curr = nil + p.state.curr = nil } - p.pos++ + p.state.pos++ } func (p *Parser) error(error string, causer *Token) error { @@ -224,10 +236,10 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { } } - oldIgnoreNewline := p.ignoreNewLine - p.ignoreNewLine = false + oldIgnoreNewline := p.state.ignoreNewLine + p.state.ignoreNewLine = false - start := p.prev.Start + start := p.state.prev.Start var statements []Node 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 - switch t.Type { + t := p.state.curr + switch t.Kind { case TokenType: p.advance() - start := p.prev.Start + start := p.state.prev.Start if err := p.expect(TokenName, "types must have a name"); err != nil { 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 { return nil, err @@ -282,7 +294,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { sig, start, - p.prev.End, + p.state.prev.End, }, nil case TokenIf: @@ -300,7 +312,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { var otherwise Node if p.accept(TokenElse) { - otherwise, err = p.expression(p.curr.Type != TokenIf) + otherwise, err = p.expression(p.state.curr.Kind != TokenIf) if err != nil { return nil, err } @@ -316,7 +328,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { case TokenReturn: p.advance() - start := p.prev.Start + start := p.state.prev.Start v, err := p.expression(false) if err != nil { @@ -326,12 +338,12 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { return &ReturnNode{ v, start, - p.prev.End, + p.state.prev.End, }, nil case TokenWhile: p.advance() - start := p.prev.Start + start := p.state.prev.Start cond, err := p.expression(false) if err != nil { @@ -347,12 +359,12 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { cond, logic, start, - p.prev.End, + p.state.prev.End, }, nil case TokenFor: p.advance() - start := p.prev.Start + start := p.state.prev.Start counter, err := p.expression(false) if err != nil { @@ -379,12 +391,12 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { logic, start, - p.prev.End, + p.state.prev.End, }, nil case TokenInclude: 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 { return nil, err @@ -392,13 +404,13 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { return &IncludeNode{ &StringNode{ - p.prev.Lexeme[1 : len(p.prev.Lexeme)-1], - p.prev.Lexeme, - p.prev.Start, - p.prev.End, + p.state.prev.Lexeme[1 : len(p.state.prev.Lexeme)-1], + p.state.prev.Lexeme, + p.state.prev.Start, + p.state.prev.End, }, start, - p.prev.End, + p.state.prev.End, }, nil default: @@ -408,7 +420,7 @@ func (p *Parser) expression(mustBeBlock bool) (Node, error) { } if p.accept(TokenDeclare) || p.accept(TokenAssign) { - isDeclaration := p.prev.Type == TokenDeclare + isDeclaration := p.state.prev.Kind == TokenDeclare // possibly assign tuples; not implemented yet v, err := p.expression(false) @@ -509,7 +521,7 @@ func (p *Parser) binary() (Node, error) { r := values.Pop() l := values.Pop() opToken := ops.Pop() - op := tokenToBinaryOperation(opToken.Type) + op := tokenToBinaryOperation(opToken.Kind) start, _ := l.Bounds() _, end := r.Bounds() @@ -524,12 +536,12 @@ func (p *Parser) binary() (Node, error) { }) } - for isBinaryOperator(p.curr.Type) { - for ops.Current > 0 && binaryPrecedence(p.curr.Type) <= binaryPrecedence(ops.Peek().Type) { + for isBinaryOperator(p.state.curr.Kind) { + for ops.Current > 0 && binaryPrecedence(p.state.curr.Kind) <= binaryPrecedence(ops.Peek().Kind) { reduce() } - ops.Push(p.curr) + ops.Push(p.state.curr) p.advance() 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 { return nil, err } - name := p.prev + name := p.state.prev f = &AccessNode{ f, - p.prev, + p.state.prev, name.Start, name.End, } - if p.curr.Type == TokenOpenParenthesis { + if p.state.curr.Kind == TokenOpenParenthesis { args, err := p.parseArgs() if err != nil { return nil, err @@ -577,11 +589,11 @@ func (p *Parser) chain() (Node, error) { f, args, name.Start, - p.prev.End, + p.state.prev.End, } } - } else if p.curr.Type == TokenOpenParenthesis { - start := p.curr.Start + } else if p.state.curr.Kind == TokenOpenParenthesis { + start := p.state.curr.Start args, err := p.parseArgs() if err != nil { return nil, err @@ -592,10 +604,10 @@ func (p *Parser) chain() (Node, error) { args, start, - p.prev.End, + p.state.prev.End, } } else if p.accept(TokenOpenBracket) { - start := p.prev.Start + start := p.state.prev.Start index, err := p.expression(false) if err != nil { @@ -610,7 +622,7 @@ func (p *Parser) chain() (Node, error) { f, index, start, - p.prev.End, + p.state.prev.End, } } else { break @@ -621,71 +633,71 @@ func (p *Parser) chain() (Node, error) { } func (p *Parser) factor() (Node, error) { - switch (*p.curr).Type { + switch (*p.state.curr).Kind { case TokenString: p.advance() return &StringNode{ - (*p.prev).Lexeme[1 : len((*p.prev).Lexeme)-1], - (*p.prev).Lexeme, - p.prev.Start, - p.prev.End, + (*p.state.prev).Lexeme[1 : len((*p.state.prev).Lexeme)-1], + (*p.state.prev).Lexeme, + p.state.prev.Start, + p.state.prev.End, }, nil case TokenInteger: 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 { - 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{ num, - p.prev.Start, - p.prev.End, + p.state.prev.Start, + p.state.prev.End, }, nil case TokenFloat: p.advance() - num, err := strconv.ParseFloat((*p.prev).Lexeme, FloatSize) + num, err := strconv.ParseFloat((*p.state.prev).Lexeme, FloatSize) 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{ num, - p.prev.Start, - p.prev.End, + p.state.prev.Start, + p.state.prev.End, }, nil case TokenHexadecimal: p.advance() - start := (*p.prev).Start - num, ok := new(big.Int).SetString(p.prev.Lexeme[2:], 16) + start := (*p.state.prev).Start + num, ok := new(big.Int).SetString(p.state.prev.Lexeme[2:], 16) 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{ num, start, - p.prev.End, + p.state.prev.End, }, nil case TokenTrue: p.advance() return &BooleanNode{ true, - p.prev.Start, - p.prev.End, + p.state.prev.Start, + p.state.prev.End, }, nil case TokenFalse: p.advance() return &BooleanNode{ false, - p.prev.Start, - p.prev.End, + p.state.prev.Start, + p.state.prev.End, }, nil case TokenNil: @@ -694,7 +706,7 @@ func (p *Parser) factor() (Node, error) { case TokenOpenBracket: p.advance() - start := p.prev.Start + start := p.state.prev.Start // TODO: find better solution; current one is messy // Maybe perform better analysis to determine the kind of the list... @@ -708,12 +720,12 @@ func (p *Parser) factor() (Node, error) { []Node{}, s, start, - p.prev.End, + p.state.prev.End, }, nil } - oldIgnoreNewline := p.ignoreNewLine - p.ignoreNewLine = true + oldIgnoreNewline := p.state.ignoreNewLine + p.state.ignoreNewLine = true var values []Node for !p.accept(TokenCloseBracket) { @@ -731,19 +743,19 @@ func (p *Parser) factor() (Node, error) { values = append(values, value) } - p.ignoreNewLine = oldIgnoreNewline + p.state.ignoreNewLine = oldIgnoreNewline return &ListNode{ values, nil, start, - p.prev.End, + p.state.prev.End, }, nil // unary minus case TokenMinus: p.advance() - op := p.prev + op := p.state.prev f, err := p.factor() if err != nil { @@ -754,12 +766,12 @@ func (p *Parser) factor() (Node, error) { f, op, op.Start, - p.prev.End, + p.state.prev.End, }, nil case TokenBang: p.advance() - op := p.prev + op := p.state.prev v, err := p.factor() if err != nil { @@ -771,16 +783,16 @@ func (p *Parser) factor() (Node, error) { v, op, op.Start, - p.prev.End, + p.state.prev.End, }, nil case TokenName: p.advance() - name := (*p.prev).Lexeme - start := p.prev.Start - nameEnd := p.prev.End + name := (*p.state.prev).Lexeme + start := p.state.prev.Start + nameEnd := p.state.prev.End - if p.curr.Type == TokenOpenParenthesis { + if p.state.curr.Kind == TokenOpenParenthesis { args, err := p.parseArgs() if err != nil { return nil, err @@ -794,7 +806,7 @@ func (p *Parser) factor() (Node, error) { }, args, start, - p.prev.End, + p.state.prev.End, }, nil } @@ -806,11 +818,11 @@ func (p *Parser) factor() (Node, error) { case TokenFunc: p.advance() - start := p.prev.Start + start := p.state.prev.Start var name *Token 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() @@ -842,7 +854,7 @@ func (p *Parser) factor() (Node, error) { yield, logic, start, - p.prev.End, + p.state.prev.End, } if name != nil { @@ -851,7 +863,7 @@ func (p *Parser) factor() (Node, error) { fn, true, start, - p.prev.End, + p.state.prev.End, }, nil } @@ -859,11 +871,67 @@ func (p *Parser) factor() (Node, error) { case TokenOpenParenthesis: 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 - if p.acceptAll(TokenName, TokenColon) { - return nil, p.error("objects are not implemented yet (TBD)", p.prev) + key := p.state.curr + 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) @@ -876,6 +944,7 @@ func (p *Parser) factor() (Node, error) { return nil, err } + p.state.ignoreNewLine = oldCare return v, nil } @@ -897,26 +966,27 @@ func (p *Parser) factor() (Node, error) { } } + p.state.ignoreNewLine = oldCare return &TupleNode{ items, start, - p.prev.End, + p.state.prev.End, }, nil case TokenBreakpoint: p.advance() return &BreakpointNode{ - p.prev.Start, - p.prev.End, + p.state.prev.Start, + p.state.prev.End, }, nil case TokenOpenBrace: return p.expression(true) 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) 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 { 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 { 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 { return nil, err } @@ -1005,9 +1075,42 @@ func (p *Parser) parseSignature() (TypeSignature, error) { var s TypeSignature if p.accept(TokenOpenParenthesis) { + oldCare := p.state.ignoreNewLine + p.state.ignoreNewLine = true + p.skipNewLines() + // we're inside an object - if p.acceptAll(TokenName, TokenColon) { - return nil, p.error("objects are not implemented yet (TBD)", p.prev) + name := p.state.curr + 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() @@ -1043,6 +1146,8 @@ func (p *Parser) parseSignature() (TypeSignature, error) { items, } } + + p.state.ignoreNewLine = oldCare } else if p.accept(TokenFunc) { if err := p.expect(TokenOpenParenthesis, "func signature must have parentheses for parameters"); err != nil { 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 { return nil, err } - name := (*p.prev).Lexeme + name := (*p.state.prev).Lexeme switch name { case "str": @@ -1130,3 +1235,8 @@ func (p *Parser) parseSignature() (TypeSignature, error) { return s, nil } + +func (p *Parser) skipNewLines() { + for p.accept(TokenNewLine) { + } +} diff --git a/core/parser_test.go b/core/parser_test.go index 79b60c9..b58330b 100644 --- a/core/parser_test.go +++ b/core/parser_test.go @@ -15,7 +15,7 @@ func TestNewParser(t *testing.T) { t.Fatal("parser should not be nil") } - if p.pos != 0 { + if p.state.pos != 0 { t.Error("parser should initialize position at 0") } @@ -715,6 +715,95 @@ func GetTokenTestData() map[string]TokenTestData { 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]) } + 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: 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{ 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.Logf("tokens were accepted") } -func TestParser_AcceptAll_TooFew(t *testing.T) { - p := NewParser("a", []string{}, []Token{ +func TestParser_AcceptSeqAndAfter(t *testing.T) { + p := NewParser("a: 1", []string{}, []Token{ 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") } diff --git a/core/types.go b/core/types.go index 34fd1f6..6fe7c99 100644 --- a/core/types.go +++ b/core/types.go @@ -50,9 +50,11 @@ func (t Type) String() string { return "composite" case TypeInner: 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 { @@ -560,7 +562,7 @@ func (s *RecordSignature) Contains(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 { diff --git a/core/values.go b/core/values.go index 357c5d2..4312d44 100644 --- a/core/values.go +++ b/core/values.go @@ -777,7 +777,30 @@ func (v *RecordValue) Type() ValueType { } 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 { diff --git a/core/vm.go b/core/vm.go index 17a1a62..7a41a33 100644 --- a/core/vm.go +++ b/core/vm.go @@ -126,7 +126,7 @@ const ( // InstructionAppend Append to a list. stack: (... > list > item) => (... > list) InstructionAppend // 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. InstructionFormList // 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. 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 // is the index. [..., container, index] -> [..., item] InstructionIndexList @@ -1076,6 +1082,18 @@ func (vm *VM) Next() bool { 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: i := vm.Stack.Pop().(*IntegerValue) l := vm.Stack.Pop().(*ListValue) diff --git a/records.ang b/records.ang new file mode 100644 index 0000000..198475c --- /dev/null +++ b/records.ang @@ -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) diff --git a/test_all.sh b/test_all.sh index 9905f24..a6bdd01 100755 --- a/test_all.sh +++ b/test_all.sh @@ -2,7 +2,7 @@ echo '=== Building WASM lib ===' 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=" exit 1 else @@ -12,7 +12,7 @@ cd .. echo '=== Building CLI ===' cd cli || exit 1 -if ! go build .; then +if ! go build . "$@"; then echo "=x= Had error building CLI =x=" exit 1 else @@ -22,7 +22,7 @@ cd .. echo "=== Running go core tests ===" cd core || exit 1 -if ! go test .; then +if ! go test . "$@"; then echo "=x= Core testing failed =x= " exit 1 else