Enhacnementsand toolchain

This commit is contained in:
allexanderbergmns
2026-08-24 15:35:11 +02:00
parent 8a4068631c
commit f77f9d4dff
19 changed files with 3071 additions and 45 deletions
+350
View File
@@ -0,0 +1,350 @@
// compiler.v — bytecode code generator for VuurRaaf.
//
// Compiles a parsed program into a VROBJ object file: flat bytecode plus a
// symbol per function and a relocation per call site. Call targets are left as
// relocations and resolved by the linker, so functions may live in other files.
module compiler
import os
import obj
// opcodes — keep in sync with vm/vm.v and assembler/assembler.v
const op_halt = u8(0)
const op_push_i = u8(1)
const op_push_s = u8(2)
const op_load = u8(3)
const op_store = u8(4)
const op_pop = u8(5)
const op_dup = u8(6)
const op_add = u8(7)
const op_sub = u8(8)
const op_mul = u8(9)
const op_div = u8(10)
const op_mod = u8(11)
const op_neg = u8(12)
const op_eq = u8(13)
const op_ne = u8(14)
const op_lt = u8(15)
const op_le = u8(16)
const op_gt = u8(17)
const op_ge = u8(18)
const op_and = u8(19)
const op_or = u8(20)
const op_not = u8(21)
const op_jmp = u8(22)
const op_jz = u8(23)
const op_jnz = u8(24)
const op_call = u8(25)
const op_ret = u8(26)
const op_retv = u8(27)
const op_print = u8(28)
const op_println = u8(29)
const op_assert = u8(30)
const op_enter = u8(31)
// compile parses and compiles VuurRaaf source into an object file.
pub fn compile(src string) !obj.Obj {
toks := tokenize(src)!
prog := parse(toks)!
return gen(prog)
}
pub fn compile_file(path string) !obj.Obj {
src := os.read_file(path)!
return compile(src)!
}
// ---------------------------------------------------------------------------
struct Fixup {
name string
off u32
}
struct Gen {
mut:
code []u8
strings []string
str_map map[string]int
symbols []obj.Symbol
relocs []obj.Reloc
locals map[string]int
local_cnt int
argc int
cur_fn string
labels map[string]int
fixups []Fixup
enter_off u32
next_lbl int
}
fn gen(prog Program) !obj.Obj {
mut g := Gen{}
for fd in prog.fns {
g.gen_fn(fd)!
}
return obj.Obj{
symbols: g.symbols
strings: g.strings
code: g.code
relocs: g.relocs
}
}
fn (mut g Gen) gen_fn(fd FnDecl) ! {
g.cur_fn = fd.name
g.symbols << obj.Symbol{ name: fd.name, entry: g.code.len }
g.locals.clear()
g.local_cnt = 0
g.argc = fd.params.len
for i, p in fd.params {
g.locals[p] = i
}
g.local_cnt = g.argc
// `enter n` reserves the non-parameter locals; n is patched once the body
// has been scanned.
g.code << op_enter
g.enter_off = u32(g.code.len)
g.code << obj.encode_i64(0)
for st in fd.body {
g.gen_stmt(st)!
}
g.code << op_ret // trailing return for fall-through
obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt - g.argc))
// resolve intra-function jump targets
for f in g.fixups {
target := g.labels[f.name] or {
return error('internal error: unresolved label ${f.name} in fn ${fd.name}')
}
obj.patch_i64(mut g.code, f.off, i64(target))
}
g.fixups.clear()
g.labels.clear()
g.cur_fn = ''
}
fn (mut g Gen) gen_stmt(st Stmt) ! {
match st.kind {
.expr_stmt {
g.gen_expr(st.expr)!
// print/println already consume their value; everything else
// leaves one on the stack that must be discarded
if st.expr.kind == .call && (st.expr.name == 'print' || st.expr.name == 'println') {
// nothing to discard
} else {
g.code << op_pop
}
}
.let_stmt {
g.gen_expr(st.expr)!
idx := g.local_cnt
g.local_cnt++
g.locals[st.target] = idx
g.code << op_store
g.code << obj.encode_i64(i64(idx))
}
.assign_stmt {
idx := g.locals[st.target] or {
return error('unknown variable "${st.target}" at line ${st.line}')
}
g.gen_expr(st.expr)!
g.code << op_store
g.code << obj.encode_i64(i64(idx))
}
.if_stmt {
else_l := g.new_label()
end_l := g.new_label()
g.gen_expr(st.cond)!
g.code << op_jz
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: else_l, off: u32(g.code.len) - 8 }
for s in st.body {
g.gen_stmt(s)!
}
g.code << op_jmp
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
g.emit_label(else_l)
for s in st.els {
g.gen_stmt(s)!
}
g.emit_label(end_l)
}
.while_stmt {
loop_l := g.new_label()
end_l := g.new_label()
g.emit_label(loop_l)
g.gen_expr(st.cond)!
g.code << op_jz
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
for s in st.body {
g.gen_stmt(s)!
}
g.code << op_jmp
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: loop_l, off: u32(g.code.len) - 8 }
g.emit_label(end_l)
}
.ret_stmt {
if st.has_val {
g.gen_expr(st.expr)!
g.code << op_retv
} else {
g.code << op_ret
}
}
.assert_stmt {
g.gen_expr(st.expr)!
g.code << op_assert
}
}
}
fn (mut g Gen) gen_expr(e Expr) ! {
match e.kind {
.int_lit {
g.code << op_push_i
g.code << obj.encode_i64(e.int_v)
}
.str_lit {
// the index is a placeholder; the linker rebases it via a string
// relocation so multi-file links keep working
g.code << op_push_s
g.code << obj.encode_i64(0)
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.str_v, kind: 1 }
}
.bool_lit {
g.code << op_push_i
g.code << obj.encode_i64(e.int_v)
}
.ident {
idx := g.locals[e.name] or {
return error('unknown variable "${e.name}" at line ${e.line}')
}
g.code << op_load
g.code << obj.encode_i64(i64(idx))
}
.unary {
g.gen_expr(*e.right)!
if e.op == .kw_not {
g.code << op_not
} else {
g.code << op_neg
}
}
.binary {
g.gen_binary(e)!
}
.call {
g.gen_call(e)!
}
}
}
fn (mut g Gen) gen_call(e Expr) ! {
if e.name == 'print' || e.name == 'println' {
if e.args.len != 1 {
return error('${e.name}() takes exactly one argument (line ${e.line})')
}
g.gen_expr(e.args[0])!
g.code << if e.name == 'print' { op_print } else { op_println }
return
}
for a in e.args {
g.gen_expr(a)!
}
g.code << op_call
g.code << obj.encode_i64(0) // placeholder — patched by the linker
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.name, kind: 0 }
g.code << obj.encode_i64(i64(e.args.len)) // argc
}
fn (mut g Gen) gen_binary(e Expr) ! {
match e.op {
.kw_and {
// a and b → short-circuit: if !a or !b then 0 else 1
false_l := g.new_label()
end_l := g.new_label()
g.gen_expr(*e.left)!
g.code << op_jz
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: false_l, off: u32(g.code.len) - 8 }
g.gen_expr(*e.right)!
g.code << op_jz
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: false_l, off: u32(g.code.len) - 8 }
g.code << op_push_i
g.code << obj.encode_i64(1)
g.code << op_jmp
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
g.emit_label(false_l)
g.code << op_push_i
g.code << obj.encode_i64(0)
g.emit_label(end_l)
}
.kw_or {
// a or b → short-circuit: if a or b then 1 else 0
true_l := g.new_label()
end_l := g.new_label()
g.gen_expr(*e.left)!
g.code << op_jnz
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: true_l, off: u32(g.code.len) - 8 }
g.gen_expr(*e.right)!
g.code << op_jnz
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: true_l, off: u32(g.code.len) - 8 }
g.code << op_push_i
g.code << obj.encode_i64(0)
g.code << op_jmp
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
g.emit_label(true_l)
g.code << op_push_i
g.code << obj.encode_i64(1)
g.emit_label(end_l)
}
else {
g.gen_expr(*e.left)!
g.gen_expr(*e.right)!
op := match e.op {
.plus { op_add }
.minus { op_sub }
.star { op_mul }
.slash { op_div }
.percent { op_mod }
.eq_eq { op_eq }
.not_eq { op_ne }
.lt { op_lt }
.le { op_le }
.gt { op_gt }
.ge { op_ge }
else {
return error('unsupported binary operator at line ${e.line}')
}
}
g.code << op
}
}
}
fn (mut g Gen) intern(s string) int {
if s in g.str_map {
return g.str_map[s]
}
idx := g.strings.len
g.strings << s
g.str_map[s] = idx
return idx
}
fn (mut g Gen) new_label() string {
g.next_lbl++
return 'L${g.next_lbl}'
}
fn (mut g Gen) emit_label(name string) {
g.labels[name] = g.code.len
}
+275
View File
@@ -0,0 +1,275 @@
// lexer.v — tokenizer for the VuurRaaf source language (.vr).
module compiler
pub enum TokKind {
eof
ident
int_lit
str_lit
lparen
rparen
lbrace
rbrace
comma
plus
minus
star
slash
percent
eq_eq
not_eq
lt
le
gt
ge
assign
kw_fn
kw_let
kw_if
kw_else
kw_while
kw_return
kw_true
kw_false
kw_and
kw_or
kw_not
kw_print
kw_println
kw_assert
}
pub struct Tok {
pub:
kind TokKind
lit string
line int
}
pub fn tokenize(src string) ![]Tok {
mut l := Lexer{ src: src }
mut toks := []Tok{}
for {
t := l.next()!
toks << t
if t.kind == .eof {
break
}
}
return toks
}
struct Lexer {
mut:
src string
pos int
line int
}
fn (mut l Lexer) peek() u8 {
if l.pos >= l.src.len {
return 0
}
return l.src[l.pos]
}
fn (mut l Lexer) peek2() u8 {
if l.pos + 1 >= l.src.len {
return 0
}
return l.src[l.pos + 1]
}
fn (mut l Lexer) advance() u8 {
c := l.src[l.pos]
l.pos++
if c == `\n` {
l.line++
}
return c
}
fn (mut l Lexer) next() !Tok {
// skip whitespace and // comments
for l.pos < l.src.len {
c := l.peek()
if c == ` ` || c == `\t` || c == `\r` || c == `\n` {
l.advance()
continue
}
if c == `/` && l.peek2() == `/` {
for l.pos < l.src.len && l.peek() != `\n` {
l.advance()
}
continue
}
break
}
line := l.line
if l.pos >= l.src.len {
return Tok{ kind: .eof, lit: '', line: line }
}
c := l.peek()
match c {
`(` {
l.advance()
return Tok{ kind: .lparen, lit: '(', line: line }
}
`)` {
l.advance()
return Tok{ kind: .rparen, lit: ')', line: line }
}
`{` {
l.advance()
return Tok{ kind: .lbrace, lit: '{', line: line }
}
`}` {
l.advance()
return Tok{ kind: .rbrace, lit: '}', line: line }
}
`,` {
l.advance()
return Tok{ kind: .comma, lit: ',', line: line }
}
`+` {
l.advance()
return Tok{ kind: .plus, lit: '+', line: line }
}
`-` {
l.advance()
return Tok{ kind: .minus, lit: '-', line: line }
}
`*` {
l.advance()
return Tok{ kind: .star, lit: '*', line: line }
}
`/` {
l.advance()
return Tok{ kind: .slash, lit: '/', line: line }
}
`%` {
l.advance()
return Tok{ kind: .percent, lit: '%', line: line }
}
`=` {
l.advance()
if l.peek() == `=` {
l.advance()
return Tok{ kind: .eq_eq, lit: '==', line: line }
}
return Tok{ kind: .assign, lit: '=', line: line }
}
`!` {
l.advance()
if l.peek() == `=` {
l.advance()
return Tok{ kind: .not_eq, lit: '!=', line: line }
}
return error('unexpected character "!" at line ${line} (did you mean "not"?)')
}
`<` {
l.advance()
if l.peek() == `=` {
l.advance()
return Tok{ kind: .le, lit: '<=', line: line }
}
return Tok{ kind: .lt, lit: '<', line: line }
}
`>` {
l.advance()
if l.peek() == `=` {
l.advance()
return Tok{ kind: .ge, lit: '>=', line: line }
}
return Tok{ kind: .gt, lit: '>', line: line }
}
`"` {
return l.lex_string(line)!
}
`0`...`9` {
return l.lex_number(line)
}
else {
if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` {
return l.lex_ident(line)
}
return error('unexpected character "${c.ascii_str()}" at line ${line}')
}
}
}
fn (mut l Lexer) lex_number(line int) Tok {
start := l.pos
for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
l.advance()
}
return Tok{ kind: .int_lit, lit: l.src[start..l.pos], line: line }
}
fn (mut l Lexer) lex_ident(line int) Tok {
start := l.pos
for l.pos < l.src.len {
c := l.peek()
if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || (c >= `0` && c <= `9`) || c == `_` {
l.advance()
} else {
break
}
}
lit := l.src[start..l.pos]
kind := match lit {
'fn' { TokKind.kw_fn }
'let' { TokKind.kw_let }
'if' { TokKind.kw_if }
'else' { TokKind.kw_else }
'while' { TokKind.kw_while }
'return' { TokKind.kw_return }
'true' { TokKind.kw_true }
'false' { TokKind.kw_false }
'and' { TokKind.kw_and }
'or' { TokKind.kw_or }
'not' { TokKind.kw_not }
'print' { TokKind.kw_print }
'println' { TokKind.kw_println }
'assert' { TokKind.kw_assert }
else { TokKind.ident }
}
return Tok{ kind: kind, lit: lit, line: line }
}
fn (mut l Lexer) lex_string(line int) !Tok {
l.advance() // opening quote
mut s := ''
for l.pos < l.src.len {
c := l.advance()
if c == `"` {
return Tok{ kind: .str_lit, lit: s, line: line }
}
if c == `\\` {
if l.pos >= l.src.len {
break
}
e := l.advance()
match e {
`n` {
s += '\n'
}
`t` {
s += '\t'
}
`"` {
s += '"'
}
`\\` {
s += '\\'
}
else {
return error('invalid escape \\${e.ascii_str()} at line ${line}')
}
}
continue
}
s += c.ascii_str()
}
return error('unterminated string at line ${line}')
}
+388
View File
@@ -0,0 +1,388 @@
// parser.v — recursive-descent parser for the VuurRaaf language.
//
// Grammar (informal):
// program := fn*
// fn := 'fn' IDENT '(' [IDENT (',' IDENT)*] ')' block
// block := '{' stmt* '}'
// stmt := 'let' IDENT '=' expr
// | IDENT '=' expr
// | 'if' cond block ['else' block]
// | 'while' cond block
// | 'return' [expr]
// | 'assert' expr
// | expr
// cond := ['('] expr [')'] // parens optional
// expr := or ('or' or)*
// or := and ('and' and)*
// and := eq (('=='|'!=') eq)*
// eq := rel (('<'|'<='|'>'|'>=') rel)*
// rel := add (('+'|'-') add)*
// add := mul (('*'|'/'|'%') mul)*
// mul := ('not'|'-') mul | primary
// primary := INT | STR | 'true' | 'false' | IDENT ['(' args ')'] | '(' expr ')'
module compiler
pub enum ExprKind {
int_lit
str_lit
bool_lit
ident
unary
binary
call
}
pub struct Expr {
pub mut:
kind ExprKind
int_v i64
str_v string
name string
op TokKind
left &Expr = unsafe { nil }
right &Expr = unsafe { nil }
args []Expr
line int
}
pub enum StmtKind {
expr_stmt
let_stmt
assign_stmt
if_stmt
while_stmt
ret_stmt
assert_stmt
}
pub struct Stmt {
pub mut:
kind StmtKind
target string
expr Expr
cond Expr
body []Stmt
els []Stmt
has_val bool
line int
}
pub struct FnDecl {
pub mut:
name string
params []string
body []Stmt
line int
}
pub struct Program {
pub mut:
fns []FnDecl
}
pub fn parse(toks []Tok) !Program {
mut p := Parser{ toks: toks }
return p.parse_program()
}
struct Parser {
mut:
toks []Tok
pos int
}
fn (mut p Parser) cur() Tok {
if p.pos < p.toks.len {
return p.toks[p.pos]
}
return p.toks[p.toks.len - 1]
}
fn (mut p Parser) advance() Tok {
t := p.cur()
if p.pos < p.toks.len - 1 {
p.pos++
}
return t
}
fn (mut p Parser) expect(k TokKind, what string) !Tok {
t := p.cur()
if t.kind != k {
return error('expected ${what}, got "${t.lit}" at line ${t.line}')
}
return p.advance()
}
// parse_cond parses a condition, accepting either `if cond {` or `if (cond) {`.
fn (mut p Parser) parse_cond() !Expr {
if p.cur().kind == .lparen {
p.advance()
e := p.parse_expr()!
p.expect(.rparen, "')'")!
return e
}
return p.parse_expr()!
}
fn (mut p Parser) parse_program() !Program {
mut prog := Program{}
for p.cur().kind != .eof {
prog.fns << p.parse_fn()!
}
if prog.fns.len == 0 {
return error('no functions found in source')
}
return prog
}
fn (mut p Parser) parse_fn() !FnDecl {
fn_tok := p.expect(.kw_fn, "'fn'")!
name := p.expect(.ident, 'function name')!
p.expect(.lparen, "'('")!
mut params := []string{}
if p.cur().kind != .rparen {
for {
params << p.expect(.ident, 'parameter name')!.lit
if p.cur().kind == .comma {
p.advance()
continue
}
break
}
}
p.expect(.rparen, "')'")!
body := p.parse_block()!
return FnDecl{ name: name.lit, params: params, body: body, line: fn_tok.line }
}
fn (mut p Parser) parse_block() ![]Stmt {
p.expect(.lbrace, "'{'")!
mut stmts := []Stmt{}
for p.cur().kind != .rbrace {
if p.cur().kind == .eof {
return error('unexpected end of file inside block (missing "}")')
}
stmts << p.parse_stmt()!
}
p.expect(.rbrace, "'}'")!
return stmts
}
fn (mut p Parser) parse_stmt() !Stmt {
t := p.cur()
match t.kind {
.kw_let {
p.advance()
name := p.expect(.ident, 'variable name')!
p.expect(.assign, "'='")!
e := p.parse_expr()!
return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line }
} .kw_if {
p.advance()
cond := p.parse_cond()!
body := p.parse_block()!
mut els := []Stmt{}
if p.cur().kind == .kw_else {
p.advance()
els = p.parse_block()!
}
return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line }
}
.kw_while {
p.advance()
cond := p.parse_cond()!
body := p.parse_block()!
return Stmt{ kind: .while_stmt, cond: cond, body: body, line: t.line }
}
.kw_return {
p.advance()
mut e := Expr{}
has_val := p.cur().kind != .rbrace
if has_val {
e = p.parse_expr()!
}
return Stmt{ kind: .ret_stmt, expr: e, has_val: has_val, line: t.line }
} .kw_assert {
p.advance()
mut e := Expr{}
if p.cur().kind == .lparen {
p.advance()
e = p.parse_expr()!
p.expect(.rparen, "')'")!
} else {
e = p.parse_expr()!
}
return Stmt{ kind: .assert_stmt, expr: e, line: t.line }
}
.ident {
p.advance()
if p.cur().kind == .assign {
p.advance()
e := p.parse_expr()!
return Stmt{ kind: .assign_stmt, target: t.lit, expr: e, line: t.line }
}
e := p.parse_call_or_ident(t)!
return Stmt{ kind: .expr_stmt, expr: e, line: t.line }
}
.kw_print, .kw_println {
p.advance()
e := p.parse_call(t)!
return Stmt{ kind: .expr_stmt, expr: e, line: t.line }
}
else {
return error('unexpected token "${t.lit}" at line ${t.line}')
}
}
}
// bin_node allocates a binary-operator node. It takes copies of the operands
// so that `&l`/`&r` target fresh heap objects (taking the address of a local
// that is later reassigned would create a self-referential node).
fn bin_node(op TokKind, left Expr, right Expr, line int) Expr {
mut l := left
mut r := right
return Expr{ kind: .binary, op: op, left: &l, right: &r, line: line }
}
fn unary_node(op TokKind, operand Expr, line int) Expr {
mut o := operand
return Expr{ kind: .unary, op: op, right: &o, line: line }
}
fn (mut p Parser) parse_expr() !Expr {
return p.parse_or()!
}
fn (mut p Parser) parse_or() !Expr {
mut e := p.parse_and()!
for p.cur().kind == .kw_or {
op := p.advance()
rhs := p.parse_and()!
e = bin_node(op.kind, e, rhs, op.line)
}
return e
}
fn (mut p Parser) parse_and() !Expr {
mut e := p.parse_eq()!
for p.cur().kind == .kw_and {
op := p.advance()
rhs := p.parse_eq()!
e = bin_node(op.kind, e, rhs, op.line)
}
return e
}
fn (mut p Parser) parse_eq() !Expr {
mut e := p.parse_rel()!
for p.cur().kind == .eq_eq || p.cur().kind == .not_eq {
op := p.advance()
rhs := p.parse_rel()!
e = bin_node(op.kind, e, rhs, op.line)
}
return e
}
fn (mut p Parser) parse_rel() !Expr {
mut e := p.parse_add()!
for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge {
op := p.advance()
rhs := p.parse_add()!
e = bin_node(op.kind, e, rhs, op.line)
}
return e
}
fn (mut p Parser) parse_add() !Expr {
mut e := p.parse_mul()!
for p.cur().kind == .plus || p.cur().kind == .minus {
op := p.advance()
rhs := p.parse_mul()!
e = bin_node(op.kind, e, rhs, op.line)
}
return e
}
fn (mut p Parser) parse_mul() !Expr {
mut e := p.parse_unary()!
for p.cur().kind == .star || p.cur().kind == .slash || p.cur().kind == .percent {
op := p.advance()
rhs := p.parse_unary()!
e = bin_node(op.kind, e, rhs, op.line)
}
return e
}
fn (mut p Parser) parse_unary() !Expr {
t := p.cur()
if t.kind == .kw_not || t.kind == .minus {
p.advance()
e := p.parse_unary()!
return unary_node(t.kind, e, t.line)
}
return p.parse_primary()!
}
fn (mut p Parser) parse_primary() !Expr {
t := p.cur()
match t.kind {
.int_lit {
p.advance()
return Expr{ kind: .int_lit, int_v: t.lit.i64(), line: t.line }
}
.str_lit {
p.advance()
return Expr{ kind: .str_lit, str_v: t.lit, line: t.line }
}
.kw_true {
p.advance()
return Expr{ kind: .bool_lit, int_v: 1, line: t.line }
}
.kw_false {
p.advance()
return Expr{ kind: .bool_lit, int_v: 0, line: t.line }
}
.lparen {
p.advance()
e := p.parse_expr()!
p.expect(.rparen, "')'")!
return e
}
.ident {
p.advance()
return p.parse_call_or_ident(t)!
}
.kw_print, .kw_println {
p.advance()
return p.parse_call(t)!
}
else {
return error('unexpected token "${t.lit}" at line ${t.line}')
}
}
}
fn (mut p Parser) parse_call_or_ident(t Tok) !Expr {
if p.cur().kind == .lparen {
return p.parse_call(t)!
}
return Expr{ kind: .ident, name: t.lit, line: t.line }
}
fn (mut p Parser) parse_call(name Tok) !Expr {
p.expect(.lparen, "'('")!
mut args := []Expr{}
if p.cur().kind != .rparen {
for {
args << p.parse_expr()!
if p.cur().kind == .comma {
p.advance()
continue
}
break
}
}
p.expect(.rparen, "')'")!
return Expr{ kind: .call, name: name.lit, args: args, line: name.line }
}