improve errors, add warnings, add some compile-time optimizations, fix variables, make sure conditionals and loops get booleans, affirm types, keep variables to a single type

This commit is contained in:
Neemek 2025-03-20 12:13:30 +01:00
parent e34b423967
commit b1b8e61f57
Signed by: neemek
GPG key ID: 28360A8951CD0E9B
14 changed files with 435 additions and 118 deletions

View file

@ -14,8 +14,9 @@ type Context struct {
} }
type RunCmd struct { type RunCmd struct {
Bytecode bool `name:"bytecode" short:"c" help:"Run file as if it's bytecode"` IgnoreWarnings bool `name:"ignore-warnings" help:"Ignore warning messages"`
File string `arg:"" name:"file" help:"File to read program from" type:"existingfile"` Bytecode bool `name:"bytecode" short:"c" help:"Run file as if it's bytecode"`
File string `arg:"" name:"file" help:"File to read program from" type:"existingfile"`
} }
// WorkingDirectoryResolver resolves imports relative to the working directory // WorkingDirectoryResolver resolves imports relative to the working directory
@ -30,9 +31,9 @@ func (r *WorkingDirectoryResolver) Resolve(path string) (core.Node, error) {
return nil, err return nil, err
} }
str := string(f) src := string(f)
l := core.NewLexer(str) l := core.NewLexer(src)
tokens, err := l.Tokenize() tokens, err := l.Tokenize()
if err != nil { if err != nil {
@ -107,7 +108,7 @@ func (cmd *RunCmd) Run(ctx *Context) error {
if ctx.Debug { if ctx.Debug {
log.Println("Initialized compiler") log.Println("Initialized compiler")
} }
c := core.NewCompiler() c := core.NewCompiler([]rune(src))
if ctx.Debug { if ctx.Debug {
log.Println("Setting imports resolver") log.Println("Setting imports resolver")
@ -125,11 +126,19 @@ func (cmd *RunCmd) Run(ctx *Context) error {
if err != nil { if err != nil {
var e core.CompilerError var e core.CompilerError
if errors.As(err, &e) { if errors.As(err, &e) {
log.Fatal(e.Format([]rune(src))) log.Fatal(e.Format())
} }
log.Fatal(err) log.Fatal(err)
} }
// if there were non-critical warnings, report them
if !cmd.IgnoreWarnings && len(c.Warnings) != 0 {
for _, warning := range c.Warnings {
log.Println(warning.Format())
}
log.Fatal("compiler reported warning(s) (ignore warnings with the --ignore-warnings option)")
}
chunk = c.Chunk chunk = c.Chunk
} else { } else {
if ctx.Debug { if ctx.Debug {
@ -216,7 +225,7 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
log.Println("Initialized compiler") log.Println("Initialized compiler")
} }
c := core.NewCompiler() c := core.NewCompiler([]rune(src))
if ctx.Debug { if ctx.Debug {
log.Println("Setting import resolver") log.Println("Setting import resolver")
@ -235,11 +244,18 @@ func (cmd *CompileCmd) Run(ctx *Context) error {
if err != nil { if err != nil {
var e core.CompilerError var e core.CompilerError
if errors.As(err, &e) { if errors.As(err, &e) {
log.Fatal(e.Format([]rune(src))) log.Fatal(e.Format())
} }
log.Fatal(err) log.Fatal(err)
} }
// if there were non-critical warnings, report them
if len(c.Warnings) != 0 {
for _, warning := range c.Warnings {
log.Println(warning.Format())
}
}
if ctx.Debug { if ctx.Debug {
log.Println("Registering GOB types") log.Println("Registering GOB types")
} }

View file

@ -22,7 +22,7 @@ func GetAllTestCases() map[string]AllTestCase {
}, },
}, },
"func": { "func": {
"func sum(a: number, b: number) {\n\treturn a + b\n}\nsum(1, 2)", "func sum(a: number, b: number) number {\n\treturn a + b\n}\n_ = sum(1, 2)",
[]Value{ []Value{
&VariableValue{ &VariableValue{
"sum", "sum",
@ -54,6 +54,21 @@ func GetAllTestCases() map[string]AllTestCase {
}, },
}, },
}, },
"list": {
"a := [1, 2]",
[]Value{
&VariableValue{
"a",
&ListValue{
[]Value{
&NumberValue{1},
&NumberValue{2},
},
},
0,
},
},
},
} }
} }
@ -84,7 +99,7 @@ func TestAll(t *testing.T) {
} }
t.Log("Initializing compiler") t.Log("Initializing compiler")
c := NewCompiler() c := NewCompiler([]rune(tc.src))
t.Log("Compiling parse tree") t.Log("Compiling parse tree")
err = c.Compile(tree) err = c.Compile(tree)
@ -119,7 +134,7 @@ func BenchmarkAll(b *testing.B) {
p := NewParser(tokens) p := NewParser(tokens)
tree, _ := p.Parse() tree, _ := p.Parse()
c := NewCompiler() c := NewCompiler([]rune(tc.src))
_ = c.Compile(tree) _ = c.Compile(tree)
vm := NewVM(c.Chunk, 256, 256) vm := NewVM(c.Chunk, 256, 256)

View file

@ -12,6 +12,8 @@ type Compiler struct {
imports map[string]Node imports map[string]Node
resolver ImportsResolver resolver ImportsResolver
source []rune
Warnings []CompilerError
stack *Stack[LocalVariable] stack *Stack[LocalVariable]
} }
@ -29,25 +31,29 @@ type LocalVariable struct {
type CompilerError struct { type CompilerError struct {
Description string Description string
Causer Node Causer Node
Source []rune
} }
func (e CompilerError) Error() string { func (e CompilerError) Error() string {
return e.Description return e.Description
} }
func (e CompilerError) Format(src []rune) string { func (e CompilerError) Format() string {
b := strings.Builder{} b := strings.Builder{}
src := e.Source
b.WriteString(e.Description) b.WriteString(e.Description)
b.WriteString("\n")
// highlight offending area // highlight offending area
start, end := e.Causer.Bounds() start, end := e.Causer.Bounds()
lineEnd := 0
lineStart := 0 lineStart := 0
line := 1 line := 1
pos := 0 pos := 0
for i := Pos(0); i < start; i++ {
for i := Pos(0); i <= start; i++ {
pos++ pos++
if src[i] == '\n' { if src[i] == '\n' {
@ -57,37 +63,51 @@ func (e CompilerError) Format(src []rune) string {
} }
} }
lineEnd := lineStart for lineEnd < int(end) {
for lineEnd < len(src) { b.WriteString("\n")
lineEnd++ lineEnd = lineStart
if src[lineEnd] == '\n' { for lineEnd < len(src) {
break lineEnd++
if src[lineEnd] == '\n' {
break
}
} }
begin := max(0, int(start)-lineStart)
length := int(min(end, Pos(lineEnd)) - max(start, Pos(lineStart)))
lineDescriptor := fmt.Sprintf("%d:%d~%d",
line,
begin,
begin+length,
)
b.WriteString(lineDescriptor)
b.WriteString(" | ")
b.WriteString(string(src[lineStart+1 : lineEnd]))
b.WriteString("\n")
b.WriteString(strings.Repeat(" ", len(lineDescriptor)))
b.WriteString(" ")
b.WriteString(strings.Repeat(" ", max(int(start)-lineStart, 0)))
b.WriteString(strings.Repeat("^", length))
lineStart = lineEnd
line++
} }
lineDescriptor := fmt.Sprintf("%d:%d~%d", line, pos, int(end-start)+pos)
b.WriteString(lineDescriptor)
b.WriteString("\t | ")
b.WriteString(string(src[lineStart+1 : lineEnd]))
b.WriteString("\n")
b.WriteString(strings.Repeat(" ", len(lineDescriptor)))
b.WriteString("\t ")
b.WriteString(strings.Repeat(" ", int(start)-lineStart-1))
b.WriteString(strings.Repeat("^", int(end-start)))
return b.String() return b.String()
} }
func NewCompiler() *Compiler { func NewCompiler(source []rune) *Compiler {
c := &Compiler{ c := &Compiler{
NewChunk(make([]Bytecode, 0), make([]Value, 0)), NewChunk(make([]Bytecode, 0), make([]Value, 0)),
0, 0,
0, 0,
make(map[string]Node), make(map[string]Node),
nil, nil,
source,
[]CompilerError{},
NewStack[LocalVariable](256), NewStack[LocalVariable](256),
} }
@ -202,20 +222,29 @@ func (c *Compiler) Compile(tree Node) error {
c.add(InstructionNil) c.add(InstructionNil)
case BlockNodeType: case BlockNodeType:
c.descend() c.addDescend()
for _, n := range tree.(*BlockNode).statements { for _, n := range tree.(*BlockNode).statements {
err := c.Compile(n) err := c.Compile(n)
if err != nil { if err != nil {
return err return err
} }
} }
c.ascend() c.addAscend()
case ConditionalNodeType: case ConditionalNodeType:
n := tree.(*ConditionalNode) n := tree.(*ConditionalNode)
// make sure condition is boolean
sig, err := c.deduceSignature(n.condition)
if err != nil {
return err
}
if sig.Type() != TypeBoolean {
return c.error(fmt.Sprintf("condition cannot give non-boolean type %s", sig), n.condition)
}
// the stack should have whether the condition was truthful // the stack should have whether the condition was truthful
err := c.Compile(n.condition) err = c.Compile(n.condition)
if err != nil { if err != nil {
return err return err
} }
@ -256,8 +285,17 @@ func (c *Compiler) Compile(tree Node) error {
case LoopNodeType: case LoopNodeType:
n := tree.(*LoopNode) n := tree.(*LoopNode)
// make sure condition is boolean
sig, err := c.deduceSignature(n.condition)
if err != nil {
return err
}
if sig.Type() != TypeBoolean {
return c.error(fmt.Sprintf("cannot loop over value of type %s; requires boolean", sig), n.condition)
}
conditionPos := c.ip conditionPos := c.ip
err := c.Compile(n.condition) err = c.Compile(n.condition)
if err != nil { if err != nil {
return err return err
} }
@ -307,6 +345,14 @@ func (c *Compiler) Compile(tree Node) error {
return c.error(fmt.Sprintf("cannot call non-function value of type %s", s), n) return c.error(fmt.Sprintf("cannot call non-function value of type %s", s), n)
} }
if !n.keep && f.Out.Type() != TypeNil {
c.warn(fmt.Sprintf("shouldn't void result of function call (output is non-nil %s)", f.Out), n)
}
if len(n.args) != len(f.In) {
return c.error(fmt.Sprintf("wrong argument count: function of signature %s got %d, requires %d", f, len(n.args), len(f.In)), n)
}
for i, arg := range n.args { for i, arg := range n.args {
sig, err := c.deduceSignature(arg) sig, err := c.deduceSignature(arg)
if err != nil { if err != nil {
@ -365,6 +411,10 @@ 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 {
return err
}
err = c.Compile(n.logic) err = c.Compile(n.logic)
if err != nil { if err != nil {
return err return err
@ -377,6 +427,7 @@ func (c *Compiler) Compile(tree Node) error {
mc.Constants[fi] = &FunctionValue{ mc.Constants[fi] = &FunctionValue{
n.name, n.name,
n.parameters, n.parameters,
n.yield,
c.Chunk, c.Chunk,
nil, nil,
} }
@ -493,18 +544,13 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
return &NumberSignature{}, nil return &NumberSignature{}, nil
case ReferenceNodeType: case ReferenceNodeType:
n := tree.(*ReferenceNode) n := tree.(*ReferenceNode)
if c.isGlobal(n.name) { sig, err := c.getVarSignature(n.name, n)
return SignatureOf(DefaultGlobals[n.name]), nil if err != nil {
return nil, err
} }
for i := c.stack.Current - 1; i >= 0; i-- { return sig, nil
v := c.stack.items[i]
if v.name == n.name {
return v.signature, nil
}
}
return nil, c.error(fmt.Sprintf("variable %s not defined", n.name), n)
case BooleanNodeType: case BooleanNodeType:
return &BooleanSignature{}, nil return &BooleanSignature{}, nil
case NilNodeType: case NilNodeType:
@ -564,7 +610,7 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
} }
case BinaryAnd, BinaryOr: case BinaryAnd, BinaryOr:
if l.Type() != TypeBoolean { if l.Type() != TypeBoolean {
return nil, c.error(fmt.Sprintf("cannot perform binary %s on type %s", l, n.BinaryOperation), n) return nil, c.error(fmt.Sprintf("cannot perform binary %s on non-boolean type %s", n.BinaryOperation, l), n)
} }
return &BooleanSignature{}, nil return &BooleanSignature{}, nil
@ -572,7 +618,7 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
return &BooleanSignature{}, nil return &BooleanSignature{}, nil
case BinaryLess, BinaryGreater, BinaryLessEqual, BinaryGreaterEqual: case BinaryLess, BinaryGreater, BinaryLessEqual, BinaryGreaterEqual:
if l.Type() != TypeNumber { if l.Type() != TypeNumber {
return nil, c.error(fmt.Sprintf("cannot perform number comparison (%s) on type %s", l, n.BinaryOperation), n) return nil, c.error(fmt.Sprintf("cannot perform number comparison (%s) on non-number type %s", n.BinaryOperation, l), n)
} }
return &BooleanSignature{}, nil return &BooleanSignature{}, nil
@ -588,15 +634,30 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
switch sig.Type() { switch sig.Type() {
case TypeString: case TypeString:
return SignatureOf(StringPrototype[n.property]), nil v := StringPrototype[n.property]
if v == nil {
return nil, c.error(fmt.Sprintf("string has no property %s", n.property), tree)
}
return SignatureOf(v), nil
case TypeList: case TypeList:
return SignatureOf(ListPrototype[n.property]), nil v := ListPrototype[n.property]
if v == nil {
return nil, c.error(fmt.Sprintf("list has no property %s", n.property), tree)
}
return SignatureOf(v), nil
case TypeObject: case TypeObject:
if v, ok := ObjectPrototype[n.property]; ok { if v, ok := ObjectPrototype[n.property]; ok {
return SignatureOf(v), nil return SignatureOf(v), nil
} }
return sig.(*ObjectSignature).Members[n.property], nil v := sig.(*ObjectSignature).Members[n.property]
if v == nil {
return nil, c.error(fmt.Sprintf("object has no property %s", n.property), tree)
}
return v, nil
default: default:
return nil, c.error(fmt.Sprintf("cannot access property from value of type %s", sig), n) return nil, c.error(fmt.Sprintf("cannot access property from value of type %s", sig), n)
@ -674,14 +735,31 @@ func (c *Compiler) deduceSignature(tree Node) (TypeSignature, error) {
} }
} }
func (c *Compiler) getVarSignature(name string, causer Node) (TypeSignature, error) {
if c.isGlobal(name) {
return SignatureOf(DefaultGlobals[name]), nil
}
for i := c.stack.Current - 1; i >= 0; i-- {
v := c.stack.items[i]
if v.name == name {
return v.signature, nil
}
}
return nil, c.error(fmt.Sprintf("variable %s not defined", name), causer)
}
func (c *Compiler) affirmReturnSignature(tree Node, sig TypeSignature) error { func (c *Compiler) affirmReturnSignature(tree Node, sig TypeSignature) error {
switch tree.Type() { switch tree.Type() {
case BlockNodeType: case BlockNodeType:
c.descend()
for _, stmt := range tree.(*BlockNode).statements { for _, stmt := range tree.(*BlockNode).statements {
if err := c.affirmReturnSignature(stmt, sig); err != nil { if err := c.affirmReturnSignature(stmt, sig); err != nil {
return err return err
} }
} }
c.ascend()
case ReturnNodeType: case ReturnNodeType:
n := tree.(*ReturnNode) n := tree.(*ReturnNode)
@ -713,6 +791,33 @@ func (c *Compiler) affirmReturnSignature(tree Node, sig TypeSignature) error {
if err := c.affirmReturnSignature(n.do, sig); err != nil { if err := c.affirmReturnSignature(n.do, sig); err != nil {
return err return err
} }
case AssignNodeType:
n := tree.(*AssignNode)
if !n.declare {
prev, err := c.getVarSignature(n.name, n)
if err != nil {
return err
}
sig, err := c.deduceSignature(n.value)
if err != nil {
return err
}
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 nil
}
sig, err := c.deduceSignature(n.value)
if err != nil {
return err
}
c.registerVar(n.name, sig)
default: default:
} }
@ -902,6 +1007,10 @@ func (c *Compiler) ascend() {
for ; c.stack.Current > 0 && c.stack.Peek().scope > int(c.scope); c.stack.Pop() { for ; c.stack.Current > 0 && c.stack.Peek().scope > int(c.scope); c.stack.Pop() {
} }
}
func (c *Compiler) addAscend() {
c.ascend()
if c.scope != 0 { if c.scope != 0 {
c.add(InstructionAscend) c.add(InstructionAscend)
@ -910,6 +1019,10 @@ func (c *Compiler) ascend() {
func (c *Compiler) descend() { func (c *Compiler) descend() {
c.scope++ c.scope++
}
func (c *Compiler) addDescend() {
c.descend()
if c.scope != 1 { if c.scope != 1 {
c.add(InstructionDescend) c.add(InstructionDescend)
} }
@ -919,9 +1032,14 @@ func (c *Compiler) error(msg string, causer Node) CompilerError {
return CompilerError{ return CompilerError{
msg, msg,
causer, causer,
c.source,
} }
} }
func (c *Compiler) warn(msg string, causer Node) {
c.Warnings = append(c.Warnings, c.error(msg, causer))
}
func (c *Compiler) resolveImport(path string) Node { func (c *Compiler) resolveImport(path string) Node {
if chunk, ok := c.imports[path]; ok { if chunk, ok := c.imports[path]; ok {
return chunk return chunk

View file

@ -6,7 +6,7 @@ import (
) )
func TestNewCompiler(t *testing.T) { func TestNewCompiler(t *testing.T) {
c := NewCompiler() c := NewCompiler([]rune{})
if c == nil { if c == nil {
t.Fatal("NewCompiler returned nil") t.Fatal("NewCompiler returned nil")
@ -23,7 +23,7 @@ func TestNewCompiler(t *testing.T) {
func BenchmarkNewCompiler(b *testing.B) { func BenchmarkNewCompiler(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = NewCompiler() _ = NewCompiler([]rune{})
} }
} }
@ -267,51 +267,52 @@ func GetCompileTestData() map[string]CompileTestData {
&NumberValue{3}, &NumberValue{3},
}, },
}, },
"sum_function": {&BlockNode{ "sum_function": {
[]Node{ &BlockNode{
&AssignNode{ []Node{
"sum", &AssignNode{
&FunctionNode{
"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{
"sum", "sum",
@ -328,6 +329,7 @@ func GetCompileTestData() map[string]CompileTestData {
&NumberSignature{}, &NumberSignature{},
}, },
}, },
&NumberSignature{},
NewChunk( NewChunk(
[]Bytecode{ []Bytecode{
InstructionDescend, InstructionDescend,
@ -355,7 +357,7 @@ func GetCompileTestData() map[string]CompileTestData {
&FunctionNode{ &FunctionNode{
"a", "a",
[]FunctionParameter{}, []FunctionParameter{},
&NilSignature{}, &NumberSignature{},
&BlockNode{ &BlockNode{
[]Node{ []Node{
&AssignNode{ &AssignNode{
@ -400,6 +402,7 @@ func GetCompileTestData() map[string]CompileTestData {
&FunctionValue{ &FunctionValue{
"a", "a",
[]FunctionParameter{}, []FunctionParameter{},
&NumberSignature{},
NewChunk( NewChunk(
[]Bytecode{ []Bytecode{
InstructionDescend, InstructionDescend,
@ -419,6 +422,54 @@ func GetCompileTestData() map[string]CompileTestData {
}, },
}, },
}, },
"two_lists": {
tree: &BlockNode{
statements: []Node{
&AssignNode{
name: "a",
value: &ListNode{
items: []Node{
&NumberNode{value: 1},
&NumberNode{value: 2},
},
},
declare: true,
},
&AssignNode{
name: "b",
value: &ListNode{
items: []Node{
&StringNode{value: "Hello"},
&StringNode{value: "world"},
},
},
declare: true,
},
},
},
expectedStack: []Value{
&VariableValue{
name: "a",
value: &ListValue{
Items: []Value{
&NumberValue{1},
&NumberValue{2},
},
},
scope: 0,
},
&VariableValue{
name: "b",
value: &ListValue{
Items: []Value{
&StringValue{"Hello"},
&StringValue{"world"},
},
},
scope: 0,
},
},
},
} }
} }
@ -448,7 +499,7 @@ 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() c := NewCompiler([]rune(testCase.tree.String()))
t.Log("Compiling node tree") t.Log("Compiling node tree")
err := c.Compile(testCase.tree) err := c.Compile(testCase.tree)
@ -477,7 +528,7 @@ func BenchmarkCompile(b *testing.B) {
for name, testCase := range data { for name, testCase := range data {
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() c := NewCompiler([]rune{})
_ = c.Compile(testCase.tree) _ = c.Compile(testCase.tree)
} }
}) })
@ -487,7 +538,7 @@ func BenchmarkCompile(b *testing.B) {
func TestCompiler_AddU16(t *testing.T) { func TestCompiler_AddU16(t *testing.T) {
for i := 0; i <= 0xffff; i++ { for i := 0; i <= 0xffff; i++ {
t.Run(fmt.Sprint(i), func(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) {
c := NewCompiler() c := NewCompiler([]rune{})
c.addU16(uint16(i)) c.addU16(uint16(i))
if c.Chunk.Bytecode[0] != Bytecode(i>>8) { if c.Chunk.Bytecode[0] != Bytecode(i>>8) {
@ -521,7 +572,7 @@ func TestCompiler_CleanStack(t *testing.T) {
} }
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
c := NewCompiler() c := NewCompiler([]rune(tc.tree.String()))
err := c.Compile(tc.tree) err := c.Compile(tc.tree)
if err != nil { if err != nil {
t.Fatalf("Compiling failed: %v", err) t.Fatalf("Compiling failed: %v", err)

View file

@ -439,6 +439,10 @@ func (n ConditionalNode) Type() NodeType {
} }
func (n ConditionalNode) String() string { func (n ConditionalNode) String() string {
if n.otherwise == nil {
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 otheriwise %s", n.condition.String(), n.do.String(), n.otherwise.String())
} }

View file

@ -148,7 +148,7 @@ func (p *Parser) factor() (Node, error) {
(*p.prev).Lexeme[1 : len((*p.prev).Lexeme)-1], (*p.prev).Lexeme[1 : len((*p.prev).Lexeme)-1],
(*p.prev).Lexeme, (*p.prev).Lexeme,
p.prev.Start, p.prev.Start,
p.prev.Length, p.prev.Start + p.prev.Length,
}, nil }, nil
case TokenNumber: case TokenNumber:
@ -162,7 +162,7 @@ func (p *Parser) factor() (Node, error) {
return &NumberNode{ return &NumberNode{
num, num,
p.prev.Start, p.prev.Start,
p.prev.Length, p.prev.Start + p.prev.Length,
}, nil }, nil
case TokenHexadecimal: case TokenHexadecimal:
@ -184,14 +184,14 @@ func (p *Parser) factor() (Node, error) {
return &BooleanNode{ return &BooleanNode{
true, true,
p.prev.Start, p.prev.Start,
p.prev.Length, p.prev.Start + p.prev.Length,
}, nil }, nil
case TokenFalse: case TokenFalse:
p.advance() p.advance()
return &BooleanNode{ return &BooleanNode{
false, false,
p.prev.Start, p.prev.Start,
p.prev.Length, p.prev.Start + p.prev.Length,
}, nil }, nil
case TokenNil: case TokenNil:

View file

@ -53,11 +53,35 @@ func SignatureOf(v Value) TypeSignature {
case *BoolValue: case *BoolValue:
return &BooleanSignature{} return &BooleanSignature{}
case *ListValue: case *ListValue:
return &ListSignature{} // try to deduce contents type
var contains TypeSignature
for _, p := range t.Items {
sig := SignatureOf(p)
if contains == nil {
contains = sig
} else if !contains.Matches(sig) {
contains = &AnySignature{}
break
}
}
return &ListSignature{
contains,
}
case *ObjectValue: case *ObjectValue:
return &ObjectSignature{} return &ObjectSignature{}
case *FunctionValue: case *FunctionValue:
return &FunctionSignature{} params := make([]TypeSignature, len(t.Params))
for i, p := range t.Params {
params[i] = p.Signature
}
return &FunctionSignature{
params,
t.Yield,
}
case *BuiltinFunctionValue: case *BuiltinFunctionValue:
return t.Signature return t.Signature
} }
@ -267,8 +291,11 @@ func (s *FunctionSignature) String() string {
b.WriteString(t.String()) b.WriteString(t.String())
} }
b.WriteString(") ") b.WriteString(")")
b.WriteString(s.Out.String()) if s.Out.Type() != TypeNil {
b.WriteString(" ")
b.WriteString(s.Out.String())
}
return b.String() return b.String()
} }

View file

@ -234,6 +234,7 @@ var ObjectPrototype = map[string]Value{
return &NilValue{}, nil return &NilValue{}, nil
}, },
nil, nil,
false,
}, },
} }
@ -338,6 +339,17 @@ var StringPrototype = map[string]*BuiltinFunctionValue{
return GoToValue(out), nil return GoToValue(out), nil
}, },
nil, nil,
true,
},
"length": {
Name: "length",
Signature: &FunctionSignature{
[]TypeSignature{},
&NumberSignature{},
},
F: func(vm *VM, this Value, _ []Value) (Value, error) {
return GoToValue(len(this.(*StringValue).Text)), nil
},
}, },
} }
@ -392,7 +404,7 @@ func (v *ListValue) Equals(other Value) bool {
return false return false
} }
for i, item := range l.Items { for i, item := range v.Items {
if !item.Equals(l.Items[i]) { if !item.Equals(l.Items[i]) {
return false return false
} }
@ -413,6 +425,7 @@ var ListPrototype = map[string]*BuiltinFunctionValue{
return &NilValue{}, nil return &NilValue{}, nil
}, },
nil, nil,
false,
}, },
"at": { "at": {
"at", "at",
@ -433,6 +446,7 @@ var ListPrototype = map[string]*BuiltinFunctionValue{
return items[index], nil return items[index], nil
}, },
nil, nil,
false,
}, },
"length": { "length": {
"length", "length",
@ -444,6 +458,7 @@ var ListPrototype = map[string]*BuiltinFunctionValue{
return GoToValue(len(this.(*ListValue).Items)), nil return GoToValue(len(this.(*ListValue).Items)), nil
}, },
nil, nil,
false,
}, },
"map": { "map": {
"map", "map",
@ -484,6 +499,7 @@ var ListPrototype = map[string]*BuiltinFunctionValue{
return list, nil return list, nil
}, },
nil, nil,
false,
}, },
"reduce": { "reduce": {
"reduce", "reduce",
@ -516,6 +532,7 @@ var ListPrototype = map[string]*BuiltinFunctionValue{
return sum, nil return sum, nil
}, },
nil, nil,
false,
}, },
} }
@ -542,6 +559,7 @@ func (v *ListValue) Clone() Value {
type FunctionValue struct { type FunctionValue struct {
Name string Name string
Params []FunctionParameter Params []FunctionParameter
Yield TypeSignature
Chunk *Chunk Chunk *Chunk
Parent Value Parent Value
} }
@ -572,6 +590,7 @@ func (v *FunctionValue) Clone() Value {
return &FunctionValue{ return &FunctionValue{
v.Name, v.Name,
v.Params, v.Params,
v.Yield,
v.Chunk, v.Chunk,
v.Parent, v.Parent,
} }
@ -582,6 +601,7 @@ type BuiltinFunctionValue struct {
Signature *FunctionSignature Signature *FunctionSignature
F func(*VM, Value, []Value) (Value, error) F func(*VM, Value, []Value) (Value, error)
Parent Value Parent Value
Constant bool
} }
func (v *BuiltinFunctionValue) Type() ValueType { func (v *BuiltinFunctionValue) Type() ValueType {
@ -611,6 +631,7 @@ func (v *BuiltinFunctionValue) Clone() Value {
v.Signature, v.Signature,
v.F, v.F,
v.Parent, v.Parent,
v.Constant,
} }
} }

View file

@ -78,6 +78,32 @@ func CompareValues(t *testing.T, got Value, want Value) {
CompareValues(t, n.value, m.value) CompareValues(t, n.value, m.value)
case ListValueType:
n := got.(*ListValue)
m := want.(*ListValue)
if len(n.Items) != len(m.Items) {
t.Fatalf("list items length mismatch: got %d, want %d", len(n.Items), len(m.Items))
}
for i, v := range n.Items {
t.Logf("comparing list items #%d: got %s, want %s", i, v, m.Items[i])
CompareValues(t, v, m.Items[i])
}
case ObjectValueType:
n := got.(*ObjectValue)
m := want.(*ObjectValue)
if len(n.Members) != len(m.Members) {
t.Fatalf("object members count mismatch: got %d, want %d", len(n.Members), len(m.Members))
}
for k, v := range n.Members {
t.Logf("comparing object member %s: got %s, want %s", k, v, m.Members[k])
CompareValues(t, v, m.Members[k])
}
default: default:
panic("unimplemented comparison") panic("unimplemented comparison")
} }

View file

@ -311,6 +311,7 @@ var DefaultGlobals = map[string]Value{
return nil, nil return nil, nil
}, },
nil, nil,
false,
}, },
"print": &BuiltinFunctionValue{ "print": &BuiltinFunctionValue{
"print", "print",
@ -323,6 +324,7 @@ var DefaultGlobals = map[string]Value{
return nil, nil return nil, nil
}, },
nil, nil,
false,
}, },
"format": &BuiltinFunctionValue{ "format": &BuiltinFunctionValue{
"format", "format",
@ -356,6 +358,7 @@ var DefaultGlobals = map[string]Value{
return GoToValue(b.String()), nil return GoToValue(b.String()), nil
}, },
nil, nil,
true,
}, },
"char": &BuiltinFunctionValue{ "char": &BuiltinFunctionValue{
"char", "char",
@ -372,6 +375,7 @@ var DefaultGlobals = map[string]Value{
}, nil }, nil
}, },
nil, nil,
true,
}, },
"assertEq": &BuiltinFunctionValue{ "assertEq": &BuiltinFunctionValue{
"assertEq", "assertEq",
@ -393,6 +397,7 @@ var DefaultGlobals = map[string]Value{
return &NilValue{}, nil return &NilValue{}, nil
}, },
nil, nil,
false,
}, },
"assertNotEq": &BuiltinFunctionValue{ "assertNotEq": &BuiltinFunctionValue{
"assertNotEq", "assertNotEq",
@ -414,6 +419,7 @@ var DefaultGlobals = map[string]Value{
return &NilValue{}, nil return &NilValue{}, nil
}, },
nil, nil,
false,
}, },
"str": &BuiltinFunctionValue{ "str": &BuiltinFunctionValue{
"str", "str",
@ -425,6 +431,21 @@ var DefaultGlobals = map[string]Value{
return GoToValue(args[0].String()), nil return GoToValue(args[0].String()), nil
}, },
nil, nil,
true,
},
"type": &BuiltinFunctionValue{
Name: "type",
Signature: &FunctionSignature{
In: []TypeSignature{&AnySignature{}},
Out: &StringSignature{},
},
F: func(vm *VM, this Value, args []Value) (Value, error) {
v := args[0]
sig := SignatureOf(v)
return GoToValue(sig.String()), nil
},
Constant: true,
}, },
"exit": &BuiltinFunctionValue{ "exit": &BuiltinFunctionValue{
"exit", "exit",
@ -437,6 +458,7 @@ var DefaultGlobals = map[string]Value{
return &NilValue{}, nil return &NilValue{}, nil
}, },
nil, nil,
false,
}, },
} }

View file

@ -1,8 +1,17 @@
NAMESPACE := ""
func namespace(name: string, test: func()) {
NAMESPACE = name
test()
}
func assertEqual(a: any, b: any) { func assertEqual(a: any, b: any) {
if a != b { if a != b {
write(format("assertion error: % should (but doesn't) equal %", [a, b])) write(format("assertion error: % should (but doesn't) equal %", [a, b]))
exit(1) exit(1)
} else if env("DEBUG") != "" {
write(format("assertion success: % equals %", [a, b]))
} }
} }
@ -10,5 +19,7 @@ func assertNotEqual(a: any, b: any) {
if a == b { if a == b {
write(format("assertion error: % shouldn't (but does) equal %", [a, b])) write(format("assertion error: % shouldn't (but does) equal %", [a, b]))
exit(1) exit(1)
} else if env("DEBUG") != "" {
write(format("assertion success: % doesn't equal %", [a, b]))
} }
} }

View file

@ -1,26 +1,24 @@
#!/bin/zsh #!/bin/zsh
echo '=== Building CLI ===' echo '=== Building CLI ==='
( cd cli || exit 1
cd cli || exit 1 if ! go build .; then
if ! go build .; then echo "=== Had error building CLI ==="
echo "=== Had error building CLI ===" exit 1
exit 1 else
else echo "=+= Successfully built CLI =+="
echo "=+= Successfully built CLI =+=" fi
fi cd ..
)
echo "=== Running go core tests ===" echo "=== Running go core tests ==="
( cd core || exit 1
cd core || exit 1 if ! go test .; then
if ! go test .; then echo "=x= Core testing failed =x= "
echo "=x= Core testing failed =x= " exit 1
exit 1 else
else echo "=+= Successfully ran core tests =+="
echo "=+= Successfully ran core tests =+=" fi
fi cd ..
)
errors=() errors=()

8
tests/types.ang Normal file
View file

@ -0,0 +1,8 @@
assertEq(type(1), "number")
assertEq(type("Hello"), "string")
assertEq(type(true), "boolean")
# lists
assertEq(type(["Hello", "world"]), "list[string]")
assertEq(type([0, 1]), "list[number]")

View file

@ -77,7 +77,7 @@ func run(_ js.Value, args []js.Value) interface{} {
log.Printf("Parsed tree: %s", tree.String()) log.Printf("Parsed tree: %s", tree.String())
compiler := core.NewCompiler() compiler := core.NewCompiler([]rune(source))
compiler.SetImportsResolver(&JsResolver{ compiler.SetImportsResolver(&JsResolver{
resolver, resolver,