mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 15:17:19 +00:00
1640 lines
46 KiB
V
1640 lines
46 KiB
V
// codegen.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 obj
|
|
|
|
struct Fixup {
|
|
name string
|
|
off u32
|
|
}
|
|
|
|
// LoopCtx records where `break` and `continue` should jump while generating
|
|
// the body of a loop. For `for` loops `continue` targets the increment, not
|
|
// the condition check, so the loop variable still advances.
|
|
struct LoopCtx {
|
|
break_l string
|
|
continue_l string
|
|
}
|
|
|
|
struct Gen {
|
|
mut:
|
|
code []u8
|
|
strings []string
|
|
str_map map[string]int
|
|
symbols []obj.Symbol
|
|
relocs []obj.Reloc
|
|
locals map[string]int
|
|
types map[string]string // local name -> declared struct type ('' = unknown)
|
|
structs map[string][]string // declared struct name -> field list
|
|
enums map[string][]string // enum name -> variant list
|
|
lam_counter int // anonymous function counter
|
|
enum_vals map[string]int // 'Enum.variant' -> integer value
|
|
consts map[string]i64 // constant name -> integer value
|
|
lines []obj.LineInfo // code offset -> source line (debug info)
|
|
local_cnt int
|
|
argc int
|
|
cur_fn string
|
|
labels map[string]int
|
|
fixups []Fixup
|
|
loops []LoopCtx
|
|
enter_off u32
|
|
next_lbl int
|
|
modules map[string]bool // imported module names (bare `import os`)
|
|
captures []string // enclosing locals captured by the closure being compiled
|
|
}
|
|
|
|
fn gen(prog Program) !obj.Obj {
|
|
mut g := Gen{}
|
|
// register enums first so their values are available everywhere
|
|
for ed in prog.enums {
|
|
if ed.name in g.enums {
|
|
return error('duplicate enum declaration "${ed.name}"')
|
|
}
|
|
g.enums[ed.name] = ed.variants
|
|
for i, v in ed.variants {
|
|
g.enum_vals['${ed.name}.${v}'] = i
|
|
}
|
|
}
|
|
// register constants
|
|
for cd in prog.consts {
|
|
if cd.name in g.consts {
|
|
return error('duplicate constant declaration "${cd.name}"')
|
|
}
|
|
// constants must be compile-time integer expressions
|
|
if cd.value.kind == .int_lit {
|
|
g.consts[cd.name] = cd.value.int_v
|
|
} else if cd.value.kind == .bool_lit {
|
|
g.consts[cd.name] = cd.value.int_v
|
|
} else {
|
|
return error('constant "${cd.name}" must be an integer or boolean literal (line ${cd.line})')
|
|
}
|
|
}
|
|
// register struct declarations
|
|
for sd in prog.structs {
|
|
if sd.name in g.structs {
|
|
return error('duplicate struct declaration "${sd.name}"')
|
|
}
|
|
g.structs[sd.name] = sd.fields
|
|
}
|
|
// compile imported files and merge their objects. Bare module imports
|
|
// (`import os`) prefix the module's function symbols and internal call
|
|
// relocations with "os.", so programs call os.exists(...) and modules can
|
|
// never collide with each other or with the program's own functions.
|
|
for imp in prog.imports {
|
|
mod_name := imp.name
|
|
prefix := if mod_name.len > 0 { mod_name + '.' } else { '' }
|
|
imported := compile_file(resolve_import(imp.path)!)!
|
|
if mod_name.len > 0 {
|
|
g.modules[mod_name] = true
|
|
}
|
|
// the imported object's own symbol names (for rewriting call sites)
|
|
mut own := map[string]bool{}
|
|
for s in imported.symbols {
|
|
own[s.name] = true
|
|
}
|
|
// append imported bytecode first so symbol entries can be rebased
|
|
code_off := g.code.len
|
|
g.code << imported.code
|
|
// merge symbols from the imported object (prefixed and rebased: entries
|
|
// are relative to the imported code, which now sits at code_off)
|
|
for s in imported.symbols {
|
|
g.symbols << obj.Symbol{ name: prefix + s.name, entry: code_off + s.entry }
|
|
}
|
|
// merge strings
|
|
for s in imported.strings {
|
|
g.strings << s
|
|
}
|
|
// adjust relocations (and prefix module-internal call targets)
|
|
for r in imported.relocs {
|
|
mut rname := r.name
|
|
if r.kind == 0 && prefix.len > 0 && r.name in own {
|
|
rname = prefix + r.name
|
|
}
|
|
g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: rname, kind: r.kind }
|
|
}
|
|
// merge debug info, rebasing offsets into this object's code space
|
|
for l in imported.lines {
|
|
g.lines << obj.LineInfo{ off: u32(code_off) + l.off, line: l.line }
|
|
}
|
|
}
|
|
for fd in prog.fns {
|
|
g.captures = []string{} // top-level functions capture nothing
|
|
g.gen_fn(fd)!
|
|
}
|
|
return obj.Obj{
|
|
symbols: g.symbols
|
|
strings: g.strings
|
|
code: g.code
|
|
relocs: g.relocs
|
|
lines: g.lines
|
|
}
|
|
}
|
|
|
|
fn (mut g Gen) gen_fn(fd FnDecl) ! {
|
|
// methods compile to functions named `Type.method`; the receiver is the
|
|
// implicit first argument, so `p.dist(x)` becomes `call Point.dist p, x`
|
|
sym := if fd.recv_type.len > 0 { '${fd.recv_type}.${fd.name}' } else { fd.name }
|
|
g.cur_fn = sym
|
|
g.symbols << obj.Symbol{ name: sym, entry: g.code.len }
|
|
g.lines << obj.LineInfo{ off: u32(g.code.len), line: fd.line }
|
|
g.locals.clear()
|
|
g.types.clear()
|
|
g.local_cnt = 0
|
|
// closure captures occupy the leading local slots (filled by the caller's
|
|
// op_call_closure), then the receiver (methods), then the parameters
|
|
g.argc = fd.params.len + g.captures.len + if fd.recv_type.len > 0 { 1 } else { 0 }
|
|
mut next := 0
|
|
if g.captures.len > 0 {
|
|
for i, c in g.captures {
|
|
g.locals[c] = i
|
|
}
|
|
next = g.captures.len
|
|
} else if fd.recv_type.len > 0 {
|
|
g.locals[fd.recv_name] = 0
|
|
g.types[fd.recv_name] = fd.recv_type
|
|
next = 1
|
|
}
|
|
// a variadic parameter does not occupy an argument slot; it gets a fresh
|
|
// local that the prologue fills with the collected vararg array
|
|
if fd.variadic {
|
|
g.argc--
|
|
}
|
|
for i, p in fd.params {
|
|
if fd.variadic && i == fd.params.len - 1 {
|
|
continue
|
|
}
|
|
g.locals[p] = i + next
|
|
}
|
|
g.local_cnt = g.argc
|
|
if fd.variadic {
|
|
vidx := g.local_cnt
|
|
g.local_cnt++
|
|
g.locals[fd.params[fd.params.len - 1]] = vidx
|
|
}
|
|
// `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)
|
|
// default parameter values: if the caller passed fewer args than this
|
|
// param's slot, evaluate the default and store it
|
|
for i, p in fd.params {
|
|
if fd.variadic && i == fd.params.len - 1 {
|
|
continue
|
|
}
|
|
if i >= fd.has_defs.len || !fd.has_defs[i] {
|
|
continue
|
|
}
|
|
slot := i + next
|
|
skip_l := g.new_label()
|
|
g.code << op_argc
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(slot))
|
|
g.code << op_le
|
|
g.code << op_jz
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: skip_l, off: u32(g.code.len) - 8 }
|
|
g.gen_expr(fd.defaults[i])!
|
|
g.emit_store(slot)
|
|
g.emit_label(skip_l)
|
|
}
|
|
// variadic collection: build an array from args[argc..actual-1]
|
|
if fd.variadic {
|
|
vidx := g.locals[fd.params[fd.params.len - 1]] or {
|
|
return error('internal: variadic param missing')
|
|
}
|
|
g.code << op_varargs
|
|
g.code << obj.encode_i64(i64(g.argc))
|
|
g.code << obj.encode_i64(i64(vidx))
|
|
}
|
|
for st in fd.body {
|
|
g.gen_stmt(st)!
|
|
}
|
|
g.code << op_ret // trailing return for fall-through
|
|
// reserve all local slots: the callee may be called with fewer arguments
|
|
// than declared (default parameters) or more (variadic), so the frame must
|
|
// always cover slots 0..local_cnt-1
|
|
obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt))
|
|
// resolve intra-function jump targets. Targets are encoded PC-relative
|
|
// (delta from the end of the 8-byte operand), so bytecode stays
|
|
// position-independent when module objects are merged or linked.
|
|
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 - (int(f.off) + 8)))
|
|
}
|
|
g.fixups.clear()
|
|
g.labels.clear()
|
|
g.cur_fn = ''
|
|
}
|
|
|
|
fn (mut g Gen) gen_stmt(st Stmt) ! {
|
|
g.lines << obj.LineInfo{ off: u32(g.code.len), line: st.line }
|
|
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.types[st.target] = g.expr_type(st.expr)
|
|
g.code << op_store
|
|
g.code << obj.encode_i64(i64(idx))
|
|
}
|
|
.destruct_stmt {
|
|
// let { a, b } = e → tmp := e; a := tmp.a; b := tmp.b
|
|
// let [a, b] = e → tmp := e; a := tmp[0]; b := tmp[1]
|
|
tmp_idx := g.new_local()
|
|
g.gen_expr(st.expr)!
|
|
g.emit_store(tmp_idx)
|
|
for i, name in st.destruct_targets {
|
|
g.emit_load(tmp_idx)
|
|
if st.destruct_field {
|
|
g.emit_field_name(name)
|
|
g.code << op_sget
|
|
} else {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(i))
|
|
g.code << op_aget
|
|
}
|
|
idx := g.new_local()
|
|
g.locals[name] = idx
|
|
g.types.delete(name)
|
|
g.emit_store(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.types[st.target] = g.expr_type(st.expr)
|
|
g.code << op_store
|
|
g.code << obj.encode_i64(i64(idx))
|
|
}
|
|
.index_assign {
|
|
// if the index is a string literal, use struct field set (map style)
|
|
if st.idx.kind == .str_lit {
|
|
g.gen_expr(st.base)!
|
|
g.gen_expr(st.expr)!
|
|
g.emit_field_name(st.idx.str_v)
|
|
g.code << op_sset
|
|
} else {
|
|
g.gen_expr(st.base)!
|
|
g.gen_expr(st.idx)!
|
|
g.gen_expr(st.expr)!
|
|
g.code << op_aset
|
|
}
|
|
}
|
|
.field_assign {
|
|
// a.b = v → a, v, "b" sset (field name on top of the stack)
|
|
g.gen_expr(st.base)!
|
|
g.gen_expr(st.expr)!
|
|
g.emit_field_name(st.target)
|
|
g.code << op_sset
|
|
}
|
|
.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)
|
|
}
|
|
.match_stmt {
|
|
// match x { v1 {..} v2 {..} else {..} } → subject := x; a chain of
|
|
// equality tests jumping to the matching arm; else falls through.
|
|
subj_idx := g.new_local()
|
|
end_l := g.new_label()
|
|
g.gen_expr(st.expr)!
|
|
g.emit_store(subj_idx)
|
|
for i, arm in st.arms {
|
|
next_l := g.new_label()
|
|
g.emit_load(subj_idx)
|
|
g.gen_expr(arm.val)!
|
|
g.code << op_eq
|
|
g.code << op_jz
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 }
|
|
for s in arm.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(next_l)
|
|
if i == st.arms.len - 1 && !st.has_else {
|
|
// no else: fall through to the end label
|
|
g.emit_label(end_l)
|
|
}
|
|
}
|
|
if st.has_else {
|
|
for s in st.els_body {
|
|
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 }
|
|
g.loops << LoopCtx{ break_l: end_l, continue_l: loop_l }
|
|
for s in st.body {
|
|
g.gen_stmt(s)!
|
|
}
|
|
g.loops.delete_last()
|
|
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)
|
|
}
|
|
.for_range_stmt {
|
|
// for i in a..b / for i in a...b → i := a; while i <(<=) b { body; i++ }
|
|
var_idx := g.new_local()
|
|
bound_idx := g.new_local()
|
|
loop_l := g.new_label()
|
|
inc_l := g.new_label()
|
|
end_l := g.new_label()
|
|
g.gen_expr(st.expr)!
|
|
g.gen_expr(st.cond)!
|
|
g.emit_store(bound_idx)
|
|
g.emit_store(var_idx)
|
|
g.emit_label(loop_l)
|
|
g.emit_load(var_idx)
|
|
g.emit_load(bound_idx)
|
|
g.code << if st.inclusive { op_le } else { op_lt }
|
|
g.code << op_jz
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
|
g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l }
|
|
prev := g.locals[st.target] or { -1 }
|
|
prev_t := g.types[st.target] or { '' }
|
|
g.locals[st.target] = var_idx
|
|
g.types.delete(st.target)
|
|
for s in st.body {
|
|
g.gen_stmt(s)!
|
|
}
|
|
if prev >= 0 {
|
|
g.locals[st.target] = prev
|
|
} else {
|
|
g.locals.delete(st.target)
|
|
}
|
|
if prev_t.len > 0 {
|
|
g.types[st.target] = prev_t
|
|
}
|
|
g.loops.delete_last()
|
|
g.emit_label(inc_l)
|
|
g.emit_load(var_idx)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(1)
|
|
g.code << op_add
|
|
g.emit_store(var_idx)
|
|
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)
|
|
}
|
|
.for_in_stmt {
|
|
// for x in EnumType { ... } → iterate over enum variants as integers
|
|
if st.expr.kind == .ident && st.expr.name in g.enums {
|
|
g.gen_for_enum(st.target, st.expr.name, st.body, st.line)!
|
|
return
|
|
}
|
|
// for x in arr → idx := 0; while idx < len(arr) { x := arr[idx]; body; idx++ }
|
|
arr_idx := g.new_local()
|
|
idx_idx := g.new_local()
|
|
elem_idx := g.new_local()
|
|
loop_l := g.new_label()
|
|
inc_l := g.new_label()
|
|
end_l := g.new_label()
|
|
g.gen_expr(st.expr)!
|
|
g.emit_store(arr_idx)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(0)
|
|
g.emit_store(idx_idx)
|
|
g.emit_label(loop_l)
|
|
g.emit_load(idx_idx)
|
|
g.emit_load(arr_idx)
|
|
g.code << op_alen
|
|
g.code << op_lt
|
|
g.code << op_jz
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
|
g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l }
|
|
g.emit_load(arr_idx)
|
|
g.emit_load(idx_idx)
|
|
g.code << op_aget
|
|
g.emit_store(elem_idx)
|
|
prev := g.locals[st.target] or { -1 }
|
|
prev_t := g.types[st.target] or { '' }
|
|
g.locals[st.target] = elem_idx
|
|
g.types.delete(st.target)
|
|
// bind the index variable if present (for i, v in arr)
|
|
prev_idx := if st.idx_target.len > 0 { g.locals[st.idx_target] or { -1 } } else { -1 }
|
|
prev_idx_t := if st.idx_target.len > 0 { g.types[st.idx_target] or { '' } } else { '' }
|
|
if st.idx_target.len > 0 {
|
|
g.locals[st.idx_target] = idx_idx
|
|
g.types.delete(st.idx_target)
|
|
}
|
|
for s in st.body {
|
|
g.gen_stmt(s)!
|
|
}
|
|
if st.idx_target.len > 0 {
|
|
if prev_idx >= 0 {
|
|
g.locals[st.idx_target] = prev_idx
|
|
} else {
|
|
g.locals.delete(st.idx_target)
|
|
}
|
|
if prev_idx_t.len > 0 {
|
|
g.types[st.idx_target] = prev_idx_t
|
|
}
|
|
}
|
|
if prev >= 0 {
|
|
g.locals[st.target] = prev
|
|
} else {
|
|
g.locals.delete(st.target)
|
|
}
|
|
if prev_t.len > 0 {
|
|
g.types[st.target] = prev_t
|
|
}
|
|
g.loops.delete_last()
|
|
g.emit_label(inc_l)
|
|
g.emit_load(idx_idx)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(1)
|
|
g.code << op_add
|
|
g.emit_store(idx_idx)
|
|
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
|
|
}
|
|
.break_stmt {
|
|
if g.loops.len == 0 {
|
|
return error('break outside of a loop (line ${st.line})')
|
|
}
|
|
ctx := g.loops[g.loops.len - 1]
|
|
g.code << op_jmp
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: ctx.break_l, off: u32(g.code.len) - 8 }
|
|
}
|
|
.continue_stmt {
|
|
if g.loops.len == 0 {
|
|
return error('continue outside of a loop (line ${st.line})')
|
|
}
|
|
ctx := g.loops[g.loops.len - 1]
|
|
g.code << op_jmp
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: ctx.continue_l, off: u32(g.code.len) - 8 }
|
|
}
|
|
.throw_stmt {
|
|
g.gen_expr(st.expr)!
|
|
g.code << op_throw
|
|
}
|
|
.try_stmt {
|
|
catch_l := g.new_label()
|
|
end_l := g.new_label()
|
|
err_idx := g.new_local()
|
|
g.code << op_try
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: catch_l, off: u32(g.code.len) - 8 }
|
|
for s in st.body {
|
|
g.gen_stmt(s)!
|
|
}
|
|
g.code << op_catch_done
|
|
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(catch_l)
|
|
g.code << op_store
|
|
g.code << obj.encode_i64(i64(err_idx))
|
|
prev := g.locals[st.target] or { -1 }
|
|
prev_t := g.types[st.target] or { '' }
|
|
g.locals[st.target] = err_idx
|
|
g.types.delete(st.target)
|
|
for s in st.els {
|
|
g.gen_stmt(s)!
|
|
}
|
|
if prev >= 0 {
|
|
g.locals[st.target] = prev
|
|
} else {
|
|
g.locals.delete(st.target)
|
|
}
|
|
if prev_t.len > 0 {
|
|
g.types[st.target] = prev_t
|
|
}
|
|
g.emit_label(end_l)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
.float_lit {
|
|
g.code << op_push_f
|
|
g.code << obj.encode_f64(e.float_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 }
|
|
}
|
|
.array_lit {
|
|
for el in e.elems {
|
|
g.gen_expr(el)!
|
|
}
|
|
g.code << op_mkarray
|
|
g.code << obj.encode_i64(i64(e.elems.len))
|
|
}
|
|
.struct_lit {
|
|
// typed literals validate their fields against the declaration
|
|
// (an undeclared type name is allowed — it may live in another
|
|
// file, where the same validation applies)
|
|
if e.name.len > 0 && e.name in g.structs {
|
|
decl_fields := g.structs[e.name]
|
|
mut seen := map[string]bool{}
|
|
for f in e.fields {
|
|
if f.name !in decl_fields {
|
|
return error('unknown field "${f.name}" for struct ${e.name} (line ${e.line})')
|
|
}
|
|
if f.name in seen {
|
|
return error('duplicate field "${f.name}" in struct literal (line ${e.line})')
|
|
}
|
|
seen[f.name] = true
|
|
}
|
|
}
|
|
// for each field: push the name string then the value; mkstruct n
|
|
// pops the (name, value) pairs and builds the record
|
|
for f in e.fields {
|
|
g.emit_field_name(f.name)
|
|
g.gen_expr(f.val)!
|
|
}
|
|
g.code << op_mkstruct
|
|
g.code << obj.encode_i64(i64(e.fields.len))
|
|
}
|
|
.field {
|
|
// check if it's an enum variant (e.g., Color.red)
|
|
if e.left.kind == .ident {
|
|
key := '${e.left.name}.${e.name}'
|
|
if key in g.enum_vals {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(g.enum_vals[key]))
|
|
return
|
|
}
|
|
}
|
|
g.gen_expr(*e.left)!
|
|
g.emit_field_name(e.name)
|
|
g.code << op_sget
|
|
}
|
|
.method_call {
|
|
// module call: os.exists(x) — the receiver is an imported module name
|
|
if e.left.kind == .ident && e.left.name in g.modules {
|
|
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.left.name}.${e.name}', kind: 0 }
|
|
g.code << obj.encode_i64(i64(e.args.len))
|
|
return
|
|
}
|
|
// p.dist(x) → call <Type>.dist p, x
|
|
recv_t := g.method_receiver_type(e)
|
|
// string methods: s.len(), s.to_upper(), s.contains(x), ... —
|
|
// the receiver type is known when it is a literal or a local that
|
|
// was assigned a string literal
|
|
if recv_t == 'string' || e.left.kind == .str_lit {
|
|
g.gen_expr(*e.left)!
|
|
for a in e.args {
|
|
g.gen_expr(a)!
|
|
}
|
|
g.code << op_str_method
|
|
g.code << obj.encode_i64(0) // name placeholder — rebased by the linker
|
|
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.name, kind: 1 }
|
|
g.code << obj.encode_i64(i64(e.args.len))
|
|
return
|
|
}
|
|
// built-in: enum.to_string() generates a match on the integer value
|
|
if e.name == 'to_string' && recv_t in g.enums && e.args.len == 0 {
|
|
g.gen_enum_to_string(recv_t, *e.left, e.line)!
|
|
return
|
|
}
|
|
// built-in: enum.count() returns the number of variants
|
|
if e.name == 'count' && recv_t in g.enums && e.args.len == 0 {
|
|
g.gen_expr(*e.left)!
|
|
g.code << op_pop
|
|
variants := g.enums[recv_t]
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(variants.len))
|
|
return
|
|
}
|
|
g.gen_expr(*e.left)!
|
|
for a in e.args {
|
|
g.gen_expr(a)!
|
|
}
|
|
// if receiver type is known, emit a static method call
|
|
if recv_t.len > 0 {
|
|
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: '${recv_t}.${e.name}', kind: 0 }
|
|
g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args
|
|
} else {
|
|
// unknown type: treat as closure call on a struct field
|
|
g.emit_field_name(e.name)
|
|
g.code << op_sget
|
|
for a in e.args {
|
|
g.gen_expr(a)!
|
|
}
|
|
g.code << op_call_closure
|
|
g.code << obj.encode_i64(i64(e.args.len))
|
|
}
|
|
return
|
|
}
|
|
.index {
|
|
// if the index is a string literal, use struct field access (map style)
|
|
if e.right.kind == .str_lit {
|
|
g.gen_expr(*e.left)!
|
|
g.emit_field_name(e.right.str_v)
|
|
g.code << op_sget
|
|
} else {
|
|
g.gen_expr(*e.left)!
|
|
g.gen_expr(*e.right)!
|
|
g.code << op_aget
|
|
}
|
|
}
|
|
.slice {
|
|
// arr[start..end] → push value, start, end; slice
|
|
g.gen_expr(*e.left)!
|
|
g.gen_expr(*e.right)!
|
|
g.gen_expr(*e.extra)!
|
|
g.code << op_slice
|
|
}
|
|
.anon_fn {
|
|
g.lam_counter++
|
|
name := '__lam_${g.lam_counter}'
|
|
// find the enclosing locals the body references (its free
|
|
// variables); they become this closure's captures
|
|
caps := g.scan_captures(e.fn_body, e.fparams)
|
|
// jump over the lambda body so callers don't fall through
|
|
g.code << op_jmp
|
|
g.code << obj.encode_i64(0)
|
|
skip_fix_off := u32(g.code.len) - 8
|
|
fd := FnDecl{
|
|
name: name
|
|
params: e.fparams
|
|
defaults: e.fdefaults
|
|
has_defs: e.fhas_defs
|
|
variadic: e.fvariadic
|
|
body: e.fn_body
|
|
line: e.line
|
|
}
|
|
// Save enclosing fixup/label/locals/type state; gen_fn clears them.
|
|
// enter_off and argc are also per-function, so they must be restored
|
|
// or the enclosing function's `enter n` patch is lost (locals would
|
|
// then collide with the stack top).
|
|
saved_fixups := g.fixups.clone()
|
|
saved_labels := g.labels.clone()
|
|
saved_locals := g.locals.clone()
|
|
saved_types := g.types.clone()
|
|
saved_local_cnt := g.local_cnt
|
|
saved_enter_off := g.enter_off
|
|
saved_argc := g.argc
|
|
saved_captures := g.captures
|
|
g.labels.clear()
|
|
g.fixups = []Fixup{}
|
|
g.captures = caps
|
|
g.gen_fn(fd)!
|
|
// Restore the enclosing state.
|
|
g.fixups = saved_fixups
|
|
g.labels = saved_labels.clone()
|
|
g.locals = saved_locals.clone()
|
|
g.types = saved_types.clone()
|
|
g.local_cnt = saved_local_cnt
|
|
g.enter_off = saved_enter_off
|
|
g.argc = saved_argc
|
|
g.captures = saved_captures
|
|
// Patch the skip jump to land at the closure opcode we emit next
|
|
// (PC-relative, like all other jump targets).
|
|
obj.patch_i64(mut g.code, skip_fix_off, i64(g.code.len - (int(skip_fix_off) + 8)))
|
|
// capture the enclosing locals' current values (capture by value)
|
|
for cname in caps {
|
|
g.emit_load(g.locals[cname])
|
|
}
|
|
g.code << op_closure
|
|
g.code << obj.encode_i64(0)
|
|
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 }
|
|
g.code << obj.encode_i64(i64(caps.len))
|
|
}
|
|
.bool_lit {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(e.int_v)
|
|
}
|
|
.none_lit {
|
|
g.code << op_push_none
|
|
}
|
|
.ident {
|
|
// check if it's a constant
|
|
if e.name in g.consts {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(g.consts[e.name])
|
|
} else if e.name in g.enum_vals {
|
|
// check if it's an enum variant (e.g., Color.red)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(g.enum_vals[e.name]))
|
|
} else {
|
|
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 {
|
|
// constant-fold unary ops on literals: -5, -2.5, not true, ~7
|
|
if e.right.kind == .int_lit && (e.op == .minus || e.op == .tilde) {
|
|
v := e.right.int_v
|
|
res := if e.op == .minus { -v } else { ~v }
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(res)
|
|
return
|
|
}
|
|
if e.right.kind == .float_lit && e.op == .minus {
|
|
g.code << op_push_f
|
|
g.code << obj.encode_f64(-e.right.float_v)
|
|
return
|
|
}
|
|
if e.right.kind == .bool_lit && e.op == .kw_not {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(if e.right.int_v == 0 { 1 } else { 0 })
|
|
return
|
|
}
|
|
g.gen_expr(*e.right)!
|
|
match e.op {
|
|
.kw_not { g.code << op_not }
|
|
.tilde { g.code << op_not_b }
|
|
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
|
|
}
|
|
if e.name == 'len' {
|
|
if e.args.len != 1 {
|
|
return error('len() takes exactly one argument (line ${e.line})')
|
|
}
|
|
g.gen_expr(e.args[0])!
|
|
g.code << op_alen
|
|
return
|
|
}
|
|
if e.name == 'push' {
|
|
if e.args.len != 2 {
|
|
return error('push() takes exactly two arguments (line ${e.line})')
|
|
}
|
|
g.gen_expr(e.args[0])!
|
|
g.gen_expr(e.args[1])!
|
|
g.code << op_apush
|
|
return
|
|
}
|
|
if e.name == 'has' {
|
|
if e.args.len != 2 {
|
|
return error('has() takes exactly two arguments (line ${e.line})')
|
|
}
|
|
g.gen_expr(e.args[0])!
|
|
g.gen_expr(e.args[1])!
|
|
g.code << op_shas
|
|
return
|
|
}
|
|
if e.name == 'delete' {
|
|
if e.args.len != 2 {
|
|
return error('delete() takes exactly two arguments (line ${e.line})')
|
|
}
|
|
g.gen_expr(e.args[0])!
|
|
g.gen_expr(e.args[1])!
|
|
g.code << op_sdel
|
|
return
|
|
}
|
|
if e.name == 'keys' {
|
|
if e.args.len != 1 {
|
|
return error('keys() takes exactly one argument (line ${e.line})')
|
|
}
|
|
g.gen_expr(e.args[0])!
|
|
g.code << op_skeys
|
|
return
|
|
}
|
|
// closure call: ident(args) where ident is a local holding a closure.
|
|
// The local's value is pushed as the call sequence's first slot;
|
|
// op_call_closure consumes it along with the args, leaving only the
|
|
// result on the stack.
|
|
if e.name in g.locals {
|
|
g.gen_expr(Expr{ kind: .ident, name: e.name, line: e.line })!
|
|
for a in e.args {
|
|
g.gen_expr(a)!
|
|
}
|
|
g.code << op_call_closure
|
|
g.code << obj.encode_i64(i64(e.args.len))
|
|
return
|
|
}
|
|
// host builtins (file I/O, OS, math, collections) go through op_native
|
|
bid, bargc := builtin_spec(e.name)
|
|
if bid >= 0 {
|
|
if e.args.len != bargc {
|
|
return error('${e.name}() takes exactly ${bargc} argument(s) (line ${e.line})')
|
|
}
|
|
for a in e.args {
|
|
g.gen_expr(a)!
|
|
}
|
|
g.code << op_native
|
|
g.code << obj.encode_i64(i64(bid))
|
|
g.code << obj.encode_i64(i64(bargc))
|
|
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
|
|
}
|
|
|
|
// builtin_spec maps a builtin function name to its (native id, arg count).
|
|
// A negative id means the name is not a builtin (it is a user function).
|
|
fn builtin_spec(name string) (int, int) {
|
|
return match name {
|
|
'abs' { native_abs, 1 }
|
|
'min' { native_min, 2 }
|
|
'max' { native_max, 2 }
|
|
'pow' { native_pow, 2 }
|
|
'sqrt' { native_sqrt, 1 }
|
|
'floor' { native_floor, 1 }
|
|
'ceil' { native_ceil, 1 }
|
|
'round' { native_round, 1 }
|
|
'rand' { native_rand, 0 }
|
|
'rand_int' { native_rand_int, 1 }
|
|
'int' { native_int, 1 }
|
|
'str' { native_str, 1 }
|
|
'float' { native_float, 1 }
|
|
'type' { native_type, 1 }
|
|
'split' { native_split, 2 }
|
|
'join' { native_join, 2 }
|
|
'contains' { native_contains, 2 }
|
|
'starts_with' { native_starts_with, 2 }
|
|
'ends_with' { native_ends_with, 2 }
|
|
'trim' { native_trim, 1 }
|
|
'lower' { native_lower, 1 }
|
|
'upper' { native_upper, 1 }
|
|
'pop' { native_pop, 1 }
|
|
'insert' { native_insert, 3 }
|
|
'remove' { native_remove, 2 }
|
|
'sort' { native_sort, 1 }
|
|
'clone' { native_clone, 1 }
|
|
'reverse' { native_reverse, 1 }
|
|
'index_of' { native_index_of, 2 }
|
|
'args' { native_args, 0 }
|
|
'getenv' { native_getenv, 1 }
|
|
'setenv' { native_setenv, 2 }
|
|
'exit' { native_exit, 1 }
|
|
'time' { native_time, 0 }
|
|
'sleep' { native_sleep, 1 }
|
|
'read_file' { native_read_file, 1 }
|
|
'write_file' { native_write_file, 2 }
|
|
'eprint' { native_eprint, 1 }
|
|
// build-module builtins (.vrmm) — see vm/native.v
|
|
'build_compile' { native_build_compile, 2 }
|
|
'build_assemble' { native_build_assemble, 2 }
|
|
'build_link' { native_build_link, 2 }
|
|
'build_run' { native_build_run, 1 }
|
|
'build_test' { native_build_test, 1 }
|
|
'build_bench' { native_build_bench, 2 }
|
|
'build_clean' { native_build_clean, 0 }
|
|
'build_exec' { native_build_exec, 1 }
|
|
'build_exec_status' { native_build_exec_status, 1 }
|
|
'build_exists' { native_build_exists, 1 }
|
|
'build_mkdir' { native_build_mkdir, 1 }
|
|
'build_rm' { native_build_rm, 1 }
|
|
'build_copy' { native_build_copy, 2 }
|
|
'build_glob' { native_build_glob, 1 }
|
|
'build_ls' { native_build_ls, 1 }
|
|
'build_base' { native_build_base, 1 }
|
|
'build_dir' { native_build_dir, 1 }
|
|
'build_join' { native_build_join, 2 }
|
|
'build_root' { native_build_root, 0 }
|
|
// stdlib: JSON + string formatting
|
|
'json_encode' { native_json_encode, 1 }
|
|
'json_decode' { native_json_decode, 1 }
|
|
'format' { native_format, 2 }
|
|
'replace' { native_replace, 3 }
|
|
'split_lines' { native_split_lines, 1 }
|
|
'pad' { native_pad, 2 }
|
|
'pad_left' { native_pad_left, 2 }
|
|
'repeat' { native_repeat, 2 }
|
|
'build_is_dir' { native_build_is_dir, 1 }
|
|
'cwd' { native_cwd, 0 }
|
|
'json_pretty' { native_json_pretty, 1 }
|
|
// HTTP client
|
|
'http_get' { native_http_get, 1 }
|
|
'http_post' { native_http_post, 2 }
|
|
// date/time
|
|
'now' { native_now, 0 }
|
|
'time_ms' { native_time_ms, 0 }
|
|
'format_time' { native_format_time, 2 }
|
|
'parse_time' { native_parse_time, 1 }
|
|
'weekday' { native_weekday, 1 }
|
|
// regex
|
|
'regex_match' { native_regex_match, 2 }
|
|
'regex_find_all' { native_regex_find_all, 2 }
|
|
'regex_replace' { native_regex_replace, 3 }
|
|
'regex_split' { native_regex_split, 2 }
|
|
// crypto/encoding
|
|
'base64_encode' { native_base64_encode, 1 }
|
|
'base64_decode' { native_base64_decode, 1 }
|
|
'sha256' { native_sha256, 1 }
|
|
'md5' { native_md5, 1 }
|
|
'csv_parse' { native_csv_parse, 1 }
|
|
// extended HTTP + path/process helpers
|
|
'http_req' { native_http_req, 5 }
|
|
'path_ext' { native_path_ext, 1 }
|
|
'path_abs' { native_path_abs, 1 }
|
|
'path_rel' { native_path_rel, 2 }
|
|
'exec_full' { native_exec_full, 1 }
|
|
else { -1, 0 }
|
|
}
|
|
}
|
|
|
|
// fold_binary constant-folds binary expressions whose operands are both
|
|
// literals, emitting the precomputed constant. Returns false when the
|
|
// expression cannot be folded (leaving it to the runtime). Division/modulo by
|
|
// zero and out-of-range shifts are deliberately not folded so the runtime
|
|
// still reports them.
|
|
fn (mut g Gen) fold_binary(e Expr) bool {
|
|
// integer folding
|
|
if e.left.kind == .int_lit && e.right.kind == .int_lit {
|
|
l := e.left.int_v
|
|
r := e.right.int_v
|
|
mut res := i64(0)
|
|
match e.op {
|
|
.plus { res = l + r }
|
|
.minus { res = l - r }
|
|
.star { res = l * r }
|
|
.slash {
|
|
if r == 0 {
|
|
return false
|
|
}
|
|
res = l / r
|
|
}
|
|
.percent {
|
|
if r == 0 {
|
|
return false
|
|
}
|
|
res = l % r
|
|
}
|
|
.amp { res = l & r }
|
|
.pipe { res = l | r }
|
|
.caret { res = l ^ r }
|
|
.lt_lt {
|
|
if r < 0 || r > 63 {
|
|
return false
|
|
}
|
|
res = l << u32(r)
|
|
}
|
|
.gt_gt {
|
|
if r < 0 || r > 63 {
|
|
return false
|
|
}
|
|
res = l >> u32(r)
|
|
}
|
|
.eq_eq { res = if l == r { 1 } else { 0 } }
|
|
.not_eq { res = if l != r { 1 } else { 0 } }
|
|
.lt { res = if l < r { 1 } else { 0 } }
|
|
.le { res = if l <= r { 1 } else { 0 } }
|
|
.gt { res = if l > r { 1 } else { 0 } }
|
|
.ge { res = if l >= r { 1 } else { 0 } }
|
|
else { return false }
|
|
}
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(res)
|
|
return true
|
|
}
|
|
// float folding
|
|
if e.left.kind == .float_lit && e.right.kind == .float_lit {
|
|
l := e.left.float_v
|
|
r := e.right.float_v
|
|
mut res := 0.0
|
|
mut is_bool := false
|
|
mut bres := false
|
|
match e.op {
|
|
.plus { res = l + r }
|
|
.minus { res = l - r }
|
|
.star { res = l * r }
|
|
.slash {
|
|
if r == 0.0 {
|
|
return false
|
|
}
|
|
res = l / r
|
|
}
|
|
.eq_eq { is_bool = true; bres = l == r }
|
|
.not_eq { is_bool = true; bres = l != r }
|
|
.lt { is_bool = true; bres = l < r }
|
|
.le { is_bool = true; bres = l <= r }
|
|
.gt { is_bool = true; bres = l > r }
|
|
.ge { is_bool = true; bres = l >= r }
|
|
else { return false }
|
|
}
|
|
if is_bool {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(if bres { 1 } else { 0 })
|
|
} else {
|
|
g.code << op_push_f
|
|
g.code << obj.encode_f64(res)
|
|
}
|
|
return true
|
|
}
|
|
// string concatenation folding: "a" + "b" → one interned constant.
|
|
// The string is emitted as a relocation so the linker interns it in the
|
|
// final table, exactly like a plain string literal.
|
|
if e.left.kind == .str_lit && e.right.kind == .str_lit && e.op == .plus {
|
|
g.code << op_push_s
|
|
g.code << obj.encode_i64(0) // placeholder — rebased by the linker
|
|
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.left.str_v + e.right.str_v, kind: 1 }
|
|
return true
|
|
}
|
|
// boolean short-circuit folding: only when both sides are bool literals
|
|
if e.left.kind == .bool_lit && e.right.kind == .bool_lit {
|
|
if e.op == .kw_and {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(if e.left.int_v != 0 && e.right.int_v != 0 { 1 } else { 0 })
|
|
return true
|
|
}
|
|
if e.op == .kw_or {
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(if e.left.int_v != 0 || e.right.int_v != 0 { 1 } else { 0 })
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
fn (mut g Gen) gen_binary(e Expr) ! {
|
|
if g.fold_binary(e) {
|
|
return
|
|
}
|
|
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 }
|
|
.amp { op_and_b }
|
|
.pipe { op_or_b }
|
|
.caret { op_xor }
|
|
.lt_lt { op_shl }
|
|
.gt_gt { op_shr }
|
|
else {
|
|
return error('unsupported binary operator at line ${e.line}')
|
|
}
|
|
}
|
|
g.code << op
|
|
}
|
|
}
|
|
}
|
|
|
|
// expr_type returns the declared struct type of an expression when it is
|
|
// statically knowable: a typed literal `Point{...}`, a copy of a typed
|
|
// variable, or an enum variant `Enum.variant`. Everything else has no
|
|
// known type ('').
|
|
fn (mut g Gen) expr_type(e Expr) string {
|
|
if e.kind == .str_lit {
|
|
return 'string'
|
|
}
|
|
if e.kind == .struct_lit {
|
|
return e.name
|
|
}
|
|
if e.kind == .ident {
|
|
return g.types[e.name] or { '' }
|
|
}
|
|
// enum variant: Color.red → type is "Color"
|
|
if e.kind == .field && e.left.kind == .ident {
|
|
key := '${e.left.name}.${e.name}'
|
|
if key in g.enum_vals {
|
|
return e.left.name
|
|
}
|
|
}
|
|
// slicing or indexing a known string yields a string
|
|
if (e.kind == .slice || e.kind == .index) && g.expr_type(*e.left) == 'string' {
|
|
return 'string'
|
|
}
|
|
// string concatenation: "a" + "b" (or anything + a string literal)
|
|
if e.kind == .binary && e.op == .plus && (e.left.kind == .str_lit || e.right.kind == .str_lit) {
|
|
return 'string'
|
|
}
|
|
// string-producing builtins typed as strings so method chains keep working
|
|
if e.kind == .call {
|
|
return match e.name {
|
|
'upper', 'lower', 'trim', 'str', 'getenv', 'read_file', 'join' { 'string' }
|
|
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
|
|
'build_dir', 'build_join', 'build_root' { 'string' }
|
|
'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { 'string' }
|
|
'cwd', 'json_pretty' { 'string' }
|
|
else { '' }
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
// method_receiver_type resolves the struct type a method call is made on.
|
|
// Returns '' when the type is statically unknown (at which point the
|
|
// call becomes a dynamic closure invocation via field access).
|
|
fn (mut g Gen) method_receiver_type(e Expr) string {
|
|
recv := e.left
|
|
if recv.kind == .ident {
|
|
t := g.types[recv.name] or { '' }
|
|
if t.len > 0 {
|
|
return t
|
|
}
|
|
}
|
|
// enum variant: Color.red -> type is "Color"
|
|
if recv.kind == .field && recv.left.kind == .ident {
|
|
key := '${recv.left.name}.${recv.name}'
|
|
if key in g.enum_vals {
|
|
return recv.left.name
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
// gen_enum_to_string generates bytecode for `e.to_string()` on an enum value.
|
|
// It emits a match statement that maps each integer variant to its string name.
|
|
fn (mut g Gen) gen_enum_to_string(enum_name string, recv Expr, line int) ! {
|
|
variants := g.enums[enum_name] or {
|
|
return error('unknown enum "${enum_name}" at line ${line}')
|
|
}
|
|
// store the receiver in a temp local
|
|
subj_idx := g.new_local()
|
|
g.gen_expr(recv)!
|
|
g.emit_store(subj_idx)
|
|
// end label for the match
|
|
end_l := g.new_label()
|
|
for i, v in variants {
|
|
next_l := g.new_label()
|
|
// load subject, push variant integer, compare
|
|
g.emit_load(subj_idx)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(i))
|
|
g.code << op_eq
|
|
g.code << op_jz
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 }
|
|
// push the variant name as a string
|
|
g.code << op_push_s
|
|
g.code << obj.encode_i64(0)
|
|
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: v, kind: 1 }
|
|
// jump to end
|
|
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(next_l)
|
|
}
|
|
// else: push "unknown"
|
|
g.code << op_push_s
|
|
g.code << obj.encode_i64(0)
|
|
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: 'unknown', kind: 1 }
|
|
g.emit_label(end_l)
|
|
}
|
|
|
|
// gen_for_enum generates a for loop that iterates over all variants of an enum.
|
|
// for x in Color { ... } → for i in 0..count { x = i; ... } (x typed as Color)
|
|
fn (mut g Gen) gen_for_enum(var_name string, enum_name string, body []Stmt, line int) ! {
|
|
variants := g.enums[enum_name] or {
|
|
return error('unknown enum "${enum_name}" at line ${line}')
|
|
}
|
|
count := variants.len
|
|
// i := 0
|
|
var_idx := g.new_local()
|
|
bound_idx := g.new_local()
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(0)
|
|
g.emit_store(var_idx)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(i64(count))
|
|
g.emit_store(bound_idx)
|
|
loop_l := g.new_label()
|
|
inc_l := g.new_label()
|
|
end_l := g.new_label()
|
|
g.emit_label(loop_l)
|
|
g.emit_load(var_idx)
|
|
g.emit_load(bound_idx)
|
|
g.code << op_lt
|
|
g.code << op_jz
|
|
g.code << obj.encode_i64(0)
|
|
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
|
g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l }
|
|
prev := g.locals[var_name] or { -1 }
|
|
prev_t := g.types[var_name] or { '' }
|
|
g.locals[var_name] = var_idx
|
|
g.types[var_name] = enum_name // type the loop variable as the enum
|
|
for s in body {
|
|
g.gen_stmt(s)!
|
|
}
|
|
if prev >= 0 {
|
|
g.locals[var_name] = prev
|
|
} else {
|
|
g.locals.delete(var_name)
|
|
}
|
|
if prev_t.len > 0 {
|
|
g.types[var_name] = prev_t
|
|
}
|
|
g.loops.delete_last()
|
|
g.emit_label(inc_l)
|
|
g.emit_load(var_idx)
|
|
g.code << op_push_i
|
|
g.code << obj.encode_i64(1)
|
|
g.code << op_add
|
|
g.emit_store(var_idx)
|
|
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)
|
|
}
|
|
|
|
// emit_field_name pushes a field name as a string constant. Like string
|
|
// literals it goes through a kind-1 relocation so multi-file links rebase it.
|
|
fn (mut g Gen) emit_field_name(name string) {
|
|
g.code << op_push_s
|
|
g.code << obj.encode_i64(0)
|
|
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 1 }
|
|
}
|
|
|
|
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_local() int {
|
|
idx := g.local_cnt
|
|
g.local_cnt++
|
|
return idx
|
|
}
|
|
|
|
fn (mut g Gen) emit_load(idx int) {
|
|
g.code << op_load
|
|
g.code << obj.encode_i64(i64(idx))
|
|
}
|
|
|
|
fn (mut g Gen) emit_store(idx int) {
|
|
g.code << op_store
|
|
g.code << obj.encode_i64(i64(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
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// closure capture analysis
|
|
//
|
|
// A closure (anonymous `fn`) may reference the enclosing function's locals.
|
|
// Since the VM compiles each function with its own frame, those references
|
|
// are resolved by capturing the values at closure-creation time: the compiler
|
|
// scans the body for free variables, registers them as the lambda's leading
|
|
// local slots, and emits loads of their current values before op_closure.
|
|
|
|
// scan_captures returns the enclosing locals a closure body references, in
|
|
// first-reference order (stable and deterministic for codegen).
|
|
fn (mut g Gen) scan_captures(body []Stmt, fparams []string) []string {
|
|
mut bound := map[string]bool{}
|
|
for p in fparams {
|
|
bound[p] = true
|
|
}
|
|
mut caps := []string{}
|
|
mut seen := map[string]bool{}
|
|
for st in body {
|
|
g.scan_stmt(st, mut bound, mut caps, mut seen)
|
|
}
|
|
return caps
|
|
}
|
|
|
|
fn (mut g Gen) maybe_capture(name string, bound map[string]bool, mut caps []string, mut seen map[string]bool) {
|
|
if name in bound {
|
|
return // bound inside the closure — a plain local
|
|
}
|
|
if name !in g.locals {
|
|
return // not an enclosing local (global fn/const/enum — resolved elsewhere)
|
|
}
|
|
if name !in seen {
|
|
seen[name] = true
|
|
caps << name
|
|
}
|
|
}
|
|
|
|
fn (mut g Gen) scan_stmt(st Stmt, mut bound map[string]bool, mut caps []string, mut seen map[string]bool) {
|
|
match st.kind {
|
|
.expr_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
.let_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
bound[st.target] = true
|
|
}
|
|
.destruct_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
for t in st.destruct_targets {
|
|
bound[t] = true
|
|
}
|
|
}
|
|
.assign_stmt {
|
|
// assignment to a name that is not a closure-local references the
|
|
// enclosing local's captured copy
|
|
g.maybe_capture(st.target, bound, mut caps, mut seen)
|
|
bound[st.target] = true
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
.index_assign {
|
|
g.scan_expr(st.base, mut bound, mut caps, mut seen)
|
|
g.scan_expr(st.idx, mut bound, mut caps, mut seen)
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
.field_assign {
|
|
g.scan_expr(st.base, mut bound, mut caps, mut seen)
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
.if_stmt {
|
|
g.scan_expr(st.cond, mut bound, mut caps, mut seen)
|
|
for s in st.body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
for s in st.els {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.match_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
for arm in st.arms {
|
|
g.scan_expr(arm.val, mut bound, mut caps, mut seen)
|
|
for s in arm.body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
for s in st.els_body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.while_stmt {
|
|
g.scan_expr(st.cond, mut bound, mut caps, mut seen)
|
|
for s in st.body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.for_range_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
g.scan_expr(st.cond, mut bound, mut caps, mut seen)
|
|
had := st.target in bound
|
|
bound[st.target] = true
|
|
for s in st.body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
if !had {
|
|
bound.delete(st.target)
|
|
}
|
|
}
|
|
.for_in_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
had := st.target in bound
|
|
bound[st.target] = true
|
|
mut had_idx := false
|
|
if st.idx_target.len > 0 {
|
|
had_idx = st.idx_target in bound
|
|
bound[st.idx_target] = true
|
|
}
|
|
for s in st.body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
if !had {
|
|
bound.delete(st.target)
|
|
}
|
|
if st.idx_target.len > 0 && !had_idx {
|
|
bound.delete(st.idx_target)
|
|
}
|
|
}
|
|
.ret_stmt {
|
|
if st.has_val {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.assert_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
.try_stmt {
|
|
for s in st.body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
had := st.target in bound
|
|
bound[st.target] = true
|
|
for s in st.els {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
if !had {
|
|
bound.delete(st.target)
|
|
}
|
|
}
|
|
.throw_stmt {
|
|
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
|
|
}
|
|
.break_stmt, .continue_stmt {}
|
|
}
|
|
}
|
|
|
|
fn (mut g Gen) scan_expr(e Expr, mut bound map[string]bool, mut caps []string, mut seen map[string]bool) {
|
|
match e.kind {
|
|
.ident {
|
|
g.maybe_capture(e.name, bound, mut caps, mut seen)
|
|
}
|
|
.call {
|
|
// a call to an enclosing local holding a closure must capture it too
|
|
g.maybe_capture(e.name, bound, mut caps, mut seen)
|
|
for a in e.args {
|
|
g.scan_expr(a, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.field {
|
|
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
|
|
}
|
|
.method_call {
|
|
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
|
|
for a in e.args {
|
|
g.scan_expr(a, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.index {
|
|
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
|
|
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
|
|
}
|
|
.slice {
|
|
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
|
|
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
|
|
g.scan_expr(*e.extra, mut bound, mut caps, mut seen)
|
|
}
|
|
.unary {
|
|
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
|
|
}
|
|
.binary {
|
|
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
|
|
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
|
|
}
|
|
.array_lit {
|
|
for el in e.elems {
|
|
g.scan_expr(el, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.struct_lit {
|
|
for f in e.fields {
|
|
g.scan_expr(f.val, mut bound, mut caps, mut seen)
|
|
}
|
|
}
|
|
.anon_fn {
|
|
// a nested closure: its parameters bind inside it, but references to
|
|
// enclosing locals still belong to this closure's capture set
|
|
mut saved := map[string]bool{}
|
|
for p in e.fparams {
|
|
saved[p] = p in bound
|
|
bound[p] = true
|
|
}
|
|
for s in e.fn_body {
|
|
g.scan_stmt(s, mut bound, mut caps, mut seen)
|
|
}
|
|
for p in e.fparams {
|
|
if !saved[p] {
|
|
bound.delete(p)
|
|
}
|
|
}
|
|
}
|
|
else {}
|
|
}
|
|
}
|