This commit is contained in:
allexanderbergmns
2026-08-25 15:26:30 +02:00
parent 76e912c922
commit 4ccc8a0f95
16 changed files with 546 additions and 50 deletions
+2 -1
View File
@@ -122,7 +122,8 @@ pub mut:
pub struct ImportDecl {
pub mut:
path string
path string // the file to load (module name for bare `import os`)
name string // module name ('' for quoted file imports: flat merge)
line int
}
+44 -11
View File
@@ -46,7 +46,8 @@ mut:
enums map[string][]string
consts map[string]TypeInfo
loop_depth int
checked map[string]bool // imported files already checked
checked map[string]bool // imported files already checked
modules map[string]bool // imported module names (bare `import os`)
}
// check validates a parsed program and returns an error on the first problem.
@@ -76,7 +77,10 @@ fn check(prog Program) ! {
}
// imported files are checked (and their symbols merged) recursively
for imp in prog.imports {
c.check_import(imp.path)!
if imp.name.len > 0 {
c.modules[imp.name] = true
}
c.check_import(imp.path, imp.name)!
}
for fd in prog.fns {
c.check_fn(fd)!
@@ -93,7 +97,7 @@ fn def_count(fd FnDecl) int {
return n
}
fn (mut c Checker) check_import(path string) ! {
fn (mut c Checker) check_import(path string, mod_name string) ! {
if path in c.checked {
return
}
@@ -101,7 +105,9 @@ fn (mut c Checker) check_import(path string) ! {
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
// merge declarations from the import (bare modules register their
// functions under both the bare name and the "mod.fn" name, so internal
// calls check against the former and program calls against the latter)
for sd in prog.structs {
if sd.name !in c.structs {
c.structs[sd.name] = sd.fields
@@ -121,9 +127,15 @@ fn (mut c Checker) check_import(path string) ! {
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 }
}
if mod_name.len > 0 {
key := '${mod_name}.${fd.name}'
if key !in c.fns {
c.fns[key] = 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)!
c.check_import(imp.path, imp.name)!
}
for fd in prog.fns {
c.check_fn(fd)!
@@ -324,16 +336,35 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
}
TypeInfo{ kind: .unknown }
}
.method_call {
recv := c.check_expr(*e.left)!
if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t || recv.kind == .none_t {
return error('cannot call a method on a ${type_name(recv.kind)} (line ${e.line})')
}
.method_call {
// module call: os.exists(x) — the receiver is an imported module name
if e.left.kind == .ident && e.left.name in c.modules {
for a in e.args {
_ = c.check_expr(a)!
}
TypeInfo{ kind: .unknown }
key := '${e.left.name}.${e.name}'
if key in c.fns {
sig := c.fns[key]
if !sig.variadic {
if e.args.len < sig.min_args {
return error('${key}() 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('${key}() expects at most ${sig.has_defs.len} argument(s), got ${e.args.len} (line ${e.line})')
}
}
}
return TypeInfo{ kind: .unknown }
}
recv := c.check_expr(*e.left)!
if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t || recv.kind == .none_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)!
@@ -560,6 +591,8 @@ fn builtin_result_type(name string) TypeInfo {
// stdlib: JSON + string formatting
'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { TypeInfo{ kind: .string_t } }
'json_decode', 'split_lines' { TypeInfo{ kind: .unknown } }
'cwd', 'json_pretty' { TypeInfo{ kind: .string_t } }
'build_is_dir' { TypeInfo{ kind: .int_t } }
// build-module builtins (.vrmm)
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
'build_dir', 'build_join', 'build_root' { TypeInfo{ kind: .string_t } }
+60 -20
View File
@@ -43,6 +43,7 @@ mut:
loops []LoopCtx
enter_off u32
next_lbl int
modules map[string]bool // imported module names (bare `import os`)
}
fn gen(prog Program) !obj.Obj {
@@ -78,22 +79,41 @@ fn gen(prog Program) !obj.Obj {
}
g.structs[sd.name] = sd.fields
}
// compile imported files and merge their objects
// 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)!)!
// merge symbols from the imported object
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 {
g.symbols << s
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
}
// append imported bytecode and adjust relocations
code_off := g.code.len
g.code << imported.code
// adjust relocations (and prefix module-internal call targets)
for r in imported.relocs {
g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: r.name, kind: r.kind }
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 {
@@ -190,12 +210,14 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
// 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
// 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))
obj.patch_i64(mut g.code, f.off, i64(target - (int(f.off) + 8)))
}
g.fixups.clear()
g.labels.clear()
@@ -603,13 +625,24 @@ fn (mut g Gen) gen_expr(e Expr) ! {
g.emit_field_name(e.name)
g.code << op_sget
}
.method_call {
// 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 {
.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)!
@@ -713,8 +746,9 @@ fn (mut g Gen) gen_expr(e Expr) ! {
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))
// 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)))
g.code << op_closure
g.code << obj.encode_i64(0)
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 }
@@ -830,10 +864,12 @@ 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
// 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 })!
g.code << op_dup // separate the closure copy from the local slot
for a in e.args {
g.gen_expr(a)!
}
@@ -935,6 +971,9 @@ fn builtin_spec(name string) (int, int) {
'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 }
else { -1, 0 }
}
}
@@ -1166,6 +1205,7 @@ fn (mut g Gen) expr_type(e Expr) 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 { '' }
}
}
+11 -4
View File
@@ -4,6 +4,10 @@ module compiler
import os
import obj
// stdlib_dir is the toolchain's bundled library (lib/*.vr), resolved at build
// time so `import os` works from any working directory.
const stdlib_dir = @VMODROOT + '/lib'
// compile parses and compiles VuurRaaf source into an object file.
pub fn compile(src string) !obj.Obj {
toks := tokenize(src)!
@@ -19,16 +23,19 @@ pub fn compile_file(path string) !obj.Obj {
}
// 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.
// the path as given first, then the package-manager layout (vendor/) and the
// toolchain's own standard library (lib/), so both `import "pkg/file.vr"`
// and the bare module form `import os` resolve from anywhere.
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'] {
for cand in ['vendor/${path}', 'vendor/${path}.vr', 'vendor/${path}/main.vr',
'vendor/${path}/src/main.vr', os.join_path(stdlib_dir, path + '.vr'),
os.join_path(stdlib_dir, path, 'main.vr')] {
if os.exists(cand) {
return cand
}
}
return error('cannot resolve import "${path}" (tried vendor/)"')
return error('cannot resolve import "${path}" (tried vendor/ and lib/)')
}
+3
View File
@@ -138,3 +138,6 @@ const native_split_lines = 161
const native_pad = 162
const native_pad_left = 163
const native_repeat = 164
const native_build_is_dir = 165
const native_cwd = 166
const native_json_pretty = 167
+9 -4
View File
@@ -2,7 +2,7 @@
//
// Grammar (informal):
// program := import* (struct | enum | const | fn)*
// import := 'import' STRING
// import := 'import' STRING | 'import' IDENT
// struct := 'struct' IDENT '{' [IDENT (',' IDENT)*] '}'
// enum := 'enum' IDENT '{' [IDENT (',' IDENT)*] '}'
// const := 'const' IDENT '=' expr
@@ -123,11 +123,16 @@ fn (mut p Parser) parse_struct_decl() !StructDecl {
return StructDecl{ name: name.lit, fields: fields, line: t.line }
}
// parse_import parses `import "path/to/file.vr"`.
// parse_import parses `import "path/to/file.vr"` (flat file merge) or a
// bare module name like `import os` (namespaced stdlib/vendor module).
fn (mut p Parser) parse_import() !ImportDecl {
t := p.expect(.kw_import, "'import'")!
path := p.expect(.str_lit, 'import path')!
return ImportDecl{ path: path.lit, line: t.line }
if p.cur().kind == .str_lit {
path := p.advance().lit
return ImportDecl{ path: path, line: t.line }
}
name := p.expect(.ident, 'module name')!.lit
return ImportDecl{ name: name, path: name, line: t.line }
}
// parse_enum_decl parses `enum Name { variant1 variant2 ... }`.