separate core from cli into submodules, add support for boolean and and or operations, make more fields public, fix passing arguments to functions

This commit is contained in:
Neemek 2024-12-13 11:33:47 +01:00
parent 7498c85424
commit 62656c6dff
Signed by: neemek
GPG key ID: 28360A8951CD0E9B
19 changed files with 202 additions and 99 deletions

10
cli/go.mod Normal file
View file

@ -0,0 +1,10 @@
module neemek.com/angalis/cli
go 1.23.0
require (
github.com/alecthomas/kong v1.5.1
neemek.com/anglais/core v0.0.0-00010101000000-000000000000
)
replace neemek.com/anglais/core => ../core

View file

@ -3,6 +3,7 @@ package main
import (
"github.com/alecthomas/kong"
"log"
"neemek.com/anglais/core"
"os"
)
@ -26,14 +27,14 @@ func (cmd *RunCmd) Run(ctx *Context) error {
return err
}
var chunk *Chunk
var chunk *core.Chunk
if !cmd.Bytecode {
src := string(f)
if ctx.Debug {
log.Println("Initialized lexer")
}
l := NewLexer(src)
l := core.NewLexer(src)
if ctx.Debug {
log.Println("Lexing all tokens")
@ -52,7 +53,7 @@ func (cmd *RunCmd) Run(ctx *Context) error {
log.Printf("Lexed %d tokens", len(tokens))
}
p := NewParser(tokens)
p := core.NewParser(tokens)
if ctx.Debug {
log.Println("Initialized parser")
@ -63,8 +64,8 @@ func (cmd *RunCmd) Run(ctx *Context) error {
if r != "no more tokens" { // if the panic was not caused by the parser, it should not be recovered.
panic(r)
}
for _, e := range p.errors {
e.Print(src)
for _, e := range p.Errors {
print(e.Format(src))
}
}
}()
@ -72,9 +73,9 @@ func (cmd *RunCmd) Run(ctx *Context) error {
tree := p.Parse()
// if there were parsing errors, print them out
if len(p.errors) > 0 {
for _, e := range p.errors {
e.Print(src)
if len(p.Errors) > 0 {
for _, e := range p.Errors {
print(e.Format(src))
}
log.Fatal("Parsing had errors")
}
@ -82,26 +83,26 @@ func (cmd *RunCmd) Run(ctx *Context) error {
if ctx.Debug {
log.Println("Initialized compiler")
}
c := NewCompiler()
c := core.NewCompiler()
if ctx.Debug {
log.Println("Compiling parse tree")
}
c.Compile(tree)
chunk = c.chunk
chunk = c.Chunk
} else {
if ctx.Debug {
log.Println("Registering GOB types")
}
RegisterGOBTypes()
core.RegisterGOBTypes()
if ctx.Debug {
log.Println("Deserializing file")
}
chunk = DeserializeChunk(f)
chunk = core.DeserializeChunk(f)
}
if ctx.Debug {
@ -111,7 +112,7 @@ func (cmd *RunCmd) Run(ctx *Context) error {
log.Println("Initialized VM")
}
vm := NewVM(chunk, 256, 256)
vm := core.NewVM(chunk, 256, 256)
if ctx.Debug {
log.Println("Executing bytecode")
@ -129,7 +130,7 @@ type CompileCmd struct {
Output string `arg:"" name:"output" optional:"" help:"File path to output bytecode to" type:"path"`
}
func (cmd *CompileCmd) Run(ctx *Context) error {
func (cmd *CompileCmd) Compile(ctx *Context) error {
if ctx.Debug {
log.Println("Reading file")
}
@ -145,7 +146,7 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
if ctx.Debug {
log.Println("Initializing lexer")
}
l := NewLexer(src)
l := core.NewLexer(src)
if ctx.Debug {
log.Println("Lexing all tokens")
@ -159,7 +160,7 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
if ctx.Debug {
log.Println("Initializing parser")
}
p := NewParser(tokens)
p := core.NewParser(tokens)
if ctx.Debug {
log.Println("Parsing tree")
@ -169,7 +170,7 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
if ctx.Debug {
log.Println("Initialized compiler")
}
c := NewCompiler()
c := core.NewCompiler()
if ctx.Debug {
log.Println("Compiling parse tree")
@ -181,13 +182,13 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
log.Println("Registering GOB types")
}
RegisterGOBTypes()
core.RegisterGOBTypes()
if ctx.Debug {
log.Println("Serializing chunk")
}
serialized := c.chunk.Serialize()
serialized := c.Chunk.Serialize()
if ctx.Debug {
log.Println("Writing file")

View file

@ -1,4 +1,4 @@
package main
package core
import (
"testing"
@ -46,8 +46,8 @@ func TestAll(t *testing.T) {
tree := p.Parse()
if p.hadError {
for _, e := range p.errors {
e.Print(tc.src)
for _, e := range p.Errors {
print(e.Format(tc.src))
}
t.Fatalf("parser had error(s)")
}
@ -58,10 +58,10 @@ func TestAll(t *testing.T) {
t.Log("Compiling parse tree")
c.Compile(tree)
printChunk(t, name, c.chunk)
printChunk(t, name, c.Chunk)
t.Log("Initializing vm")
vm := NewVM(c.chunk, 256, 256)
vm := NewVM(c.Chunk, 256, 256)
t.Log("Running bytecode")
for vm.HasNext() && vm.Next() {
@ -87,7 +87,7 @@ func BenchmarkAll(b *testing.B) {
c := NewCompiler()
c.Compile(tree)
vm := NewVM(c.chunk, 256, 256)
vm := NewVM(c.Chunk, 256, 256)
for vm.HasNext() && vm.Next() {
}

View file

@ -1,9 +1,7 @@
package main
import "log"
package core
type Compiler struct {
chunk *Chunk
Chunk *Chunk
ip Pos
scope Pos
@ -17,7 +15,7 @@ type LocalVariable struct {
func NewCompiler() *Compiler {
c := &Compiler{
chunk: NewChunk(make([]Bytecode, 0), make([]Value, 0)),
Chunk: NewChunk(make([]Bytecode, 0), make([]Value, 0)),
ip: 0,
scope: 0,
stack: NewStack[LocalVariable](256),
@ -27,17 +25,17 @@ func NewCompiler() *Compiler {
}
func (c *Compiler) add(instruction Bytecode) {
for len(c.chunk.Bytecode) <= int(c.ip) {
c.chunk.Bytecode = append(c.chunk.Bytecode, 0)
for len(c.Chunk.Bytecode) <= int(c.ip) {
c.Chunk.Bytecode = append(c.Chunk.Bytecode, 0)
}
c.chunk.Bytecode[c.ip] = instruction
c.Chunk.Bytecode[c.ip] = instruction
c.advance(1)
}
func (c *Compiler) addConstant(value Value) {
chunk := c.chunk
chunk := c.Chunk
for i := 0; i < len(chunk.Constants); i++ {
if chunk.Constants[i] == value {
c.add(Bytecode(i))
@ -168,19 +166,19 @@ func (c *Compiler) Compile(tree Node) {
case FunctionNodeType:
n := tree.(*FunctionNode)
fi := len(c.chunk.Constants)
c.chunk.Constants = append(c.chunk.Constants, nil)
fi := len(c.Chunk.Constants)
c.Chunk.Constants = append(c.Chunk.Constants, nil)
c.add(InstructionConstant)
c.add(Bytecode(fi))
// keep track of main chunk
mc := c.chunk
mc := c.Chunk
// and ip
mip := c.ip
// assign a new empty chunk
c.chunk = NewChunk(make([]Bytecode, 0), make([]Value, 0))
c.Chunk = NewChunk(make([]Bytecode, 0), make([]Value, 0))
// reset instruction pointer (ip)
c.ip = 0
@ -195,16 +193,19 @@ func (c *Compiler) Compile(tree Node) {
mc.Constants[fi] = FunctionValue{
n.name,
n.params,
c.chunk,
c.Chunk,
}
// restore old chunk and ip
c.chunk = mc
c.Chunk = mc
c.ip = mip
case ReturnNodeType:
c.Compile(tree.(*ReturnNode).value)
c.add(InstructionReturn)
case BreakpointNodeType:
c.add(InstructionBreakpoint)
}
}
@ -233,18 +234,20 @@ func (c *Compiler) compileBinary(binary *BinaryNode) {
c.add(InstructionLessOrEqual)
case BinaryGreaterEqual:
c.add(InstructionGreaterOrEqual)
case BinaryAnd:
c.add(InstructionAnd)
case BinaryOr:
c.add(InstructionOr)
}
}
func (c *Compiler) getVar(name string) {
if c.isLocal(name) {
c.add(InstructionGetLocal)
c.addConstant(StringValue(name))
} else if c.isGlobal(name) {
if c.isGlobal(name) {
c.add(InstructionGetGlobal)
c.addConstant(StringValue(name))
} else {
log.Fatalf("compiling: undefined variable %s", name)
c.add(InstructionGetLocal)
c.addConstant(StringValue(name))
}
}

View file

@ -1,4 +1,4 @@
package main
package core
import (
"fmt"
@ -16,7 +16,7 @@ func TestNewCompiler(t *testing.T) {
t.Error("compiler ip doesn't start at zero")
}
if c.chunk == nil {
if c.Chunk == nil {
t.Error("compiler chunk initialized to nil")
}
}
@ -361,10 +361,10 @@ func TestCompile(t *testing.T) {
c.Compile(testCase.tree)
t.Log("Initializing vm")
vm := NewVM(c.chunk, 256, 256)
vm := NewVM(c.Chunk, 256, 256)
t.Log("Printing chunk for debug")
printChunk(t, name, c.chunk)
printChunk(t, name, c.Chunk)
t.Log("Executing bytecode")
for vm.HasNext() && vm.Next() {
@ -394,12 +394,12 @@ func TestCompiler_AddU16(t *testing.T) {
c := NewCompiler()
c.addU16(uint16(i))
if c.chunk.Bytecode[0] != Bytecode(i>>8) {
t.Errorf("first 8 bits don't match (got %s, expected %b)", c.chunk.Bytecode[0], byte(i>>8))
if c.Chunk.Bytecode[0] != Bytecode(i>>8) {
t.Errorf("first 8 bits don't match (got %s, expected %b)", c.Chunk.Bytecode[0], byte(i>>8))
}
if c.chunk.Bytecode[1] != Bytecode(i&0xff) {
t.Errorf("last 8 bits don't match (got %s, expected %b)", c.chunk.Bytecode[1], byte(i&0xff))
if c.Chunk.Bytecode[1] != Bytecode(i&0xff) {
t.Errorf("last 8 bits don't match (got %s, expected %b)", c.Chunk.Bytecode[1], byte(i&0xff))
}
})
}
@ -432,7 +432,7 @@ func TestCompiler_CleanStack(t *testing.T) {
c := NewCompiler()
c.Compile(tc.tree)
vm := NewVM(c.chunk, 256, 256)
vm := NewVM(c.Chunk, 256, 256)
for vm.HasNext() && vm.Next() {
}

3
core/go.mod Normal file
View file

@ -0,0 +1,3 @@
module neemek.com/anglais/core
go 1.23.0

View file

View file

@ -1,4 +1,4 @@
package main
package core
import (
"errors"
@ -60,6 +60,10 @@ const (
TokenGreaterThanOrEqual
TokenLessThanOrEqual
TokenDoubleAmpersand
TokenDoublePipe
TokenBreakpoint
TokenEOF
TokenError
)
@ -134,6 +138,12 @@ func (t TokenType) String() string {
return "comma"
case TokenDot:
return "dot"
case TokenBreakpoint:
return "breakpoint"
case TokenDoubleAmpersand:
return "double ampersand"
case TokenDoublePipe:
return "double pipe"
}
return "UNDEFINED TOKENTYPE STRING CONVERSION"
@ -241,6 +251,20 @@ func (l *Lexer) NextToken() (Token, error) {
return l.makeToken(TokenLessThan), nil
case '&':
if l.accept('&') {
return l.makeToken(TokenDoubleAmpersand), nil
}
return l.makeToken(TokenError), errors.New("malformed token (got '&', expected '&' to follow)")
case '|':
if l.accept('|') {
return l.makeToken(TokenDoublePipe), nil
}
return l.makeToken(TokenError), errors.New("malformed token (got '|', expected '|' to follow)")
case '"':
// include ending quote
for !l.accept('"') {
@ -281,6 +305,8 @@ func (l *Lexer) NextToken() (Token, error) {
return l.makeToken(TokenFunc), nil
case "while":
return l.makeToken(TokenWhile), nil
case "breakpoint":
return l.makeToken(TokenBreakpoint), nil
case "return":
return l.makeToken(TokenReturn), nil
default:

View file

@ -1,4 +1,4 @@
package main
package core
import (
"testing"

View file

@ -1,4 +1,4 @@
package main
package core
import (
"fmt"
@ -135,6 +135,9 @@ const (
BinaryMultiplication
BinaryDivision
BinaryAnd
BinaryOr
// Comparison
BinaryEquality
BinaryInequality

View file

@ -1,10 +1,11 @@
package main
package core
import (
"errors"
"fmt"
"log"
"strconv"
"strings"
)
type ParsingError struct {
@ -13,7 +14,9 @@ type ParsingError struct {
}
// Print a rich and informative error
func (p *ParsingError) Print(src string) {
func (p *ParsingError) Format(src string) string {
builder := strings.Builder{}
lineNumber := 1
lineBeginning := 0
for i := 0; i < int(p.Causer.Start); i++ {
@ -31,20 +34,23 @@ func (p *ParsingError) Print(src string) {
}
}
print(" \t v ")
println(p.Description)
builder.WriteString(" \t v ")
builder.WriteString(p.Description)
builder.WriteRune('\n')
println(fmt.Sprintf(" %d:%d\t | %s", lineNumber, int(p.Causer.Start)-lineBeginning+1, src[lineBeginning:lineEnd]))
builder.WriteString(fmt.Sprintf(" %d:%d\t | %s", lineNumber, int(p.Causer.Start)-lineBeginning+1, src[lineBeginning:lineEnd]))
print("\t ^")
builder.WriteString("\t ^")
for i := lineBeginning; i <= int(p.Causer.Start); i++ {
print(" ")
builder.WriteRune(' ')
}
for i := 0; i < int(p.Causer.Length); i++ {
print("^")
builder.WriteRune('^')
}
println()
builder.WriteRune('\n')
return builder.String()
}
type Parser struct {
@ -54,7 +60,7 @@ type Parser struct {
pos Pos
hadError bool
errors []ParsingError
Errors []ParsingError
}
func NewParser(tokens []Token) *Parser {
@ -62,7 +68,7 @@ func NewParser(tokens []Token) *Parser {
tokens: tokens,
pos: 0,
hadError: false,
errors: make([]ParsingError, 0),
Errors: make([]ParsingError, 0),
}
}
@ -99,7 +105,7 @@ func (p *Parser) accept(tokenType TokenType) bool {
func (p *Parser) expect(tokenType TokenType) {
if !p.accept(tokenType) {
p.error("Expected token "+tokenType.String()+", got "+p.curr.Type.String(), p.curr)
p.advance() // just move on
p.advance()
}
}
@ -124,7 +130,7 @@ func (p *Parser) advance() {
func (p *Parser) error(error string, causer *Token) {
p.hadError = true
p.errors = append(p.errors, ParsingError{
p.Errors = append(p.Errors, ParsingError{
Description: error,
Causer: causer,
})
@ -257,7 +263,7 @@ func (p *Parser) term() Node {
return left
}
func (p *Parser) condition() Node {
func (p *Parser) comparison() Node {
left := p.term()
op := BinaryEquality
@ -287,6 +293,28 @@ func (p *Parser) condition() Node {
}
}
func (p *Parser) condition() Node {
left := p.comparison()
op := BinaryEquality
switch (*p.curr).Type {
case TokenDoubleAmpersand:
op = BinaryAnd
case TokenDoublePipe:
op = BinaryOr
default:
return left
}
p.advance()
return &BinaryNode{
op,
left,
p.comparison(),
}
}
func (p *Parser) statement() Node {
switch (*p.curr).Type {
case TokenIf:

View file

@ -1,4 +1,4 @@
package main
package core
import (
"strconv"
@ -605,7 +605,7 @@ func TestParser_Parse(t *testing.T) {
tree := p.Parse()
if p.hadError {
t.Fatalf("Unexpected error(s): %s", p.errors)
t.Fatalf("Unexpected error(s): %s", p.Errors)
}
t.Logf("Checking parsed tree")

View file

@ -1,4 +1,4 @@
package main
package core
type Stack[T any] struct {
Current Pos

View file

@ -1,4 +1,4 @@
package main
package core
import (
"fmt"

View file

@ -1,4 +1,4 @@
package main
package core
import (
"fmt"
@ -126,9 +126,9 @@ func (v FunctionValue) String() string {
}
type BuiltinFunctionValue struct {
name string
parameters []string
f func(map[string]Value) Value
Name string
Parameters []string
F func(map[string]Value) Value
}
func (v BuiltinFunctionValue) Type() ValueType {
@ -136,7 +136,7 @@ func (v BuiltinFunctionValue) Type() ValueType {
}
func (v BuiltinFunctionValue) String() string {
return fmt.Sprintf("<function name=%s builtin>", v.name)
return fmt.Sprintf("<function name=%s builtin>", v.Name)
}
// VariableValue a value wrapper for variables kept on the stack

View file

@ -1,4 +1,4 @@
package main
package core
import "testing"
@ -56,22 +56,22 @@ func CompareValues(t *testing.T, got Value, want Value) {
n := got.(BuiltinFunctionValue)
m := want.(BuiltinFunctionValue)
if n.name != m.name {
t.Errorf("builtin function name mismatch: got %v, want %v", n.name, m.name)
if n.Name != m.Name {
t.Errorf("builtin function name mismatch: got %v, want %v", n.Name, m.Name)
}
if len(n.parameters) != len(m.parameters) {
t.Errorf("builtin function parameter count mismatch: got %v, want %v", n.parameters, m.parameters)
if len(n.Parameters) != len(m.Parameters) {
t.Errorf("builtin function parameter count mismatch: got %v, want %v", n.Parameters, m.Parameters)
}
for i, v := range n.parameters {
if v != m.parameters[i] {
t.Errorf("builtin function parameter %d mismatch: got %v, want %v", i, v, m.parameters[i])
for i, v := range n.Parameters {
if v != m.Parameters[i] {
t.Errorf("builtin function parameter %d mismatch: got %v, want %v", i, v, m.Parameters[i])
}
}
if &n.f != &m.f {
t.Errorf("builtin function f mismatch: got %v, want %v", &n.f, &m.f)
if &n.F != &m.F {
t.Errorf("builtin function f mismatch: got %v, want %v", &n.F, &m.F)
}
case VariableValueType:
n := got.(*VariableValue)

View file

@ -1,4 +1,4 @@
package main
package core
import (
"bytes"
@ -75,6 +75,11 @@ const (
// InstructionSwap swap the two top items on the stack (1, 2 -> 2, 1)
InstructionSwap
// InstructionAnd pop two booleans and push true if both are true
InstructionAnd
// InstructionOr pop two booleans and push true if either are true
InstructionOr
// InstructionConstant Push a constant to the stack (2 bytes, second = constant index)
InstructionConstant
// InstructionTrue Push a true literal to the stack
@ -152,6 +157,10 @@ func (b Bytecode) String() string {
return "STRING_CONCATENATION"
case InstructionSwap:
return "SWAP"
case InstructionAnd:
return "AND"
case InstructionOr:
return "OR"
case InstructionBreakpoint:
return "BREAKPOINT"
}
@ -263,6 +272,14 @@ var DefaultGlobals = map[string]Value{
return nil
},
},
"print": BuiltinFunctionValue{
"print",
[]string{"value"},
func(v map[string]Value) Value {
print(v["value"].String())
return nil
},
},
}
func NewVM(chunk *Chunk, stackSize Pos, callstackSize Pos) *VM {
@ -345,6 +362,12 @@ func (vm *VM) Next() bool {
b := vm.stack.Pop().(BoolValue)
vm.stack.Push(!b)
case InstructionAnd:
vm.stack.Push(vm.stack.Pop().(BoolValue) && vm.stack.Pop().(BoolValue))
case InstructionOr:
vm.stack.Push(vm.stack.Pop().(BoolValue) || vm.stack.Pop().(BoolValue))
case InstructionLess:
r := vm.stack.Pop().(NumberValue)
l := vm.stack.Pop().(NumberValue)
@ -381,7 +404,7 @@ func (vm *VM) Next() bool {
})
for i := len(f.Params) - 1; i >= 0; i-- {
p := vm.stack.Current - Pos(i) - 1
p := vm.stack.Current - Pos(len(f.Params)) + Pos(i)
vm.stack.items[p] = &VariableValue{
f.Params[i],
vm.stack.items[p],
@ -396,11 +419,11 @@ func (vm *VM) Next() bool {
case BuiltinFunctionValue:
args := map[string]Value{}
for i := len(f.parameters) - 1; i >= 0; i-- {
args[f.parameters[i]] = vm.stack.Pop()
for i := len(f.Parameters) - 1; i >= 0; i-- {
args[f.Parameters[i]] = vm.stack.Pop()
}
vm.stack.Push(f.f(args))
vm.stack.Push(f.F(args))
default:
vm.error(fmt.Sprintf("value called is not a function (%s)", v.String()))
return false
@ -570,3 +593,11 @@ func (vm *VM) NextU16() uint16 {
func (vm *VM) error(error string) {
log.Fatal(error)
}
func (vm *VM) SetGlobal(name string, value Value) {
vm.globals[name] = value
}
func (vm *VM) GetGlobal(name string) Value {
return vm.globals[name]
}

View file

@ -1,4 +1,4 @@
package main
package core
import (
"fmt"

4
go.mod
View file

@ -1,5 +1,3 @@
module neemek.com/anglais
go 1.23.0
require github.com/alecthomas/kong v1.2.1
go 1.23