From db3a3e29ebcb76df820555ffbf5203813ac033ad Mon Sep 17 00:00:00 2001 From: Neemek Date: Wed, 15 Jul 2026 21:48:30 +0200 Subject: [PATCH 01/15] add basic records --- core/types.go | 54 +++++++++++++++++++++++++++++++++++++++++++ core/values.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/core/types.go b/core/types.go index 13f9e1b..34fd1f6 100644 --- a/core/types.go +++ b/core/types.go @@ -21,6 +21,7 @@ const ( TypeComposite TypeInner TypeNamed + TypeRecord ) func (t Type) String() string { @@ -521,3 +522,56 @@ func (s *NamedSignature) Equal(t TypeSignature) bool { func (s *NamedSignature) String() string { return s.Name } + +type RecordSignature struct { + Entries map[string]TypeSignature +} + +func (*RecordSignature) Type() Type { + return TypeRecord +} + +func (s *RecordSignature) Contains(t TypeSignature) bool { + if t.Type() != TypeRecord { + return false + } + + rs := t.(*RecordSignature) + + /* + // not quite sure about this one + if len(s.Entries) != len(rs.Entries) { + return false + } + */ + + for k, v := range s.Entries { + is, ok := rs.Entries[k] + if !ok { + return false + } + + if !v.Contains(is) { + return false + } + } + + return true +} + +func (s *RecordSignature) Equal(t TypeSignature) bool { + return s.Contains(t) && t.Contains(s) +} + +func (s *RecordSignature) String() string { + b := strings.Builder{} + b.WriteString("(") + + for name, kind := range s.Entries { + b.WriteString(fmt.Sprintf("%s: %s, ", name, kind)) + } + + b.WriteString(")") + + return b.String() +} diff --git a/core/values.go b/core/values.go index 94c666f..357c5d2 100644 --- a/core/values.go +++ b/core/values.go @@ -22,7 +22,7 @@ const ( ObjectValueType FunctionValueType BuiltinFunctionValueType - VariableValueType + RecordValueType ) func (v ValueType) String() string { @@ -47,8 +47,8 @@ func (v ValueType) String() string { return "function" case BuiltinFunctionValueType: return "builtin function" - case VariableValueType: - return "variable" + case RecordValueType: + return "record" } return "undefined" @@ -767,3 +767,59 @@ func (v *BuiltinFunctionValue) Clone() Value { v.Constant, } } + +type RecordValue struct { + Entries map[string]Value +} + +func (v *RecordValue) Type() ValueType { + return RecordValueType +} + +func (v *RecordValue) String() string { + return "" +} + +func (v *RecordValue) DebugString() string { + return v.String() +} + +func (v *RecordValue) Clone() Value { + return &RecordValue{ + v.Entries, + } +} + +func (v *RecordValue) Equals(other Value) bool { + if other.Type() != RecordValueType { + return false + } + + r := other.(*RecordValue) + + if len(r.Entries) != len(v.Entries) { + return false + } + + for key, val := range v.Entries { + oth, ok := r.Entries[key] + if !ok { + return false + } + + if !val.Equals(oth) { + return false + } + } + + return true +} + +func (v *RecordValue) Get(key string) (Value, error) { + val, ok := v.Entries[key] + if !ok { + return nil, errors.New(fmt.Sprintf("record has no property \"%s\"", key)) + } + + return val, nil +} From ca0031431d100dea9c79257b43ebe2560267c63e Mon Sep 17 00:00:00 2001 From: Neemek Date: Wed, 15 Jul 2026 21:50:23 +0200 Subject: [PATCH 02/15] more examples --- examples/bad.ang | 40 +++++++++++++++++++++++++++++++ examples/blemish.ang | 20 ++++++++++++++++ examples/chars.ang | 15 ++++++++++++ examples/codegen.ang | 24 +++++++++++++++++++ examples/emoji.ang | 2 ++ examples/era3.ang | 11 +++++++++ examples/fails.ang | 4 ++++ examples/foo.ang | 8 +++++++ examples/imp.ang | 15 ++++++++++++ examples/inc.ang | 2 ++ examples/massacre.ang | 4 ++++ examples/network.ang | 8 +++++++ examples/neutral-pH.ang | 18 ++++++++++++++ examples/pos.ang | 21 +++++++++++++++++ examples/sqrt.ang | 6 +++++ examples/very_bad.ang | 8 +++++++ examples/warp.ang | 25 ++++++++++++++++++++ examples/worst.ang | 4 ++++ tests/forp.ang | 52 +++++++++++++++++++++++++++++++++++++++++ 19 files changed, 287 insertions(+) create mode 100644 examples/bad.ang create mode 100644 examples/blemish.ang create mode 100644 examples/chars.ang create mode 100644 examples/codegen.ang create mode 100644 examples/emoji.ang create mode 100644 examples/era3.ang create mode 100644 examples/fails.ang create mode 100644 examples/foo.ang create mode 100644 examples/imp.ang create mode 100644 examples/inc.ang create mode 100644 examples/massacre.ang create mode 100644 examples/network.ang create mode 100644 examples/neutral-pH.ang create mode 100644 examples/pos.ang create mode 100755 examples/sqrt.ang create mode 100644 examples/very_bad.ang create mode 100644 examples/warp.ang create mode 100644 examples/worst.ang create mode 100644 tests/forp.ang diff --git a/examples/bad.ang b/examples/bad.ang new file mode 100644 index 0000000..80c9cb7 --- /dev/null +++ b/examples/bad.ang @@ -0,0 +1,40 @@ +import "lib/math.ang" + +primes := [2] + +func is_prime(x: number) boolean { + i := 0 + while i < primes.length() && primes.at(i)*primes.at(i) < x { + if mod(x, primes.at(i)) == 0 { + return false + } + i = i + 1 + } + + return true +} + +n := 1 +max := 100000 + +while n < max { + n = n + 2 + + if is_prime(n) { + primes.append(n) + + # Update counter + print(char(0x0D)) + print(str(n)) + print("/") + print(str(max)) + print(char(0x09)) + print(str(roundd(n/max*100, 2))) + print("%") + print(char(0x09)) + print(str(primes.length())) + print(" primes") + } +} + +write(str(primes)) diff --git a/examples/blemish.ang b/examples/blemish.ang new file mode 100644 index 0000000..ee25ddf --- /dev/null +++ b/examples/blemish.ang @@ -0,0 +1,20 @@ +fn fib(n: int) -> int { + if n < 2 { + n + } else { + fib(n-1) + fib(n-2) + } +} + +println(fib(2)) + +fn other(n: int) -> int { + if n <= 0 { + n + } else { + println(n) + (n - 1) + } +} + +println(other(2)) diff --git a/examples/chars.ang b/examples/chars.ang new file mode 100644 index 0000000..343ccee --- /dev/null +++ b/examples/chars.ang @@ -0,0 +1,15 @@ + +MAX_WIDTH := 16 + +print(" ") +w := 1 +n := 0x21 +while n < 0xA0 { + print(char(n)) + n = n + 1 + w = w + 1 + if w >= MAX_WIDTH { + write("") + w = 0 + } +} diff --git a/examples/codegen.ang b/examples/codegen.ang new file mode 100644 index 0000000..7d44558 --- /dev/null +++ b/examples/codegen.ang @@ -0,0 +1,24 @@ + +passphrase := "Hello world!".split("") +start := [0, 0, 0] +modulus := 10 +base := byte("!") + +i := 0 +n := 0 +while n < passphrase.length() { + b := byte(passphrase.at(n)) + + v = start.at(i) + b - base + while v >= modulus { + v = v - modulus + } + + start.put(i, v) + + if i >= 3 { + i = 0 + } + n = n + 1 +} + diff --git a/examples/emoji.ang b/examples/emoji.ang new file mode 100644 index 0000000..09f0cb1 --- /dev/null +++ b/examples/emoji.ang @@ -0,0 +1,2 @@ + +write(char(0x12) + char(0x85) + char(0x07)) diff --git a/examples/era3.ang b/examples/era3.ang new file mode 100644 index 0000000..966a458 --- /dev/null +++ b/examples/era3.ang @@ -0,0 +1,11 @@ +fn counter() -> (fn() -> int) { + i := 0 + + fn() -> int { i = i + 1 } +} + +next := counter() + +println(next()) +println(next()) +println(next()) diff --git a/examples/fails.ang b/examples/fails.ang new file mode 100644 index 0000000..8e3174d --- /dev/null +++ b/examples/fails.ang @@ -0,0 +1,4 @@ +import "lib/honning.ang" + +write(_bell+_italic+"Hello "+_underline+"world "+_strike+"micheal"+_reset) + diff --git a/examples/foo.ang b/examples/foo.ang new file mode 100644 index 0000000..1047386 --- /dev/null +++ b/examples/foo.ang @@ -0,0 +1,8 @@ + +fn foo(n: int) -> str { + if n % 2 == 0 { + "foo" + } else { + "bar" + } +} diff --git a/examples/imp.ang b/examples/imp.ang new file mode 100644 index 0000000..0ccb3ed --- /dev/null +++ b/examples/imp.ang @@ -0,0 +1,15 @@ + +func is_cool(x: number|string) boolean { + if x == "cool" { + return true + } else if x == 69 { + return true + } + + return nil +} + +write(str(is_cool("not cool"))) +write(str(is_cool("cool"))) +write(str(is_cool(0))) +write(str(is_cool(69))) diff --git a/examples/inc.ang b/examples/inc.ang new file mode 100644 index 0000000..8a697a7 --- /dev/null +++ b/examples/inc.ang @@ -0,0 +1,2 @@ + +foo := include "foo.ang" diff --git a/examples/massacre.ang b/examples/massacre.ang new file mode 100644 index 0000000..6dbde3c --- /dev/null +++ b/examples/massacre.ang @@ -0,0 +1,4 @@ +a := fn b(n: int) -> int { + n + 1 +} + diff --git a/examples/network.ang b/examples/network.ang new file mode 100644 index 0000000..86ba041 --- /dev/null +++ b/examples/network.ang @@ -0,0 +1,8 @@ + +func get(url: string) string { + res := request("GET", url, "") + + return res.text +} + +write(get("https://www.neemek.com/hello.txt")) diff --git a/examples/neutral-pH.ang b/examples/neutral-pH.ang new file mode 100644 index 0000000..f867a72 --- /dev/null +++ b/examples/neutral-pH.ang @@ -0,0 +1,18 @@ +import "../lib/math.ang" + +# Inputs +c := 1.0*pow(10.0, -2.0) + +println(c) +pH := -log(c, 10.0) + +println(pH) + +if pH == 7.0 { + println("pH-en er nøytral (=7)") +} else if pH < 7.0 { + println("pH-en er sur (<7)") +} else { + println("pH-en er basisk (>7)") +} + diff --git a/examples/pos.ang b/examples/pos.ang new file mode 100644 index 0000000..2eda099 --- /dev/null +++ b/examples/pos.ang @@ -0,0 +1,21 @@ + +type Vec2 = (float, float) + +fn add(a: Vec2, b: Vec2) -> Vec2 { + (a[0] + b[0], a[1] + b[1]) +} + +fn sub(a: Vec2, b: Vec2) -> Vec2 { + (a[0] - b[0], a[1] - b[1]) +} + +fn dot(a: Vec2, b: Vec2) -> float { + a[0]*b[0] + a[1]*b[1] +} + +u := (0.0, 1.0) +v := (2.0, 3.0) + +println(add(u, v)) +println(sub(u, v)) +println(dot(u, v)) diff --git a/examples/sqrt.ang b/examples/sqrt.ang new file mode 100755 index 0000000..f013699 --- /dev/null +++ b/examples/sqrt.ang @@ -0,0 +1,6 @@ +#!/Users/neemek/Code/anglais/cli/cli run + +import "lib/math.ang" + +x := sqrt(9485739448) +write(str(x)) diff --git a/examples/very_bad.ang b/examples/very_bad.ang new file mode 100644 index 0000000..dd18680 --- /dev/null +++ b/examples/very_bad.ang @@ -0,0 +1,8 @@ + +a := 0 +while a < 10 { + print(".") + a = a + 1 +} + +write("") diff --git a/examples/warp.ang b/examples/warp.ang new file mode 100644 index 0000000..3186556 --- /dev/null +++ b/examples/warp.ang @@ -0,0 +1,25 @@ + +# Aliases +type Receiver = fn() -> any +type Sender = fn(any) + +fn new_channel() -> (Sender, Receiver) { + queue := [] + + fn send(n: any) { + queue.push(n) # mutate in-place + } + + fn recv() -> any | nil { + queue.pop() + } + + (send, recv) +} + +(send, recv) := new_channel() + +( + send:, + recv:, +) diff --git a/examples/worst.ang b/examples/worst.ang new file mode 100644 index 0000000..47b90db --- /dev/null +++ b/examples/worst.ang @@ -0,0 +1,4 @@ +import "worst.ang" + +write("Hello world!") + diff --git a/tests/forp.ang b/tests/forp.ang new file mode 100644 index 0000000..0891ab6 --- /dev/null +++ b/tests/forp.ang @@ -0,0 +1,52 @@ + +fn range(from: int, to: int) -> (fn() -> (int, bool)) { + i := from - 1 + + fn() -> (int, bool) { + i = i + 1 + if i >= to { + (-1, false) + } else { + (i, true) + } + } +} + +for i in range(0, 10) { + println(i) +} + +fn chars(s: str) -> (fn() -> (str, bool)) { + i := 0 + + fn() -> (str, bool) { + if i >= s.length() { + ("", false) + } else { + c := s[i] + i = i + 1 + + (c, true) + } + } +} + +for c in chars("hello") { + print(c) + print(" ") +} + +println("") + +fn items(l: [any]) -> (fn() -> (any, bool)) { + i := 0 + + fn() -> (any, bool) { + if i >= l.length() { + (nil, false) + } else { + i = i + 1 + (l[i - 1], true) + } + } +} From 42fa039daf4f65f135b39459c3714e68e0bb0c57 Mon Sep 17 00:00:00 2001 From: neemek Date: Mon, 17 Aug 2026 17:08:24 +0200 Subject: [PATCH 03/15] 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 From 8d12c0bdf80ffece9c29b8687659f55bb99cf47b Mon Sep 17 00:00:00 2001 From: neemek Date: Mon, 17 Aug 2026 17:15:34 +0200 Subject: [PATCH 04/15] fix missing string-conversions --- core/lexer.go | 2 ++ core/nodes.go | 6 +++++- core/values.go | 4 ++-- core/vm.go | 8 ++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/lexer.go b/core/lexer.go index da8aa70..1701479 100644 --- a/core/lexer.go +++ b/core/lexer.go @@ -185,6 +185,8 @@ func (t TokenKind) String() string { return "for" case TokenIn: return "in" + case TokenPercent: + return "percent" } panic("UNDEFINED TOKENTYPE STRING CONVERSION") diff --git a/core/nodes.go b/core/nodes.go index 54ddcdc..0a04c44 100644 --- a/core/nodes.go +++ b/core/nodes.go @@ -96,6 +96,10 @@ func (n NodeType) String() string { return "Index" case RecordNodeType: return "Record" + case ForNodeType: + return "For" + case IncludeNodeType: + return "Include" } return "Invalid Node Type" } @@ -534,7 +538,7 @@ func (n ConditionalNode) String() string { return fmt.Sprintf("if %s then %s", n.condition.String(), n.do.String()) } - return fmt.Sprintf("if %s then %s otheriwise %s", n.condition.String(), n.do.String(), n.otherwise.String()) + return fmt.Sprintf("if %s then %s otherwise %s", n.condition.String(), n.do.String(), n.otherwise.String()) } func (n ConditionalNode) Bounds() (Pos, Pos) { diff --git a/core/values.go b/core/values.go index 4312d44..8f95a1c 100644 --- a/core/values.go +++ b/core/values.go @@ -383,7 +383,7 @@ var StringPrototype = map[string]*BuiltinFunctionValue{ } if prev != len(str) { - out = append(out, &StringValue{str[prev:len(str)]}) + out = append(out, &StringValue{str[prev:]}) } return &ListValue{out}, nil @@ -661,7 +661,7 @@ func (v *TupleValue) Equals(other Value) bool { } var TuplePrototype = map[string]*BuiltinFunctionValue{ - "at": &BuiltinFunctionValue{ + "at": { "at", &FunctionSignature{ []TypeSignature{&IntegerSignature{}}, diff --git a/core/vm.go b/core/vm.go index 7a41a33..22a568e 100644 --- a/core/vm.go +++ b/core/vm.go @@ -268,6 +268,14 @@ func (b Bytecode) String() string { return "INDEX_TUPLE" case InstructionDestructureTuple: return "DESTRUCTURE_TUPLE" + case InstructionModInt: + return "MOD_INT" + case InstructionNewRecord: + return "NEW_RECORD" + case InstructionSetRecordItem: + return "SET_PROPERTY" + case InstructionIndexString: + return "INDEX_STRING" } return "UNDEFINED" } From 0f905a7fea05f8e472984388d3a0ed404a77309d Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:44:10 +0200 Subject: [PATCH 05/15] Add set index in list, and update lib+examples+tests --- core/compiler.go | 35 ++++++++++++++++++++++++++++++++ core/values.go | 46 ++++++++++++++++++------------------------ core/vm.go | 34 ++++++++++++++++++++++--------- examples/fib.ang | 7 +------ examples/list.ang | 14 ++++++------- examples/recursive.ang | 9 +++++---- foo.ang | 0 lib/list.ang | 2 +- lib/math.ang | 2 ++ lib/testing.ang | 14 ++++++------- tests/list.ang | 7 +++++++ tests/tuple.ang | 7 +++++++ 12 files changed, 117 insertions(+), 60 deletions(-) create mode 100644 foo.ang diff --git a/core/compiler.go b/core/compiler.go index 36f0f7a..83098d5 100644 --- a/core/compiler.go +++ b/core/compiler.go @@ -977,6 +977,41 @@ func (c *Compiler) compileAssignFromStack(to Node, sig TypeSignature, declare bo } return sig, nil + case IndexNodeType: + if declare { + return nil, c.error(fmt.Sprintf("cannot declare an indexed item"), to) + } + + n := to.(*IndexNode) + + ssig, err := c.compile(n.source) + if err != nil { + return nil, err + } + + isig, err := c.compile(n.index) + if err != nil { + return nil, err + } + + if isig.Type() != TypeInteger { + return nil, c.error(fmt.Sprintf("cannot index into list with non-integer (%s)", isig), n.index) + } + + switch ssig.Type() { + case TypeList: + lsig := ssig.(*ListSignature) + if !c.typeMatches(sig, lsig.Contents) { + return nil, c.error(fmt.Sprintf("cannot assign value of type %s to list with items of type %s", sig, lsig.Contents), to) + } + + c.add(InstructionSetIndexList) + default: + return nil, c.error(fmt.Sprintf("cannot set index of %s", ssig.Type()), n.source) + } + + return sig, nil + default: return nil, c.error(fmt.Sprintf("cannot assign to %s", to.Type()), to) } diff --git a/core/values.go b/core/values.go index 8f95a1c..efb5f41 100644 --- a/core/values.go +++ b/core/values.go @@ -117,8 +117,12 @@ type Value interface { // Get a member from the value. An error is returned if the member does not exist Get(string) (Value, error) - // Clone create a clone of the value. The returned value is a pointer to a new value of the same type as the value. - Clone() Value + // Copy create a copy of the value. A copy is a direct copy for small data (numbers) and a copy of pointer + // for bigger data (lists, tuples, dicts) + Copy() Value + + // Clone create a clone of the value. A clone is new data for all data. + //Clone() Value } type NilValue struct{} @@ -143,7 +147,7 @@ func (v *NilValue) Get(_ string) (Value, error) { return nil, errors.New("nil has no properties") } -func (v *NilValue) Clone() Value { +func (v *NilValue) Copy() Value { return &NilValue{} } @@ -175,7 +179,7 @@ func (v *BoolValue) Get(_ string) (Value, error) { return nil, errors.New("booleans have no properties") } -func (v *BoolValue) Clone() Value { +func (v *BoolValue) Copy() Value { return &BoolValue{ v.Boolean, } @@ -258,11 +262,11 @@ func (v *ObjectValue) Get(key string) (Value, error) { } } -func (v *ObjectValue) Clone() Value { +func (v *ObjectValue) Copy() Value { m := make(map[string]Value, len(v.Members)) for name, mem := range v.Members { - m[name] = mem.Clone() + m[name] = mem.Copy() } return &ObjectValue{ @@ -303,7 +307,7 @@ func (v *FloatValue) Get(_ string) (Value, error) { return nil, errors.New("numbers have no properties") } -func (v *FloatValue) Clone() Value { +func (v *FloatValue) Copy() Value { return &FloatValue{ v.Number, } @@ -334,7 +338,7 @@ func (v *IntegerValue) Get(_ string) (Value, error) { return nil, errors.New("numbers have no properties") } -func (v *IntegerValue) Clone() Value { +func (v *IntegerValue) Copy() Value { return &IntegerValue{ new(big.Int).Set(v.Number), } @@ -427,7 +431,7 @@ func (v *StringValue) Get(key string) (Value, error) { return nil, errors.New(fmt.Sprintf("string has no property \"%s\"", key)) } -func (v *StringValue) Clone() Value { +func (v *StringValue) Copy() Value { return &StringValue{ v.Text, } @@ -594,15 +598,9 @@ func (v *ListValue) Get(key string) (Value, error) { return nil, errors.New(fmt.Sprintf("list has no property \"%s\"", key)) } -func (v *ListValue) Clone() Value { - n := make([]Value, len(v.Items)) - - for i, item := range v.Items { - n[i] = item.Clone() - } - +func (v *ListValue) Copy() Value { return &ListValue{ - n, + v.Items, } } @@ -635,13 +633,9 @@ func (v *TupleValue) DebugString() string { return v.String() } -func (v *TupleValue) Clone() Value { - n := make([]Value, len(v.Items)) - for i, item := range v.Items { - n[i] = item.Clone() - } +func (v *TupleValue) Copy() Value { return &TupleValue{ - n, + v.Items, } } @@ -718,7 +712,7 @@ func (v *FunctionValue) Get(_ string) (Value, error) { return nil, errors.New("functions have no properties") } -func (v *FunctionValue) Clone() Value { +func (v *FunctionValue) Copy() Value { return &FunctionValue{ v.Name, v.Params, @@ -758,7 +752,7 @@ func (v *BuiltinFunctionValue) Get(_ string) (Value, error) { return nil, errors.New("functions have no properties") } -func (v *BuiltinFunctionValue) Clone() Value { +func (v *BuiltinFunctionValue) Copy() Value { return &BuiltinFunctionValue{ v.Name, v.Signature, @@ -807,7 +801,7 @@ func (v *RecordValue) DebugString() string { return v.String() } -func (v *RecordValue) Clone() Value { +func (v *RecordValue) Copy() Value { return &RecordValue{ v.Entries, } diff --git a/core/vm.go b/core/vm.go index 22a568e..f07890f 100644 --- a/core/vm.go +++ b/core/vm.go @@ -152,10 +152,14 @@ const ( // is the index. [..., container, index] -> [..., item] InstructionIndexTuple // InstructionIndexString index into a string. The lower item is the container, and the top item - // is the index. [..., container, index] -> [..., item]. Produces a new string with the character - // at the position + // is the index. [..., container, index] -> [..., item]. Produces a new string with only the character + // at the indexed position InstructionIndexString + // InstructionSetIndexList set the item at a given index in a list. + // [..., item, container, index] -> [..., item] + InstructionSetIndexList + // InstructionBreakpoint for debugging purposes InstructionBreakpoint ) @@ -638,7 +642,7 @@ var DefaultGlobals = map[string]Value{ n, _ := v.Number.Float64() return &FloatValue{n}, nil case *FloatValue: - return v.Clone(), nil + return v.Copy(), nil case *StringValue: num, err := strconv.ParseFloat(v.Text, FloatSize) if err != nil { @@ -978,7 +982,7 @@ func (vm *VM) Next() bool { vm.Stack.Push(v) case InstructionSetLocal: - value := vm.Stack.Peek().Clone() + value := vm.Stack.Peek().Copy() name := vm.GetConstant(vm.NextByte()).(*StringValue).Text vm.setVar(name, value) @@ -986,7 +990,7 @@ func (vm *VM) Next() bool { case InstructionDeclareLocal: vm.addVar( vm.GetConstant(vm.NextByte()).(*StringValue).Text, - vm.Stack.Peek().Clone(), + vm.Stack.Peek().Copy(), ) case InstructionGetGlobal: @@ -1070,7 +1074,7 @@ func (vm *VM) Next() bool { vm.Stack.Push(r, l) case InstructionDuplicate: - vm.Stack.Push(vm.Stack.Peek().Clone()) + vm.Stack.Push(vm.Stack.Peek().Copy()) case InstructionAccessProperty: source := vm.Stack.Pop() @@ -1112,7 +1116,7 @@ func (vm *VM) Next() bool { vm.error(fmt.Sprintf("index %d out of bounds", n)) } - vm.Stack.Push(l.Items[n].Clone()) + vm.Stack.Push(l.Items[n].Copy()) case InstructionIndexTuple: i := vm.Stack.Pop().(*IntegerValue) @@ -1124,7 +1128,7 @@ func (vm *VM) Next() bool { vm.error(fmt.Sprintf("index %d out of bounds", n)) } - vm.Stack.Push(t.Items[n].Clone()) + vm.Stack.Push(t.Items[n].Copy()) case InstructionIndexString: i := vm.Stack.Pop().(*IntegerValue) @@ -1138,6 +1142,18 @@ func (vm *VM) Next() bool { vm.Stack.Push(&StringValue{string(s.Text[n])}) + case InstructionSetIndexList: + n := vm.Stack.Pop().(*IntegerValue) + l := vm.Stack.Pop().(*ListValue) + + i := n.Number.Int64() + + if i < 0 || int64(len(l.Items)) <= i { + vm.error(fmt.Sprintf("index %d out of bounds", i)) + } + + l.Items[i] = vm.Stack.Peek().Copy() + case InstructionBreakpoint: /* // I'm keeping this @@ -1284,7 +1300,7 @@ func (vm *VM) HasNext() bool { } func (vm *VM) GetConstant(id Bytecode) Value { - return vm.chunk.Constants[id].Clone() + return vm.chunk.Constants[id].Copy() } func (vm *VM) ReadConstant() Value { diff --git a/examples/fib.ang b/examples/fib.ang index 63a432a..4860a1e 100644 --- a/examples/fib.ang +++ b/examples/fib.ang @@ -3,14 +3,9 @@ fn range(from: int, to: int) -> (fn() -> (int, bool)) { i := from - 1 - end := to - 1 fn() -> (int, bool) { - if i < end { - (i = i+1, true) - } else { - (-1, false) - } + (i = i+1, i+1 < to) } } diff --git a/examples/list.ang b/examples/list.ang index 9866d3e..d34887e 100644 --- a/examples/list.ang +++ b/examples/list.ang @@ -1,6 +1,6 @@ # Empty list -println([]) +println([]any) # List with items println([3, 1, 4, 1, 5, 9, 2, 6, 5]) @@ -8,25 +8,25 @@ println([3, 1, 4, 1, 5, 9, 2, 6, 5]) # List with items of different types println(["", "私はかっこいいです。", true, nil, nil, 1, 2]) -a := [] +a := []int -a = a + [1] -a = a + [2] +a.push(1) +a.push(2) println(a) -list := [] +list := []int x := 0 for n in 0..100 { x = x + 2*n + 1 - list = list + [x] + list.push(x) } println(list) -println(list.map(func(a) { +println(list.map(fn(a: int) -> int { return a - 1 })) println(list.length()) diff --git a/examples/recursive.ang b/examples/recursive.ang index caa0e37..f30109f 100644 --- a/examples/recursive.ang +++ b/examples/recursive.ang @@ -1,14 +1,15 @@ # This program computes the fibonacci numbers using recursion (O(2^n)) # It is very slow -func fib(x: number) number { +fn fib(x: number) number { if x <= 1 { - return x + x + } else { + fib(x - 1) + fib(x - 2) } - return fib(x - 1) + fib(x - 2) } n := 0 while n < 100 { - write(str(fib(n))) + println(fib(n)) n = n + 1 } diff --git a/foo.ang b/foo.ang new file mode 100644 index 0000000..e69de29 diff --git a/lib/list.ang b/lib/list.ang index 254912d..792751f 100644 --- a/lib/list.ang +++ b/lib/list.ang @@ -1,5 +1,5 @@ -fn ([T]) map(f: fn(T) -> R) -> [R] { +fn map(list: [T], f: fn(T) -> R) -> [R] { out := [] for v in list.iter() { diff --git a/lib/math.ang b/lib/math.ang index 120e60e..9dac01a 100644 --- a/lib/math.ang +++ b/lib/math.ang @@ -249,3 +249,5 @@ fn tan(x: float) -> float { # todo 0.0 } + +(E:, PI:, sqrt:, log:, ln:, exp:, pow:) diff --git a/lib/testing.ang b/lib/testing.ang index 59aed2c..82e7b77 100644 --- a/lib/testing.ang +++ b/lib/testing.ang @@ -1,25 +1,25 @@ NAMESPACE := "" -func namespace(name: string, test: func()) { +fn namespace(name: str, test: fn()) { NAMESPACE = name test() } -func assertEqual(a: any, b: any) { +fn eq(a: T, b: T) { if a != b { - write(format("assertion error: % should (but doesn't) equal %", [a, b])) + println(format("assertion error: % should (but doesn't) equal %", [a, b])) exit(1) } else if env("DEBUG") != "" { - write(format("assertion success: % equals %", [a, b])) + println(format("assertion success: % equals %", [a, b])) } } -func assertNotEqual(a: any, b: any) { +fn neq(a: T, b: T) { if a == b { - write(format("assertion error: % shouldn't (but does) equal %", [a, b])) + println(format("assertion error: % shouldn't (but does) equal %", [a, b])) exit(1) } else if env("DEBUG") != "" { - write(format("assertion success: % doesn't equal %", [a, b])) + println(format("assertion success: % doesn't equal %", [a, b])) } } diff --git a/tests/list.ang b/tests/list.ang index 8554f14..e98fcf8 100644 --- a/tests/list.ang +++ b/tests/list.ang @@ -34,3 +34,10 @@ assertEq("the, first, time".split(", "), ["the", "first", "time"]) # list indexing assertEq([1, 2, 3].at(1), 2) assertEq(["a", "b", "c"].at(2), "c") + +# mutating list +a := [1, 2, 3] +assertEq(a, [1, 2, 3]) + +a[1] = 4 +assertEq(a, [1, 4, 3]) diff --git a/tests/tuple.ang b/tests/tuple.ang index 81aa508..65e99e9 100644 --- a/tests/tuple.ang +++ b/tests/tuple.ang @@ -9,3 +9,10 @@ fn neighbours(n: int) -> (int, int) { } assertEq(neighbours(2), (1, 3)) + +a := (1, 2) +assertEq(a, (1, 2)) + +(x, y) := a +assertEq(x, 1) +assertEq(y, 2) From aeb305210cea362df530540ab9e63a5e56759774 Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:44:26 +0200 Subject: [PATCH 06/15] new go.work.sum --- go.work.sum | 1 + 1 file changed, 1 insertion(+) create mode 100644 go.work.sum diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..2d35219 --- /dev/null +++ b/go.work.sum @@ -0,0 +1 @@ +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= From 72b2ec224bda76ef937333635033838408e2a43d Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:50:09 +0200 Subject: [PATCH 07/15] add composite simplifaction --- core/types.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/types.go b/core/types.go index 6fe7c99..81dafd8 100644 --- a/core/types.go +++ b/core/types.go @@ -487,6 +487,27 @@ func quickComposite(a ...TypeSignature) TypeSignature { return s } +func simplifyComposite(a TypeSignature) TypeSignature { + var atoms []TypeSignature + + s := NewStack[TypeSignature](16) + s.Push(a) + + for s.Current > 0 { + i := s.Pop() + + c, ok := i.(*CompositeSignature) + if !ok { + + atoms = append(atoms, i) + } else { + s.Push(c.A, c.B) + } + } + + return quickComposite(atoms...) +} + type InnerSignature struct{} func (*InnerSignature) Type() Type { From c5a843ea39b698356690ef006cbfe0ae34b42b1b Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:52:16 +0200 Subject: [PATCH 08/15] intellij settings --- .idea/.gitignore | 8 -------- .idea/anglais.iml | 14 -------------- .idea/inspectionProfiles/Project_Default.xml | 10 ---------- .idea/modules.xml | 8 -------- 4 files changed, 40 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/anglais.iml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/modules.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/anglais.iml b/.idea/anglais.iml deleted file mode 100644 index 11646b4..0000000 --- a/.idea/anglais.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 6c7658f..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 36d0426..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file From 19011e67da0c9b09843da5fa4e03bb9e3d94d719 Mon Sep 17 00:00:00 2001 From: Neemek Date: Wed, 15 Jul 2026 21:48:30 +0200 Subject: [PATCH 09/15] add basic records --- core/types.go | 54 +++++++++++++++++++++++++++++++++++++++++++ core/values.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/core/types.go b/core/types.go index 13f9e1b..34fd1f6 100644 --- a/core/types.go +++ b/core/types.go @@ -21,6 +21,7 @@ const ( TypeComposite TypeInner TypeNamed + TypeRecord ) func (t Type) String() string { @@ -521,3 +522,56 @@ func (s *NamedSignature) Equal(t TypeSignature) bool { func (s *NamedSignature) String() string { return s.Name } + +type RecordSignature struct { + Entries map[string]TypeSignature +} + +func (*RecordSignature) Type() Type { + return TypeRecord +} + +func (s *RecordSignature) Contains(t TypeSignature) bool { + if t.Type() != TypeRecord { + return false + } + + rs := t.(*RecordSignature) + + /* + // not quite sure about this one + if len(s.Entries) != len(rs.Entries) { + return false + } + */ + + for k, v := range s.Entries { + is, ok := rs.Entries[k] + if !ok { + return false + } + + if !v.Contains(is) { + return false + } + } + + return true +} + +func (s *RecordSignature) Equal(t TypeSignature) bool { + return s.Contains(t) && t.Contains(s) +} + +func (s *RecordSignature) String() string { + b := strings.Builder{} + b.WriteString("(") + + for name, kind := range s.Entries { + b.WriteString(fmt.Sprintf("%s: %s, ", name, kind)) + } + + b.WriteString(")") + + return b.String() +} diff --git a/core/values.go b/core/values.go index 94c666f..357c5d2 100644 --- a/core/values.go +++ b/core/values.go @@ -22,7 +22,7 @@ const ( ObjectValueType FunctionValueType BuiltinFunctionValueType - VariableValueType + RecordValueType ) func (v ValueType) String() string { @@ -47,8 +47,8 @@ func (v ValueType) String() string { return "function" case BuiltinFunctionValueType: return "builtin function" - case VariableValueType: - return "variable" + case RecordValueType: + return "record" } return "undefined" @@ -767,3 +767,59 @@ func (v *BuiltinFunctionValue) Clone() Value { v.Constant, } } + +type RecordValue struct { + Entries map[string]Value +} + +func (v *RecordValue) Type() ValueType { + return RecordValueType +} + +func (v *RecordValue) String() string { + return "" +} + +func (v *RecordValue) DebugString() string { + return v.String() +} + +func (v *RecordValue) Clone() Value { + return &RecordValue{ + v.Entries, + } +} + +func (v *RecordValue) Equals(other Value) bool { + if other.Type() != RecordValueType { + return false + } + + r := other.(*RecordValue) + + if len(r.Entries) != len(v.Entries) { + return false + } + + for key, val := range v.Entries { + oth, ok := r.Entries[key] + if !ok { + return false + } + + if !val.Equals(oth) { + return false + } + } + + return true +} + +func (v *RecordValue) Get(key string) (Value, error) { + val, ok := v.Entries[key] + if !ok { + return nil, errors.New(fmt.Sprintf("record has no property \"%s\"", key)) + } + + return val, nil +} From ff11d7e0ed04d60cf933f968f347d3df2f2eba8b Mon Sep 17 00:00:00 2001 From: neemek Date: Mon, 17 Aug 2026 17:08:24 +0200 Subject: [PATCH 10/15] 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 From 09ac16a42d0d31daa8966301fa073ceeb0f88172 Mon Sep 17 00:00:00 2001 From: neemek Date: Mon, 17 Aug 2026 17:15:34 +0200 Subject: [PATCH 11/15] fix missing string-conversions --- core/lexer.go | 2 ++ core/nodes.go | 6 +++++- core/values.go | 4 ++-- core/vm.go | 8 ++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/lexer.go b/core/lexer.go index da8aa70..1701479 100644 --- a/core/lexer.go +++ b/core/lexer.go @@ -185,6 +185,8 @@ func (t TokenKind) String() string { return "for" case TokenIn: return "in" + case TokenPercent: + return "percent" } panic("UNDEFINED TOKENTYPE STRING CONVERSION") diff --git a/core/nodes.go b/core/nodes.go index 54ddcdc..0a04c44 100644 --- a/core/nodes.go +++ b/core/nodes.go @@ -96,6 +96,10 @@ func (n NodeType) String() string { return "Index" case RecordNodeType: return "Record" + case ForNodeType: + return "For" + case IncludeNodeType: + return "Include" } return "Invalid Node Type" } @@ -534,7 +538,7 @@ func (n ConditionalNode) String() string { return fmt.Sprintf("if %s then %s", n.condition.String(), n.do.String()) } - return fmt.Sprintf("if %s then %s otheriwise %s", n.condition.String(), n.do.String(), n.otherwise.String()) + return fmt.Sprintf("if %s then %s otherwise %s", n.condition.String(), n.do.String(), n.otherwise.String()) } func (n ConditionalNode) Bounds() (Pos, Pos) { diff --git a/core/values.go b/core/values.go index 4312d44..8f95a1c 100644 --- a/core/values.go +++ b/core/values.go @@ -383,7 +383,7 @@ var StringPrototype = map[string]*BuiltinFunctionValue{ } if prev != len(str) { - out = append(out, &StringValue{str[prev:len(str)]}) + out = append(out, &StringValue{str[prev:]}) } return &ListValue{out}, nil @@ -661,7 +661,7 @@ func (v *TupleValue) Equals(other Value) bool { } var TuplePrototype = map[string]*BuiltinFunctionValue{ - "at": &BuiltinFunctionValue{ + "at": { "at", &FunctionSignature{ []TypeSignature{&IntegerSignature{}}, diff --git a/core/vm.go b/core/vm.go index 7a41a33..22a568e 100644 --- a/core/vm.go +++ b/core/vm.go @@ -268,6 +268,14 @@ func (b Bytecode) String() string { return "INDEX_TUPLE" case InstructionDestructureTuple: return "DESTRUCTURE_TUPLE" + case InstructionModInt: + return "MOD_INT" + case InstructionNewRecord: + return "NEW_RECORD" + case InstructionSetRecordItem: + return "SET_PROPERTY" + case InstructionIndexString: + return "INDEX_STRING" } return "UNDEFINED" } From 3bccc227ba41f6028467c09b0c0361ac955853c1 Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:44:10 +0200 Subject: [PATCH 12/15] Add set index in list, and update lib+examples+tests --- core/compiler.go | 35 ++++++++++++++++++++++++++++++++ core/values.go | 46 ++++++++++++++++++------------------------ core/vm.go | 34 ++++++++++++++++++++++--------- examples/fib.ang | 7 +------ examples/list.ang | 14 ++++++------- examples/recursive.ang | 9 +++++---- foo.ang | 0 lib/list.ang | 2 +- lib/math.ang | 2 ++ lib/testing.ang | 14 ++++++------- tests/list.ang | 7 +++++++ tests/tuple.ang | 7 +++++++ 12 files changed, 117 insertions(+), 60 deletions(-) create mode 100644 foo.ang diff --git a/core/compiler.go b/core/compiler.go index 36f0f7a..83098d5 100644 --- a/core/compiler.go +++ b/core/compiler.go @@ -977,6 +977,41 @@ func (c *Compiler) compileAssignFromStack(to Node, sig TypeSignature, declare bo } return sig, nil + case IndexNodeType: + if declare { + return nil, c.error(fmt.Sprintf("cannot declare an indexed item"), to) + } + + n := to.(*IndexNode) + + ssig, err := c.compile(n.source) + if err != nil { + return nil, err + } + + isig, err := c.compile(n.index) + if err != nil { + return nil, err + } + + if isig.Type() != TypeInteger { + return nil, c.error(fmt.Sprintf("cannot index into list with non-integer (%s)", isig), n.index) + } + + switch ssig.Type() { + case TypeList: + lsig := ssig.(*ListSignature) + if !c.typeMatches(sig, lsig.Contents) { + return nil, c.error(fmt.Sprintf("cannot assign value of type %s to list with items of type %s", sig, lsig.Contents), to) + } + + c.add(InstructionSetIndexList) + default: + return nil, c.error(fmt.Sprintf("cannot set index of %s", ssig.Type()), n.source) + } + + return sig, nil + default: return nil, c.error(fmt.Sprintf("cannot assign to %s", to.Type()), to) } diff --git a/core/values.go b/core/values.go index 8f95a1c..efb5f41 100644 --- a/core/values.go +++ b/core/values.go @@ -117,8 +117,12 @@ type Value interface { // Get a member from the value. An error is returned if the member does not exist Get(string) (Value, error) - // Clone create a clone of the value. The returned value is a pointer to a new value of the same type as the value. - Clone() Value + // Copy create a copy of the value. A copy is a direct copy for small data (numbers) and a copy of pointer + // for bigger data (lists, tuples, dicts) + Copy() Value + + // Clone create a clone of the value. A clone is new data for all data. + //Clone() Value } type NilValue struct{} @@ -143,7 +147,7 @@ func (v *NilValue) Get(_ string) (Value, error) { return nil, errors.New("nil has no properties") } -func (v *NilValue) Clone() Value { +func (v *NilValue) Copy() Value { return &NilValue{} } @@ -175,7 +179,7 @@ func (v *BoolValue) Get(_ string) (Value, error) { return nil, errors.New("booleans have no properties") } -func (v *BoolValue) Clone() Value { +func (v *BoolValue) Copy() Value { return &BoolValue{ v.Boolean, } @@ -258,11 +262,11 @@ func (v *ObjectValue) Get(key string) (Value, error) { } } -func (v *ObjectValue) Clone() Value { +func (v *ObjectValue) Copy() Value { m := make(map[string]Value, len(v.Members)) for name, mem := range v.Members { - m[name] = mem.Clone() + m[name] = mem.Copy() } return &ObjectValue{ @@ -303,7 +307,7 @@ func (v *FloatValue) Get(_ string) (Value, error) { return nil, errors.New("numbers have no properties") } -func (v *FloatValue) Clone() Value { +func (v *FloatValue) Copy() Value { return &FloatValue{ v.Number, } @@ -334,7 +338,7 @@ func (v *IntegerValue) Get(_ string) (Value, error) { return nil, errors.New("numbers have no properties") } -func (v *IntegerValue) Clone() Value { +func (v *IntegerValue) Copy() Value { return &IntegerValue{ new(big.Int).Set(v.Number), } @@ -427,7 +431,7 @@ func (v *StringValue) Get(key string) (Value, error) { return nil, errors.New(fmt.Sprintf("string has no property \"%s\"", key)) } -func (v *StringValue) Clone() Value { +func (v *StringValue) Copy() Value { return &StringValue{ v.Text, } @@ -594,15 +598,9 @@ func (v *ListValue) Get(key string) (Value, error) { return nil, errors.New(fmt.Sprintf("list has no property \"%s\"", key)) } -func (v *ListValue) Clone() Value { - n := make([]Value, len(v.Items)) - - for i, item := range v.Items { - n[i] = item.Clone() - } - +func (v *ListValue) Copy() Value { return &ListValue{ - n, + v.Items, } } @@ -635,13 +633,9 @@ func (v *TupleValue) DebugString() string { return v.String() } -func (v *TupleValue) Clone() Value { - n := make([]Value, len(v.Items)) - for i, item := range v.Items { - n[i] = item.Clone() - } +func (v *TupleValue) Copy() Value { return &TupleValue{ - n, + v.Items, } } @@ -718,7 +712,7 @@ func (v *FunctionValue) Get(_ string) (Value, error) { return nil, errors.New("functions have no properties") } -func (v *FunctionValue) Clone() Value { +func (v *FunctionValue) Copy() Value { return &FunctionValue{ v.Name, v.Params, @@ -758,7 +752,7 @@ func (v *BuiltinFunctionValue) Get(_ string) (Value, error) { return nil, errors.New("functions have no properties") } -func (v *BuiltinFunctionValue) Clone() Value { +func (v *BuiltinFunctionValue) Copy() Value { return &BuiltinFunctionValue{ v.Name, v.Signature, @@ -807,7 +801,7 @@ func (v *RecordValue) DebugString() string { return v.String() } -func (v *RecordValue) Clone() Value { +func (v *RecordValue) Copy() Value { return &RecordValue{ v.Entries, } diff --git a/core/vm.go b/core/vm.go index 22a568e..f07890f 100644 --- a/core/vm.go +++ b/core/vm.go @@ -152,10 +152,14 @@ const ( // is the index. [..., container, index] -> [..., item] InstructionIndexTuple // InstructionIndexString index into a string. The lower item is the container, and the top item - // is the index. [..., container, index] -> [..., item]. Produces a new string with the character - // at the position + // is the index. [..., container, index] -> [..., item]. Produces a new string with only the character + // at the indexed position InstructionIndexString + // InstructionSetIndexList set the item at a given index in a list. + // [..., item, container, index] -> [..., item] + InstructionSetIndexList + // InstructionBreakpoint for debugging purposes InstructionBreakpoint ) @@ -638,7 +642,7 @@ var DefaultGlobals = map[string]Value{ n, _ := v.Number.Float64() return &FloatValue{n}, nil case *FloatValue: - return v.Clone(), nil + return v.Copy(), nil case *StringValue: num, err := strconv.ParseFloat(v.Text, FloatSize) if err != nil { @@ -978,7 +982,7 @@ func (vm *VM) Next() bool { vm.Stack.Push(v) case InstructionSetLocal: - value := vm.Stack.Peek().Clone() + value := vm.Stack.Peek().Copy() name := vm.GetConstant(vm.NextByte()).(*StringValue).Text vm.setVar(name, value) @@ -986,7 +990,7 @@ func (vm *VM) Next() bool { case InstructionDeclareLocal: vm.addVar( vm.GetConstant(vm.NextByte()).(*StringValue).Text, - vm.Stack.Peek().Clone(), + vm.Stack.Peek().Copy(), ) case InstructionGetGlobal: @@ -1070,7 +1074,7 @@ func (vm *VM) Next() bool { vm.Stack.Push(r, l) case InstructionDuplicate: - vm.Stack.Push(vm.Stack.Peek().Clone()) + vm.Stack.Push(vm.Stack.Peek().Copy()) case InstructionAccessProperty: source := vm.Stack.Pop() @@ -1112,7 +1116,7 @@ func (vm *VM) Next() bool { vm.error(fmt.Sprintf("index %d out of bounds", n)) } - vm.Stack.Push(l.Items[n].Clone()) + vm.Stack.Push(l.Items[n].Copy()) case InstructionIndexTuple: i := vm.Stack.Pop().(*IntegerValue) @@ -1124,7 +1128,7 @@ func (vm *VM) Next() bool { vm.error(fmt.Sprintf("index %d out of bounds", n)) } - vm.Stack.Push(t.Items[n].Clone()) + vm.Stack.Push(t.Items[n].Copy()) case InstructionIndexString: i := vm.Stack.Pop().(*IntegerValue) @@ -1138,6 +1142,18 @@ func (vm *VM) Next() bool { vm.Stack.Push(&StringValue{string(s.Text[n])}) + case InstructionSetIndexList: + n := vm.Stack.Pop().(*IntegerValue) + l := vm.Stack.Pop().(*ListValue) + + i := n.Number.Int64() + + if i < 0 || int64(len(l.Items)) <= i { + vm.error(fmt.Sprintf("index %d out of bounds", i)) + } + + l.Items[i] = vm.Stack.Peek().Copy() + case InstructionBreakpoint: /* // I'm keeping this @@ -1284,7 +1300,7 @@ func (vm *VM) HasNext() bool { } func (vm *VM) GetConstant(id Bytecode) Value { - return vm.chunk.Constants[id].Clone() + return vm.chunk.Constants[id].Copy() } func (vm *VM) ReadConstant() Value { diff --git a/examples/fib.ang b/examples/fib.ang index 63a432a..4860a1e 100644 --- a/examples/fib.ang +++ b/examples/fib.ang @@ -3,14 +3,9 @@ fn range(from: int, to: int) -> (fn() -> (int, bool)) { i := from - 1 - end := to - 1 fn() -> (int, bool) { - if i < end { - (i = i+1, true) - } else { - (-1, false) - } + (i = i+1, i+1 < to) } } diff --git a/examples/list.ang b/examples/list.ang index 9866d3e..d34887e 100644 --- a/examples/list.ang +++ b/examples/list.ang @@ -1,6 +1,6 @@ # Empty list -println([]) +println([]any) # List with items println([3, 1, 4, 1, 5, 9, 2, 6, 5]) @@ -8,25 +8,25 @@ println([3, 1, 4, 1, 5, 9, 2, 6, 5]) # List with items of different types println(["", "私はかっこいいです。", true, nil, nil, 1, 2]) -a := [] +a := []int -a = a + [1] -a = a + [2] +a.push(1) +a.push(2) println(a) -list := [] +list := []int x := 0 for n in 0..100 { x = x + 2*n + 1 - list = list + [x] + list.push(x) } println(list) -println(list.map(func(a) { +println(list.map(fn(a: int) -> int { return a - 1 })) println(list.length()) diff --git a/examples/recursive.ang b/examples/recursive.ang index caa0e37..f30109f 100644 --- a/examples/recursive.ang +++ b/examples/recursive.ang @@ -1,14 +1,15 @@ # This program computes the fibonacci numbers using recursion (O(2^n)) # It is very slow -func fib(x: number) number { +fn fib(x: number) number { if x <= 1 { - return x + x + } else { + fib(x - 1) + fib(x - 2) } - return fib(x - 1) + fib(x - 2) } n := 0 while n < 100 { - write(str(fib(n))) + println(fib(n)) n = n + 1 } diff --git a/foo.ang b/foo.ang new file mode 100644 index 0000000..e69de29 diff --git a/lib/list.ang b/lib/list.ang index 254912d..792751f 100644 --- a/lib/list.ang +++ b/lib/list.ang @@ -1,5 +1,5 @@ -fn ([T]) map(f: fn(T) -> R) -> [R] { +fn map(list: [T], f: fn(T) -> R) -> [R] { out := [] for v in list.iter() { diff --git a/lib/math.ang b/lib/math.ang index 120e60e..9dac01a 100644 --- a/lib/math.ang +++ b/lib/math.ang @@ -249,3 +249,5 @@ fn tan(x: float) -> float { # todo 0.0 } + +(E:, PI:, sqrt:, log:, ln:, exp:, pow:) diff --git a/lib/testing.ang b/lib/testing.ang index 59aed2c..82e7b77 100644 --- a/lib/testing.ang +++ b/lib/testing.ang @@ -1,25 +1,25 @@ NAMESPACE := "" -func namespace(name: string, test: func()) { +fn namespace(name: str, test: fn()) { NAMESPACE = name test() } -func assertEqual(a: any, b: any) { +fn eq(a: T, b: T) { if a != b { - write(format("assertion error: % should (but doesn't) equal %", [a, b])) + println(format("assertion error: % should (but doesn't) equal %", [a, b])) exit(1) } else if env("DEBUG") != "" { - write(format("assertion success: % equals %", [a, b])) + println(format("assertion success: % equals %", [a, b])) } } -func assertNotEqual(a: any, b: any) { +fn neq(a: T, b: T) { if a == b { - write(format("assertion error: % shouldn't (but does) equal %", [a, b])) + println(format("assertion error: % shouldn't (but does) equal %", [a, b])) exit(1) } else if env("DEBUG") != "" { - write(format("assertion success: % doesn't equal %", [a, b])) + println(format("assertion success: % doesn't equal %", [a, b])) } } diff --git a/tests/list.ang b/tests/list.ang index 8554f14..e98fcf8 100644 --- a/tests/list.ang +++ b/tests/list.ang @@ -34,3 +34,10 @@ assertEq("the, first, time".split(", "), ["the", "first", "time"]) # list indexing assertEq([1, 2, 3].at(1), 2) assertEq(["a", "b", "c"].at(2), "c") + +# mutating list +a := [1, 2, 3] +assertEq(a, [1, 2, 3]) + +a[1] = 4 +assertEq(a, [1, 4, 3]) diff --git a/tests/tuple.ang b/tests/tuple.ang index 81aa508..65e99e9 100644 --- a/tests/tuple.ang +++ b/tests/tuple.ang @@ -9,3 +9,10 @@ fn neighbours(n: int) -> (int, int) { } assertEq(neighbours(2), (1, 3)) + +a := (1, 2) +assertEq(a, (1, 2)) + +(x, y) := a +assertEq(x, 1) +assertEq(y, 2) From 7a5203cc263bed23c8f9ed742da88c19537d35c3 Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:44:26 +0200 Subject: [PATCH 13/15] new go.work.sum --- go.work.sum | 1 + 1 file changed, 1 insertion(+) create mode 100644 go.work.sum diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..2d35219 --- /dev/null +++ b/go.work.sum @@ -0,0 +1 @@ +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= From 86760689f1072160d31c7c977fefa1de13eba58d Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:50:09 +0200 Subject: [PATCH 14/15] add composite simplifaction --- core/types.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/types.go b/core/types.go index 6fe7c99..81dafd8 100644 --- a/core/types.go +++ b/core/types.go @@ -487,6 +487,27 @@ func quickComposite(a ...TypeSignature) TypeSignature { return s } +func simplifyComposite(a TypeSignature) TypeSignature { + var atoms []TypeSignature + + s := NewStack[TypeSignature](16) + s.Push(a) + + for s.Current > 0 { + i := s.Pop() + + c, ok := i.(*CompositeSignature) + if !ok { + + atoms = append(atoms, i) + } else { + s.Push(c.A, c.B) + } + } + + return quickComposite(atoms...) +} + type InnerSignature struct{} func (*InnerSignature) Type() Type { From df639235b0ad0d751fc8140332145626940e3013 Mon Sep 17 00:00:00 2001 From: Neemek Date: Thu, 20 Aug 2026 13:52:16 +0200 Subject: [PATCH 15/15] intellij settings --- .idea/.gitignore | 8 -------- .idea/anglais.iml | 14 -------------- .idea/inspectionProfiles/Project_Default.xml | 10 ---------- .idea/modules.xml | 8 -------- 4 files changed, 40 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/anglais.iml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/modules.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/anglais.iml b/.idea/anglais.iml deleted file mode 100644 index 11646b4..0000000 --- a/.idea/anglais.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 6c7658f..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 36d0426..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file