mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 14:57:18 +00:00
Production pass: floats, GC, type checking, and tooling
Adds floats, bitwise ops, UTF-8 strings with methods, try/catch, closures, generics validation, a compile-time type checker, a mark-and-sweep GC, source-level debug info, constant folding, and the repl/fmt/package-manager commands. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
+51
-13
@@ -3,6 +3,7 @@ module compiler
|
||||
|
||||
pub enum ExprKind {
|
||||
int_lit
|
||||
float_lit
|
||||
str_lit
|
||||
bool_lit
|
||||
ident
|
||||
@@ -11,7 +12,9 @@ pub enum ExprKind {
|
||||
index
|
||||
field
|
||||
method_call
|
||||
slice
|
||||
unary
|
||||
anon_fn
|
||||
binary
|
||||
call
|
||||
}
|
||||
@@ -25,22 +28,31 @@ pub mut:
|
||||
|
||||
pub struct Expr {
|
||||
pub mut:
|
||||
kind ExprKind
|
||||
int_v i64
|
||||
str_v string
|
||||
kind ExprKind
|
||||
int_v i64
|
||||
float_v f64
|
||||
str_v string
|
||||
name string // ident/call name, or the field name of a `.field` access
|
||||
op TokKind
|
||||
left &Expr = unsafe { nil }
|
||||
right &Expr = unsafe { nil }
|
||||
extra &Expr = unsafe { nil } // slice: end index expression
|
||||
elems []Expr
|
||||
fields []StructField // struct_lit: the named fields
|
||||
args []Expr
|
||||
line int
|
||||
args []Expr
|
||||
type_args []string // call: explicit generic type arguments (first[int](...))
|
||||
fparams []string // anon_fn: parameter names
|
||||
fdefaults []Expr // anon_fn: default values (parallel to fparams)
|
||||
fhas_defs []bool // anon_fn: which params have defaults
|
||||
fvariadic bool // anon_fn: last param is variadic
|
||||
fn_body []Stmt // anon_fn: function body
|
||||
line int
|
||||
}
|
||||
|
||||
pub enum StmtKind {
|
||||
expr_stmt
|
||||
let_stmt
|
||||
destruct_stmt
|
||||
assign_stmt
|
||||
index_assign
|
||||
field_assign
|
||||
@@ -53,6 +65,8 @@ pub enum StmtKind {
|
||||
continue_stmt
|
||||
ret_stmt
|
||||
assert_stmt
|
||||
try_stmt
|
||||
throw_stmt
|
||||
}
|
||||
|
||||
// MatchArm is a single `value { body }` arm of a match statement.
|
||||
@@ -66,6 +80,9 @@ pub struct Stmt {
|
||||
pub mut:
|
||||
kind StmtKind
|
||||
target string
|
||||
idx_target string // for_in_stmt: index variable name (empty when unused)
|
||||
destruct_targets []string // destruct_stmt: names to bind
|
||||
destruct_field bool // destruct_stmt: struct ({ a, b }) vs array ([a, b])
|
||||
expr Expr
|
||||
cond Expr
|
||||
base Expr // index_assign: the indexed expression
|
||||
@@ -90,10 +107,14 @@ pub mut:
|
||||
|
||||
pub struct FnDecl {
|
||||
pub mut:
|
||||
name string
|
||||
recv_name string // method receiver local name ('' for plain functions)
|
||||
recv_type string // method receiver struct type ('' for plain functions)
|
||||
name string
|
||||
type_params []string // generic type parameters (fn first[T, U](...) { ... })
|
||||
recv_name string // method receiver local name ('' for plain functions)
|
||||
recv_type string // method receiver struct type ('' for plain functions)
|
||||
params []string
|
||||
defaults []Expr // parallel to params; empty Expr{} when no default
|
||||
has_defs []bool // parallel to params: whether a default exists
|
||||
variadic bool // last param is variadic (nums...)
|
||||
body []Stmt
|
||||
line int
|
||||
}
|
||||
@@ -120,11 +141,28 @@ pub mut:
|
||||
line int
|
||||
}
|
||||
|
||||
// InterfaceDecl is an `interface Name { method1(); method2() type }` declaration.
|
||||
// Methods are stored as (name, return_type) pairs. The interface is satisfied
|
||||
// by any struct that implements all listed methods (structural/duck typing).
|
||||
pub struct InterfaceDecl {
|
||||
pub mut:
|
||||
name string
|
||||
methods []InterfaceMethod
|
||||
line int
|
||||
}
|
||||
|
||||
pub struct InterfaceMethod {
|
||||
pub mut:
|
||||
name string
|
||||
line int
|
||||
}
|
||||
|
||||
pub struct Program {
|
||||
pub mut:
|
||||
fns []FnDecl
|
||||
structs []StructDecl
|
||||
enums []EnumDecl
|
||||
imports []ImportDecl
|
||||
consts []ConstDecl
|
||||
fns []FnDecl
|
||||
structs []StructDecl
|
||||
enums []EnumDecl
|
||||
imports []ImportDecl
|
||||
consts []ConstDecl
|
||||
interfaces []InterfaceDecl
|
||||
}
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
// check.v — a compile-time type checker for VuurRaaf.
|
||||
//
|
||||
// Runs after parsing and before codegen. The language is dynamically typed at
|
||||
// runtime, so this pass is deliberately conservative: it rejects programs that
|
||||
// are *provably* wrong (unknown variables, field access on numbers, arithmetic
|
||||
// on strings, wrong arity on known functions) while leaving genuinely dynamic
|
||||
// programs (untyped parameters, unknown receiver types, mixed containers, and
|
||||
// calls to functions defined in other objects) alone. Unresolved function
|
||||
// names are deferred to the linker, matching the toolchain's separate
|
||||
// compilation model.
|
||||
module compiler
|
||||
|
||||
import os
|
||||
|
||||
enum CType {
|
||||
unknown
|
||||
int_t
|
||||
float_t
|
||||
string_t
|
||||
bool_t
|
||||
array_t
|
||||
struct_t
|
||||
enum_t
|
||||
closure_t
|
||||
}
|
||||
|
||||
struct TypeInfo {
|
||||
kind CType
|
||||
name string // struct/enum type name when statically known
|
||||
}
|
||||
|
||||
struct FnSig {
|
||||
min_args int
|
||||
has_defs []bool
|
||||
variadic bool
|
||||
n_type_params int // generic type parameters declared on the function
|
||||
returns TypeInfo
|
||||
}
|
||||
|
||||
struct Checker {
|
||||
mut:
|
||||
types map[string]TypeInfo // current scope: local name -> type
|
||||
fns map[string]FnSig
|
||||
structs map[string][]string
|
||||
enums map[string][]string
|
||||
consts map[string]TypeInfo
|
||||
loop_depth int
|
||||
checked map[string]bool // imported files already checked
|
||||
}
|
||||
|
||||
// check validates a parsed program and returns an error on the first problem.
|
||||
fn check(prog Program) ! {
|
||||
mut c := Checker{}
|
||||
// register declarations
|
||||
for sd in prog.structs {
|
||||
if sd.name in c.structs {
|
||||
return error('duplicate struct declaration "${sd.name}" (line ${sd.line})')
|
||||
}
|
||||
c.structs[sd.name] = sd.fields
|
||||
}
|
||||
for ed in prog.enums {
|
||||
if ed.name in c.enums {
|
||||
return error('duplicate enum declaration "${ed.name}" (line ${ed.line})')
|
||||
}
|
||||
c.enums[ed.name] = ed.variants
|
||||
}
|
||||
for cd in prog.consts {
|
||||
c.consts[cd.name] = TypeInfo{ kind: .int_t }
|
||||
}
|
||||
for fd in prog.fns {
|
||||
if fd.name in c.fns {
|
||||
return error('duplicate function "${fd.name}" (line ${fd.line})')
|
||||
}
|
||||
c.fns[fd.name] = FnSig{ min_args: fd.params.len - def_count(fd), has_defs: fd.has_defs, variadic: fd.variadic, n_type_params: fd.type_params.len }
|
||||
}
|
||||
// imported files are checked (and their symbols merged) recursively
|
||||
for imp in prog.imports {
|
||||
c.check_import(imp.path)!
|
||||
}
|
||||
for fd in prog.fns {
|
||||
c.check_fn(fd)!
|
||||
}
|
||||
}
|
||||
|
||||
fn def_count(fd FnDecl) int {
|
||||
mut n := 0
|
||||
for has in fd.has_defs {
|
||||
if has {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn (mut c Checker) check_import(path string) ! {
|
||||
if path in c.checked {
|
||||
return
|
||||
}
|
||||
c.checked[path] = true
|
||||
resolved := resolve_import(path) or { return error('cannot read import "${path}"') }
|
||||
src := os.read_file(resolved) or { return error('cannot read import "${path}"') }
|
||||
prog := parse(tokenize(src)!)!
|
||||
// merge declarations from the import
|
||||
for sd in prog.structs {
|
||||
if sd.name !in c.structs {
|
||||
c.structs[sd.name] = sd.fields
|
||||
}
|
||||
}
|
||||
for ed in prog.enums {
|
||||
if ed.name !in c.enums {
|
||||
c.enums[ed.name] = ed.variants
|
||||
}
|
||||
}
|
||||
for cd in prog.consts {
|
||||
if cd.name !in c.consts {
|
||||
c.consts[cd.name] = TypeInfo{ kind: .int_t }
|
||||
}
|
||||
}
|
||||
for fd in prog.fns {
|
||||
if fd.name !in c.fns {
|
||||
c.fns[fd.name] = FnSig{ min_args: fd.params.len - def_count(fd), has_defs: fd.has_defs, variadic: fd.variadic, n_type_params: fd.type_params.len }
|
||||
}
|
||||
}
|
||||
for imp in prog.imports {
|
||||
c.check_import(imp.path)!
|
||||
}
|
||||
for fd in prog.fns {
|
||||
c.check_fn(fd)!
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) check_fn(fd FnDecl) ! {
|
||||
c.types.clear()
|
||||
c.loop_depth = 0
|
||||
// receiver and parameters are untyped (unknown) — the runtime is dynamic
|
||||
if fd.recv_name.len > 0 {
|
||||
c.types[fd.recv_name] = TypeInfo{ kind: .struct_t }
|
||||
}
|
||||
for p in fd.params {
|
||||
c.types[p] = TypeInfo{ kind: .unknown }
|
||||
}
|
||||
for st in fd.body {
|
||||
c.check_stmt(st)!
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) check_stmt(st Stmt) ! {
|
||||
match st.kind {
|
||||
.expr_stmt {
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
.let_stmt {
|
||||
t := c.check_expr(st.expr)!
|
||||
c.types[st.target] = t
|
||||
}
|
||||
.destruct_stmt {
|
||||
base := c.check_expr(st.expr)!
|
||||
for name in st.destruct_targets {
|
||||
c.types[name] = if st.destruct_field { TypeInfo{ kind: .unknown } } else { TypeInfo{ kind: .unknown } }
|
||||
}
|
||||
_ = base
|
||||
}
|
||||
.assign_stmt {
|
||||
if st.target !in c.types {
|
||||
return error('unknown variable "${st.target}" (line ${st.line})')
|
||||
}
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
.index_assign {
|
||||
base := c.check_expr(st.base)!
|
||||
_ = c.check_expr(st.idx)!
|
||||
c.expect_container(base, 'index assignment', st.line)!
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
.field_assign {
|
||||
base := c.check_expr(st.base)!
|
||||
c.expect_struct_like(base, 'field assignment', st.line)!
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
.if_stmt {
|
||||
_ = c.check_expr(st.cond)!
|
||||
for s in st.body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
for s in st.els {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
}
|
||||
.match_stmt {
|
||||
_ = c.check_expr(st.expr)!
|
||||
for arm in st.arms {
|
||||
_ = c.check_expr(arm.val)!
|
||||
for s in arm.body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
}
|
||||
for s in st.els_body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
}
|
||||
.while_stmt {
|
||||
_ = c.check_expr(st.cond)!
|
||||
c.loop_depth++
|
||||
for s in st.body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
c.loop_depth--
|
||||
}
|
||||
.for_range_stmt {
|
||||
start_t := c.check_expr(st.expr)!
|
||||
end_t := c.check_expr(st.cond)!
|
||||
c.expect_numeric(start_t, 'range start', st.line)!
|
||||
c.expect_numeric(end_t, 'range end', st.line)!
|
||||
c.types[st.target] = TypeInfo{ kind: .int_t }
|
||||
c.loop_depth++
|
||||
for s in st.body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
c.loop_depth--
|
||||
}
|
||||
.for_in_stmt {
|
||||
seq := c.check_expr(st.expr)!
|
||||
// iterate enums and arrays; unknown is allowed (dynamic)
|
||||
if seq.kind == .int_t || seq.kind == .float_t || seq.kind == .bool_t {
|
||||
return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})')
|
||||
}
|
||||
c.types[st.target] = TypeInfo{ kind: .unknown }
|
||||
if st.idx_target.len > 0 {
|
||||
c.types[st.idx_target] = TypeInfo{ kind: .int_t }
|
||||
}
|
||||
c.loop_depth++
|
||||
for s in st.body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
c.loop_depth--
|
||||
}
|
||||
.break_stmt, .continue_stmt {
|
||||
if c.loop_depth == 0 {
|
||||
what := if st.kind == .break_stmt { 'break' } else { 'continue' }
|
||||
return error('${what} outside of a loop (line ${st.line})')
|
||||
}
|
||||
}
|
||||
.ret_stmt {
|
||||
if st.has_val {
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
}
|
||||
.assert_stmt {
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
.try_stmt {
|
||||
c.loop_depth++ // errors unwind through loops; keep depth permissive
|
||||
c.loop_depth--
|
||||
for s in st.body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
c.types[st.target] = TypeInfo{ kind: .string_t }
|
||||
for s in st.els {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
}
|
||||
.throw_stmt {
|
||||
_ = c.check_expr(st.expr)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) check_expr(e Expr) !TypeInfo {
|
||||
return match e.kind {
|
||||
.int_lit { TypeInfo{ kind: .int_t } }
|
||||
.float_lit { TypeInfo{ kind: .float_t } }
|
||||
.str_lit { TypeInfo{ kind: .string_t } }
|
||||
.bool_lit { TypeInfo{ kind: .bool_t } }
|
||||
.ident {
|
||||
if e.name in c.types {
|
||||
c.types[e.name]
|
||||
} else if e.name in c.consts {
|
||||
c.consts[e.name]
|
||||
} else {
|
||||
return error('unknown variable "${e.name}" (line ${e.line})')
|
||||
}
|
||||
}
|
||||
.array_lit {
|
||||
for el in e.elems {
|
||||
_ = c.check_expr(el)!
|
||||
}
|
||||
TypeInfo{ kind: .array_t }
|
||||
}
|
||||
.struct_lit {
|
||||
if e.name.len > 0 && e.name in c.structs {
|
||||
fields := c.structs[e.name]
|
||||
mut seen := map[string]bool{}
|
||||
for f in e.fields {
|
||||
if f.name !in 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
|
||||
_ = c.check_expr(f.val)!
|
||||
}
|
||||
return TypeInfo{ kind: .struct_t, name: e.name }
|
||||
}
|
||||
for f in e.fields {
|
||||
_ = c.check_expr(f.val)!
|
||||
}
|
||||
TypeInfo{ kind: .struct_t }
|
||||
}
|
||||
.index {
|
||||
base := c.check_expr(*e.left)!
|
||||
_ = c.check_expr(*e.right)!
|
||||
c.expect_container(base, 'indexing', e.line)!
|
||||
TypeInfo{ kind: .unknown }
|
||||
}
|
||||
.field {
|
||||
base := c.check_expr(*e.left)!
|
||||
c.expect_struct_like(base, 'field access', e.line)!
|
||||
// enum variant: Color.red → enum_t
|
||||
if base.kind == .enum_t {
|
||||
return TypeInfo{ kind: .enum_t, name: base.name }
|
||||
}
|
||||
TypeInfo{ kind: .unknown }
|
||||
}
|
||||
.method_call {
|
||||
recv := c.check_expr(*e.left)!
|
||||
if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t {
|
||||
return error('cannot call a method on a ${type_name(recv.kind)} (line ${e.line})')
|
||||
}
|
||||
for a in e.args {
|
||||
_ = c.check_expr(a)!
|
||||
}
|
||||
TypeInfo{ kind: .unknown }
|
||||
}
|
||||
.slice {
|
||||
base := c.check_expr(*e.left)!
|
||||
_ = c.check_expr(*e.right)!
|
||||
_ = c.check_expr(*e.extra)!
|
||||
if base.kind == .int_t || base.kind == .float_t || base.kind == .bool_t {
|
||||
return error('cannot slice a ${type_name(base.kind)} (line ${e.line})')
|
||||
}
|
||||
if base.kind == .string_t {
|
||||
TypeInfo{ kind: .string_t }
|
||||
} else {
|
||||
TypeInfo{ kind: .unknown }
|
||||
}
|
||||
}
|
||||
.unary {
|
||||
op := c.check_expr(*e.right)!
|
||||
match e.op {
|
||||
.kw_not { TypeInfo{ kind: .bool_t } }
|
||||
.tilde {
|
||||
c.expect_int(op, 'bitwise not', e.line)!
|
||||
TypeInfo{ kind: .int_t }
|
||||
}
|
||||
else {
|
||||
c.expect_numeric(op, 'unary minus', e.line)!
|
||||
op
|
||||
}
|
||||
}
|
||||
}
|
||||
.binary {
|
||||
c.check_binary(e)!
|
||||
}
|
||||
.call {
|
||||
c.check_call(e)!
|
||||
}
|
||||
.anon_fn {
|
||||
for p in e.fparams {
|
||||
c.types[p] = TypeInfo{ kind: .unknown }
|
||||
}
|
||||
for s in e.fn_body {
|
||||
c.check_stmt(s)!
|
||||
}
|
||||
TypeInfo{ kind: .closure_t }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) check_binary(e Expr) !TypeInfo {
|
||||
l := c.check_expr(*e.left)!
|
||||
r := c.check_expr(*e.right)!
|
||||
return match e.op {
|
||||
.plus {
|
||||
if l.kind == .string_t || r.kind == .string_t {
|
||||
return TypeInfo{ kind: .string_t }
|
||||
}
|
||||
if l.kind == .array_t || r.kind == .array_t {
|
||||
return error('cannot add arrays with + (line ${e.line})')
|
||||
}
|
||||
if l.kind == .struct_t || r.kind == .struct_t {
|
||||
return error('cannot add structs with + (line ${e.line})')
|
||||
}
|
||||
if l.kind == .bool_t || r.kind == .bool_t {
|
||||
return error('cannot add a bool with + (line ${e.line})')
|
||||
}
|
||||
if l.kind == .unknown || r.kind == .unknown {
|
||||
return TypeInfo{ kind: .unknown }
|
||||
}
|
||||
if l.kind == .float_t || r.kind == .float_t {
|
||||
return TypeInfo{ kind: .float_t }
|
||||
}
|
||||
return TypeInfo{ kind: .int_t }
|
||||
}
|
||||
.minus, .star, .slash, .percent {
|
||||
c.expect_numeric(l, 'arithmetic', e.line)!
|
||||
c.expect_numeric(r, 'arithmetic', e.line)!
|
||||
if l.kind == .float_t || r.kind == .float_t {
|
||||
TypeInfo{ kind: .float_t }
|
||||
} else {
|
||||
TypeInfo{ kind: .int_t }
|
||||
}
|
||||
}
|
||||
.eq_eq, .not_eq {
|
||||
TypeInfo{ kind: .bool_t }
|
||||
}
|
||||
.lt, .le, .gt, .ge {
|
||||
if l.kind == .array_t || r.kind == .array_t {
|
||||
return error('cannot order arrays (line ${e.line})')
|
||||
}
|
||||
if l.kind == .struct_t || r.kind == .struct_t {
|
||||
return error('cannot order structs (line ${e.line})')
|
||||
}
|
||||
if l.kind == .bool_t && r.kind == .bool_t {
|
||||
return error('cannot order booleans (line ${e.line})')
|
||||
}
|
||||
if l.kind != .unknown && r.kind != .unknown && l.kind != r.kind && !(is_num_kind(l.kind) && is_num_kind(r.kind)) {
|
||||
return error('cannot compare a ${type_name(l.kind)} and a ${type_name(r.kind)} (line ${e.line})')
|
||||
}
|
||||
TypeInfo{ kind: .bool_t }
|
||||
}
|
||||
.kw_and, .kw_or {
|
||||
TypeInfo{ kind: .bool_t }
|
||||
}
|
||||
.amp, .pipe, .caret, .lt_lt, .gt_gt {
|
||||
c.expect_int(l, 'bitwise operator', e.line)!
|
||||
c.expect_int(r, 'bitwise operator', e.line)!
|
||||
TypeInfo{ kind: .int_t }
|
||||
}
|
||||
else {
|
||||
return error('unsupported operator at line ${e.line}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) check_call(e Expr) !TypeInfo {
|
||||
// builtin calls
|
||||
if e.name == 'len' {
|
||||
if e.args.len != 1 {
|
||||
return error('len() takes exactly one argument (line ${e.line})')
|
||||
}
|
||||
t := c.check_expr(e.args[0])!
|
||||
if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .closure_t {
|
||||
return error('len() on a ${type_name(t.kind)} (line ${e.line})')
|
||||
}
|
||||
return TypeInfo{ kind: .int_t }
|
||||
}
|
||||
if e.name == 'push' || e.name == 'insert' || e.name == 'remove' {
|
||||
if e.args.len == 0 {
|
||||
return error('${e.name}() expects arguments (line ${e.line})')
|
||||
}
|
||||
seq := c.check_expr(e.args[0])!
|
||||
c.expect_container(seq, '${e.name}()', e.line)!
|
||||
for a in e.args[1..] {
|
||||
_ = c.check_expr(a)!
|
||||
}
|
||||
return TypeInfo{ kind: .unknown }
|
||||
}
|
||||
if e.name == 'has' || e.name == 'delete' {
|
||||
if e.args.len != 2 {
|
||||
return error('${e.name}() takes exactly two arguments (line ${e.line})')
|
||||
}
|
||||
seq := c.check_expr(e.args[0])!
|
||||
c.expect_struct_like(seq, '${e.name}()', e.line)!
|
||||
_ = c.check_expr(e.args[1])!
|
||||
return TypeInfo{ kind: .unknown }
|
||||
}
|
||||
if e.name == 'keys' {
|
||||
if e.args.len != 1 {
|
||||
return error('keys() takes exactly one argument (line ${e.line})')
|
||||
}
|
||||
seq := c.check_expr(e.args[0])!
|
||||
c.expect_struct_like(seq, 'keys()', e.line)!
|
||||
return TypeInfo{ kind: .array_t }
|
||||
}
|
||||
if e.name == 'print' || e.name == 'println' {
|
||||
if e.args.len != 1 {
|
||||
return error('${e.name}() takes exactly one argument (line ${e.line})')
|
||||
}
|
||||
_ = c.check_expr(e.args[0])!
|
||||
return TypeInfo{ kind: .unknown }
|
||||
}
|
||||
// host builtins (native) — validate arity from the spec table
|
||||
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 {
|
||||
_ = c.check_expr(a)!
|
||||
}
|
||||
return builtin_result_type(e.name)
|
||||
}
|
||||
// closure call: a local holding a function value
|
||||
if e.name in c.types && c.types[e.name].kind == .closure_t {
|
||||
for a in e.args {
|
||||
_ = c.check_expr(a)!
|
||||
}
|
||||
return TypeInfo{ kind: .unknown }
|
||||
}
|
||||
// user function — arity is checked only when the signature is known.
|
||||
// Unknown names are allowed: this toolchain supports separate compilation,
|
||||
// so a call may resolve to a function in another object at link time.
|
||||
// Truly missing functions are reported by the linker, not the checker.
|
||||
if e.name in c.fns {
|
||||
sig := c.fns[e.name]
|
||||
// generic type arguments must match the declared type parameters
|
||||
if e.type_args.len > 0 && e.type_args.len != sig.n_type_params {
|
||||
return error('${e.name}() takes ${sig.n_type_params} type argument(s), got ${e.type_args.len} (line ${e.line})')
|
||||
}
|
||||
if e.type_args.len == 0 && sig.n_type_params > 0 {
|
||||
// calling a generic function without explicit type args is fine —
|
||||
// the VM infers from the values at runtime
|
||||
}
|
||||
if !sig.variadic {
|
||||
if e.args.len < sig.min_args {
|
||||
return error('${e.name}() expects at least ${sig.min_args} argument(s), got ${e.args.len} (line ${e.line})')
|
||||
}
|
||||
if e.args.len > sig.has_defs.len {
|
||||
return error('${e.name}() expects at most ${sig.has_defs.len} argument(s), got ${e.args.len} (line ${e.line})')
|
||||
}
|
||||
}
|
||||
}
|
||||
for a in e.args {
|
||||
_ = c.check_expr(a)!
|
||||
}
|
||||
return TypeInfo{ kind: .unknown }
|
||||
}
|
||||
|
||||
fn builtin_result_type(name string) TypeInfo {
|
||||
return match name {
|
||||
'abs', 'min', 'max', 'floor', 'ceil', 'round', 'rand_int' { TypeInfo{ kind: .int_t } }
|
||||
'pow', 'sqrt', 'rand', 'float', 'time' { TypeInfo{ kind: .float_t } }
|
||||
'int' { TypeInfo{ kind: .int_t } }
|
||||
'str', 'type', 'lower', 'upper', 'trim', 'getenv', 'read_file' { TypeInfo{ kind: .string_t } }
|
||||
'contains', 'starts_with', 'ends_with' { TypeInfo{ kind: .bool_t } }
|
||||
'split' { TypeInfo{ kind: .array_t } }
|
||||
'join' { TypeInfo{ kind: .string_t } }
|
||||
'sort', 'reverse' { TypeInfo{ kind: .array_t } }
|
||||
'pop' { TypeInfo{ kind: .unknown } }
|
||||
'clone', 'index_of' { TypeInfo{ kind: .unknown } }
|
||||
'args', 'keys' { TypeInfo{ kind: .array_t } }
|
||||
'len' { TypeInfo{ kind: .int_t } }
|
||||
'write_file', 'setenv', 'exit', 'sleep', 'eprint' { TypeInfo{ kind: .unknown } }
|
||||
else { TypeInfo{ kind: .unknown } }
|
||||
}
|
||||
}
|
||||
|
||||
fn is_num_kind(k CType) bool {
|
||||
return k == .int_t || k == .float_t
|
||||
}
|
||||
|
||||
fn type_name(k CType) string {
|
||||
return match k {
|
||||
.int_t { 'int' }
|
||||
.float_t { 'float' }
|
||||
.string_t { 'string' }
|
||||
.bool_t { 'bool' }
|
||||
.array_t { 'array' }
|
||||
.struct_t { 'struct' }
|
||||
.enum_t { 'enum' }
|
||||
.closure_t { 'function' }
|
||||
else { 'value' }
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) expect_numeric(t TypeInfo, what string, line int) ! {
|
||||
if t.kind == .string_t || t.kind == .array_t || t.kind == .struct_t || t.kind == .bool_t || t.kind == .closure_t {
|
||||
return error('${what} on a ${type_name(t.kind)} (line ${line})')
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) expect_int(t TypeInfo, what string, line int) ! {
|
||||
if t.kind == .string_t || t.kind == .array_t || t.kind == .struct_t || t.kind == .bool_t || t.kind == .closure_t || t.kind == .float_t {
|
||||
return error('${what} requires an int, got a ${type_name(t.kind)} (line ${line})')
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) expect_container(t TypeInfo, what string, line int) ! {
|
||||
if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .closure_t {
|
||||
return error('${what} on a ${type_name(t.kind)} (line ${line})')
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut c Checker) expect_struct_like(t TypeInfo, what string, line int) ! {
|
||||
if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .string_t || t.kind == .closure_t {
|
||||
return error('${what} on a ${type_name(t.kind)} (line ${line})')
|
||||
}
|
||||
}
|
||||
+459
-17
@@ -31,8 +31,10 @@ mut:
|
||||
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
|
||||
@@ -78,7 +80,7 @@ fn gen(prog Program) !obj.Obj {
|
||||
}
|
||||
// compile imported files and merge their objects
|
||||
for imp in prog.imports {
|
||||
imported := compile_file(imp.path)!
|
||||
imported := compile_file(resolve_import(imp.path)!)!
|
||||
// merge symbols from the imported object
|
||||
for s in imported.symbols {
|
||||
g.symbols << s
|
||||
@@ -93,6 +95,10 @@ fn gen(prog Program) !obj.Obj {
|
||||
for r in imported.relocs {
|
||||
g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: r.name, 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.gen_fn(fd)!
|
||||
@@ -102,6 +108,7 @@ fn gen(prog Program) !obj.Obj {
|
||||
strings: g.strings
|
||||
code: g.code
|
||||
relocs: g.relocs
|
||||
lines: g.lines
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +118,7 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
|
||||
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
|
||||
@@ -121,20 +129,67 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
|
||||
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
|
||||
obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt - g.argc))
|
||||
// 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
|
||||
for f in g.fixups {
|
||||
target := g.labels[f.name] or {
|
||||
@@ -148,6 +203,7 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
|
||||
}
|
||||
|
||||
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)!
|
||||
@@ -168,6 +224,28 @@ fn (mut g Gen) gen_stmt(st Stmt) ! {
|
||||
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}')
|
||||
@@ -350,9 +428,26 @@ fn (mut g Gen) gen_stmt(st Stmt) ! {
|
||||
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 {
|
||||
@@ -403,6 +498,44 @@ fn (mut g Gen) gen_stmt(st Stmt) ! {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +545,10 @@ fn (mut g Gen) gen_expr(e Expr) ! {
|
||||
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
|
||||
@@ -468,7 +605,21 @@ fn (mut g Gen) gen_expr(e Expr) ! {
|
||||
}
|
||||
.method_call {
|
||||
// p.dist(x) → call <Type>.dist p, x
|
||||
recv_t := g.method_receiver_type(e)!
|
||||
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)!
|
||||
@@ -487,11 +638,24 @@ fn (mut g Gen) gen_expr(e Expr) ! {
|
||||
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: '${recv_t}.${e.name}', kind: 0 }
|
||||
g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args
|
||||
// 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 {
|
||||
@@ -504,6 +668,57 @@ fn (mut g Gen) gen_expr(e Expr) ! {
|
||||
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}'
|
||||
// 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
|
||||
g.labels.clear()
|
||||
g.fixups = []Fixup{}
|
||||
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
|
||||
// Patch the skip jump to land at the closure opcode we emit next.
|
||||
obj.patch_i64(mut g.code, skip_fix_off, i64(g.code.len))
|
||||
g.code << op_closure
|
||||
g.code << obj.encode_i64(0)
|
||||
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 }
|
||||
}
|
||||
.bool_lit {
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(e.int_v)
|
||||
@@ -526,11 +741,29 @@ fn (mut g Gen) gen_expr(e Expr) ! {
|
||||
}
|
||||
}
|
||||
.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)!
|
||||
if e.op == .kw_not {
|
||||
g.code << op_not
|
||||
} else {
|
||||
g.code << op_neg
|
||||
match e.op {
|
||||
.kw_not { g.code << op_not }
|
||||
.tilde { g.code << op_not_b }
|
||||
else { g.code << op_neg }
|
||||
}
|
||||
}
|
||||
.binary {
|
||||
@@ -594,6 +827,31 @@ fn (mut g Gen) gen_call(e Expr) ! {
|
||||
g.code << op_skeys
|
||||
return
|
||||
}
|
||||
// closure call: ident(args) where ident is a local holding a closure
|
||||
if e.name in g.locals {
|
||||
g.gen_expr(Expr{ kind: .ident, name: e.name, line: e.line })!
|
||||
g.code << op_dup // separate the closure copy from the local slot
|
||||
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)!
|
||||
}
|
||||
@@ -603,7 +861,169 @@ fn (mut g Gen) gen_call(e Expr) ! {
|
||||
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 }
|
||||
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
|
||||
@@ -664,6 +1084,11 @@ fn (mut g Gen) gen_binary(e Expr) ! {
|
||||
.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}')
|
||||
}
|
||||
@@ -678,6 +1103,9 @@ fn (mut g Gen) gen_binary(e Expr) ! {
|
||||
// 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
|
||||
}
|
||||
@@ -691,14 +1119,28 @@ fn (mut g Gen) expr_type(e Expr) string {
|
||||
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' }
|
||||
else { '' }
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// method_receiver_type resolves the struct type a method call is made on.
|
||||
// The receiver must be a plain variable whose type the compiler knows
|
||||
// (from a typed literal, an assignment, or a method receiver binding)
|
||||
// or an enum variant expression (e.g. Color.red).
|
||||
fn (mut g Gen) method_receiver_type(e Expr) !string {
|
||||
// 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 { '' }
|
||||
@@ -706,14 +1148,14 @@ fn (mut g Gen) method_receiver_type(e Expr) !string {
|
||||
return t
|
||||
}
|
||||
}
|
||||
// enum variant: Color.red → type is "Color"
|
||||
// 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 error('cannot resolve method "${e.name}": receiver type unknown (line ${e.line})')
|
||||
return ''
|
||||
}
|
||||
|
||||
// gen_enum_to_string generates bytecode for `e.to_string()` on an enum value.
|
||||
|
||||
@@ -8,6 +8,8 @@ import obj
|
||||
pub fn compile(src string) !obj.Obj {
|
||||
toks := tokenize(src)!
|
||||
prog := parse(toks)!
|
||||
// conservative compile-time type check; catches provable errors early
|
||||
check(prog)!
|
||||
return gen(prog)
|
||||
}
|
||||
|
||||
@@ -15,3 +17,18 @@ pub fn compile_file(path string) !obj.Obj {
|
||||
src := os.read_file(path)!
|
||||
return compile(src)!
|
||||
}
|
||||
|
||||
// resolve_import turns an import path into a readable source file. It tries
|
||||
// the path as given first, then falls back to the package-manager layout so
|
||||
// `import "pkg/file.vr"` finds vendor/pkg/file.vr.
|
||||
pub fn resolve_import(path string) !string {
|
||||
if os.exists(path) {
|
||||
return path
|
||||
}
|
||||
for cand in ['vendor/${path}', 'vendor/${path}.vr', 'vendor/${path}/main.vr', 'vendor/${path}/src/main.vr'] {
|
||||
if os.exists(cand) {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
return error('cannot resolve import "${path}" (tried vendor/)"')
|
||||
}
|
||||
|
||||
+115
-45
@@ -2,7 +2,7 @@
|
||||
module compiler
|
||||
|
||||
pub fn tokenize(src string) ![]Tok {
|
||||
mut l := Lexer{ src: src }
|
||||
mut l := Lexer{ src: src, line: 1 }
|
||||
mut toks := []Tok{}
|
||||
for {
|
||||
t := l.next()!
|
||||
@@ -16,9 +16,10 @@ pub fn tokenize(src string) ![]Tok {
|
||||
|
||||
struct Lexer {
|
||||
mut:
|
||||
src string
|
||||
pos int
|
||||
line int
|
||||
src string
|
||||
pos int
|
||||
line int
|
||||
line_start int // byte offset where the current line begins
|
||||
}
|
||||
|
||||
fn (mut l Lexer) peek() u8 {
|
||||
@@ -40,6 +41,7 @@ fn (mut l Lexer) advance() u8 {
|
||||
l.pos++
|
||||
if c == `\n` {
|
||||
l.line++
|
||||
l.line_start = l.pos
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -61,34 +63,35 @@ fn (mut l Lexer) next() !Tok {
|
||||
break
|
||||
}
|
||||
line := l.line
|
||||
col := l.pos - l.line_start + 1 // 1-based column
|
||||
if l.pos >= l.src.len {
|
||||
return Tok{ kind: .eof, lit: '', line: line }
|
||||
return Tok{ kind: .eof, lit: '', line: line, col: col }
|
||||
}
|
||||
c := l.peek()
|
||||
match c {
|
||||
`(` {
|
||||
l.advance()
|
||||
return Tok{ kind: .lparen, lit: '(', line: line }
|
||||
return Tok{ kind: .lparen, lit: '(', line: line, col: col }
|
||||
}
|
||||
`)` {
|
||||
l.advance()
|
||||
return Tok{ kind: .rparen, lit: ')', line: line }
|
||||
return Tok{ kind: .rparen, lit: ')', line: line, col: col }
|
||||
}
|
||||
`{` {
|
||||
l.advance()
|
||||
return Tok{ kind: .lbrace, lit: '{', line: line }
|
||||
return Tok{ kind: .lbrace, lit: '{', line: line, col: col }
|
||||
}
|
||||
`}` {
|
||||
l.advance()
|
||||
return Tok{ kind: .rbrace, lit: '}', line: line }
|
||||
return Tok{ kind: .rbrace, lit: '}', line: line, col: col }
|
||||
}
|
||||
`[` {
|
||||
l.advance()
|
||||
return Tok{ kind: .lbracket, lit: '[', line: line }
|
||||
return Tok{ kind: .lbracket, lit: '[', line: line, col: col }
|
||||
}
|
||||
`]` {
|
||||
l.advance()
|
||||
return Tok{ kind: .rbracket, lit: ']', line: line }
|
||||
return Tok{ kind: .rbracket, lit: ']', line: line, col: col }
|
||||
}
|
||||
`.` {
|
||||
l.advance()
|
||||
@@ -96,112 +99,165 @@ fn (mut l Lexer) next() !Tok {
|
||||
l.advance()
|
||||
if l.peek() == `.` {
|
||||
l.advance()
|
||||
return Tok{ kind: .dotdotdot, lit: '...', line: line }
|
||||
return Tok{ kind: .dotdotdot, lit: '...', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .dotdot, lit: '..', line: line }
|
||||
return Tok{ kind: .dotdot, lit: '..', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .dot, lit: '.', line: line }
|
||||
return Tok{ kind: .dot, lit: '.', line: line, col: col }
|
||||
}
|
||||
`,` {
|
||||
l.advance()
|
||||
return Tok{ kind: .comma, lit: ',', line: line }
|
||||
return Tok{ kind: .comma, lit: ',', line: line, col: col }
|
||||
}
|
||||
`:` {
|
||||
l.advance()
|
||||
return Tok{ kind: .colon, lit: ':', line: line }
|
||||
return Tok{ kind: .colon, lit: ':', line: line, col: col }
|
||||
}
|
||||
`+` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .plus_eq, lit: '+=', line: line }
|
||||
return Tok{ kind: .plus_eq, lit: '+=', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .plus, lit: '+', line: line }
|
||||
return Tok{ kind: .plus, lit: '+', line: line, col: col }
|
||||
}
|
||||
`-` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .minus_eq, lit: '-=', line: line }
|
||||
return Tok{ kind: .minus_eq, lit: '-=', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .minus, lit: '-', line: line }
|
||||
return Tok{ kind: .minus, lit: '-', line: line, col: col }
|
||||
}
|
||||
`*` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .star_eq, lit: '*=', line: line }
|
||||
return Tok{ kind: .star_eq, lit: '*=', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .star, lit: '*', line: line }
|
||||
return Tok{ kind: .star, lit: '*', line: line, col: col }
|
||||
}
|
||||
`/` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .slash_eq, lit: '/=', line: line }
|
||||
return Tok{ kind: .slash_eq, lit: '/=', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .slash, lit: '/', line: line }
|
||||
return Tok{ kind: .slash, lit: '/', line: line, col: col }
|
||||
}
|
||||
`%` {
|
||||
l.advance()
|
||||
return Tok{ kind: .percent, lit: '%', line: line }
|
||||
return Tok{ kind: .percent, lit: '%', line: line, col: col }
|
||||
}
|
||||
`&` {
|
||||
l.advance()
|
||||
return Tok{ kind: .amp, lit: '&', line: line, col: col }
|
||||
}
|
||||
`|` {
|
||||
l.advance()
|
||||
return Tok{ kind: .pipe, lit: '|', line: line, col: col }
|
||||
}
|
||||
`^` {
|
||||
l.advance()
|
||||
return Tok{ kind: .caret, lit: '^', line: line, col: col }
|
||||
}
|
||||
`~` {
|
||||
l.advance()
|
||||
return Tok{ kind: .tilde, lit: '~', line: line, col: col }
|
||||
}
|
||||
`=` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .eq_eq, lit: '==', line: line }
|
||||
return Tok{ kind: .eq_eq, lit: '==', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .assign, lit: '=', line: line }
|
||||
return Tok{ kind: .assign, lit: '=', line: line, col: col }
|
||||
}
|
||||
`!` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .not_eq, lit: '!=', line: line }
|
||||
return Tok{ kind: .not_eq, lit: '!=', line: line, col: col }
|
||||
}
|
||||
return error('unexpected character "!" at line ${line} (did you mean "not"?)')
|
||||
return error('unexpected character "!" at line ${line}, col ${col} (did you mean "not"?)')
|
||||
}
|
||||
`<` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .le, lit: '<=', line: line }
|
||||
return Tok{ kind: .le, lit: '<=', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .lt, lit: '<', line: line }
|
||||
if l.peek() == `<` {
|
||||
l.advance()
|
||||
return Tok{ kind: .lt_lt, lit: '<<', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .lt, lit: '<', line: line, col: col }
|
||||
}
|
||||
`>` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .ge, lit: '>=', line: line }
|
||||
return Tok{ kind: .ge, lit: '>=', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .gt, lit: '>', line: line }
|
||||
if l.peek() == `>` {
|
||||
l.advance()
|
||||
return Tok{ kind: .gt_gt, lit: '>>', line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .gt, lit: '>', line: line, col: col }
|
||||
}
|
||||
`\"` {
|
||||
return l.lex_string(line)!
|
||||
return l.lex_string(line, col)!
|
||||
}
|
||||
`0`...`9` {
|
||||
return l.lex_number(line)
|
||||
return l.lex_number(line, col)
|
||||
}
|
||||
else {
|
||||
if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` {
|
||||
return l.lex_ident(line)
|
||||
return l.lex_ident(line, col)
|
||||
}
|
||||
return error('unexpected character "${c.ascii_str()}" at line ${line}')
|
||||
return error('unexpected character "${c.ascii_str()}" at line ${line}, col ${col}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut l Lexer) lex_number(line int) Tok {
|
||||
fn (mut l Lexer) lex_number(line int, col 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 }
|
||||
mut is_float := false
|
||||
// fractional part: `.` followed by a digit (so `1..3` and `1...3` stay ints)
|
||||
if l.peek() == `.` && l.pos + 1 < l.src.len && l.peek2() >= `0` && l.peek2() <= `9` {
|
||||
is_float = true
|
||||
l.advance() // consume `.`
|
||||
for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
|
||||
l.advance()
|
||||
}
|
||||
}
|
||||
// exponent part: e / E followed by optional sign and digits
|
||||
if l.peek() == `e` || l.peek() == `E` {
|
||||
save := l.pos
|
||||
l.advance()
|
||||
if l.peek() == `+` || l.peek() == `-` {
|
||||
l.advance()
|
||||
}
|
||||
if l.peek() >= `0` && l.peek() <= `9` {
|
||||
is_float = true
|
||||
for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
|
||||
l.advance()
|
||||
}
|
||||
} else {
|
||||
l.pos = save // not an exponent after all
|
||||
}
|
||||
}
|
||||
lit := l.src[start..l.pos]
|
||||
if is_float {
|
||||
return Tok{ kind: .float_lit, lit: lit, line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .int_lit, lit: lit, line: line, col: col }
|
||||
}
|
||||
|
||||
fn (mut l Lexer) lex_ident(line int) Tok {
|
||||
fn (mut l Lexer) lex_ident(line int, col int) Tok {
|
||||
start := l.pos
|
||||
for l.pos < l.src.len {
|
||||
c := l.peek()
|
||||
@@ -236,18 +292,26 @@ fn (mut l Lexer) lex_ident(line int) Tok {
|
||||
'import' { TokKind.kw_import }
|
||||
'enum' { TokKind.kw_enum }
|
||||
'const' { TokKind.kw_const }
|
||||
'interface' { TokKind.kw_interface }
|
||||
'try' { TokKind.kw_try }
|
||||
'catch' { TokKind.kw_catch }
|
||||
'throw' { TokKind.kw_throw }
|
||||
else { TokKind.ident }
|
||||
}
|
||||
return Tok{ kind: kind, lit: lit, line: line }
|
||||
return Tok{ kind: kind, lit: lit, line: line, col: col }
|
||||
}
|
||||
|
||||
fn (mut l Lexer) lex_string(line int) !Tok {
|
||||
fn (mut l Lexer) lex_string(line int, col int) !Tok {
|
||||
l.advance() // opening quote
|
||||
mut s := ''
|
||||
mut has_interp := false
|
||||
for l.pos < l.src.len {
|
||||
c := l.advance()
|
||||
if c == `\"` {
|
||||
return Tok{ kind: .str_lit, lit: s, line: line }
|
||||
if has_interp {
|
||||
return Tok{ kind: .str_interp, lit: s, line: line, col: col }
|
||||
}
|
||||
return Tok{ kind: .str_lit, lit: s, line: line, col: col }
|
||||
}
|
||||
if c == `\\` {
|
||||
if l.pos >= l.src.len {
|
||||
@@ -268,12 +332,18 @@ fn (mut l Lexer) lex_string(line int) !Tok {
|
||||
s += '\\'
|
||||
}
|
||||
else {
|
||||
return error('invalid escape \\${e.ascii_str()} at line ${line}')
|
||||
return error('invalid escape \\${e.ascii_str()} at line ${line}, col ${col}')
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c == `$` && l.peek() == `{` {
|
||||
has_interp = true
|
||||
s += '\${'
|
||||
l.advance() // skip the '{'
|
||||
continue
|
||||
}
|
||||
s += c.ascii_str()
|
||||
}
|
||||
return error('unterminated string at line ${line}')
|
||||
return error('unterminated string at line ${line}, col ${col}')
|
||||
}
|
||||
|
||||
@@ -47,3 +47,61 @@ const op_shas = u8(40)
|
||||
const op_sdel = u8(41)
|
||||
const op_slen = u8(42)
|
||||
const op_skeys = u8(43)
|
||||
const op_slice = u8(44)
|
||||
const op_push_f = u8(45)
|
||||
const op_native = u8(46)
|
||||
const op_and_b = u8(47)
|
||||
const op_or_b = u8(48)
|
||||
const op_xor = u8(49)
|
||||
const op_shl = u8(50)
|
||||
const op_shr = u8(51)
|
||||
const op_not_b = u8(52)
|
||||
const op_try = u8(53)
|
||||
const op_throw = u8(54)
|
||||
const op_catch_done = u8(55)
|
||||
const op_closure = u8(56)
|
||||
const op_call_closure = u8(57)
|
||||
const op_argc = u8(58) // push the current frame's arg count
|
||||
const op_load_dyn = u8(59) // pop idx, push stack[bp + idx]
|
||||
const op_varargs = u8(60) // <named:i64> <dst:i64> — collect args[named..argc-1] into an array at local dst
|
||||
const op_str_method = u8(61) // <name:str> <argc:i64> — call a string method (s.len(), s.contains(x), ...)
|
||||
|
||||
// native builtin ids (keep in sync with vm/opcodes.v)
|
||||
const native_abs = 100
|
||||
const native_min = 101
|
||||
const native_max = 102
|
||||
const native_pow = 103
|
||||
const native_sqrt = 104
|
||||
const native_floor = 105
|
||||
const native_ceil = 106
|
||||
const native_round = 107
|
||||
const native_rand = 108
|
||||
const native_rand_int = 109
|
||||
const native_int = 110
|
||||
const native_str = 111
|
||||
const native_float = 112
|
||||
const native_type = 113
|
||||
const native_split = 114
|
||||
const native_join = 115
|
||||
const native_contains = 116
|
||||
const native_starts_with = 117
|
||||
const native_ends_with = 118
|
||||
const native_trim = 119
|
||||
const native_lower = 120
|
||||
const native_upper = 121
|
||||
const native_pop = 122
|
||||
const native_insert = 123
|
||||
const native_remove = 124
|
||||
const native_sort = 125
|
||||
const native_clone = 126
|
||||
const native_reverse = 127
|
||||
const native_index_of = 128
|
||||
const native_args = 129
|
||||
const native_getenv = 130
|
||||
const native_setenv = 131
|
||||
const native_exit = 132
|
||||
const native_time = 133
|
||||
const native_sleep = 134
|
||||
const native_read_file = 135
|
||||
const native_write_file = 136
|
||||
const native_eprint = 137
|
||||
|
||||
+394
-23
@@ -64,7 +64,7 @@ fn (mut p Parser) advance() Tok {
|
||||
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 error('expected ${what}, got "${t.lit}" at line ${t.line}, col ${t.col}')
|
||||
}
|
||||
return p.advance()
|
||||
}
|
||||
@@ -92,8 +92,9 @@ fn (mut p Parser) parse_program() !Program {
|
||||
.kw_struct { prog.structs << p.parse_struct_decl()! }
|
||||
.kw_enum { prog.enums << p.parse_enum_decl()! }
|
||||
.kw_const { prog.consts << p.parse_const_decl()! }
|
||||
.kw_interface { prog.interfaces << p.parse_interface_decl()! }
|
||||
.kw_fn { prog.fns << p.parse_fn()! }
|
||||
else { return error('unexpected token "${p.cur().lit}" at line ${p.cur().line}') }
|
||||
else { return error('unexpected token "${p.cur().lit}" at line ${p.cur().line}, col ${p.cur().col}') }
|
||||
}
|
||||
}
|
||||
if prog.fns.len == 0 {
|
||||
@@ -163,6 +164,42 @@ fn (mut p Parser) parse_const_decl() !ConstDecl {
|
||||
return ConstDecl{ name: name.lit, value: value, line: t.line }
|
||||
}
|
||||
|
||||
// parse_interface_decl parses `interface Name { method1() method2() ... }`.
|
||||
// Methods may optionally be followed by `()` (any parameter list is ignored)
|
||||
// and are separated by commas or newlines.
|
||||
fn (mut p Parser) parse_interface_decl() !InterfaceDecl {
|
||||
t := p.expect(.kw_interface, "'interface'")!
|
||||
name := p.expect(.ident, 'interface name')!
|
||||
p.expect(.lbrace, "'{'")!
|
||||
mut methods := []InterfaceMethod{}
|
||||
if p.cur().kind != .rbrace {
|
||||
for {
|
||||
mname := p.expect(.ident, 'method name')!
|
||||
if p.cur().kind == .lparen {
|
||||
p.advance()
|
||||
for p.cur().kind != .rparen {
|
||||
if p.cur().kind == .eof {
|
||||
return error('unexpected end of file in interface method (line ${mname.line})')
|
||||
}
|
||||
p.advance()
|
||||
}
|
||||
p.expect(.rparen, "')'")!
|
||||
}
|
||||
methods << InterfaceMethod{ name: mname.lit, line: mname.line }
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
continue
|
||||
}
|
||||
if p.cur().kind != .rbrace {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
p.expect(.rbrace, "'}'")!
|
||||
return InterfaceDecl{ name: name.lit, methods: methods, line: t.line }
|
||||
}
|
||||
|
||||
// parse_fn parses `fn name(params) { }` or a method `fn (p Type) name(params) { }`.
|
||||
fn (mut p Parser) parse_fn() !FnDecl {
|
||||
fn_tok := p.expect(.kw_fn, "'fn'")!
|
||||
@@ -176,18 +213,76 @@ fn (mut p Parser) parse_fn() !FnDecl {
|
||||
p.expect(.rparen, "')'")!
|
||||
}
|
||||
name := p.expect(.ident, 'function name')!
|
||||
params := p.parse_params()!
|
||||
// generic type parameters: fn first[T, U](arr) { ... } — captured so the
|
||||
// type checker can validate call sites; the VM is dynamically typed, so
|
||||
// they erase to a single function at runtime
|
||||
mut type_params := []string{}
|
||||
if p.cur().kind == .lbracket {
|
||||
p.advance()
|
||||
for p.cur().kind != .rbracket {
|
||||
tp := p.expect(.ident, 'type parameter')!.lit
|
||||
if tp in type_params {
|
||||
return error('duplicate type parameter "${tp}" in function ${name.lit} (line ${name.line}, col ${name.col})')
|
||||
}
|
||||
type_params << tp
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
}
|
||||
}
|
||||
p.expect(.rbracket, "']'")!
|
||||
}
|
||||
params, defaults, has_defs, variadic := p.parse_params()!
|
||||
body := p.parse_block()!
|
||||
return FnDecl{ name: name.lit, recv_name: recv_name, recv_type: recv_type, params: params, body: body, line: fn_tok.line }
|
||||
return FnDecl{
|
||||
name: name.lit
|
||||
type_params: type_params
|
||||
recv_name: recv_name
|
||||
recv_type: recv_type
|
||||
params: params
|
||||
defaults: defaults
|
||||
has_defs: has_defs
|
||||
variadic: variadic
|
||||
body: body
|
||||
line: fn_tok.line
|
||||
}
|
||||
}
|
||||
|
||||
// parse_params parses `(a, b, c)` — the parameter list of a function.
|
||||
fn (mut p Parser) parse_params() ![]string {
|
||||
// parse_params parses `(a, b, c = expr, rest...)` — the parameter list of a
|
||||
// function. Returns the names, the default-value expressions (parallel array,
|
||||
// empty Expr{} when no default), whether each has a default, and whether the
|
||||
// last parameter is variadic.
|
||||
fn (mut p Parser) parse_params() !([]string, []Expr, []bool, bool) {
|
||||
p.expect(.lparen, "'('")!
|
||||
mut params := []string{}
|
||||
mut defaults := []Expr{}
|
||||
mut has_defs := []bool{}
|
||||
mut variadic := false
|
||||
if p.cur().kind != .rparen {
|
||||
for {
|
||||
params << p.expect(.ident, 'parameter name')!.lit
|
||||
name_tok := p.expect(.ident, 'parameter name')!
|
||||
name := name_tok.lit
|
||||
if p.cur().kind == .dotdotdot {
|
||||
// variadic parameter: `rest...` (must be last)
|
||||
p.advance()
|
||||
params << name
|
||||
defaults << Expr{}
|
||||
has_defs << false
|
||||
variadic = true
|
||||
if p.cur().kind == .comma {
|
||||
return error('a variadic parameter must be last (line ${name_tok.line})')
|
||||
}
|
||||
break
|
||||
}
|
||||
mut def := Expr{}
|
||||
mut has_def := false
|
||||
if p.cur().kind == .assign {
|
||||
p.advance()
|
||||
def = p.parse_expr()!
|
||||
has_def = true
|
||||
}
|
||||
params << name
|
||||
defaults << def
|
||||
has_defs << has_def
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
continue
|
||||
@@ -196,7 +291,7 @@ fn (mut p Parser) parse_params() ![]string {
|
||||
}
|
||||
}
|
||||
p.expect(.rparen, "')'")!
|
||||
return params
|
||||
return params, defaults, has_defs, variadic
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_block() ![]Stmt {
|
||||
@@ -217,6 +312,30 @@ fn (mut p Parser) parse_stmt() !Stmt {
|
||||
match t.kind {
|
||||
.kw_let {
|
||||
p.advance()
|
||||
// destructuring let: `let { a, b } = expr` or `let [a, b] = expr`
|
||||
if p.cur().kind == .lbrace || p.cur().kind == .lbracket {
|
||||
is_field := p.cur().kind == .lbrace
|
||||
p.advance()
|
||||
mut names := []string{}
|
||||
mut closing := TokKind.rbracket
|
||||
if is_field {
|
||||
closing = .rbrace
|
||||
}
|
||||
if p.cur().kind != closing {
|
||||
for {
|
||||
names << p.expect(.ident, 'binding name')!.lit
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
p.expect(closing, "']' or '}'")!
|
||||
p.expect(.assign, "'='")!
|
||||
e := p.parse_expr()!
|
||||
return Stmt{ kind: .destruct_stmt, expr: e, destruct_targets: names, destruct_field: is_field, line: t.line }
|
||||
}
|
||||
name := p.expect(.ident, 'variable name')!
|
||||
p.expect(.assign, "'='")!
|
||||
e := p.parse_expr()!
|
||||
@@ -257,6 +376,14 @@ fn (mut p Parser) parse_stmt() !Stmt {
|
||||
.kw_for {
|
||||
p.advance()
|
||||
name := p.expect(.ident, 'loop variable')!
|
||||
mut idx_name := ''
|
||||
mut val_name := name.lit
|
||||
if p.cur().kind == .comma {
|
||||
// for i, v in arr { ... }
|
||||
p.advance()
|
||||
idx_name = name.lit
|
||||
val_name = p.expect(.ident, 'loop value variable')!.lit
|
||||
}
|
||||
p.expect(.kw_in, "'in'")!
|
||||
first := p.parse_expr()!
|
||||
if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot {
|
||||
@@ -264,10 +391,10 @@ fn (mut p Parser) parse_stmt() !Stmt {
|
||||
p.advance()
|
||||
end := p.parse_expr()!
|
||||
body := p.parse_block()!
|
||||
return Stmt{ kind: .for_range_stmt, target: name.lit, expr: first, cond: end, inclusive: inclusive, body: body, line: t.line }
|
||||
return Stmt{ kind: .for_range_stmt, target: val_name, expr: first, cond: end, inclusive: inclusive, body: body, line: t.line }
|
||||
}
|
||||
body := p.parse_block()!
|
||||
return Stmt{ kind: .for_in_stmt, target: name.lit, expr: first, body: body, line: t.line }
|
||||
return Stmt{ kind: .for_in_stmt, target: val_name, idx_target: idx_name, expr: first, body: body, line: t.line }
|
||||
}
|
||||
.kw_return {
|
||||
p.advance()
|
||||
@@ -297,13 +424,29 @@ fn (mut p Parser) parse_stmt() !Stmt {
|
||||
}
|
||||
return Stmt{ kind: .assert_stmt, expr: e, line: t.line }
|
||||
}
|
||||
.kw_try {
|
||||
p.advance()
|
||||
return p.parse_try_stmt(t)!
|
||||
}
|
||||
.kw_throw {
|
||||
p.advance()
|
||||
e := p.parse_expr()!
|
||||
return Stmt{ kind: .throw_stmt, expr: e, line: t.line }
|
||||
}
|
||||
.ident {
|
||||
p.advance()
|
||||
// generic type args in statement position: first[int](...)
|
||||
mut type_args := []string{}
|
||||
if p.cur().kind == .lbracket && p.looks_like_generic_args() {
|
||||
type_args = p.parse_type_args()!
|
||||
}
|
||||
mut e := Expr{}
|
||||
if p.cur().kind == .lparen {
|
||||
// a call statement: foo(args), optionally chained foo().x
|
||||
e = p.parse_call(t)!
|
||||
e = p.parse_call(t, type_args)!
|
||||
e = p.parse_postfix_tail(e)!
|
||||
} else if type_args.len > 0 {
|
||||
return error('generic type arguments on a non-call "${t.lit}" (line ${t.line}, col ${t.col})')
|
||||
} else {
|
||||
e = p.parse_postfix_tail(Expr{ kind: .ident, name: t.lit, line: t.line })!
|
||||
}
|
||||
@@ -360,11 +503,11 @@ fn (mut p Parser) parse_stmt() !Stmt {
|
||||
}
|
||||
.kw_print, .kw_println {
|
||||
p.advance()
|
||||
e := p.parse_call(t)!
|
||||
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}')
|
||||
return error('unexpected token "${t.lit}" at line ${t.line}, col ${t.col}')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,6 +531,21 @@ fn (mut p Parser) parse_if(t Tok) !Stmt {
|
||||
return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line }
|
||||
}
|
||||
|
||||
// parse_try_stmt parses `try { body } catch ident { body }`.
|
||||
fn (mut p Parser) parse_try_stmt(t Tok) !Stmt {
|
||||
body := p.parse_block()!
|
||||
p.expect(.kw_catch, "'catch'")!
|
||||
catch_var := p.expect(.ident, 'error variable')!.lit
|
||||
catch_body := p.parse_block()!
|
||||
return Stmt{
|
||||
kind: .try_stmt
|
||||
body: body
|
||||
target: catch_var
|
||||
els: catch_body
|
||||
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).
|
||||
@@ -409,6 +567,15 @@ fn index_node(base Expr, idx Expr, line int) Expr {
|
||||
return Expr{ kind: .index, left: &b, right: &i, line: line }
|
||||
}
|
||||
|
||||
// slice_node builds `base[start..end]`.
|
||||
// end uses -1 as sentinel for "open-ended" (slice to end).
|
||||
fn slice_node(base Expr, start Expr, end Expr, _inclusive bool, line int) Expr {
|
||||
mut b := base
|
||||
mut s := start
|
||||
mut e := end
|
||||
return Expr{ kind: .slice, left: &b, right: &s, extra: &e, line: line }
|
||||
}
|
||||
|
||||
// field_node builds `base.name`.
|
||||
fn field_node(base Expr, name string, line int) Expr {
|
||||
mut b := base
|
||||
@@ -436,8 +603,41 @@ fn (mut p Parser) parse_or() !Expr {
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_and() !Expr {
|
||||
mut e := p.parse_eq()!
|
||||
mut e := p.parse_bitor()!
|
||||
for p.cur().kind == .kw_and {
|
||||
op := p.advance()
|
||||
rhs := p.parse_bitor()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// parse_bitor handles `|` (bitwise OR).
|
||||
fn (mut p Parser) parse_bitor() !Expr {
|
||||
mut e := p.parse_bitxor()!
|
||||
for p.cur().kind == .pipe {
|
||||
op := p.advance()
|
||||
rhs := p.parse_bitxor()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// parse_bitxor handles `^` (bitwise XOR).
|
||||
fn (mut p Parser) parse_bitxor() !Expr {
|
||||
mut e := p.parse_bitand()!
|
||||
for p.cur().kind == .caret {
|
||||
op := p.advance()
|
||||
rhs := p.parse_bitand()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// parse_bitand handles `&` (bitwise AND).
|
||||
fn (mut p Parser) parse_bitand() !Expr {
|
||||
mut e := p.parse_eq()!
|
||||
for p.cur().kind == .amp {
|
||||
op := p.advance()
|
||||
rhs := p.parse_eq()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
@@ -456,8 +656,19 @@ fn (mut p Parser) parse_eq() !Expr {
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_rel() !Expr {
|
||||
mut e := p.parse_add()!
|
||||
mut e := p.parse_shift()!
|
||||
for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge {
|
||||
op := p.advance()
|
||||
rhs := p.parse_shift()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// parse_shift handles `<<` and `>>` (bitwise shift).
|
||||
fn (mut p Parser) parse_shift() !Expr {
|
||||
mut e := p.parse_add()!
|
||||
for p.cur().kind == .lt_lt || p.cur().kind == .gt_gt {
|
||||
op := p.advance()
|
||||
rhs := p.parse_add()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
@@ -487,7 +698,7 @@ fn (mut p Parser) parse_mul() !Expr {
|
||||
|
||||
fn (mut p Parser) parse_unary() !Expr {
|
||||
t := p.cur()
|
||||
if t.kind == .kw_not || t.kind == .minus {
|
||||
if t.kind == .kw_not || t.kind == .minus || t.kind == .tilde {
|
||||
p.advance()
|
||||
e := p.parse_unary()!
|
||||
return unary_node(t.kind, e, t.line)
|
||||
@@ -514,8 +725,22 @@ fn (mut p Parser) parse_postfix_tail(e Expr) !Expr {
|
||||
if p.cur().kind == .lbracket {
|
||||
p.advance()
|
||||
idx := p.parse_expr()!
|
||||
p.expect(.rbracket, "']'")!
|
||||
cur = index_node(cur, idx, cur.line)
|
||||
if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot {
|
||||
// arr[start..end] or arr[start..] slicing
|
||||
p.advance()
|
||||
if p.cur().kind == .rbracket {
|
||||
// arr[start..] — slice to end
|
||||
p.advance()
|
||||
cur = slice_node(cur, idx, Expr{ kind: .int_lit, int_v: -1, line: cur.line }, false, cur.line)
|
||||
} else {
|
||||
end := p.parse_expr()!
|
||||
p.expect(.rbracket, "']'")!
|
||||
cur = slice_node(cur, idx, end, false, cur.line)
|
||||
}
|
||||
} else {
|
||||
p.expect(.rbracket, "']'")!
|
||||
cur = index_node(cur, idx, cur.line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if p.cur().kind == .dot {
|
||||
@@ -542,10 +767,18 @@ fn (mut p Parser) parse_primary() !Expr {
|
||||
p.advance()
|
||||
return Expr{ kind: .int_lit, int_v: t.lit.i64(), line: t.line }
|
||||
}
|
||||
.float_lit {
|
||||
p.advance()
|
||||
return Expr{ kind: .float_lit, float_v: t.lit.f64(), line: t.line }
|
||||
}
|
||||
.str_lit {
|
||||
p.advance()
|
||||
return Expr{ kind: .str_lit, str_v: t.lit, line: t.line }
|
||||
}
|
||||
.str_interp {
|
||||
p.advance()
|
||||
return p.parse_str_interp(t)!
|
||||
}
|
||||
.kw_true {
|
||||
p.advance()
|
||||
return Expr{ kind: .bool_lit, int_v: 1, line: t.line }
|
||||
@@ -594,24 +827,52 @@ fn (mut p Parser) parse_primary() !Expr {
|
||||
}
|
||||
.kw_print, .kw_println {
|
||||
p.advance()
|
||||
return p.parse_call(t)!
|
||||
return p.parse_call(t, [])!
|
||||
}
|
||||
.kw_fn {
|
||||
// anonymous function expression: fn(params) { body }
|
||||
p.advance()
|
||||
return p.parse_anon_fn(t)!
|
||||
}
|
||||
else {
|
||||
return error('unexpected token "${t.lit}" at line ${t.line}')
|
||||
return error('unexpected token "${t.lit}" at line ${t.line}, col ${t.col}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_call_or_ident(t Tok) !Expr {
|
||||
// generic type args: first[int](...) — captured for the type checker;
|
||||
// only treated as type args when [ is followed by idents then ] then (
|
||||
mut type_args := []string{}
|
||||
if p.cur().kind == .lbracket && p.looks_like_generic_args() {
|
||||
type_args = p.parse_type_args()!
|
||||
}
|
||||
if p.cur().kind == .lparen {
|
||||
return p.parse_call(t)!
|
||||
return p.parse_call(t, type_args)!
|
||||
}
|
||||
if type_args.len > 0 {
|
||||
return error('generic type arguments on a non-call "${t.lit}" (line ${t.line}, col ${t.col})')
|
||||
}
|
||||
return Expr{ kind: .ident, name: t.lit, line: t.line }
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_call(name Tok) !Expr {
|
||||
// parse_type_args parses `[int, string]` into a list of type names.
|
||||
fn (mut p Parser) parse_type_args() ![]string {
|
||||
p.advance() // consume '['
|
||||
mut args := []string{}
|
||||
for p.cur().kind != .rbracket {
|
||||
args << p.expect(.ident, 'type argument')!.lit
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
}
|
||||
}
|
||||
p.expect(.rbracket, "']'")!
|
||||
return args
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_call(name Tok, type_args []string) !Expr {
|
||||
args := p.parse_args()!
|
||||
return Expr{ kind: .call, name: name.lit, args: args, line: name.line }
|
||||
return Expr{ kind: .call, name: name.lit, type_args: type_args, args: args, line: name.line }
|
||||
}
|
||||
|
||||
// parse_args parses `(e1, e2, ...)` and returns the argument expressions.
|
||||
@@ -668,3 +929,113 @@ fn (mut p Parser) looks_like_struct_lit() bool {
|
||||
// typed struct literal: `{ ident :` or map literal: `{ "key" :`
|
||||
return (p.toks[p.pos + 1].kind == .ident || p.toks[p.pos + 1].kind == .str_lit) && p.toks[p.pos + 2].kind == .colon
|
||||
}
|
||||
|
||||
// looks_like_generic_args checks if `[` starts generic type args like `[T]` or `[T, U]`
|
||||
// rather than array indexing. It peeks ahead to see `ident ... ] (`.
|
||||
fn (mut p Parser) looks_like_generic_args() bool {
|
||||
// current token must be lbracket (the caller already checked this)
|
||||
if p.pos + 2 >= p.toks.len {
|
||||
return false
|
||||
}
|
||||
// must start with an ident after the [
|
||||
if p.toks[p.pos + 1].kind != .ident {
|
||||
return false
|
||||
}
|
||||
// scan forward: ident, comma, ident, ..., rbracket, then lparen
|
||||
mut i := p.pos + 2
|
||||
for i < p.toks.len && p.toks[i].kind != .rbracket {
|
||||
if p.toks[i].kind != .ident && p.toks[i].kind != .comma {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= p.toks.len {
|
||||
return false
|
||||
}
|
||||
// p.toks[i] should be rbracket
|
||||
if i + 1 >= p.toks.len {
|
||||
return false
|
||||
}
|
||||
return p.toks[i + 1].kind == .lparen
|
||||
}
|
||||
|
||||
// parse_str_interp handles string interpolation: "hello ${name} ${age}".
|
||||
// It splits the raw string into alternating text/expression parts and builds
|
||||
// a chain of + concatenations so the compiler needs no special handling.
|
||||
fn (mut p Parser) parse_str_interp(t Tok) !Expr {
|
||||
parts := split_str_interp(t.lit)
|
||||
// parts alternates: text, expr, text, expr, ..., text
|
||||
if parts.len == 1 {
|
||||
return Expr{ kind: .str_lit, str_v: parts[0], line: t.line }
|
||||
}
|
||||
// build the first string part
|
||||
mut result := Expr{ kind: .str_lit, str_v: parts[0], line: t.line }
|
||||
mut i := 1
|
||||
for i < parts.len {
|
||||
// parts[i] is an expression — tokenise and parse it
|
||||
expr_src := parts[i]
|
||||
expr_toks := tokenize(expr_src)!
|
||||
mut ep := Parser{ toks: expr_toks }
|
||||
expr := ep.parse_expr()!
|
||||
result = bin_node(.plus, result, expr, t.line)
|
||||
i++
|
||||
// parts[i] is the next text fragment
|
||||
if i < parts.len {
|
||||
if parts[i].len > 0 {
|
||||
str_e := Expr{ kind: .str_lit, str_v: parts[i], line: t.line }
|
||||
result = bin_node(.plus, result, str_e, t.line)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parse_anon_fn parses an anonymous function expression: `fn(params) { body }`.
|
||||
fn (mut p Parser) parse_anon_fn(t Tok) !Expr {
|
||||
params, defaults, has_defs, variadic := p.parse_params()!
|
||||
body := p.parse_block()!
|
||||
return Expr{
|
||||
kind: .anon_fn
|
||||
fparams: params
|
||||
fdefaults: defaults
|
||||
fhas_defs: has_defs
|
||||
fvariadic: variadic
|
||||
fn_body: body
|
||||
line: t.line
|
||||
}
|
||||
}
|
||||
|
||||
// split_str_interp splits an interpolated string at ${...} boundaries.
|
||||
// Returns alternating [text, expr, text, expr, ..., text] fragments.
|
||||
fn split_str_interp(s string) []string {
|
||||
mut parts := []string{}
|
||||
mut i := 0
|
||||
mut current := ''
|
||||
for i < s.len {
|
||||
if s[i] == `$` && i + 1 < s.len && s[i + 1] == `{` {
|
||||
parts << current
|
||||
current = ''
|
||||
i += 2
|
||||
mut depth := 1
|
||||
mut expr := ''
|
||||
for i < s.len && depth > 0 {
|
||||
if s[i] == `{` {
|
||||
depth++
|
||||
} else if s[i] == `}` {
|
||||
depth--
|
||||
}
|
||||
if depth > 0 {
|
||||
expr += s[i].ascii_str()
|
||||
}
|
||||
i++
|
||||
}
|
||||
parts << expr
|
||||
} else {
|
||||
current += s[i].ascii_str()
|
||||
i++
|
||||
}
|
||||
}
|
||||
parts << current
|
||||
return parts
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ pub enum TokKind {
|
||||
eof
|
||||
ident
|
||||
int_lit
|
||||
float_lit
|
||||
str_lit
|
||||
str_interp
|
||||
lparen
|
||||
rparen
|
||||
lbrace
|
||||
@@ -20,6 +22,12 @@ pub enum TokKind {
|
||||
star
|
||||
slash
|
||||
percent
|
||||
amp
|
||||
pipe
|
||||
caret
|
||||
tilde
|
||||
lt_lt
|
||||
gt_gt
|
||||
eq_eq
|
||||
not_eq
|
||||
lt
|
||||
@@ -56,6 +64,10 @@ pub enum TokKind {
|
||||
kw_import
|
||||
kw_enum
|
||||
kw_const
|
||||
kw_interface
|
||||
kw_try
|
||||
kw_catch
|
||||
kw_throw
|
||||
}
|
||||
|
||||
pub struct Tok {
|
||||
@@ -63,4 +75,5 @@ pub:
|
||||
kind TokKind
|
||||
lit string
|
||||
line int
|
||||
col int // 1-based column within the line
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user