add basic support for tuples

This commit is contained in:
Neemek 2026-07-11 12:37:15 +02:00
parent 7bb277045c
commit e03d18c7db
Signed by: neemek
GPG key ID: 84FFE4D7D40AB25E
7 changed files with 339 additions and 18 deletions

View file

@ -193,6 +193,18 @@ func (c *Compiler) compile(tree Node) error {
panic("compile called with nil value") panic("compile called with nil value")
} }
if c.optimize && c.isTreeConstant(tree) {
v, err := c.compute(tree)
if err != nil {
panic(err) // this shouldn't happen
}
c.add(InstructionConstant)
c.addConstant(v)
return nil
}
switch tree.Type() { switch tree.Type() {
case StringNodeType: case StringNodeType:
n := tree.(*StringNode) n := tree.(*StringNode)
@ -236,19 +248,23 @@ func (c *Compiler) compile(tree Node) error {
c.add(InstructionConstant) c.add(InstructionConstant)
c.addConstant(&IntegerValue{tree.(*IntegerNode).value}) c.addConstant(&IntegerValue{tree.(*IntegerNode).value})
case TupleNodeType:
n := tree.(*TupleNode)
for _, n := range n.items {
err := c.compile(n)
if err != nil {
return err
}
}
c.add(InstructionFormTuple)
c.addU16(uint16(len(n.items)))
case ListNodeType: case ListNodeType:
l := tree.(*ListNode) l := tree.(*ListNode)
if len(l.items) == 0 { if len(l.items) == 0 {
c.add(InstructionNewList) c.add(InstructionNewList)
} else if c.optimize && c.isTreeConstant(l) {
v, err := c.compute(l)
if err != nil {
panic(err) // this shouldn't happen
}
c.add(InstructionConstant)
c.addConstant(v)
} else { } else {
for _, n := range l.items { for _, n := range l.items {
err := c.compile(n) err := c.compile(n)
@ -1001,6 +1017,21 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
return c.deduceSignature(n.statements[len(n.statements)-1]) return c.deduceSignature(n.statements[len(n.statements)-1])
case AssignNodeType: case AssignNodeType:
return c.deduceSignature(tree.(*AssignNode).value) return c.deduceSignature(tree.(*AssignNode).value)
case TupleNodeType:
n := tree.(*TupleNode)
var items []TypeSignature
for _, i := range n.items {
t, err := c.deduceSignature(i)
if err != nil {
return nil, err
}
items = append(items, t)
}
return &TupleSignature{
items,
}, nil
default: default:
return nil, c.error(fmt.Sprintf("impossible to deduce signature of %s", tree.Type()), tree) return nil, c.error(fmt.Sprintf("impossible to deduce signature of %s", tree.Type()), tree)
} }

View file

@ -198,6 +198,21 @@ func (p *Parser) accept(tokenType TokenType) bool {
return false return false
} }
func (p *Parser) acceptAll(tokenTypes ...TokenType) bool {
if int(p.pos)+len(tokenTypes) > len(p.tokens) {
return false
}
for i, tokenType := range tokenTypes {
if p.tokens[int(p.pos)+i].Type != tokenType {
return false
}
}
p.pos += Pos(len(tokenTypes))
return true
}
func (p *Parser) expect(tokenType TokenType, reason string) error { func (p *Parser) expect(tokenType TokenType, reason string) error {
if !p.accept(tokenType) { if !p.accept(tokenType) {
return p.error(fmt.Sprintf("Expected token %s, got %s; %s", tokenType, p.curr.Type, reason), p.curr) return p.error(fmt.Sprintf("Expected token %s, got %s; %s", tokenType, p.curr.Type, reason), p.curr)
@ -813,15 +828,50 @@ func (p *Parser) factor() (Node, error) {
case TokenOpenParenthesis: case TokenOpenParenthesis:
p.advance() p.advance()
v, err := p.condition() start := p.prev.Start
// we're inside an object
if p.acceptAll(TokenName, TokenColon) {
return nil, p.error("objects are not implemented yet (TBD)", p.prev)
}
v, err := p.expression(false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := p.expect(TokenCloseParenthesis, "an opened parenthesis must be closed"); err != nil {
if !p.accept(TokenComma) {
if err := p.expect(TokenCloseParenthesis, "parenthesis must be closed"); err != nil {
return nil, err return nil, err
} }
return v, nil return v, nil
}
items := []Node{v}
for !p.accept(TokenCloseParenthesis) {
i, err := p.expression(false)
if err != nil {
return nil, err
}
items = append(items, i)
if !p.accept(TokenComma) {
if err := p.expect(TokenCloseParenthesis, "tuples must be closed"); err != nil {
return nil, err
}
break
}
}
return &TupleNode{
items,
start,
p.prev.End,
}, nil
case TokenBreakpoint: case TokenBreakpoint:
p.advance() p.advance()
@ -831,6 +881,9 @@ func (p *Parser) factor() (Node, error) {
p.prev.End, p.prev.End,
}, nil }, nil
case TokenOpenBrace:
return p.expression(true)
default: default:
return nil, p.error(fmt.Sprintf("invalid factor %s", p.curr), p.curr) return nil, p.error(fmt.Sprintf("invalid factor %s", p.curr), p.curr)
} }
@ -1355,15 +1408,44 @@ func (p *Parser) parseSignature() (TypeSignature, error) {
var s TypeSignature var s TypeSignature
if p.accept(TokenOpenParenthesis) { if p.accept(TokenOpenParenthesis) {
is, err := p.parseSignature() // we're inside an object
if p.acceptAll(TokenName, TokenColon) {
return nil, p.error("objects are not implemented yet (TBD)", p.prev)
}
v, err := p.parseSignature()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := p.expect(TokenCloseParenthesis, "expected closing parenthesis"); err != nil {
if !p.accept(TokenComma) {
if err := p.expect(TokenCloseParenthesis, "parenthesis must be closed"); err != nil {
return nil, err return nil, err
} }
s = is s = v
} else {
items := []TypeSignature{v}
for !p.accept(TokenCloseParenthesis) {
i, err := p.parseSignature()
if err != nil {
return nil, err
}
items = append(items, i)
if !p.accept(TokenComma) {
if err := p.expect(TokenCloseParenthesis, "tuples must be closed"); err != nil {
return nil, err
}
break
}
}
s = &TupleSignature{
items,
}
}
} else if p.accept(TokenFunc) { } else if p.accept(TokenFunc) {
if err := p.expect(TokenOpenParenthesis, "func signature must have parentheses for parameters"); err != nil { if err := p.expect(TokenOpenParenthesis, "func signature must have parentheses for parameters"); err != nil {
return nil, err return nil, err

View file

@ -960,6 +960,31 @@ func TestParser_Parse(t *testing.T) {
} }
} }
func TestParser_AcceptAll(t *testing.T) {
p := NewParser("a:", []string{}, []Token{
NewToken(TokenName, 0, 1, 0, "a"),
NewToken(TokenColon, 1, 2, 0, "a"),
})
if !p.acceptAll(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{
NewToken(TokenName, 0, 1, 0, "a"),
})
if p.acceptAll(TokenName, TokenColon) {
t.Fatalf("tokens were incorrectly accepted")
}
t.Logf("tokens were, as expected, not accepted")
}
func BenchmarkParser_Parse(b *testing.B) { func BenchmarkParser_Parse(b *testing.B) {
tokenData := GetTokenTestData() tokenData := GetTokenTestData()

View file

@ -14,6 +14,7 @@ const (
TypeBoolean TypeBoolean
TypeNil TypeNil
TypeList TypeList
TypeTuple
TypeObject TypeObject
TypeFunction TypeFunction
TypeAny TypeAny
@ -35,6 +36,8 @@ func (t Type) String() string {
return "nil" return "nil"
case TypeList: case TypeList:
return "list" return "list"
case TypeTuple:
return "tuple"
case TypeObject: case TypeObject:
return "object" return "object"
case TypeFunction: case TypeFunction:
@ -92,6 +95,16 @@ func SignatureOf(v Value) TypeSignature {
} }
case *BuiltinFunctionValue: case *BuiltinFunctionValue:
return t.Signature return t.Signature
case *TupleValue:
var contains []TypeSignature
for _, p := range t.Items {
sig := SignatureOf(p)
contains = append(contains, sig)
}
return &TupleSignature{
contains,
}
} }
panic(fmt.Sprintf("unknown value; cannot get signature of %s", v)) panic(fmt.Sprintf("unknown value; cannot get signature of %s", v))
@ -217,6 +230,58 @@ func (s *ListSignature) String() string {
return fmt.Sprintf("list[%s]", s.Contents) return fmt.Sprintf("list[%s]", s.Contents)
} }
type TupleSignature struct {
Contents []TypeSignature
}
func (*TupleSignature) Type() Type {
return TypeTuple
}
func (s *TupleSignature) Matches(other TypeSignature) bool {
if other.Type() == TypeComposite {
return other.Matches(s)
}
if other.Type() == TypeAny {
return true
}
if other.Type() != TypeTuple || len(other.(*TupleSignature).Contents) != len(s.Contents) {
return false
}
n := other.(*TupleSignature).Contents
for i, c := range s.Contents {
if !c.Matches(n[i]) {
return false
}
}
return true
}
func (s *TupleSignature) String() string {
sb := strings.Builder{}
sb.WriteString("(")
for i, t := range s.Contents {
if i > 0 {
sb.WriteString(",")
sb.WriteString(" ")
}
sb.WriteString(t.String())
}
if len(s.Contents) <= 1 {
sb.WriteString(",")
}
sb.WriteString(")")
return sb.String()
}
type ObjectSignature struct { type ObjectSignature struct {
Members map[string]TypeSignature Members map[string]TypeSignature
} }

View file

@ -6,6 +6,7 @@ import (
"math/big" "math/big"
"reflect" "reflect"
"strconv" "strconv"
"strings"
) )
type ValueType int type ValueType int
@ -17,6 +18,7 @@ const (
IntegerValueType IntegerValueType
StringValueType StringValueType
ListValueType ListValueType
TupleValueType
ObjectValueType ObjectValueType
FunctionValueType FunctionValueType
BuiltinFunctionValueType BuiltinFunctionValueType
@ -39,6 +41,8 @@ func (v ValueType) String() string {
return "string" return "string"
case ListValueType: case ListValueType:
return "list" return "list"
case TupleValueType:
return "tuple"
case FunctionValueType: case FunctionValueType:
return "function" return "function"
case BuiltinFunctionValueType: case BuiltinFunctionValueType:
@ -278,7 +282,12 @@ func (v *FloatValue) Type() ValueType {
} }
func (v *FloatValue) String() string { func (v *FloatValue) String() string {
return strconv.FormatFloat(v.Number, 'g', -1, FloatSize) s := strconv.FormatFloat(v.Number, 'g', -1, FloatSize)
if strings.Index(s, ".") == -1 {
s += ".0"
}
return s
} }
func (v *FloatValue) DebugString() string { func (v *FloatValue) DebugString() string {
@ -597,6 +606,88 @@ func (v *ListValue) Clone() Value {
} }
} }
type TupleValue struct {
Items []Value
}
func (v *TupleValue) Type() ValueType {
return TupleValueType
}
func (v *TupleValue) String() string {
out := "("
for i, item := range v.Items {
if i != 0 {
out += ", "
}
out += item.DebugString()
}
if len(v.Items) <= 1 {
out += ","
}
out += ")"
return out
}
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()
}
return &TupleValue{
n,
}
}
func (v *TupleValue) Equals(other Value) bool {
if other.Type() != TupleValueType {
return false
}
t := other.(*TupleValue).Items
for i, item := range v.Items {
if !item.Equals(t[i]) {
return false
}
}
return true
}
var TuplePrototype = map[string]*BuiltinFunctionValue{
"at": &BuiltinFunctionValue{
"at",
&FunctionSignature{
[]TypeSignature{&IntegerSignature{}},
&InnerSignature{},
},
func(vm *VM, this Value, args []Value) (Value, error) {
i := args[0].(*IntegerValue).Number.Int64()
if i < 0 || i >= int64(len(this.(*TupleValue).Items)) {
return nil, errors.New(fmt.Sprintf("index %x out of range", i))
}
return this.(*TupleValue).Items[i], nil
},
nil,
true,
},
}
func (v *TupleValue) Get(key string) (Value, error) {
if prop, ok := TuplePrototype[key]; ok {
return prop, nil
}
return nil, errors.New(fmt.Sprintf("tuple has no property \"%s\"", key))
}
type FunctionValue struct { type FunctionValue struct {
Name string Name string
Params []FunctionParameter Params []FunctionParameter

View file

@ -132,6 +132,10 @@ const (
// InstructionConcatLists concatenate lists, producing a new list with the values of both lists. Pops two lists. // InstructionConcatLists concatenate lists, producing a new list with the values of both lists. Pops two lists.
InstructionConcatLists InstructionConcatLists
// InstructionFormTuple pop n+1 (u16) items from the stack, and create a new tuple with the items. The top value
// on the stack is the last value in the tuple.
InstructionFormTuple
// InstructionBreakpoint for debugging purposes // InstructionBreakpoint for debugging purposes
InstructionBreakpoint InstructionBreakpoint
) )
@ -992,6 +996,18 @@ func (vm *VM) Next() bool {
append(l.Items, r.Items...), append(l.Items, r.Items...),
}) })
case InstructionFormTuple:
n := int(vm.NextU16())
items := make([]Value, n)
for i := n - 1; i >= 0; i-- {
items[i] = vm.Stack.Pop()
}
vm.Stack.Push(&TupleValue{
items,
})
case InstructionDescend: case InstructionDescend:
vm.descend() vm.descend()

11
tests/tuple.ang Normal file
View file

@ -0,0 +1,11 @@
assertEq((1, 2), (1, 2))
assertEq(type((1,)), type((1,)))
assertEq((1,), (1,))
fn neighbours(n: int) -> (int, int) {
(n-1, n+1)
}
assertEq(neighbours(2), (1, 3))