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
import (
"bufio"
"errors"
"github.com/alecthomas/kong"
"log"
@ -226,11 +227,59 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
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 {
Debug bool `short:"D" name:"debug" help:"Enable debug mode."`
Run RunCmd `cmd:"" name:"run" help:"Run program."`
Compile CompileCmd `cmd:"" name:"compile" help:"Compile program to bytecode."`
Repl ReplCmd `cmd:"" name:"repl" help:"Start a REPL loop."`
}
func main() {

View file

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

View file

@ -28,23 +28,43 @@ func BenchmarkNewCompiler(b *testing.B) {
}
type CompileTestData struct {
tree Node
program *Program
expectedStack []Value
}
func GetCompileTestData() map[string]CompileTestData {
return map[string]CompileTestData{
"constant_string": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
"a",
&StringNode{
"Hello world!",
"\"Hello world!\"",
0, 0,
},
true,
0, 0,
},
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
&StringValue{"Hello world!"},
0,
},
},
},
"conditional_false": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
@ -81,6 +101,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
@ -90,6 +112,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
"conditional_true": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
@ -126,6 +150,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
@ -135,6 +161,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
"conditional_else_false": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
@ -184,6 +212,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
@ -193,6 +223,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
"conditional_else_true": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
@ -242,6 +274,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
@ -251,6 +285,12 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
"addition": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
"a",
&BinaryNode{
BinaryAddition,
&NumberNode{
@ -263,11 +303,25 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
true,
0, 0,
},
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
&NumberValue{3},
0,
},
},
},
"sum_function": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
@ -313,6 +367,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"sum",
@ -350,6 +406,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
"remove_func_vars": {
&Program{
[]string{},
&BlockNode{
[]Node{
&AssignNode{
@ -396,6 +454,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
0, 0,
},
"",
},
[]Value{
&VariableValue{
"a",
@ -423,7 +483,9 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
"two_lists": {
tree: &BlockNode{
program: &Program{
[]string{},
&BlockNode{
statements: []Node{
&AssignNode{
name: "a",
@ -447,6 +509,8 @@ func GetCompileTestData() map[string]CompileTestData {
},
},
},
"",
},
expectedStack: []Value{
&VariableValue{
name: "a",
@ -499,10 +563,10 @@ func TestCompile(t *testing.T) {
for name, testCase := range data {
t.Run(name, func(t *testing.T) {
t.Log("Initializing compiler")
c := NewCompiler([]rune(testCase.tree.String()))
c := NewCompiler([]rune(testCase.program.String()))
t.Log("Compiling node tree")
err := c.compile(testCase.tree)
err := c.Compile(testCase.program)
if err != nil {
t.Fatalf("Compiling failed: %v", err)
}
@ -529,7 +593,7 @@ func BenchmarkCompile(b *testing.B) {
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
c := NewCompiler([]rune{})
_ = c.compile(testCase.tree)
_ = c.Compile(testCase.program)
}
})
}
@ -556,24 +620,9 @@ func TestCompiler_CleanStack(t *testing.T) {
cases := GetCompileTestData()
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) {
c := NewCompiler([]rune(tc.tree.String()))
err := c.compile(tc.tree)
c := NewCompiler([]rune(tc.program.String()))
err := c.Compile(tc.program)
if err != nil {
t.Fatalf("Compiling failed: %v", err)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -25,16 +25,10 @@ func TestNewStack(t *testing.T) {
s := NewStack[any](Pos(size))
if s.Size != Pos(size) {
t.Errorf("Stack size (%d) does not match expected size (%d)", s.Size, size)
if s.Capacity != Pos(size) {
t.Errorf("Stack size (%d) does not match expected size (%d)", s.Capacity, size)
} else {
t.Logf("Stack size is expected size (%d)", s.Size)
}
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))
t.Logf("Stack size is expected size (%d)", s.Capacity)
}
if s.Current != 0 {

View file

@ -186,7 +186,7 @@ func (v *ObjectValue) String() string {
out += ", "
}
out += fmt.Sprintf("%q=%s", key, value.String())
out += fmt.Sprintf("%q=%s", key, value.DebugString())
}
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()))
}
func (vm *VM) SetChunk(c *Chunk) {
vm.chunk = c
}
func (vm *VM) TryNextByte() (Bytecode, error) {
if !vm.HasNext() {
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
if vm.stack.Size != stackSize {
t.Errorf("vm.stack.Size = %d, want %d", vm.stack.Size, stackSize)
if vm.stack.Capacity != stackSize {
t.Errorf("vm.stack.Capacity = %d, want %d", vm.stack.Capacity, stackSize)
}
// should have given call stack size
if vm.call.Size != callstackSize {
t.Errorf("vm.call.Size = %d, want %d", vm.call.Size, callstackSize)
if vm.call.Capacity != callstackSize {
t.Errorf("vm.call.Capacity = %d, want %d", vm.call.Capacity, callstackSize)
}
}