var type checking, fix importing, fix creating empty lists, repl command, dynamic stacks

This commit is contained in:
Neemek 2025-04-18 09:33:39 +02:00
parent f633a02c32
commit 5bcab07681
Signed by: neemek
GPG key ID: 28360A8951CD0E9B
11 changed files with 407 additions and 284 deletions

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bufio"
"errors" "errors"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"log" "log"
@ -226,11 +227,59 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
return nil return nil
} }
type ReplCmd struct {
}
func (cmd *ReplCmd) Run(ctx *Context) error {
c := core.NewCompiler([]rune(""))
vm := core.NewVM(core.NewChunk([]core.Bytecode{}, []core.Value{}), 256, 256)
reader := bufio.NewReader(os.Stdin)
for {
print("> ")
src, err := reader.ReadString('\n')
if err != nil {
return err
}
l := core.NewLexer(src)
tokens, err := l.Tokenize()
if err != nil {
log.Println(err)
continue
}
p := core.NewParser(src, tokens)
prog, err := p.Parse("REPL")
if err != nil {
var e core.FormatedError
if errors.As(err, &e) {
log.Print(e.Format())
}
continue
}
c.SetSource(src)
if err = c.Compile(prog); err != nil {
var e core.FormatedError
if errors.As(err, &e) {
log.Print(e.Format())
}
continue
}
vm.SetChunk(c.Chunk)
for vm.Next() {
}
}
}
var cli struct { var cli struct {
Debug bool `short:"D" name:"debug" help:"Enable debug mode."` Debug bool `short:"D" name:"debug" help:"Enable debug mode."`
Run RunCmd `cmd:"" name:"run" help:"Run program."` Run RunCmd `cmd:"" name:"run" help:"Run program."`
Compile CompileCmd `cmd:"" name:"compile" help:"Compile program to bytecode."` Compile CompileCmd `cmd:"" name:"compile" help:"Compile program to bytecode."`
Repl ReplCmd `cmd:"" name:"repl" help:"Start a REPL loop."`
} }
func main() { func main() {

View file

@ -157,7 +157,6 @@ func (c *Compiler) addConstant(value Value) {
func (c *Compiler) Compile(p *Program) error { func (c *Compiler) Compile(p *Program) error {
c.fileStack.Push(p.Path) c.fileStack.Push(p.Path)
defer c.fileStack.Pop()
for _, i := range p.Imports { for _, i := range p.Imports {
if err := c.resolveImport(i); err != nil { if err := c.resolveImport(i); err != nil {
@ -171,6 +170,8 @@ func (c *Compiler) Compile(p *Program) error {
} }
} }
c.fileStack.Pop()
return nil return nil
} }
@ -451,7 +452,8 @@ func (c *Compiler) compile(tree Node) error {
c.registerVar(p.Name, p.Signature) c.registerVar(p.Name, p.Signature)
} }
if err := c.affirmReturnSignature(n.logic, n.yield); err != nil { err = c.affirmReturnSignature(n.logic, n.yield)
if err != nil {
return err return err
} }
@ -585,7 +587,7 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
case ListNodeType: case ListNodeType:
n := tree.(*ListNode) n := tree.(*ListNode)
var contents TypeSignature contents := n.content
// check for contents type // check for contents type
for _, v := range n.items { for _, v := range n.items {
sig, err := c.deduceSignature(v) sig, err := c.deduceSignature(v)
@ -596,8 +598,7 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
if contents == nil { if contents == nil {
contents = sig contents = sig
} else if !contents.Matches(sig) { } else if !contents.Matches(sig) {
contents = &AnySignature{} return nil, c.error(fmt.Sprintf("non-conforming item type"), v)
break
} }
} }
@ -804,7 +805,7 @@ func (c *Compiler) affirmReturnSignature(tree Node, sig TypeSignature) error {
} }
if !sig.Matches(v) { if !sig.Matches(v) {
return c.error(fmt.Sprintf("function cannot return a value with type %s. defined to be %s", v, sig), n.value) return c.error(fmt.Sprintf("function cannot return a value with type %s. defined to be %s", v, sig), n)
} }
case ConditionalNodeType: case ConditionalNodeType:
@ -842,7 +843,7 @@ func (c *Compiler) affirmReturnSignature(tree Node, sig TypeSignature) error {
} }
if !sig.Matches(prev) { if !sig.Matches(prev) {
return c.error(fmt.Sprintf("cannot assign value of type %s to variable of type %s", prev, sig), n.value) return c.error(fmt.Sprintf("cannot assign value of type %s to variable %s of type %s", sig, n.name, prev), n.value)
} }
return nil return nil
@ -891,14 +892,24 @@ func (c *Compiler) addSetVar(name string, value Node, declare bool) error {
return err return err
} }
t, err := c.deduceSignature(value)
if err != nil {
return err
}
if declare { if declare {
c.add(InstructionDeclareLocal) c.add(InstructionDeclareLocal)
t, err := c.deduceSignature(value) c.registerVar(name, t)
} else {
vt, err := c.getVarSignature(name, value)
if err != nil { if err != nil {
return err return err
} }
c.registerVar(name, t)
} else { if !vt.Matches(t) {
return c.error(fmt.Sprintf("cannot assign value of type %s to variable %s of type %s", t, name, vt), value)
}
c.add(InstructionSetLocal) c.add(InstructionSetLocal)
} }
@ -1090,10 +1101,7 @@ func (c *Compiler) ascend() {
func (c *Compiler) addAscend() { func (c *Compiler) addAscend() {
c.ascend() c.ascend()
c.add(InstructionAscend)
if c.scope != 0 {
c.add(InstructionAscend)
}
} }
func (c *Compiler) descend() { func (c *Compiler) descend() {
@ -1102,9 +1110,7 @@ func (c *Compiler) descend() {
func (c *Compiler) addDescend() { func (c *Compiler) addDescend() {
c.descend() c.descend()
if c.scope != 1 { c.add(InstructionDescend)
c.add(InstructionDescend)
}
} }
func (c *Compiler) error(msg string, causer Node) CompilerError { func (c *Compiler) error(msg string, causer Node) CompilerError {
@ -1169,6 +1175,10 @@ func (c *Compiler) SetImportsResolver(resolver ImportsResolver) {
c.resolver = resolver c.resolver = resolver
} }
func (c *Compiler) SetSource(src string) {
c.source = []rune(src)
}
func (c *Compiler) advance(amount Pos) { func (c *Compiler) advance(amount Pos) {
c.ip += amount c.ip += amount
} }

View file

@ -28,58 +28,80 @@ func BenchmarkNewCompiler(b *testing.B) {
} }
type CompileTestData struct { type CompileTestData struct {
tree Node program *Program
expectedStack []Value expectedStack []Value
} }
func GetCompileTestData() map[string]CompileTestData { func GetCompileTestData() map[string]CompileTestData {
return map[string]CompileTestData{ return map[string]CompileTestData{
"constant_string": { "constant_string": {
&StringNode{ &Program{
"Hello world!", []string{},
"\"Hello world!\"", &BlockNode{
0, 0, []Node{
&AssignNode{
"a",
&StringNode{
"Hello world!",
"\"Hello world!\"",
0, 0,
},
true,
0, 0,
},
},
0, 0,
},
"",
}, },
[]Value{ []Value{
&StringValue{"Hello world!"}, &VariableValue{
"a",
&StringValue{"Hello world!"},
0,
},
}, },
}, },
"conditional_false": { "conditional_false": {
&BlockNode{ &Program{
[]Node{ []string{},
&AssignNode{ &BlockNode{
"a", []Node{
&NumberNode{ &AssignNode{
0, "a",
&NumberNode{
0,
0, 0,
},
true,
0, 0, 0, 0,
}, },
true, &ConditionalNode{
0, 0, &BooleanNode{
}, false,
&ConditionalNode{ 0, 0,
&BooleanNode{ },
false, &BlockNode{
0, 0, []Node{
}, &AssignNode{
&BlockNode{ "a",
[]Node{ &NumberNode{
&AssignNode{ 1,
"a", 0, 0,
&NumberNode{ },
1, false,
0, 0, 0, 0,
}, },
false,
0, 0,
}, },
0, 0,
}, },
nil,
0, 0, 0, 0,
}, },
nil,
0, 0,
}, },
0, 0,
}, },
0, 0, "",
}, },
[]Value{ []Value{
&VariableValue{ &VariableValue{
@ -90,41 +112,45 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
"conditional_true": { "conditional_true": {
&BlockNode{ &Program{
[]Node{ []string{},
&AssignNode{ &BlockNode{
"a", []Node{
&NumberNode{ &AssignNode{
0, "a",
0, 0, &NumberNode{
}, 0,
true, 0, 0,
0, 0, },
},
&ConditionalNode{
&BooleanNode{
true, true,
0, 0, 0, 0,
}, },
&BlockNode{ &ConditionalNode{
[]Node{ &BooleanNode{
&AssignNode{ true,
"a", 0, 0,
&NumberNode{ },
1, &BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
1,
0, 0,
},
false,
0, 0, 0, 0,
}, },
false,
0, 0,
}, },
0, 0,
}, },
nil,
0, 0, 0, 0,
}, },
nil,
0, 0,
}, },
0, 0,
}, },
0, 0, "",
}, },
[]Value{ []Value{
&VariableValue{ &VariableValue{
@ -135,54 +161,58 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
"conditional_else_false": { "conditional_else_false": {
&BlockNode{ &Program{
[]Node{ []string{},
&AssignNode{ &BlockNode{
"a", []Node{
&NumberNode{ &AssignNode{
0, "a",
&NumberNode{
0,
0, 0,
},
true,
0, 0, 0, 0,
}, },
true, &ConditionalNode{
0, 0, &BooleanNode{
}, false,
&ConditionalNode{ 0, 0,
&BooleanNode{ },
false, &BlockNode{
0, 0, []Node{
}, &AssignNode{
&BlockNode{ "a",
[]Node{ &NumberNode{
&AssignNode{ 1,
"a", 0, 0,
&NumberNode{ },
1, false,
0, 0, 0, 0,
}, },
false,
0, 0,
}, },
0, 0,
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
2,
0, 0,
},
false,
0, 0,
},
},
0, 0,
}, },
0, 0, 0, 0,
}, },
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
2,
0, 0,
},
false,
0, 0,
},
},
0, 0,
},
0, 0,
}, },
0, 0,
}, },
0, 0, "",
}, },
[]Value{ []Value{
&VariableValue{ &VariableValue{
@ -193,54 +223,58 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
"conditional_else_true": { "conditional_else_true": {
&BlockNode{ &Program{
[]Node{ []string{},
&AssignNode{ &BlockNode{
"a", []Node{
&NumberNode{ &AssignNode{
0, "a",
0, 0, &NumberNode{
}, 0,
true, 0, 0,
0, 0, },
},
&ConditionalNode{
&BooleanNode{
true, true,
0, 0, 0, 0,
}, },
&BlockNode{ &ConditionalNode{
[]Node{ &BooleanNode{
&AssignNode{ true,
"a", 0, 0,
&NumberNode{ },
1, &BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
1,
0, 0,
},
false,
0, 0, 0, 0,
}, },
false,
0, 0,
}, },
0, 0,
},
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
2,
0, 0,
},
false,
0, 0,
},
},
0, 0,
}, },
0, 0, 0, 0,
}, },
&BlockNode{
[]Node{
&AssignNode{
"a",
&NumberNode{
2,
0, 0,
},
false,
0, 0,
},
},
0, 0,
},
0, 0,
}, },
0, 0,
}, },
0, 0, "",
}, },
[]Value{ []Value{
&VariableValue{ &VariableValue{
@ -251,67 +285,89 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
"addition": { "addition": {
&BinaryNode{ &Program{
BinaryAddition, []string{},
&NumberNode{ &BlockNode{
1, []Node{
&AssignNode{
"a",
&BinaryNode{
BinaryAddition,
&NumberNode{
1,
0, 0,
},
&NumberNode{
2,
0, 0,
},
0, 0,
},
true,
0, 0,
},
},
0, 0, 0, 0,
}, },
&NumberNode{ "",
2,
0, 0,
},
0, 0,
}, },
[]Value{ []Value{
&NumberValue{3}, &VariableValue{
"a",
&NumberValue{3},
0,
},
}, },
}, },
"sum_function": { "sum_function": {
&BlockNode{ &Program{
[]Node{ []string{},
&AssignNode{ &BlockNode{
"sum", []Node{
&FunctionNode{ &AssignNode{
"sum", "sum",
[]FunctionParameter{ &FunctionNode{
{ "sum",
"a", []FunctionParameter{
&NumberSignature{}, {
"a",
&NumberSignature{},
},
{
"b",
&NumberSignature{},
},
}, },
{ &NumberSignature{},
"b", &BlockNode{
&NumberSignature{}, []Node{
}, &ReturnNode{
}, &BinaryNode{
&NumberSignature{}, BinaryAddition,
&BlockNode{ &ReferenceNode{
[]Node{ "a",
&ReturnNode{ 0, 0,
&BinaryNode{ },
BinaryAddition, &ReferenceNode{
&ReferenceNode{ "b",
"a", 0, 0,
0, 0, },
},
&ReferenceNode{
"b",
0, 0, 0, 0,
}, },
0, 0, 0, 0,
}, },
0, 0,
}, },
0, 0,
}, },
0, 0, 0, 0,
}, },
true,
0, 0, 0, 0,
}, },
true,
0, 0,
}, },
0, 0,
}, },
0, 0, "",
}, },
[]Value{ []Value{
&VariableValue{ &VariableValue{
@ -350,51 +406,55 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
"remove_func_vars": { "remove_func_vars": {
&BlockNode{ &Program{
[]Node{ []string{},
&AssignNode{ &BlockNode{
"a", []Node{
&FunctionNode{ &AssignNode{
"a", "a",
[]FunctionParameter{}, &FunctionNode{
&NumberSignature{}, "a",
&BlockNode{ []FunctionParameter{},
[]Node{ &NumberSignature{},
&AssignNode{ &BlockNode{
"b", []Node{
&NumberNode{ &AssignNode{
1,
0, 0,
},
true,
0, 0,
},
&ReturnNode{
&ReferenceNode{
"b", "b",
&NumberNode{
1,
0, 0,
},
true,
0, 0,
},
&ReturnNode{
&ReferenceNode{
"b",
0, 0,
},
0, 0, 0, 0,
}, },
0, 0,
}, },
0, 0,
}, },
0, 0, 0, 0,
}, },
true,
0, 0, 0, 0,
}, },
true, &CallNode{
0, 0, &ReferenceNode{
}, "a",
&CallNode{ 0, 0,
&ReferenceNode{ },
"a", []Node{},
false,
0, 0, 0, 0,
}, },
[]Node{},
false,
0, 0,
}, },
0, 0,
}, },
0, 0, "",
}, },
[]Value{ []Value{
&VariableValue{ &VariableValue{
@ -423,29 +483,33 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
"two_lists": { "two_lists": {
tree: &BlockNode{ program: &Program{
statements: []Node{ []string{},
&AssignNode{ &BlockNode{
name: "a", statements: []Node{
value: &ListNode{ &AssignNode{
items: []Node{ name: "a",
&NumberNode{value: 1}, value: &ListNode{
&NumberNode{value: 2}, items: []Node{
&NumberNode{value: 1},
&NumberNode{value: 2},
},
}, },
declare: true,
}, },
declare: true, &AssignNode{
}, name: "b",
&AssignNode{ value: &ListNode{
name: "b", items: []Node{
value: &ListNode{ &StringNode{value: "Hello"},
items: []Node{ &StringNode{value: "world"},
&StringNode{value: "Hello"}, },
&StringNode{value: "world"},
}, },
declare: true,
}, },
declare: true,
}, },
}, },
"",
}, },
expectedStack: []Value{ expectedStack: []Value{
&VariableValue{ &VariableValue{
@ -499,10 +563,10 @@ func TestCompile(t *testing.T) {
for name, testCase := range data { for name, testCase := range data {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Log("Initializing compiler") t.Log("Initializing compiler")
c := NewCompiler([]rune(testCase.tree.String())) c := NewCompiler([]rune(testCase.program.String()))
t.Log("Compiling node tree") t.Log("Compiling node tree")
err := c.compile(testCase.tree) err := c.Compile(testCase.program)
if err != nil { if err != nil {
t.Fatalf("Compiling failed: %v", err) t.Fatalf("Compiling failed: %v", err)
} }
@ -529,7 +593,7 @@ func BenchmarkCompile(b *testing.B) {
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
c := NewCompiler([]rune{}) c := NewCompiler([]rune{})
_ = c.compile(testCase.tree) _ = c.Compile(testCase.program)
} }
}) })
} }
@ -556,24 +620,9 @@ func TestCompiler_CleanStack(t *testing.T) {
cases := GetCompileTestData() cases := GetCompileTestData()
for name, tc := range cases { for name, tc := range cases {
switch tc.tree.Type() {
// skip all expected unclean nodes
case StringNodeType, NumberNodeType, ReferenceNodeType, BooleanNodeType, NilNodeType, BinaryNodeType, ReturnNodeType:
continue
case CallNodeType:
if tc.tree.(*CallNode).keep {
// if we know it should be unclean, skip it
continue
}
// clean statements
default:
}
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
c := NewCompiler([]rune(tc.tree.String())) c := NewCompiler([]rune(tc.program.String()))
err := c.compile(tc.tree) err := c.Compile(tc.program)
if err != nil { if err != nil {
t.Fatalf("Compiling failed: %v", err) t.Fatalf("Compiling failed: %v", err)
} }

View file

@ -137,7 +137,8 @@ func (n NumberNode) Bounds() (Pos, Pos) {
// ListNode a list or sequence of values (items) // ListNode a list or sequence of values (items)
type ListNode struct { type ListNode struct {
items []Node items []Node
content TypeSignature
start Pos start Pos
end Pos end Pos

View file

@ -253,9 +253,21 @@ func (p *Parser) factor() (Node, error) {
case TokenOpenBracket: case TokenOpenBracket:
p.advance() p.advance()
start := p.prev.Start start := p.prev.Start
if p.accept(TokenCloseBracket) {
s, err := p.parseSignature()
if err != nil {
return nil, err
}
return &ListNode{
[]Node{},
s,
start,
p.prev.Start + p.prev.Length,
}, nil
}
var values []Node var values []Node
for !p.accept(TokenCloseBracket) { for !p.accept(TokenCloseBracket) {
if len(values) > 0 { if len(values) > 0 {
@ -275,6 +287,7 @@ func (p *Parser) factor() (Node, error) {
return &ListNode{ return &ListNode{
values, values,
nil,
start, start,
p.prev.Start + p.prev.Length, p.prev.Start + p.prev.Length,
}, nil }, nil
@ -386,9 +399,7 @@ func (p *Parser) factor() (Node, error) {
return v, nil return v, nil
default: default:
err := p.error("invalid factor", p.curr) return nil, p.error("invalid factor", p.curr)
p.advance()
return nil, err
} }
} }
@ -688,10 +699,10 @@ func (p *Parser) statement() (Node, error) {
start, start,
p.prev.Start + p.prev.Length, p.prev.Start + p.prev.Length,
}, nil }, nil
} else {
return p.condition()
} }
return nil, p.error("invalid statement", p.curr)
case TokenFunc: case TokenFunc:
p.advance() p.advance()

View file

@ -630,9 +630,11 @@ func GetTokenTestData() map[string]TokenTestData {
0, 0, 0, 0,
}, },
}, },
nil,
0, 0, 0, 0,
}, },
}, },
nil,
0, 0, 0, 0,
}, },
true, true,

View file

@ -1,25 +1,28 @@
package core package core
type Stack[T any] struct { type Stack[T any] struct {
Current Pos Current Pos
Size Pos Capacity Pos
items []T items []T
} }
func NewStack[T any](size Pos) *Stack[T] { func NewStack[T any](capacity Pos) *Stack[T] {
return &Stack[T]{ return &Stack[T]{
items: make([]T, size), items: make([]T, 16),
Size: size, Capacity: capacity,
Current: 0, Current: 0,
} }
} }
func (s *Stack[T]) Push(items ...T) { func (s *Stack[T]) Push(items ...T) {
for _, item := range items { for _, item := range items {
if s.Current >= s.Size { if s.Current >= s.Capacity {
panic("stack overflow") panic("stack overflow")
} }
if int(s.Current) == len(s.items) {
s.items = append(s.items, item)
}
s.items[s.Current] = item s.items[s.Current] = item
s.Current++ s.Current++
@ -45,7 +48,7 @@ func (s *Stack[T]) Peek() T {
// check whether the stack is invalid (stack over-/underflow) // check whether the stack is invalid (stack over-/underflow)
func (s *Stack[T]) check() { func (s *Stack[T]) check() {
if s.Current >= s.Size { if s.Current >= s.Capacity {
panic("stack underflow") panic("stack underflow")
} }

View file

@ -25,16 +25,10 @@ func TestNewStack(t *testing.T) {
s := NewStack[any](Pos(size)) s := NewStack[any](Pos(size))
if s.Size != Pos(size) { if s.Capacity != Pos(size) {
t.Errorf("Stack size (%d) does not match expected size (%d)", s.Size, size) t.Errorf("Stack size (%d) does not match expected size (%d)", s.Capacity, size)
} else { } else {
t.Logf("Stack size is expected size (%d)", s.Size) t.Logf("Stack size is expected size (%d)", s.Capacity)
}
if len(s.items) != size {
t.Errorf("internal items slice size (%d) does not match expected size (%d)", len(s.items), size)
} else {
t.Logf("internal items slice size is as expected (%d)", len(s.items))
} }
if s.Current != 0 { if s.Current != 0 {

View file

@ -186,7 +186,7 @@ func (v *ObjectValue) String() string {
out += ", " out += ", "
} }
out += fmt.Sprintf("%q=%s", key, value.String()) out += fmt.Sprintf("%q=%s", key, value.DebugString())
} }
out += "}" out += "}"

View file

@ -826,6 +826,10 @@ func (vm *VM) Call(v Value, args []Value) (Value, error) {
return nil, errors.New(fmt.Sprintf("value is not a function (%s)", v.DebugString())) return nil, errors.New(fmt.Sprintf("value is not a function (%s)", v.DebugString()))
} }
func (vm *VM) SetChunk(c *Chunk) {
vm.chunk = c
}
func (vm *VM) TryNextByte() (Bytecode, error) { func (vm *VM) TryNextByte() (Bytecode, error) {
if !vm.HasNext() { if !vm.HasNext() {
return 0, errors.New("there are no more instructions") return 0, errors.New("there are no more instructions")

View file

@ -76,13 +76,13 @@ func TestNewVM(t *testing.T) {
} }
// should have given stack size // should have given stack size
if vm.stack.Size != stackSize { if vm.stack.Capacity != stackSize {
t.Errorf("vm.stack.Size = %d, want %d", vm.stack.Size, stackSize) t.Errorf("vm.stack.Capacity = %d, want %d", vm.stack.Capacity, stackSize)
} }
// should have given call stack size // should have given call stack size
if vm.call.Size != callstackSize { if vm.call.Capacity != callstackSize {
t.Errorf("vm.call.Size = %d, want %d", vm.call.Size, callstackSize) t.Errorf("vm.call.Capacity = %d, want %d", vm.call.Capacity, callstackSize)
} }
} }