Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
## Description
`v2` is a namespace containing a new version of the compiler (still in development).
## Flat AST
`v2.ast` is the default index-based AST graph used by the v2 builder for
parsing, type checking, markused, and native SSA input:
- `ast.flatten_files(files)` builds `ast.FlatAst` with contiguous `nodes` and `edges`.
- `ast.legacy_ast_stats(files)` estimates memory/shape metrics for recursive AST files.
- `flat.stats()` and `flat.count_reachable_nodes()` report flat graph size and reachability.
- `flat.to_files()` remains available for focused flat-reader roundtrip tests.
+638
View File
@@ -0,0 +1,638 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module abi
import v2.mir
import v2.pref
pub enum X64Abi {
sysv
windows
}
const sysv_int_arg_reg_count = 6
const sysv_sse_arg_reg_count = 8
const sysv_max_direct_aggregate_size = 16
struct SysVLocationState {
mut:
int_regs int
sse_regs int
stack_slots int
}
// lower annotates MIR with ABI classification metadata.
// Current scope is intentionally conservative: it classifies which arguments
// and return values must be passed indirectly for each function.
pub fn lower(mut m mir.Module, arch pref.Arch) {
lower_with_x64_abi(mut m, arch, .sysv)
}
pub fn lower_with_x64_abi(mut m mir.Module, arch pref.Arch, x64_abi X64Abi) {
is_x64 := arch == .x64
mut fn_by_name := map[string]int{}
for i := 0; i < m.funcs.len; i++ {
mut f := &m.funcs[i]
fn_by_name[f.name] = i
if is_x64 {
f.abi_ret_class = abi_value_class(m, f.typ, arch, x64_abi)
f.abi_ret_indirect = abi_class_is_indirect(f.abi_ret_class, m, f.typ, arch, x64_abi)
} else {
f.abi_ret_indirect = needs_indirect(m, f.typ, arch, x64_abi)
}
f.abi_param_class = []mir.AbiArgClass{len: f.params.len, init: .in_reg}
if is_x64 {
f.abi_param_classes = []mir.AbiValueClass{len: f.params.len}
f.abi_param_layouts = []mir.AbiValueLayout{len: f.params.len}
}
mut param_loc_state := SysVLocationState{}
if arch == .x64 && x64_abi == .sysv && f.abi_ret_indirect {
param_loc_state.int_regs = 1
}
for pi, param_id in f.params {
if param_id < 0 || param_id >= m.values.len {
continue
}
param_typ := m.values[param_id].typ
if is_x64 {
param_class := abi_value_class(m, param_typ, arch, x64_abi)
f.abi_param_classes[pi] = param_class
if x64_abi == .sysv {
f.abi_param_layouts[pi] = sysv_assign_value_layout(param_class, mut
param_loc_state)
}
if abi_class_is_indirect(param_class, m, param_typ, arch, x64_abi) {
f.abi_param_class[pi] = .indirect
}
} else if needs_indirect(m, param_typ, arch, x64_abi) {
f.abi_param_class[pi] = .indirect
}
}
}
lower_calls(mut m, arch, x64_abi, fn_by_name)
}
fn needs_indirect(m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) bool {
ssa_mod := m.ssa()
if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len {
return false
}
typ := ssa_mod.type_store.types[typ_id]
if typ.kind !in [.struct_t, .array_t] {
return false
}
size := m.type_size(typ_id)
return match arch {
.arm64 {
size > 16
}
.x64 {
match x64_abi {
.sysv { size > sysv_max_direct_aggregate_size }
.windows { size !in [1, 2, 4, 8] }
}
}
else {
size > 16
}
}
}
fn abi_class_is_indirect(value_class mir.AbiValueClass, m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) bool {
if value_class.mode == .indirect {
return true
}
return needs_indirect(m, typ_id, arch, x64_abi)
}
fn abi_value_class(m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) mir.AbiValueClass {
ssa_mod := m.ssa()
size := m.type_size(typ_id)
if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len {
return mir.AbiValueClass{
mode: .direct
size: size
}
}
typ := ssa_mod.type_store.types[typ_id]
if arch == .x64 && x64_abi == .sysv {
if typ.kind !in [.struct_t, .array_t] {
return sysv_scalar_abi_value_class(m, typ_id)
}
return sysv_abi_value_class(m, typ_id)
}
indirect := needs_indirect(m, typ_id, arch, x64_abi)
return mir.AbiValueClass{
mode: if indirect { .indirect } else { .direct }
size: size
}
}
fn sysv_scalar_abi_value_class(m mir.Module, typ_id int) mir.AbiValueClass {
ssa_mod := m.ssa()
size := m.type_size(typ_id)
if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len {
return mir.AbiValueClass{
mode: .direct
size: size
}
}
typ := ssa_mod.type_store.types[typ_id]
classes := match typ.kind {
.int_t, .ptr_t, .func_t {
[mir.AbiEightbyteClass.integer]
}
.float_t {
match typ.width {
32, 64 { [mir.AbiEightbyteClass.sse] }
128 { [mir.AbiEightbyteClass.sse, .sseup] }
else { []mir.AbiEightbyteClass{} }
}
}
else {
[]mir.AbiEightbyteClass{}
}
}
return mir.AbiValueClass{
mode: .direct
size: size
classes: classes
}
}
fn sysv_abi_value_class(m mir.Module, typ_id int) mir.AbiValueClass {
size := m.type_size(typ_id)
if size <= 0 {
return mir.AbiValueClass{
mode: .direct
size: size
}
}
if sysv_aggregate_must_be_memory_before_classification(size) {
return sysv_memory_value_class(size)
}
mut classes := []mir.AbiEightbyteClass{len: (size + 7) / 8, init: .no_class}
mut visiting := map[int]bool{}
if !sysv_classify_type_into(m, typ_id, 0, mut classes, mut visiting) {
return sysv_memory_value_class(size)
}
classes = sysv_post_merge_classes(size, classes)
if classes.len == 1 && classes[0] == .memory {
return sysv_memory_value_class(size)
}
return mir.AbiValueClass{
mode: .direct
size: size
classes: classes
}
}
fn sysv_aggregate_must_be_memory_before_classification(size int) bool {
// Current SSA aggregate types do not model a SysV vector aggregate larger
// than two eightbytes, so larger struct/array aggregates cannot be passed
// in registers and should not be classified element by element.
return size > sysv_max_direct_aggregate_size
}
fn sysv_memory_value_class(size int) mir.AbiValueClass {
return mir.AbiValueClass{
mode: .indirect
size: size
classes: [.memory]
}
}
fn sysv_classify_type_into(m mir.Module, typ_id int, byte_offset int, mut classes []mir.AbiEightbyteClass, mut visiting map[int]bool) bool {
ssa_mod := m.ssa()
if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len {
return true
}
if visiting[typ_id] {
return sysv_merge_class_range(mut classes, byte_offset, 8, .integer)
}
visiting[typ_id] = true
typ := ssa_mod.type_store.types[typ_id]
ok := match typ.kind {
.int_t, .ptr_t, .func_t {
sysv_merge_class_range(mut classes, byte_offset, m.type_size(typ_id), .integer)
}
.float_t {
match typ.width {
32, 64 {
sysv_merge_class_range(mut classes, byte_offset, m.type_size(typ_id), .sse)
}
80 {
false
}
128 {
sysv_merge_class_range(mut classes, byte_offset, 8, .sse)
&& sysv_merge_class_range(mut classes, byte_offset + 8, 8, .sseup)
}
else {
false
}
}
}
.array_t {
sysv_classify_array_into(m, typ.elem_type, typ.len, byte_offset, mut classes, mut
visiting)
}
.struct_t {
sysv_classify_struct_into(m, typ.fields, byte_offset, mut classes, mut visiting)
}
.void_t, .label_t, .metadata_t {
true
}
}
visiting.delete(typ_id)
return ok
}
fn sysv_classify_array_into(m mir.Module, elem_type int, len int, byte_offset int, mut classes []mir.AbiEightbyteClass, mut visiting map[int]bool) bool {
elem_size := m.type_size(elem_type)
for i := 0; i < len; i++ {
if !sysv_classify_type_into(m, elem_type, byte_offset + i * elem_size, mut classes, mut
visiting) {
return false
}
}
return true
}
fn sysv_classify_struct_into(m mir.Module, fields []int, byte_offset int, mut classes []mir.AbiEightbyteClass, mut visiting map[int]bool) bool {
mut field_offset := 0
for field_typ in fields {
align := m.type_align(field_typ)
if align > 1 && field_offset % align != 0 {
field_offset = (field_offset + align - 1) & ~(align - 1)
}
if !sysv_classify_type_into(m, field_typ, byte_offset + field_offset, mut classes, mut
visiting) {
return false
}
field_offset += m.type_size(field_typ)
}
return true
}
fn sysv_merge_class_range(mut classes []mir.AbiEightbyteClass, byte_offset int, size int, class mir.AbiEightbyteClass) bool {
if size <= 0 {
return true
}
start := byte_offset / 8
end := (byte_offset + size + 7) / 8
if start < 0 || end > classes.len {
return false
}
for i := start; i < end; i++ {
classes[i] = sysv_merge_eightbyte_class(classes[i], class)
if classes[i] == .memory {
return false
}
}
return true
}
fn sysv_merge_eightbyte_class(a mir.AbiEightbyteClass, b mir.AbiEightbyteClass) mir.AbiEightbyteClass {
if a == b {
return a
}
if a == .no_class {
return b
}
if b == .no_class {
return a
}
if a == .memory || b == .memory {
return .memory
}
if a == .integer || b == .integer {
return .integer
}
return .sse
}
fn sysv_post_merge_classes(size int, input []mir.AbiEightbyteClass) []mir.AbiEightbyteClass {
mut classes := input.clone()
for class in classes {
if class == .memory {
return [.memory]
}
}
if size > sysv_max_direct_aggregate_size {
mut vector_like := classes.len > 0 && classes[0] == .sse
for i := 1; i < classes.len; i++ {
if classes[i] != .sseup {
vector_like = false
break
}
}
if !vector_like {
return [.memory]
}
}
for i, class in classes {
if class == .sseup && (i == 0 || classes[i - 1] !in [.sse, .sseup]) {
classes[i] = .sse
}
}
return classes
}
fn sysv_assign_value_layout(value_class mir.AbiValueClass, mut state SysVLocationState) mir.AbiValueLayout {
if value_class.classes.len == 0 {
return mir.AbiValueLayout{
value_class: value_class
}
}
if value_class.mode == .indirect || value_class.classes == [mir.AbiEightbyteClass.memory] {
return sysv_indirect_pointer_layout(value_class, mut state)
}
mut needed_int := 0
mut needed_sse := 0
for class in value_class.classes {
match class {
.integer { needed_int++ }
.sse { needed_sse++ }
else {}
}
}
if state.int_regs + needed_int > sysv_int_arg_reg_count
|| state.sse_regs + needed_sse > sysv_sse_arg_reg_count {
return sysv_stack_value_layout(value_class, mut state)
}
mut locs := []mir.AbiLocation{cap: value_class.classes.len}
mut last_sse := -1
for i, class in value_class.classes {
offset := i * 8
match class {
.integer {
locs << mir.AbiLocation{
kind: .int_reg
index: state.int_regs
offset: offset
class: class
}
state.int_regs++
}
.sse {
last_sse = state.sse_regs
locs << mir.AbiLocation{
kind: .sse_reg
index: state.sse_regs
offset: offset
class: class
}
state.sse_regs++
}
.sseup {
locs << mir.AbiLocation{
kind: .sse_reg
index: if last_sse >= 0 { last_sse } else { state.sse_regs }
offset: offset
class: class
}
}
else {
locs << mir.AbiLocation{
kind: .none
index: -1
offset: offset
class: class
}
}
}
}
return mir.AbiValueLayout{
value_class: value_class
locs: locs
}
}
fn sysv_indirect_pointer_layout(value_class mir.AbiValueClass, mut state SysVLocationState) mir.AbiValueLayout {
mut locs := []mir.AbiLocation{cap: 1}
if state.int_regs < sysv_int_arg_reg_count {
locs << mir.AbiLocation{
kind: .int_reg
index: state.int_regs
offset: 0
class: .integer
}
state.int_regs++
} else {
locs << mir.AbiLocation{
kind: .stack
index: state.stack_slots
offset: 0
class: .integer
}
state.stack_slots++
}
return mir.AbiValueLayout{
value_class: value_class
locs: locs
}
}
fn sysv_stack_value_layout(value_class mir.AbiValueClass, mut state SysVLocationState) mir.AbiValueLayout {
slots := if value_class.size > 0 { (value_class.size + 7) / 8 } else { value_class.classes.len }
mut locs := []mir.AbiLocation{cap: slots}
for i := 0; i < slots; i++ {
class := if i < value_class.classes.len {
value_class.classes[i]
} else {
mir.AbiEightbyteClass.memory
}
locs << mir.AbiLocation{
kind: .stack
index: state.stack_slots
offset: i * 8
class: class
}
state.stack_slots++
}
return mir.AbiValueLayout{
value_class: value_class
locs: locs
}
}
fn fallback_arg_type(m mir.Module, arg_id int) int {
if arg_id < 0 || arg_id >= m.values.len {
return 0
}
if logical_typ := logical_arg_type_from_value(m, arg_id, 0) {
return logical_typ
}
return m.values[arg_id].typ
}
fn logical_arg_type_from_value(m mir.Module, val_id int, depth int) ?int {
if depth > 8 || val_id < 0 || val_id >= m.values.len {
return none
}
val := m.values[val_id]
if val.kind != .instruction {
return none
}
instr := m.instrs[val.index]
match instr.op {
.alloca {
if val.typ <= 0 || val.typ >= m.type_store.types.len {
return none
}
typ := m.type_store.types[val.typ]
if typ.kind != .ptr_t || typ.elem_type <= 0 || typ.elem_type >= m.type_store.types.len {
return none
}
elem_typ := m.type_store.types[typ.elem_type]
if elem_typ.kind in [.struct_t, .array_t] {
return typ.elem_type
}
}
.bitcast {
if instr.operands.len > 0 {
return logical_arg_type_from_value(m, instr.operands[0], depth + 1)
}
}
.assign {
if instr.operands.len > 1 {
return logical_arg_type_from_value(m, instr.operands[1], depth + 1)
}
}
else {}
}
return none
}
fn lower_calls(mut m mir.Module, arch pref.Arch, x64_abi X64Abi, fn_by_name map[string]int) {
if arch !in [.arm64, .x64] {
return
}
is_x64 := arch == .x64
for i := 0; i < m.instrs.len; i++ {
mut instr := &m.instrs[i]
if instr.op !in [.call, .call_indirect, .call_sret] || instr.operands.len == 0 {
continue
}
ret_typ, sig_param_types := call_signature(m, instr, fn_by_name)
ret_class := if is_x64 {
abi_value_class(m, ret_typ, arch, x64_abi)
} else {
mir.AbiValueClass{}
}
ret_indirect := if is_x64 {
abi_class_is_indirect(ret_class, m, ret_typ, arch, x64_abi)
} else {
needs_indirect(m, ret_typ, arch, x64_abi)
}
num_args := instr.operands.len - 1
instr.abi_arg_class = []mir.AbiArgClass{len: num_args, init: .in_reg}
if is_x64 {
instr.abi_arg_classes = []mir.AbiValueClass{len: num_args}
instr.abi_arg_layouts = []mir.AbiValueLayout{len: num_args}
}
mut arg_loc_state := SysVLocationState{}
if arch == .x64 && x64_abi == .sysv && ret_indirect {
arg_loc_state.int_regs = 1
}
for arg_idx := 0; arg_idx < num_args; arg_idx++ {
mut arg_typ := 0
if arg_idx < sig_param_types.len && sig_param_types[arg_idx] > 0 {
arg_typ = sig_param_types[arg_idx]
} else {
arg_id := instr.operands[arg_idx + 1]
arg_typ = fallback_arg_type(m, arg_id)
}
if is_x64 {
arg_class := abi_value_class(m, arg_typ, arch, x64_abi)
instr.abi_arg_classes[arg_idx] = arg_class
if x64_abi == .sysv {
instr.abi_arg_layouts[arg_idx] = sysv_assign_value_layout(arg_class, mut
arg_loc_state)
}
if abi_class_is_indirect(arg_class, m, arg_typ, arch, x64_abi) {
instr.abi_arg_class[arg_idx] = .indirect
}
} else if needs_indirect(m, arg_typ, arch, x64_abi) {
instr.abi_arg_class[arg_idx] = .indirect
}
}
instr.abi_ret_class = ret_class
instr.abi_ret_indirect = ret_indirect
// Lower ABI-indirect returns to call_sret for backend consumption.
if instr.abi_ret_indirect {
instr.op = .call_sret
}
}
}
fn call_signature(m mir.Module, instr &mir.Instruction, fn_by_name map[string]int) (int, []int) {
mut ret_typ := instr.typ
mut param_types := []int{}
if instr.operands.len == 0 {
return ret_typ, param_types
}
if instr.op in [.call, .call_sret] {
callee_id := instr.operands[0]
if callee_id >= 0 && callee_id < m.values.len {
callee_name := m.values[callee_id].name
if callee_name != '' && callee_name in fn_by_name {
fn_idx := fn_by_name[callee_name]
if fn_idx >= 0 && fn_idx < m.funcs.len {
callee := m.funcs[fn_idx]
ret_typ = callee.typ
param_types = []int{len: callee.params.len}
for i, pid in callee.params {
if pid >= 0 && pid < m.values.len {
param_types[i] = m.values[pid].typ
}
}
}
}
}
} else if instr.op == .call_indirect {
callee_id := instr.operands[0]
if callee_id >= 0 && callee_id < m.values.len {
fn_ptr_typ_id := m.values[callee_id].typ
if fn_ptr_typ_id > 0 && fn_ptr_typ_id < m.type_store.types.len {
fn_ptr_typ := m.type_store.types[fn_ptr_typ_id]
if fn_ptr_typ.kind == .ptr_t && fn_ptr_typ.elem_type > 0
&& fn_ptr_typ.elem_type < m.type_store.types.len {
fn_typ := m.type_store.types[fn_ptr_typ.elem_type]
if fn_typ.kind == .func_t {
ret_typ = fn_typ.ret_type
param_types = fn_typ.params.clone()
}
} else if fn_ptr_typ.kind == .func_t {
// Function pointer extracted from struct field via extractvalue
// has func_t type directly (not wrapped in ptr_t).
ret_typ = fn_ptr_typ.ret_type
param_types = fn_ptr_typ.params.clone()
}
}
}
}
// Fallback to call operand types when we could not resolve a signature.
if param_types.len == 0 {
num_args := if instr.operands.len > 0 { instr.operands.len - 1 } else { 0 }
param_types = []int{len: num_args}
for i := 0; i < num_args; i++ {
arg_id := instr.operands[i + 1]
param_types[i] = fallback_arg_type(m, arg_id)
}
}
return ret_typ, param_types
}
+388
View File
@@ -0,0 +1,388 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
// vtest build: macos
module abi
import os
import v2.ast
import v2.mir
import v2.parser
import v2.pref
import v2.ssa
import v2.token
fn parse_code_for_test(code string) []ast.File {
tmp_file := os.join_path(os.temp_dir(), 'v2_abi_parser_test_${os.getpid()}.v')
os.write_file(tmp_file, code) or { panic(err) }
defer {
os.rm(tmp_file) or {}
}
prefs := &pref.Preferences{}
mut file_set := token.FileSet.new()
mut p := parser.Parser.new(prefs)
return p.parse_files([tmp_file], mut file_set)
}
fn first_fn_decl(file ast.File) ast.FnDecl {
for stmt in file.stmts {
if stmt is ast.FnDecl {
return stmt
}
}
panic('expected function declaration in parsed file')
}
fn test_parser_sql_expr_allows_nested_braces_in_body() {
files := parse_code_for_test("
module main
fn main() {
req_status := true
sql db {
update User set
name = 'Jengro',
status = if req_status { 1 } else { 0 }
status = fn [req_status] () u8 { if req_status { return 1 } else { return 0 } }()
where id == '100'
}
}
")
assert files.len == 1
main_fn := first_fn_decl(files[0])
assert main_fn.stmts.len == 2
assert main_fn.stmts[1] is ast.ExprStmt
sql_stmt := main_fn.stmts[1] as ast.ExprStmt
assert sql_stmt.expr is ast.SqlExpr
}
fn test_arm64_large_struct_call_is_lowered_to_call_sret() {
mut ssa_mod := ssa.Module.new('abi_test')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
ptr_t := ssa_mod.type_store.get_ptr(i8_t)
large_struct_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [ptr_t, i64_t, i64_t] // 24 bytes
})
callee_id := ssa_mod.new_function('callee', large_struct_t, [])
callee_entry := ssa_mod.add_block(callee_id, 'entry')
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, callee_entry, 0, [zero])
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, large_struct_t, [fn_val])
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .arm64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.op == .call_sret
}
fn test_x64_large_struct_call_is_lowered_to_call_sret() {
mut ssa_mod := ssa.Module.new('abi_test_x64')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
ptr_t := ssa_mod.type_store.get_ptr(i8_t)
large_struct_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [ptr_t, i64_t, i64_t]
})
callee_id := ssa_mod.new_function('callee', large_struct_t, [])
callee_entry := ssa_mod.add_block(callee_id, 'entry')
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, callee_entry, 0, [zero])
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, large_struct_t, [fn_val])
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .x64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.op == .call_sret
}
fn test_x64_sysv_sixteen_byte_struct_return_stays_direct() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_16_ret')
i64_t := ssa_mod.type_store.get_int(64)
struct16_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i64_t]
})
callee_id := ssa_mod.new_function('callee', struct16_t, [])
callee_entry := ssa_mod.add_block(callee_id, 'entry')
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, callee_entry, 0, [zero])
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, struct16_t, [fn_val])
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .x64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.op == .call
assert !call_instr.abi_ret_indirect
}
fn test_x64_windows_struct_return_classification() {
mut ssa_mod := ssa.Module.new('abi_test_x64_windows_ret')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
struct8_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t]
})
struct9_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i8_t]
})
struct16_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i64_t]
})
fn8 := ssa_mod.new_function('ret8', struct8_t, [])
fn9 := ssa_mod.new_function('ret9', struct9_t, [])
fn16 := ssa_mod.new_function('ret16', struct16_t, [])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .windows)
assert !mir_mod.funcs[fn8].abi_ret_indirect
assert mir_mod.funcs[fn9].abi_ret_indirect
assert mir_mod.funcs[fn16].abi_ret_indirect
}
fn test_x64_windows_struct_param_classification() {
mut ssa_mod := ssa.Module.new('abi_test_x64_windows_param')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
struct8_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t]
})
struct9_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i8_t]
})
struct16_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i64_t]
})
fn_id := ssa_mod.new_function('callee', i64_t, [])
param8 := ssa_mod.add_value_node(.argument, struct8_t, 's8', 0)
param9 := ssa_mod.add_value_node(.argument, struct9_t, 's9', 0)
param16 := ssa_mod.add_value_node(.argument, struct16_t, 's16', 0)
ssa_mod.funcs[fn_id].params << [param8, param9, param16]
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .windows)
assert mir_mod.funcs[fn_id].abi_param_class == [.in_reg, .indirect, .indirect]
}
fn test_x64_windows_callsite_struct_arg_classification() {
mut ssa_mod := ssa.Module.new('abi_test_x64_windows_call_arg')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
struct8_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t]
})
struct9_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i8_t]
})
struct16_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [i64_t, i64_t]
})
callee_id := ssa_mod.new_function('callee', i64_t, [])
param8 := ssa_mod.add_value_node(.argument, struct8_t, 's8', 0)
param9 := ssa_mod.add_value_node(.argument, struct9_t, 's9', 0)
param16 := ssa_mod.add_value_node(.argument, struct16_t, 's16', 0)
ssa_mod.funcs[callee_id].params << [param8, param9, param16]
caller_id := ssa_mod.new_function('caller', i64_t, [])
arg8 := ssa_mod.add_value_node(.argument, struct8_t, 'arg8', 0)
arg9 := ssa_mod.add_value_node(.argument, struct9_t, 'arg9', 0)
arg16 := ssa_mod.add_value_node(.argument, struct16_t, 'arg16', 0)
ssa_mod.funcs[caller_id].params << [arg8, arg9, arg16]
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, arg8, arg9, arg16])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .windows)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class == [.in_reg, .indirect, .indirect]
}
fn test_arm64_external_large_struct_call_is_lowered_to_call_sret() {
mut ssa_mod := ssa.Module.new('abi_test_external')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
ptr_t := ssa_mod.type_store.get_ptr(i8_t)
large_struct_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [ptr_t, i64_t, i64_t]
})
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
external_fn := ssa_mod.add_value_node(.unknown, 0, 'external_make', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, large_struct_t, [external_fn])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .arm64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.op == .call_sret
}
fn test_arm64_indirect_call_large_struct_return_is_lowered_to_call_sret() {
mut ssa_mod := ssa.Module.new('abi_test_indirect')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
ptr_t := ssa_mod.type_store.get_ptr(i8_t)
large_struct_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [ptr_t, i64_t, i64_t]
})
fn_sig_t := ssa_mod.type_store.register(ssa.Type{
kind: .func_t
params: []ssa.TypeID{}
ret_type: large_struct_t
})
fn_ptr_t := ssa_mod.type_store.get_ptr(fn_sig_t)
caller_id := ssa_mod.new_function('caller', i64_t, [])
fn_ptr := ssa_mod.add_value_node(.argument, fn_ptr_t, 'fn_ptr', 0)
ssa_mod.funcs[caller_id].params << fn_ptr
caller_entry := ssa_mod.add_block(caller_id, 'entry')
call_val := ssa_mod.add_instr(.call_indirect, caller_entry, large_struct_t, [
fn_ptr,
])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .arm64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.op == .call_sret
}
fn test_arm64_callsite_marks_large_struct_arg_indirect() {
mut ssa_mod := ssa.Module.new('abi_test_arg_class')
i64_t := ssa_mod.type_store.get_int(64)
i8_t := ssa_mod.type_store.get_int(8)
ptr_t := ssa_mod.type_store.get_ptr(i8_t)
large_struct_t := ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: [ptr_t, i64_t, i64_t]
})
callee_id := ssa_mod.new_function('callee', i64_t, [])
callee_param := ssa_mod.add_value_node(.argument, large_struct_t, 'arg', 0)
ssa_mod.funcs[callee_id].params << callee_param
callee_entry := ssa_mod.add_block(callee_id, 'entry')
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, callee_entry, 0, [zero])
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_param := ssa_mod.add_value_node(.argument, large_struct_t, 'in', 0)
ssa_mod.funcs[caller_id].params << caller_param
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, caller_param])
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .arm64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class.len == 1
assert call_instr.abi_arg_class[0] == .indirect
}
fn test_arm64_callsite_marks_large_array_arg_indirect() {
mut ssa_mod := ssa.Module.new('abi_test_array_arg_class')
i64_t := ssa_mod.type_store.get_int(64)
large_array_t := ssa_mod.type_store.register(ssa.Type{
kind: .array_t
elem_type: i64_t
len: 3
})
callee_id := ssa_mod.new_function('callee', i64_t, [])
callee_param := ssa_mod.add_value_node(.argument, large_array_t, 'arg', 0)
ssa_mod.funcs[callee_id].params << callee_param
callee_entry := ssa_mod.add_block(callee_id, 'entry')
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, callee_entry, 0, [zero])
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_param := ssa_mod.add_value_node(.argument, large_array_t, 'in', 0)
ssa_mod.funcs[caller_id].params << caller_param
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, caller_param])
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .arm64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class.len == 1
assert call_instr.abi_arg_class[0] == .indirect
}
fn test_arm64_unresolved_callsite_marks_large_array_alloca_arg_indirect() {
mut ssa_mod := ssa.Module.new('abi_test_array_alloca_arg_class')
i64_t := ssa_mod.type_store.get_int(64)
large_array_t := ssa_mod.type_store.register(ssa.Type{
kind: .array_t
elem_type: i64_t
len: 3
})
large_array_ptr_t := ssa_mod.type_store.get_ptr(large_array_t)
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
tmp := ssa_mod.add_instr(.alloca, caller_entry, large_array_ptr_t, []ssa.ValueID{})
external_fn := ssa_mod.add_value_node(.unknown, 0, 'external_append_like', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [external_fn, tmp])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower(mut mir_mod, .arm64)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class.len == 1
assert call_instr.abi_arg_class[0] == .indirect
}
@@ -0,0 +1,999 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module abi
import v2.mir
import v2.ssa
fn register_x64_matrix_struct_bytes(mut ssa_mod ssa.Module, size int) ssa.TypeID {
i8_t := ssa_mod.type_store.get_int(8)
mut fields := []ssa.TypeID{}
for _ in 0 .. size {
fields << i8_t
}
return register_x64_matrix_struct_fields(mut ssa_mod, fields)
}
fn register_x64_matrix_struct_fields(mut ssa_mod ssa.Module, fields []ssa.TypeID) ssa.TypeID {
return ssa_mod.type_store.register(ssa.Type{
kind: .struct_t
fields: fields
})
}
fn assert_abi_value_class(class mir.AbiValueClass, mode mir.AbiPassMode, classes []mir.AbiEightbyteClass) {
assert class.mode == mode
assert class.classes == classes
}
fn assert_abi_layout_locs(layout mir.AbiValueLayout, kinds []mir.AbiLocationKind, indexes []int) {
assert layout.locs.len == kinds.len
assert indexes.len == kinds.len
for i, kind in kinds {
assert layout.locs[i].kind == kind
assert layout.locs[i].index == indexes[i]
}
}
fn assert_abi_layout(layout mir.AbiValueLayout, kinds []mir.AbiLocationKind, indexes []int, offsets []int, classes []mir.AbiEightbyteClass) {
assert layout.locs.len == kinds.len
assert indexes.len == kinds.len
assert offsets.len == kinds.len
assert classes.len == kinds.len
for i, kind in kinds {
assert layout.locs[i].kind == kind
assert layout.locs[i].index == indexes[i]
assert layout.locs[i].offset == offsets[i]
assert layout.locs[i].class == classes[i]
}
}
fn build_x64_matrix_direct_call_module(x64_abi X64Abi) (mir.Module, ssa.ValueID) {
mut ssa_mod := ssa.Module.new('abi_test_x64_direct_call_matrix')
i64_t := ssa_mod.type_store.get_int(64)
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
mut struct_types := []ssa.TypeID{}
for size in sizes {
struct_types << register_x64_matrix_struct_bytes(mut ssa_mod, size)
}
callee_id := ssa_mod.new_function('callee', i64_t, [])
for i, struct_t in struct_types {
param := ssa_mod.add_value_node(.argument, struct_t, 'p${sizes[i]}', 0)
ssa_mod.funcs[callee_id].params << param
}
caller_id := ssa_mod.new_function('caller', i64_t, [])
mut args := []ssa.ValueID{}
for i, struct_t in struct_types {
arg := ssa_mod.add_value_node(.argument, struct_t, 'a${sizes[i]}', 0)
ssa_mod.funcs[caller_id].params << arg
args << arg
}
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
mut operands := [fn_val]
operands << args
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, operands)
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, x64_abi)
return mir_mod, call_val
}
fn build_x64_matrix_return_call_module(x64_abi X64Abi, size int) (mir.Module, ssa.ValueID) {
mut ssa_mod := ssa.Module.new('abi_test_x64_return_call_matrix')
i64_t := ssa_mod.type_store.get_int(64)
struct_t := register_x64_matrix_struct_bytes(mut ssa_mod, size)
callee_id := ssa_mod.new_function('callee', struct_t, [])
callee_entry := ssa_mod.add_block(callee_id, 'entry')
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, callee_entry, 0, [zero])
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, struct_t, [fn_val])
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, x64_abi)
return mir_mod, call_val
}
fn build_sysv_layout_call_module(prefix_count int, prefix_float bool, aggregate_kind string) (mir.Module, int, ssa.ValueID, int) {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_location_metadata')
i64_t := ssa_mod.type_store.get_int(64)
f64_t := ssa_mod.type_store.get_float(64)
prefix_t := if prefix_float { f64_t } else { i64_t }
aggregate_t := match aggregate_kind {
'i64_i64' { register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, i64_t]) }
'f64_f64' { register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, f64_t]) }
'i64_f64' { register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, f64_t]) }
else { register_x64_matrix_struct_fields(mut ssa_mod, [i64_t]) }
}
callee_id := ssa_mod.new_function('callee', i64_t, [])
mut param_types := []ssa.TypeID{len: prefix_count, init: prefix_t}
param_types << aggregate_t
for i, typ in param_types {
param := ssa_mod.add_value_node(.argument, typ, 'p${i}', 0)
ssa_mod.funcs[callee_id].params << param
}
caller_id := ssa_mod.new_function('caller', i64_t, [])
mut args := []ssa.ValueID{}
for i, typ in param_types {
arg := ssa_mod.add_value_node(.argument, typ, 'a${i}', 0)
ssa_mod.funcs[caller_id].params << arg
args << arg
}
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
mut operands := [fn_val]
operands << args
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, operands)
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
return mir_mod, callee_id, call_val, prefix_count
}
fn build_sysv_scalar_boundary_module(mut ssa_mod ssa.Module, typ ssa.TypeID, count int) (mir.Module, int, ssa.ValueID) {
i64_t := ssa_mod.type_store.get_int(64)
callee_id := ssa_mod.new_function('callee', i64_t, [])
for i := 0; i < count; i++ {
param := ssa_mod.add_value_node(.argument, typ, 'p${i}', 0)
ssa_mod.funcs[callee_id].params << param
}
caller_id := ssa_mod.new_function('caller', i64_t, [])
mut args := []ssa.ValueID{}
for i := 0; i < count; i++ {
arg := ssa_mod.add_value_node(.argument, typ, 'a${i}', 0)
ssa_mod.funcs[caller_id].params << arg
args << arg
}
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
mut operands := [fn_val]
operands << args
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, operands)
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
return mir_mod, callee_id, call_val
}
fn test_x64_windows_struct_size_matrix_classification() {
mut ssa_mod := ssa.Module.new('abi_test_x64_windows_size_matrix')
i64_t := ssa_mod.type_store.get_int(64)
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
direct_sizes := [1, 2, 4, 8]
mut struct_types := []ssa.TypeID{}
for size in sizes {
struct_types << register_x64_matrix_struct_bytes(mut ssa_mod, size)
}
param_fn_id := ssa_mod.new_function('param_matrix', i64_t, [])
for i, struct_t in struct_types {
param := ssa_mod.add_value_node(.argument, struct_t, 's${sizes[i]}', 0)
ssa_mod.funcs[param_fn_id].params << param
}
mut ret_fn_ids := []int{}
for i, struct_t in struct_types {
ret_fn_ids << ssa_mod.new_function('ret${sizes[i]}', struct_t, [])
}
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .windows)
for i, size in sizes {
should_be_indirect := size !in direct_sizes
assert (mir_mod.funcs[param_fn_id].abi_param_class[i] == .indirect) == should_be_indirect
assert mir_mod.funcs[ret_fn_ids[i]].abi_ret_indirect == should_be_indirect
}
}
fn test_x64_windows_callsite_return_size_matrix_classification() {
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
direct_sizes := [1, 2, 4, 8]
for size in sizes {
mir_mod, call_val := build_x64_matrix_return_call_module(.windows, size)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
should_be_indirect := size !in direct_sizes
assert call_instr.abi_ret_indirect == should_be_indirect
assert (call_instr.op == .call_sret) == should_be_indirect
}
}
fn test_x64_windows_direct_callsite_struct_size_matrix_classification() {
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
direct_sizes := [1, 2, 4, 8]
mir_mod, call_val := build_x64_matrix_direct_call_module(.windows)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class.len == sizes.len
for i, size in sizes {
should_be_indirect := size !in direct_sizes
assert (call_instr.abi_arg_class[i] == .indirect) == should_be_indirect
}
}
fn test_non_sysv_legacy_thresholds_stay_unchanged_with_metadata_fields() {
mut ssa_mod := ssa.Module.new('abi_test_non_sysv_legacy_metadata_guard')
i64_t := ssa_mod.type_store.get_int(64)
struct9_t := register_x64_matrix_struct_bytes(mut ssa_mod, 9)
struct16_t := register_x64_matrix_struct_bytes(mut ssa_mod, 16)
struct17_t := register_x64_matrix_struct_bytes(mut ssa_mod, 17)
param_fn_id := ssa_mod.new_function('param_matrix', i64_t, [])
for typ in [struct9_t, struct16_t, struct17_t] {
param := ssa_mod.add_value_node(.argument, typ, 'p', 0)
ssa_mod.funcs[param_fn_id].params << param
}
ret16_id := ssa_mod.new_function('ret16', struct16_t, [])
ret17_id := ssa_mod.new_function('ret17', struct17_t, [])
mut win_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut win_mod, .x64, .windows)
assert win_mod.funcs[param_fn_id].abi_param_class == [.indirect, .indirect, .indirect]
assert win_mod.funcs[ret16_id].abi_ret_indirect == true
assert win_mod.funcs[ret17_id].abi_ret_indirect == true
for layout in win_mod.funcs[param_fn_id].abi_param_layouts {
assert layout.locs.len == 0
}
mut arm_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut arm_mod, .arm64, .sysv)
assert arm_mod.funcs[param_fn_id].abi_param_class == [.in_reg, .in_reg, .indirect]
assert arm_mod.funcs[ret16_id].abi_ret_indirect == false
assert arm_mod.funcs[ret17_id].abi_ret_indirect == true
for layout in arm_mod.funcs[param_fn_id].abi_param_layouts {
assert layout.locs.len == 0
}
}
fn test_x64_sysv_location_metadata_rolls_back_integer_aggregate_when_gprs_are_insufficient() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(6, false,
'i64_i64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.stack,
.stack,
], [0, 1])
assert_abi_layout_locs(call_instr.abi_arg_layouts[aggregate_idx], [.stack, .stack], [
0,
1,
])
}
fn test_x64_sysv_location_metadata_uses_two_gprs_when_available_for_integer_aggregate() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(4, false,
'i64_i64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.int_reg,
.int_reg,
], [4, 5])
assert_abi_layout_locs(call_instr.abi_arg_layouts[aggregate_idx], [.int_reg, .int_reg], [
4,
5,
])
}
fn test_x64_sysv_location_metadata_rolls_back_integer_aggregate_after_five_gprs() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(5, false,
'i64_i64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.stack,
.stack,
], [0, 1])
assert_abi_layout_locs(call_instr.abi_arg_layouts[aggregate_idx], [.stack, .stack], [
0,
1,
])
}
fn test_x64_sysv_location_metadata_rolls_back_sse_aggregate_when_xmms_are_insufficient() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(7, true, 'f64_f64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.stack,
.stack,
], [0, 1])
assert_abi_layout_locs(call_instr.abi_arg_layouts[aggregate_idx], [.stack, .stack], [
0,
1,
])
}
fn test_x64_sysv_location_metadata_assigns_mixed_integer_sse_aggregate() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(0, false,
'i64_f64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.int_reg,
.sse_reg,
], [0, 0])
assert_abi_layout_locs(call_instr.abi_arg_layouts[aggregate_idx], [.int_reg, .sse_reg], [
0,
0,
])
}
fn test_x64_sysv_location_metadata_reserves_rdi_for_hidden_sret_pointer() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_sret_location_metadata')
i64_t := ssa_mod.type_store.get_int(64)
f64_t := ssa_mod.type_store.get_float(64)
ret_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, i64_t, i64_t])
aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, f64_t])
callee_id := ssa_mod.new_function('callee', ret_t, [])
callee_param := ssa_mod.add_value_node(.argument, aggregate_t, 'p', 0)
ssa_mod.funcs[callee_id].params << callee_param
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_arg := ssa_mod.add_value_node(.argument, aggregate_t, 'a', 0)
ssa_mod.funcs[caller_id].params << caller_arg
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, ret_t, [fn_val, caller_arg])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert mir_mod.funcs[callee_id].abi_ret_indirect
assert call_instr.abi_ret_indirect
assert call_instr.op == .call_sret
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[0], [
.int_reg,
.sse_reg,
], [1, 0], [0, 8], [.integer, .sse])
assert_abi_layout(call_instr.abi_arg_layouts[0], [.int_reg, .sse_reg], [
1,
0,
], [0, 8], [.integer, .sse])
}
fn test_x64_sysv_location_metadata_places_seventh_integer_scalar_on_stack() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_integer_scalar_boundary')
i64_t := ssa_mod.type_store.get_int(64)
mir_mod, callee_id, call_val := build_sysv_scalar_boundary_module(mut ssa_mod, i64_t, 7)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[6], [.stack], [0], [
0,
], [.integer])
assert_abi_layout(call_instr.abi_arg_layouts[6], [.stack], [0], [0], [.integer])
}
fn test_x64_sysv_location_metadata_places_ninth_sse_scalar_on_stack() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_sse_scalar_boundary')
f64_t := ssa_mod.type_store.get_float(64)
mir_mod, callee_id, call_val := build_sysv_scalar_boundary_module(mut ssa_mod, f64_t, 9)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[8], [.stack], [0], [
0,
], [.sse])
assert_abi_layout(call_instr.abi_arg_layouts[8], [.stack], [0], [0], [.sse])
}
fn test_x64_sysv_location_metadata_float128_uses_one_sse_register() {
// x86-64 psABI 3.2.3 classifies __float128 as SSE plus SSEUP; SSEUP is
// carried in the upper half of the previous SSE register, not a new XMM.
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_float128_one_xmm')
f128_t := ssa_mod.type_store.get_float(128)
mir_mod, callee_id, call_val := build_sysv_scalar_boundary_module(mut ssa_mod, f128_t, 1)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[0], [.sse_reg, .sse_reg], [
0,
0,
], [0, 8], [.sse, .sseup])
assert_abi_layout(call_instr.abi_arg_layouts[0], [.sse_reg, .sse_reg], [
0,
0,
], [0, 8], [.sse, .sseup])
}
fn test_x64_sysv_location_metadata_float128_rolls_back_when_sse_regs_are_exhausted() {
// x86-64 psABI 3.2.3 says an argument that cannot fit entirely in the
// available registers is passed on the stack; this guards the SSE/SSEUP
// rollback path without claiming codegen consumes the metadata yet.
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_float128_sse_exhausted')
f64_t := ssa_mod.type_store.get_float(64)
f128_t := ssa_mod.type_store.get_float(128)
i64_t := ssa_mod.type_store.get_int(64)
mut param_types := []ssa.TypeID{len: 8, init: f64_t}
param_types << f128_t
callee_id := ssa_mod.new_function('callee', i64_t, [])
for i, typ in param_types {
param := ssa_mod.add_value_node(.argument, typ, 'p${i}', 0)
ssa_mod.funcs[callee_id].params << param
}
caller_id := ssa_mod.new_function('caller', i64_t, [])
mut args := []ssa.ValueID{}
for i, typ in param_types {
arg := ssa_mod.add_value_node(.argument, typ, 'a${i}', 0)
ssa_mod.funcs[caller_id].params << arg
args << arg
}
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
mut operands := [fn_val]
operands << args
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, operands)
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[8], [.stack, .stack], [
0,
1,
], [0, 8], [.sse, .sseup])
assert_abi_layout(call_instr.abi_arg_layouts[8], [.stack, .stack], [0, 1], [
0,
8,
], [.sse, .sseup])
assert call_instr.abi_arg_class[8] == .in_reg
assert call_instr.op == .call
}
fn test_x64_sysv_location_metadata_assigns_two_xmms_when_available_for_sse_aggregate() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(0, true, 'f64_f64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.sse_reg,
.sse_reg,
], [0, 1], [0, 8], [.sse, .sse])
assert_abi_layout(call_instr.abi_arg_layouts[aggregate_idx], [.sse_reg, .sse_reg], [
0,
1,
], [0, 8], [.sse, .sse])
}
fn test_x64_sysv_location_metadata_passes_memory_class_aggregate_indirectly() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_memory_class_layout')
i64_t := ssa_mod.type_store.get_int(64)
f64_t := ssa_mod.type_store.get_float(64)
memory_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, f64_t, f64_t])
callee_id := ssa_mod.new_function('callee', i64_t, [])
param := ssa_mod.add_value_node(.argument, memory_t, 'p', 0)
ssa_mod.funcs[callee_id].params << param
caller_id := ssa_mod.new_function('caller', i64_t, [])
arg := ssa_mod.add_value_node(.argument, memory_t, 'a', 0)
ssa_mod.funcs[caller_id].params << arg
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, arg])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert mir_mod.funcs[callee_id].abi_param_class == [.indirect]
assert call_instr.abi_arg_class == [.indirect]
assert_abi_value_class(mir_mod.funcs[callee_id].abi_param_classes[0], .indirect, [
.memory,
])
assert_abi_value_class(call_instr.abi_arg_classes[0], .indirect, [.memory])
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[0], [.int_reg], [0], [
0,
], [.integer])
assert_abi_layout(call_instr.abi_arg_layouts[0], [.int_reg], [0], [0], [.integer])
}
fn test_x64_sysv_location_metadata_rolls_back_mixed_aggregate_when_gprs_are_exhausted() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(6, false,
'i64_f64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.stack,
.stack,
], [0, 1], [0, 8], [.integer, .sse])
assert_abi_layout(call_instr.abi_arg_layouts[aggregate_idx], [.stack, .stack], [0, 1], [
0,
8,
], [.integer, .sse])
assert call_instr.abi_arg_class[aggregate_idx] == .in_reg
assert call_instr.op == .call
}
fn test_x64_sysv_location_metadata_rolls_back_mixed_aggregate_when_xmms_are_exhausted() {
mir_mod, callee_id, call_val, aggregate_idx := build_sysv_layout_call_module(8, true, 'i64_f64')
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[aggregate_idx], [
.stack,
.stack,
], [0, 1], [0, 8], [.integer, .sse])
assert_abi_layout(call_instr.abi_arg_layouts[aggregate_idx], [.stack, .stack], [0, 1], [
0,
8,
], [.integer, .sse])
assert call_instr.abi_arg_class[aggregate_idx] == .in_reg
assert call_instr.op == .call
}
fn test_x64_sysv_location_metadata_stack_indexes_continue_after_scalar_spill() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_stack_continuation_types')
i64_t := ssa_mod.type_store.get_int(64)
struct_i64_i64_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, i64_t])
mut param_types := []ssa.TypeID{len: 6, init: i64_t}
param_types << i64_t
param_types << struct_i64_i64_t
callee_id := ssa_mod.new_function('callee', i64_t, [])
for i, typ in param_types {
param := ssa_mod.add_value_node(.argument, typ, 'p${i}', 0)
ssa_mod.funcs[callee_id].params << param
}
caller_id := ssa_mod.new_function('caller', i64_t, [])
mut args := []ssa.ValueID{}
for i, typ in param_types {
arg := ssa_mod.add_value_node(.argument, typ, 'a${i}', 0)
ssa_mod.funcs[caller_id].params << arg
args << arg
}
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
mut operands := [fn_val]
operands << args
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, operands)
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[6], [.stack], [
0,
])
assert_abi_layout_locs(mir_mod.funcs[callee_id].abi_param_layouts[7], [.stack, .stack], [
1,
2,
])
assert_abi_layout_locs(call_instr.abi_arg_layouts[6], [.stack], [0])
assert_abi_layout_locs(call_instr.abi_arg_layouts[7], [.stack, .stack], [1, 2])
}
fn test_x64_sysv_direct_callsite_struct_size_matrix_current_threshold() {
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
mir_mod, call_val := build_x64_matrix_direct_call_module(.sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class.len == sizes.len
for i, size in sizes {
should_be_indirect := size > 16
assert (call_instr.abi_arg_class[i] == .indirect) == should_be_indirect
}
}
fn test_x64_sysv_callsite_return_size_matrix_current_threshold() {
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
for size in sizes {
mir_mod, call_val := build_x64_matrix_return_call_module(.sysv, size)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
should_be_indirect := size > 16
assert call_instr.abi_ret_indirect == should_be_indirect
assert (call_instr.op == .call_sret) == should_be_indirect
}
}
fn test_x64_sysv_struct_size_matrix_current_lowering_threshold() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_size_matrix')
i64_t := ssa_mod.type_store.get_int(64)
sizes := [1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 32]
mut struct_types := []ssa.TypeID{}
for size in sizes {
struct_types << register_x64_matrix_struct_bytes(mut ssa_mod, size)
}
param_fn_id := ssa_mod.new_function('param_matrix', i64_t, [])
for i, struct_t in struct_types {
param := ssa_mod.add_value_node(.argument, struct_t, 's${sizes[i]}', 0)
ssa_mod.funcs[param_fn_id].params << param
}
mut ret_fn_ids := []int{}
for i, struct_t in struct_types {
ret_fn_ids << ssa_mod.new_function('ret${sizes[i]}', struct_t, [])
}
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
for i, size in sizes {
should_be_indirect := size > 16
assert (mir_mod.funcs[param_fn_id].abi_param_class[i] == .indirect) == should_be_indirect
assert mir_mod.funcs[ret_fn_ids[i]].abi_ret_indirect == should_be_indirect
}
}
fn test_x64_sysv_aggregate_memory_class_for_representable_mir_types() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_memory_class')
i64_t := ssa_mod.type_store.get_int(64)
f64_t := ssa_mod.type_store.get_float(64)
f80_t := ssa_mod.type_store.get_float(80)
f128_t := ssa_mod.type_store.get_float(128)
two_eightbytes_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, f64_t])
three_eightbytes_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, f64_t, f64_t])
x87_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f80_t])
float128_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f128_t])
param_fn_id := ssa_mod.new_function('sysv_memory_params', i64_t, [])
for typ in [two_eightbytes_t, three_eightbytes_t, x87_aggregate_t, float128_aggregate_t] {
param := ssa_mod.add_value_node(.argument, typ, 'p', 0)
ssa_mod.funcs[param_fn_id].params << param
}
ret_two_id := ssa_mod.new_function('ret_two_eightbytes', two_eightbytes_t, [])
ret_three_id := ssa_mod.new_function('ret_three_eightbytes', three_eightbytes_t, [])
ret_x87_id := ssa_mod.new_function('ret_x87_aggregate', x87_aggregate_t, [])
ret_float128_id := ssa_mod.new_function('ret_float128_aggregate', float128_aggregate_t, [])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
assert_abi_value_class(mir_mod.funcs[param_fn_id].abi_param_classes[0], .direct, [
.sse,
.sse,
])
assert_abi_value_class(mir_mod.funcs[param_fn_id].abi_param_classes[1], .indirect, [
.memory,
])
assert_abi_value_class(mir_mod.funcs[param_fn_id].abi_param_classes[2], .indirect, [
.memory,
])
assert_abi_value_class(mir_mod.funcs[param_fn_id].abi_param_classes[3], .direct, [
.sse,
.sseup,
])
assert_abi_value_class(mir_mod.funcs[ret_two_id].abi_ret_class, .direct, [.sse, .sse])
assert_abi_value_class(mir_mod.funcs[ret_three_id].abi_ret_class, .indirect, [
.memory,
])
assert_abi_value_class(mir_mod.funcs[ret_x87_id].abi_ret_class, .indirect, [
.memory,
])
assert_abi_value_class(mir_mod.funcs[ret_float128_id].abi_ret_class, .direct, [
.sse,
.sseup,
])
}
fn test_x64_sysv_metadata_eightbyte_classification_for_representable_types() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_eightbyte_metadata')
i64_t := ssa_mod.type_store.get_int(64)
f64_t := ssa_mod.type_store.get_float(64)
f80_t := ssa_mod.type_store.get_float(80)
f128_t := ssa_mod.type_store.get_float(128)
struct_i64_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t])
struct_i64_i64_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, i64_t])
struct_f64_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t])
struct_f64_f64_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, f64_t])
struct_i64_f64_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, f64_t])
struct_f64_i64_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, i64_t])
too_large_t := register_x64_matrix_struct_fields(mut ssa_mod, [f64_t, f64_t, f64_t])
x87_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f80_t])
float128_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f128_t])
types := [
struct_i64_t,
struct_i64_i64_t,
struct_f64_t,
struct_f64_f64_t,
struct_i64_f64_t,
struct_f64_i64_t,
too_large_t,
x87_aggregate_t,
float128_aggregate_t,
]
param_fn_id := ssa_mod.new_function('sysv_metadata_params', i64_t, [])
for typ in types {
param := ssa_mod.add_value_node(.argument, typ, 'p', 0)
ssa_mod.funcs[param_fn_id].params << param
}
mut ret_fn_ids := []int{}
for i, typ in types {
ret_fn_ids << ssa_mod.new_function('sysv_metadata_ret_${i}', typ, [])
}
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
expected_classes := [
[mir.AbiEightbyteClass.integer],
[mir.AbiEightbyteClass.integer, .integer],
[mir.AbiEightbyteClass.sse],
[mir.AbiEightbyteClass.sse, .sse],
[mir.AbiEightbyteClass.integer, .sse],
[mir.AbiEightbyteClass.sse, .integer],
[mir.AbiEightbyteClass.memory],
[mir.AbiEightbyteClass.memory],
[mir.AbiEightbyteClass.sse, .sseup],
]
expected_modes := [
mir.AbiPassMode.direct,
.direct,
.direct,
.direct,
.direct,
.direct,
.indirect,
.indirect,
.direct,
]
for i in 0 .. types.len {
assert_abi_value_class(mir_mod.funcs[param_fn_id].abi_param_classes[i], expected_modes[i],
expected_classes[i])
assert_abi_value_class(mir_mod.funcs[ret_fn_ids[i]].abi_ret_class, expected_modes[i],
expected_classes[i])
}
}
fn test_x64_sysv_memory_class_projects_to_legacy_indirect_flags() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_callsite_memory_class')
i64_t := ssa_mod.type_store.get_int(64)
f80_t := ssa_mod.type_store.get_float(80)
x87_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f80_t])
callee_id := ssa_mod.new_function('callee', x87_aggregate_t, [])
callee_param := ssa_mod.add_value_node(.argument, x87_aggregate_t, 'p', 0)
ssa_mod.funcs[callee_id].params << callee_param
caller_id := ssa_mod.new_function('caller', i64_t, [])
arg := ssa_mod.add_value_node(.argument, x87_aggregate_t, 'a', 0)
ssa_mod.funcs[caller_id].params << arg
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, x87_aggregate_t, [fn_val, arg])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
assert_abi_value_class(mir_mod.funcs[callee_id].abi_param_classes[0], .indirect, [
.memory,
])
assert_abi_value_class(mir_mod.funcs[callee_id].abi_ret_class, .indirect, [
.memory,
])
assert mir_mod.funcs[callee_id].abi_param_class == [.indirect]
assert mir_mod.funcs[callee_id].abi_ret_indirect == true
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[0], [.int_reg], [1], [
0,
], [.integer])
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert_abi_value_class(call_instr.abi_arg_classes[0], .indirect, [.memory])
assert_abi_value_class(call_instr.abi_ret_class, .indirect, [.memory])
assert call_instr.abi_arg_class == [.indirect]
assert call_instr.abi_ret_indirect == true
assert call_instr.op == .call_sret
assert_abi_layout(call_instr.abi_arg_layouts[0], [.int_reg], [1], [0], [.integer])
}
fn test_x64_sysv_direct_aggregate_size_guard_boundary() {
assert !sysv_aggregate_must_be_memory_before_classification(16)
assert sysv_aggregate_must_be_memory_before_classification(17)
}
fn test_x64_sysv_huge_fixed_array_uses_early_memory_guard_and_bounded_metadata() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_huge_fixed_array_memory')
i64_t := ssa_mod.type_store.get_int(64)
u8_t := ssa_mod.type_store.get_uint(8)
huge_array_t := ssa_mod.type_store.register(ssa.Type{
kind: .array_t
elem_type: u8_t
len: 1_000_000
})
callee_id := ssa_mod.new_function('callee', huge_array_t, [])
param := ssa_mod.add_value_node(.argument, huge_array_t, 'p', 0)
ssa_mod.funcs[callee_id].params << param
caller_id := ssa_mod.new_function('caller', i64_t, [])
arg := ssa_mod.add_value_node(.argument, huge_array_t, 'a', 0)
ssa_mod.funcs[caller_id].params << arg
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, huge_array_t, [fn_val, arg])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
huge_size := mir_mod.type_size(huge_array_t)
assert huge_size == 1_000_000
assert sysv_aggregate_must_be_memory_before_classification(huge_size)
direct_class := sysv_abi_value_class(mir_mod, huge_array_t)
assert_abi_value_class(direct_class, .indirect, [.memory])
assert direct_class.classes.len == 1
assert direct_class.classes.len < (huge_size + 7) / 8
param_class := mir_mod.funcs[callee_id].abi_param_classes[0]
ret_class := mir_mod.funcs[callee_id].abi_ret_class
assert_abi_value_class(param_class, .indirect, [.memory])
assert_abi_value_class(ret_class, .indirect, [.memory])
assert param_class.classes.len == 1
assert ret_class.classes.len == 1
assert mir_mod.funcs[callee_id].abi_param_class == [.indirect]
assert mir_mod.funcs[callee_id].abi_ret_indirect
assert mir_mod.funcs[callee_id].abi_param_layouts[0].locs.len == 1
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[0], [.int_reg], [1], [
0,
], [.integer])
assert_abi_value_class(call_instr.abi_arg_classes[0], .indirect, [.memory])
assert_abi_value_class(call_instr.abi_ret_class, .indirect, [.memory])
assert call_instr.abi_arg_classes[0].classes.len == 1
assert call_instr.abi_ret_class.classes.len == 1
assert call_instr.abi_arg_class == [.indirect]
assert call_instr.abi_ret_indirect
assert call_instr.op == .call_sret
assert call_instr.abi_arg_layouts[0].locs.len == 1
assert_abi_layout(call_instr.abi_arg_layouts[0], [.int_reg], [1], [0], [.integer])
}
fn test_x64_sysv_indirect_argument_consumes_gpr_before_direct_aggregate_layout() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_indirect_arg_gpr_consumption')
i64_t := ssa_mod.type_store.get_int(64)
f80_t := ssa_mod.type_store.get_float(80)
x87_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f80_t])
integer_pair_t := register_x64_matrix_struct_fields(mut ssa_mod, [i64_t, i64_t])
callee_id := ssa_mod.new_function('callee', i64_t, [])
memory_param := ssa_mod.add_value_node(.argument, x87_aggregate_t, 'memory_arg', 0)
pair_param := ssa_mod.add_value_node(.argument, integer_pair_t, 'pair_arg', 0)
ssa_mod.funcs[callee_id].params << [memory_param, pair_param]
caller_id := ssa_mod.new_function('caller', i64_t, [])
memory_arg := ssa_mod.add_value_node(.argument, x87_aggregate_t, 'memory_arg', 0)
pair_arg := ssa_mod.add_value_node(.argument, integer_pair_t, 'pair_arg', 0)
ssa_mod.funcs[caller_id].params << [memory_arg, pair_arg]
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, memory_arg, pair_arg])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert mir_mod.funcs[callee_id].abi_param_class == [.indirect, .in_reg]
assert call_instr.abi_arg_class == [.indirect, .in_reg]
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[0], [.int_reg], [0], [
0,
], [.integer])
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[1], [
.int_reg,
.int_reg,
], [1, 2], [0, 8], [.integer, .integer])
assert_abi_layout(call_instr.abi_arg_layouts[0], [.int_reg], [0], [0], [.integer])
assert_abi_layout(call_instr.abi_arg_layouts[1], [.int_reg, .int_reg], [1, 2], [
0,
8,
], [.integer, .integer])
}
fn test_x64_sysv_indirect_argument_spills_after_gprs_are_exhausted() {
mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_indirect_arg_stack_spill')
i64_t := ssa_mod.type_store.get_int(64)
f80_t := ssa_mod.type_store.get_float(80)
x87_aggregate_t := register_x64_matrix_struct_fields(mut ssa_mod, [f80_t])
mut param_types := []ssa.TypeID{len: 6, init: i64_t}
param_types << x87_aggregate_t
param_types << i64_t
callee_id := ssa_mod.new_function('callee', i64_t, [])
for i, typ in param_types {
param := ssa_mod.add_value_node(.argument, typ, 'p${i}', 0)
ssa_mod.funcs[callee_id].params << param
}
caller_id := ssa_mod.new_function('caller', i64_t, [])
mut args := []ssa.ValueID{}
for i, typ in param_types {
arg := ssa_mod.add_value_node(.argument, typ, 'a${i}', 0)
ssa_mod.funcs[caller_id].params << arg
args << arg
}
caller_entry := ssa_mod.add_block(caller_id, 'entry')
fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0)
mut operands := [fn_val]
operands << args
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, operands)
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .sysv)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert mir_mod.funcs[callee_id].abi_param_class[6] == .indirect
assert call_instr.abi_arg_class[6] == .indirect
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[6], [.stack], [0], [
0,
], [.integer])
assert_abi_layout(call_instr.abi_arg_layouts[6], [.stack], [0], [0], [.integer])
assert_abi_layout(mir_mod.funcs[callee_id].abi_param_layouts[7], [.stack], [1], [
0,
], [.integer])
assert_abi_layout(call_instr.abi_arg_layouts[7], [.stack], [1], [0], [.integer])
}
fn test_x64_windows_indirect_call_uses_function_pointer_signature_for_aggregate_args() {
mut ssa_mod := ssa.Module.new('abi_test_x64_windows_indirect_call_matrix')
i64_t := ssa_mod.type_store.get_int(64)
struct8_t := register_x64_matrix_struct_bytes(mut ssa_mod, 8)
struct9_t := register_x64_matrix_struct_bytes(mut ssa_mod, 9)
fn_sig_t := ssa_mod.type_store.register(ssa.Type{
kind: .func_t
params: [struct8_t, struct9_t]
ret_type: i64_t
})
fn_ptr_t := ssa_mod.type_store.get_ptr(fn_sig_t)
caller_id := ssa_mod.new_function('caller', i64_t, [])
fn_ptr := ssa_mod.add_value_node(.argument, fn_ptr_t, 'fn_ptr', 0)
arg8 := ssa_mod.add_value_node(.argument, struct8_t, 'arg8', 0)
arg9 := ssa_mod.add_value_node(.argument, struct9_t, 'arg9', 0)
ssa_mod.funcs[caller_id].params << [fn_ptr, arg8, arg9]
caller_entry := ssa_mod.add_block(caller_id, 'entry')
call_val := ssa_mod.add_instr(.call_indirect, caller_entry, i64_t, [fn_ptr, arg8, arg9])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .windows)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.op == .call_indirect
assert call_instr.abi_arg_class == [.in_reg, .indirect]
}
fn test_x64_windows_unresolved_callsite_uses_alloca_logical_aggregate_type() {
mut ssa_mod := ssa.Module.new('abi_test_x64_windows_alloca_logical_arg')
i64_t := ssa_mod.type_store.get_int(64)
struct9_t := register_x64_matrix_struct_bytes(mut ssa_mod, 9)
struct9_ptr_t := ssa_mod.type_store.get_ptr(struct9_t)
caller_id := ssa_mod.new_function('caller', i64_t, [])
caller_entry := ssa_mod.add_block(caller_id, 'entry')
tmp := ssa_mod.add_instr(.alloca, caller_entry, struct9_ptr_t, []ssa.ValueID{})
external_fn := ssa_mod.add_value_node(.unknown, 0, 'external_accept_struct9', 0)
call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [external_fn, tmp])
zero := ssa_mod.get_or_add_const(i64_t, '0')
ssa_mod.add_instr(.ret, caller_entry, 0, [zero])
mut mir_mod := mir.lower_from_ssa(ssa_mod)
lower_with_x64_abi(mut mir_mod, .x64, .windows)
call_instr := mir_mod.instrs[mir_mod.values[call_val].index]
assert call_instr.abi_arg_class == [.indirect]
}
File diff suppressed because it is too large. Load diff
+778
View File
@@ -0,0 +1,778 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
import v2.token
// Cursor is a lightweight, by-value handle pointing at one FlatNode inside a
// FlatAst. Consumers walk the AST via `match c.kind() {}` and descend via
// `c.edge(i)` / `c.list_at(i)` without rehydrating to ast.Stmt / ast.Expr.
//
// A Cursor is 16 bytes: an &FlatAst pointer plus a FlatNodeId. It is safe to
// copy, store in arrays, and pass by value. An "invalid" Cursor (id < 0) is
// used as a sentinel where the legacy AST would use empty_stmt / empty_expr.
pub struct Cursor {
pub:
flat &FlatAst = unsafe { nil }
id FlatNodeId
}
@[inline]
pub fn (c Cursor) is_valid() bool {
return c.flat != unsafe { nil } && c.id >= 0 && c.id < c.flat.nodes.len
}
@[inline]
pub fn (c Cursor) kind() FlatNodeKind {
return c.flat.nodes[c.id].kind
}
@[inline]
pub fn (c Cursor) pos() token.Pos {
return c.flat.nodes[c.id].pos
}
@[inline]
pub fn (c Cursor) flags() u8 {
return c.flat.nodes[c.id].flags
}
@[inline]
pub fn (c Cursor) flag(bit u8) bool {
return (c.flat.nodes[c.id].flags & bit) != 0
}
@[inline]
pub fn (c Cursor) aux() u16 {
return c.flat.nodes[c.id].aux
}
@[inline]
pub fn (c Cursor) extra_int() int {
return c.flat.nodes[c.id].extra
}
// extra_str interprets the node's `extra` field as an interned string id.
// Use only on kinds where the schema documents `extra` as a string slot
// (e.g. stmt_directive's value, stmt_import's alias).
@[inline]
pub fn (c Cursor) extra_str() string {
return c.flat.string_at(c.flat.nodes[c.id].extra)
}
// name returns the interned primary string (FlatNode.name_id) for this node.
// Most kinds use this for identifiers, type names, fn names, etc.
@[inline]
pub fn (c Cursor) name() string {
return c.flat.string_at(c.flat.nodes[c.id].name_id)
}
@[inline]
pub fn (c Cursor) name_id() int {
return c.flat.nodes[c.id].name_id
}
// ident reads an expr_ident cursor directly into an Ident.
pub fn (c Cursor) ident() Ident {
if !c.is_valid() || c.kind() != .expr_ident {
return Ident{}
}
return Ident{
pos: c.pos()
name: c.name()
}
}
// import_stmt reads a stmt_import cursor directly into an ImportStmt.
pub fn (c Cursor) import_stmt() ImportStmt {
if !c.is_valid() || c.kind() != .stmt_import {
return ImportStmt{}
}
mut symbols := []Expr{cap: c.edge_count()}
for i in 0 .. c.edge_count() {
sym := c.edge(i)
if sym.kind() == .expr_ident {
symbols << Expr(sym.ident())
}
}
return ImportStmt{
name: c.name()
alias: c.extra_str()
is_aliased: c.flag(flag_is_aliased)
symbols: symbols
}
}
// fn_decl_signature reads a stmt_fn_decl cursor into a body-less FnDecl. This
// mirrors FlatAst.decode_fn_decl_signature without going through FlatReader.
pub fn (c Cursor) fn_decl_signature() FnDecl {
if !c.is_valid() || c.kind() != .stmt_fn_decl {
return FnDecl{}
}
return FnDecl{
attributes: attrs_from_cursor(c.list_at(2))
is_public: c.flag(flag_is_public)
is_method: c.flag(flag_is_method)
is_static: c.flag(flag_is_static)
receiver: parameter_from_cursor(c.edge(0))
language: unsafe { Language(int(c.aux())) }
name: c.name()
typ: fn_type_from_cursor(c.edge(1))
pos: c.pos()
}
}
// fn_decl reads a stmt_fn_decl cursor into a legacy FnDecl. Unlike
// fn_decl_signature, this materializes the body statements for consumers that
// still use legacy statement walkers internally.
pub fn (c Cursor) fn_decl() FnDecl {
if !c.is_valid() || c.kind() != .stmt_fn_decl {
return FnDecl{}
}
signature := c.fn_decl_signature()
return FnDecl{
attributes: signature.attributes
is_public: signature.is_public
is_method: signature.is_method
is_static: signature.is_static
receiver: signature.receiver
language: signature.language
name: signature.name
typ: signature.typ
stmts: c.list_at(3).stmts()
pos: signature.pos
}
}
// stmt reads a cursor through FlatAst.decode_stmt. This is the escape hatch
// for legacy statement walkers; prefer cursor-specific readers when possible.
pub fn (c Cursor) stmt() Stmt {
if !c.is_valid() {
return empty_stmt
}
return c.flat.decode_stmt(c.id)
}
// expr reads a cursor through FlatAst.decode_expr. This is the escape hatch
// for legacy expression walkers; prefer cursor-specific readers when possible.
pub fn (c Cursor) expr() Expr {
if !c.is_valid() {
return empty_expr
}
return c.flat.decode_expr(c.id)
}
// type_expr reads a type-expression cursor into the legacy Expr shape used by
// signature consumers. It is intentionally narrower than FlatReader.read_expr:
// non-type payloads such as field defaults or statement bodies stay omitted.
pub fn (c Cursor) type_expr() Expr {
if !c.is_valid() {
return empty_expr
}
match c.kind() {
.expr_empty {
return empty_expr
}
.expr_basic_literal {
return Expr(BasicLiteral{
kind: unsafe { token.Token(int(c.aux())) }
value: c.name()
pos: c.pos()
})
}
.expr_ident {
return Expr(c.ident())
}
.expr_lifetime {
return Expr(LifetimeExpr{
name: c.name()
pos: c.pos()
})
}
.expr_modifier {
return Expr(ModifierExpr{
kind: unsafe { token.Token(int(c.aux())) }
expr: c.edge(0).type_expr()
pos: c.pos()
})
}
.expr_prefix {
return Expr(PrefixExpr{
op: unsafe { token.Token(int(c.aux())) }
expr: c.edge(0).type_expr()
pos: c.pos()
})
}
.expr_selector {
rhs := c.edge(1)
return Expr(SelectorExpr{
lhs: c.edge(0).type_expr()
rhs: Ident{
name: rhs.name()
pos: rhs.pos()
}
pos: c.pos()
})
}
.expr_generic_args {
return Expr(GenericArgs{
lhs: c.edge(0).type_expr()
args: type_exprs_from_edges(c, 1)
pos: c.pos()
})
}
.expr_generic_arg_or_index {
return Expr(GenericArgOrIndexExpr{
lhs: c.edge(0).type_expr()
expr: c.edge(1).type_expr()
pos: c.pos()
})
}
.typ_anon_struct {
return Expr(Type(AnonStructType{
generic_params: c.list_at(0).type_exprs()
embedded: c.list_at(1).type_exprs()
fields: field_decl_type_list(c.list_at(2))
}))
}
.typ_array_fixed {
return Expr(Type(ArrayFixedType{
len: c.edge(0).type_expr()
elem_type: c.edge(1).type_expr()
}))
}
.typ_array {
return Expr(Type(ArrayType{
elem_type: c.edge(0).type_expr()
}))
}
.typ_channel {
return Expr(Type(ChannelType{
cap: c.edge(0).type_expr()
elem_type: c.edge(1).type_expr()
}))
}
.typ_fn {
return Expr(Type(FnType{
generic_params: c.list_at(0).type_exprs()
params: parameter_list_from_cursor(c.list_at(1))
return_type: c.edge(2).type_expr()
}))
}
.typ_generic {
return Expr(Type(GenericType{
name: c.edge(0).type_expr()
params: type_exprs_from_edges(c, 1)
}))
}
.typ_map {
return Expr(Type(MapType{
key_type: c.edge(0).type_expr()
value_type: c.edge(1).type_expr()
}))
}
.typ_nil {
return Expr(Type(NilType{}))
}
.typ_none {
return Expr(Type(NoneType{}))
}
.typ_option {
return Expr(Type(OptionType{
base_type: c.edge(0).type_expr()
}))
}
.typ_pointer {
return Expr(Type(PointerType{
base_type: c.edge(0).type_expr()
lifetime: c.name()
}))
}
.typ_result {
return Expr(Type(ResultType{
base_type: c.edge(0).type_expr()
}))
}
.typ_thread {
return Expr(Type(ThreadType{
elem_type: c.edge(0).type_expr()
}))
}
.typ_tuple {
return Expr(Type(TupleType{
types: type_exprs_from_edges(c, 0)
}))
}
else {
return empty_expr
}
}
}
@[inline]
pub fn (c Cursor) edge_count() int {
return c.flat.nodes[c.id].edge_count
}
// edge returns a Cursor over the i-th direct child edge of this node. Out-of-
// range indices return an invalid Cursor (id == invalid_flat_node_id), which
// callers can detect via `c.is_valid()`. This matches FlatReader's behaviour
// where `r.edge(n, i)` may return invalid_flat_node_id for missing slots.
@[inline]
pub fn (c Cursor) edge(i int) Cursor {
return Cursor{
flat: c.flat
id: c.flat.child_at(c.id, i)
}
}
// list_at treats edge `i` as a reference to an `aux_list` node and returns a
// CursorList over its children. The flat schema represents list-typed fields
// (e.g. StructDecl.fields, FnDecl.stmts, EnumDecl.attributes) as a single
// edge to an aux_list whose children are the actual list items. Use `edge()`
// for fields stored as direct child edges of the parent (e.g. AssignStmt's
// LHS/RHS, AssertStmt's expr/extra).
@[inline]
pub fn (c Cursor) list_at(edge_i int) CursorList {
return CursorList{
flat: c.flat
parent_id: c.flat.child_at(c.id, edge_i)
}
}
// for_body_list views a stmt_for node's body as a CursorList. The stmt_for flat
// layout is edges [init, cond, post, body_stmt_0, body_stmt_1, ...] — the body
// is trailing edges of the for-node itself, NOT a separate aux_list — so the
// body list is this node's children from edge index 3 onward.
@[inline]
pub fn (c Cursor) for_body_list() CursorList {
return CursorList{
flat: c.flat
parent_id: c.id
offset: 3
}
}
// CursorList is a view over the children of an `aux_list` node. It is the
// flat equivalent of `[]ast.Stmt` / `[]ast.Expr` / `[]FieldDecl` etc., except
// no slice is materialised — `at(i)` decodes one child at a time.
pub struct CursorList {
pub:
flat &FlatAst = unsafe { nil }
parent_id FlatNodeId
// offset skips the first `offset` child edges of `parent_id`. Default 0 (the
// whole child list). Lets a trailing edge range be viewed as a list when the
// items are direct child edges of a node rather than an aux_list — e.g. a
// ForStmt body lives in edges [3..] of the stmt_for node itself (see
// `Cursor.for_body_list`).
offset int
}
@[inline]
pub fn (l CursorList) len() int {
if l.flat == unsafe { nil } || l.parent_id < 0 || l.parent_id >= l.flat.nodes.len {
return 0
}
return l.flat.nodes[l.parent_id].edge_count - l.offset
}
@[inline]
pub fn (l CursorList) at(i int) Cursor {
return Cursor{
flat: l.flat
id: l.flat.child_at(l.parent_id, l.offset + i)
}
}
// type_exprs reads every item in a cursor list through Cursor.type_expr.
pub fn (l CursorList) type_exprs() []Expr {
mut out := []Expr{cap: l.len()}
for i in 0 .. l.len() {
out << l.at(i).type_expr()
}
return out
}
// stmts reads every item in a cursor list through FlatAst.decode_stmt. This is
// the escape hatch for legacy statement walkers; prefer cursor-specific
// readers when the caller only needs a declaration signature or metadata.
pub fn (l CursorList) stmts() []Stmt {
mut out := []Stmt{cap: l.len()}
for i in 0 .. l.len() {
c := l.at(i)
if !c.is_valid() {
continue
}
out << c.stmt()
}
return out
}
// attribute_expr reads the small expression subset used inside attributes.
// Attribute payloads are normally identifiers or strings; type_expr handles
// the type-like fallback cases without opening the full FlatReader.
pub fn (c Cursor) attribute_expr() Expr {
if !c.is_valid() {
return empty_expr
}
match c.kind() {
.expr_empty {
return empty_expr
}
.expr_basic_literal {
return Expr(BasicLiteral{
kind: unsafe { token.Token(int(c.aux())) }
value: c.name()
pos: c.pos()
})
}
.expr_ident {
return Expr(c.ident())
}
.expr_string {
return Expr(StringLiteral{
kind: unsafe { StringLiteralKind(int(c.aux())) }
value: c.name()
pos: c.pos()
})
}
.expr_selector {
rhs := c.edge(1)
return Expr(SelectorExpr{
lhs: c.edge(0).attribute_expr()
rhs: Ident{
name: rhs.name()
pos: rhs.pos()
}
pos: c.pos()
})
}
else {
return c.type_expr()
}
}
}
// attribute reads an aux_attribute cursor into the legacy Attribute shape.
pub fn (c Cursor) attribute() Attribute {
if !c.is_valid() || c.kind() != .aux_attribute {
return Attribute{}
}
return Attribute{
name: c.name()
value: c.edge(0).attribute_expr()
// comptime_cond (`@[if cond ?]`) can be an arbitrary expression, not the
// ident/string subset attribute_expr handles — decode it fully (mirrors
// FlatReader.read_attribute), else complex conditions silently become
// empty_expr and `@[if ...]` functions are never elided.
comptime_cond: c.edge(1).expr()
pos: c.pos()
}
}
// attributes reads every aux_attribute in a cursor list.
pub fn (l CursorList) attributes() []Attribute {
mut out := []Attribute{cap: l.len()}
for i in 0 .. l.len() {
attr := l.at(i)
if !attr.is_valid() {
continue
}
out << attr.attribute()
}
return out
}
// import_stmts reads every stmt_import in a cursor list.
pub fn (l CursorList) import_stmts() []ImportStmt {
mut out := []ImportStmt{cap: l.len()}
for i in 0 .. l.len() {
imp := l.at(i)
if !imp.is_valid() || imp.kind() != .stmt_import {
continue
}
out << imp.import_stmt()
}
return out
}
// field_init reads an aux_field_init cursor. Field values are still legacy
// expression consumers today, so this materializes only the value expression,
// not the parent declaration.
pub fn (c Cursor) field_init() FieldInit {
if !c.is_valid() || c.kind() != .aux_field_init {
return FieldInit{}
}
value_c := c.edge(0)
return FieldInit{
name: c.name()
value: value_c.expr()
}
}
// field_inits reads every aux_field_init in a cursor list.
pub fn (l CursorList) field_inits() []FieldInit {
mut out := []FieldInit{cap: l.len()}
for i in 0 .. l.len() {
out << l.at(i).field_init()
}
return out
}
// field_decl reads an aux_field_decl cursor. Set decode_value to false when a
// caller only needs declaration metadata and type expressions.
pub fn (c Cursor) field_decl(decode_value bool) FieldDecl {
if !c.is_valid() || c.kind() != .aux_field_decl {
return FieldDecl{}
}
value_c := c.edge(1)
return FieldDecl{
name: c.name()
typ: c.edge(0).type_expr()
value: if decode_value { value_c.expr() } else { empty_expr }
attributes: c.list_at(2).attributes()
is_public: c.flag(flag_is_public)
is_mut: c.flag(flag_is_mut)
is_module_mut: c.flag(flag_field_is_module_mut)
is_interface_method: c.flag(flag_field_is_interface_method)
}
}
// field_decls reads every aux_field_decl in a cursor list.
pub fn (l CursorList) field_decls(decode_values bool) []FieldDecl {
mut out := []FieldDecl{cap: l.len()}
for i in 0 .. l.len() {
out << l.at(i).field_decl(decode_values)
}
return out
}
// const_decl reads a stmt_const_decl cursor.
pub fn (c Cursor) const_decl() ConstDecl {
if !c.is_valid() || c.kind() != .stmt_const_decl {
return ConstDecl{}
}
return ConstDecl{
is_public: c.flag(flag_is_public)
fields: c.list_at(0).field_inits()
}
}
// enum_decl reads a stmt_enum_decl cursor. Set decode_values to false when
// only field names/attributes are needed.
pub fn (c Cursor) enum_decl(decode_values bool) EnumDecl {
if !c.is_valid() || c.kind() != .stmt_enum_decl {
return EnumDecl{}
}
return EnumDecl{
attributes: c.list_at(1).attributes()
is_public: c.flag(flag_is_public)
name: c.name()
as_type: c.edge(0).type_expr()
fields: c.list_at(2).field_decls(decode_values)
}
}
// global_decl reads a stmt_global_decl cursor. Set decode_values to false when
// only field metadata and declared types are needed.
pub fn (c Cursor) global_decl(decode_values bool) GlobalDecl {
if !c.is_valid() || c.kind() != .stmt_global_decl {
return GlobalDecl{}
}
return GlobalDecl{
attributes: c.list_at(0).attributes()
fields: c.list_at(1).field_decls(decode_values)
is_public: c.flag(flag_is_public)
}
}
// interface_decl reads a stmt_interface_decl cursor.
pub fn (c Cursor) interface_decl() InterfaceDecl {
if !c.is_valid() || c.kind() != .stmt_interface_decl {
return InterfaceDecl{}
}
return InterfaceDecl{
is_public: c.flag(flag_is_public)
attributes: c.list_at(0).attributes()
name: c.name()
generic_params: c.list_at(1).type_exprs()
embedded: c.list_at(2).type_exprs()
fields: c.list_at(3).field_decls(true)
}
}
// struct_decl reads a stmt_struct_decl cursor.
pub fn (c Cursor) struct_decl() StructDecl {
if !c.is_valid() || c.kind() != .stmt_struct_decl {
return StructDecl{}
}
return StructDecl{
attributes: c.list_at(0).attributes()
is_public: c.flag(flag_is_public)
is_union: c.flag(flag_is_union)
implements: c.list_at(1).type_exprs()
embedded: c.list_at(2).type_exprs()
language: unsafe { Language(int(c.aux())) }
name: c.name()
generic_params: c.list_at(3).type_exprs()
fields: c.list_at(4).field_decls(true)
pos: c.pos()
}
}
// type_decl reads a stmt_type_decl cursor.
pub fn (c Cursor) type_decl() TypeDecl {
if !c.is_valid() || c.kind() != .stmt_type_decl {
return TypeDecl{}
}
return TypeDecl{
is_public: c.flag(flag_is_public)
language: unsafe { Language(int(c.aux())) }
name: c.name()
generic_params: c.list_at(2).type_exprs()
base_type: c.edge(0).type_expr()
variants: c.list_at(3).type_exprs()
}
}
fn attrs_from_cursor(list CursorList) []Attribute {
return list.attributes()
}
fn fn_type_from_cursor(c Cursor) FnType {
if !c.is_valid() || c.kind() != .typ_fn {
return FnType{}
}
return FnType{
generic_params: c.list_at(0).type_exprs()
params: parameter_list_from_cursor(c.list_at(1))
return_type: c.edge(2).type_expr()
}
}
fn type_exprs_from_edges(c Cursor, start int) []Expr {
if !c.is_valid() || start >= c.edge_count() {
return []Expr{}
}
mut out := []Expr{cap: c.edge_count() - start}
for i in start .. c.edge_count() {
out << c.edge(i).type_expr()
}
return out
}
fn parameter_from_cursor(c Cursor) Parameter {
if !c.is_valid() {
return Parameter{}
}
return Parameter{
name: c.name()
typ: c.edge(0).type_expr()
is_mut: c.flag(flag_is_mut)
pos: c.pos()
}
}
fn parameter_list_from_cursor(list CursorList) []Parameter {
mut out := []Parameter{cap: list.len()}
for i in 0 .. list.len() {
out << parameter_from_cursor(list.at(i))
}
return out
}
fn field_decl_type_list(list CursorList) []FieldDecl {
mut out := []FieldDecl{cap: list.len()}
for i in 0 .. list.len() {
field := list.at(i)
if !field.is_valid() {
continue
}
out << FieldDecl{
name: field.name()
typ: field.edge(0).type_expr()
is_public: field.flag(flag_is_public)
is_mut: field.flag(flag_is_mut)
is_module_mut: field.flag(flag_field_is_module_mut)
is_interface_method: field.flag(flag_field_is_interface_method)
}
}
return out
}
// FileCursor is a typed wrapper over a FlatFile entry. It exposes the
// file-level metadata (name, mod, selector_names) and the three top-level
// child lists (attributes, imports, stmts) without rehydrating a full
// ast.File.
pub struct FileCursor {
pub:
flat &FlatAst = unsafe { nil }
idx int // index into flat.files
}
// file_cursor returns a FileCursor over the i-th FlatFile in this FlatAst.
@[inline]
pub fn (flat &FlatAst) file_cursor(idx int) FileCursor {
return FileCursor{
flat: unsafe { flat }
idx: idx
}
}
// file_cursors returns one FileCursor per FlatFile. Allocates an int-sized
// array; for hot loops prefer `for i in 0 .. flat.files.len { flat.file_cursor(i) }`.
pub fn (flat &FlatAst) file_cursors() []FileCursor {
mut out := []FileCursor{cap: flat.files.len}
for i in 0 .. flat.files.len {
out << flat.file_cursor(i)
}
return out
}
@[inline]
pub fn (fc FileCursor) flat_file() FlatFile {
return fc.flat.files[fc.idx]
}
// root returns a Cursor positioned at this file's root FlatNode (kind == .file).
@[inline]
pub fn (fc FileCursor) root() Cursor {
return Cursor{
flat: fc.flat
id: fc.flat.files[fc.idx].file_id
}
}
@[inline]
pub fn (fc FileCursor) name() string {
return fc.flat.string_at(fc.flat.files[fc.idx].name_idx)
}
@[inline]
pub fn (fc FileCursor) mod() string {
return fc.flat.string_at(fc.flat.files[fc.idx].mod_idx)
}
@[inline]
pub fn (fc FileCursor) selector_names() map[int]string {
return fc.flat.files[fc.idx].selector_names
}
// attrs returns the file's top-level attribute list (edge 0 of the file node).
@[inline]
pub fn (fc FileCursor) attrs() CursorList {
return fc.root().list_at(0)
}
// imports returns the file's top-level import list (edge 1 of the file node).
@[inline]
pub fn (fc FileCursor) imports() CursorList {
return fc.root().list_at(1)
}
// stmts returns the file's top-level statement list (edge 2 of the file node).
@[inline]
pub fn (fc FileCursor) stmts() CursorList {
return fc.root().list_at(2)
}
@@ -0,0 +1,362 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for the Cursor API live alongside the implementation since they
// operate on hand-built FlatAst fixtures — building them via the parser
// would introduce a circular dep (parser depends on ast). The fixtures
// mirror the shape that parse_file emits for a few representative inputs.
// build_minimal_file constructs a FlatAst with one file containing:
// module foo
// fn add(a int, b int) int { return a + b }
// using the FlatBuilder API directly. We then exercise Cursor over it.
fn build_minimal_file() FlatAst {
mut b := new_flat_builder()
src := File{
name: 'inline_minimal.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(FnDecl{
name: 'add'
typ: FnType{
params: [
Parameter{
name: 'a'
typ: Expr(Ident{
name: 'int'
})
},
Parameter{
name: 'b'
typ: Expr(Ident{
name: 'int'
})
},
]
return_type: Expr(Ident{
name: 'int'
})
}
stmts: [
Stmt(ReturnStmt{
exprs: [
Expr(InfixExpr{
op: .plus
lhs: Expr(Ident{
name: 'a'
})
rhs: Expr(Ident{
name: 'b'
})
}),
]
}),
]
}),
]
}
b.append_file(src)
return b.flat
}
fn test_file_cursor_basics() {
flat := build_minimal_file()
assert flat.files.len == 1
fc := flat.file_cursor(0)
assert fc.name() == 'inline_minimal.v'
assert fc.mod() == 'foo'
root := fc.root()
assert root.is_valid()
assert root.kind() == .file
}
fn test_file_cursor_stmts_iteration() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
stmts := fc.stmts()
// module decl + fn decl
assert stmts.len() == 2
assert stmts.at(0).kind() == .stmt_module
assert stmts.at(0).name() == 'foo'
assert stmts.at(1).kind() == .stmt_fn_decl
assert stmts.at(1).name() == 'add'
}
fn test_fn_decl_flags_and_body() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
fn_cur := fc.stmts().at(1)
assert fn_cur.kind() == .stmt_fn_decl
// is_method/is_public/is_static all false for plain `fn add(...)`
assert !fn_cur.flag(flag_is_method)
assert !fn_cur.flag(flag_is_public)
assert !fn_cur.flag(flag_is_static)
// edge layout per read_stmt: 0=receiver, 1=typ, 2=attrs, 3=stmts(aux_list)
stmts := fn_cur.list_at(3)
assert stmts.len() == 1
ret := stmts.at(0)
assert ret.kind() == .stmt_return
}
fn test_fn_decl_signature_from_cursor() {
flat := build_minimal_file()
fn_cur := flat.file_cursor(0).stmts().at(1)
decl := fn_cur.fn_decl_signature()
assert decl.name == 'add'
assert !decl.is_method
assert decl.stmts.len == 0
assert decl.typ.params.len == 2
assert decl.typ.params[0].name == 'a'
assert decl.typ.params[0].typ is Ident
assert (decl.typ.params[0].typ as Ident).name == 'int'
assert decl.typ.params[1].name == 'b'
assert decl.typ.return_type is Ident
assert (decl.typ.return_type as Ident).name == 'int'
}
fn test_fn_decl_from_cursor_reads_body() {
flat := build_minimal_file()
fn_cur := flat.file_cursor(0).stmts().at(1)
decl := fn_cur.fn_decl()
assert decl.name == 'add'
assert decl.stmts.len == 1
assert decl.stmts[0] is ReturnStmt
}
fn test_stmt_and_expr_escape_hatches_from_cursor() {
flat := build_minimal_file()
ret_cur := flat.file_cursor(0).stmts().at(1).list_at(3).at(0)
ret_stmt := ret_cur.stmt()
assert ret_stmt is ReturnStmt
infix_expr := ret_cur.edge(0).expr()
assert infix_expr is InfixExpr
assert (infix_expr as InfixExpr).op == .plus
}
fn test_return_stmt_exprs_via_edges() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
ret := fc.stmts().at(1).list_at(3).at(0)
assert ret.kind() == .stmt_return
// ReturnStmt stores its exprs as direct edges of the parent (not aux_list)
assert ret.edge_count() == 1
infix := ret.edge(0)
assert infix.kind() == .expr_infix
// InfixExpr edges: 0=lhs, 1=rhs (per read_expr)
assert infix.edge_count() == 2
lhs := infix.edge(0)
rhs := infix.edge(1)
assert lhs.kind() == .expr_ident
assert lhs.name() == 'a'
assert rhs.kind() == .expr_ident
assert rhs.name() == 'b'
}
fn test_invalid_cursor_sentinels() {
flat := build_minimal_file()
fc := flat.file_cursor(0)
bogus := fc.stmts().at(99) // out of range
assert !bogus.is_valid()
bogus_edge := fc.root().edge(99) // out of range
assert !bogus_edge.is_valid()
empty_list := fc.root().list_at(99)
assert empty_list.len() == 0
}
fn test_type_expr_from_cursor_reads_signature_types() {
mut b := new_flat_builder()
src := File{
name: 'inline_type_expr.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(FnDecl{
name: 'use_box'
typ: FnType{
params: [
Parameter{
name: 'items'
typ: Expr(Type(ArrayType{
elem_type: Expr(GenericArgs{
lhs: Expr(Ident{
name: 'Box'
})
args: [
Expr(Ident{
name: 'int'
}),
]
})
}))
},
]
return_type: Expr(Type(OptionType{
base_type: Expr(Ident{
name: 'string'
})
}))
}
}),
]
}
b.append_file(src)
fn_cur := b.flat.file_cursor(0).stmts().at(1)
fn_type := fn_cur.edge(1)
params := fn_type.list_at(1)
param_typ := params.at(0).edge(0).type_expr()
assert param_typ is Type
array_typ := param_typ as Type
assert array_typ is ArrayType
array_expr := array_typ as ArrayType
assert array_expr.elem_type is GenericArgs
generic := array_expr.elem_type as GenericArgs
assert generic.lhs is Ident
assert (generic.lhs as Ident).name == 'Box'
assert generic.args.len == 1
assert generic.args[0] is Ident
assert (generic.args[0] as Ident).name == 'int'
return_typ := fn_type.edge(2).type_expr()
assert return_typ is Type
option_typ := return_typ as Type
assert option_typ is OptionType
option_expr := option_typ as OptionType
assert option_expr.base_type is Ident
assert (option_expr.base_type as Ident).name == 'string'
}
fn test_decl_readers_from_cursor() {
mut b := new_flat_builder()
src := File{
name: 'inline_decl_readers.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'answer'
value: Expr(BasicLiteral{
kind: .number
value: '42'
})
},
]
}),
Stmt(EnumDecl{
name: 'Mode'
attributes: [
Attribute{
name: 'flag'
},
]
fields: [
FieldDecl{
name: 'read'
value: Expr(BasicLiteral{
kind: .number
value: '1'
})
},
]
}),
Stmt(StructDecl{
name: 'Person'
attributes: [
Attribute{
name: 'heap'
},
]
fields: [
FieldDecl{
name: 'name'
typ: Expr(Ident{
name: 'string'
})
value: Expr(StringLiteral{
value: 'unknown'
})
attributes: [
Attribute{
name: 'json'
value: Expr(StringLiteral{
value: 'full_name'
})
},
]
is_public: true
},
]
}),
]
}
b.append_file(src)
stmts := b.flat.file_cursor(0).stmts()
const_decl := stmts.at(1).const_decl()
assert const_decl.fields.len == 1
assert const_decl.fields[0].name == 'answer'
assert const_decl.fields[0].value is BasicLiteral
assert (const_decl.fields[0].value as BasicLiteral).value == '42'
enum_decl := stmts.at(2).enum_decl(true)
assert enum_decl.name == 'Mode'
assert enum_decl.attributes.has('flag')
assert enum_decl.fields.len == 1
assert enum_decl.fields[0].value is BasicLiteral
assert (enum_decl.fields[0].value as BasicLiteral).value == '1'
struct_decl := stmts.at(3).struct_decl()
assert struct_decl.name == 'Person'
assert struct_decl.attributes.has('heap')
assert struct_decl.fields.len == 1
assert struct_decl.fields[0].name == 'name'
assert struct_decl.fields[0].is_public
assert struct_decl.fields[0].typ is Ident
assert (struct_decl.fields[0].typ as Ident).name == 'string'
assert struct_decl.fields[0].value is StringLiteral
assert (struct_decl.fields[0].value as StringLiteral).value == 'unknown'
assert struct_decl.fields[0].attributes.len == 1
assert struct_decl.fields[0].attributes[0].name == 'json'
assert struct_decl.fields[0].attributes[0].value is StringLiteral
assert (struct_decl.fields[0].attributes[0].value as StringLiteral).value == 'full_name'
}
// count_unique_kinds_via_cursor walks the whole graph rooted at fc through
// edges with dedup (the FlatBuilder shares aux_list nodes for empty lists,
// so a naive DFS revisits them). Used to cross-check that Cursor traversal
// reaches every reachable node in the FlatAst.
fn count_unique_kinds_via_cursor(fc FileCursor) (map[string]int, int) {
flat := fc.flat
mut seen := []bool{len: flat.nodes.len}
mut out := map[string]int{}
mut stack := []Cursor{cap: 64}
mut unique := 0
stack << fc.root()
for stack.len > 0 {
c := stack.pop()
if !c.is_valid() || seen[c.id] {
continue
}
seen[c.id] = true
unique++
out[c.kind().str()]++
for i in 0 .. c.edge_count() {
stack << c.edge(i)
}
}
return out, unique
}
fn test_cursor_walk_matches_reachable_count() {
flat := build_minimal_file()
_, unique := count_unique_kinds_via_cursor(flat.file_cursor(0))
// Walking from the single file root via Cursor must reach the same
// set of unique nodes as the reference reachability walker.
assert unique == flat.count_reachable_nodes()
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// NOTE: this is just a very naive example of how it could possibly work.
// actual implementation may work during AST -> IR (or not). it may also
// need type information which we don't have here. as I said, just an example.
pub fn (match_expr &MatchExpr) desugar() Expr {
// Keep this helper conservative for now; match lowering happens in transformer.
return match_expr.expr
}
pub fn (or_expr &OrExpr) desugar() Expr {
// Keep this helper conservative for now; or-lowering happens in transformer.
return or_expr.expr
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,92 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.append_file_stmts`. The helper is the foundational
// primitive behind the transformer's upcoming `post_pass_to_flat` port —
// it appends pre-emitted stmt nodes to a registered file's stmt list and
// re-emits the file root, mirroring the post-pass mutation pattern used by
// `inject_embed_file_helper`, `inject_test_main`, and `generated_fns_parts`.
fn make_minimal_file_with_stmts(extra_stmts []Stmt) File {
mut stmts := []Stmt{cap: 1 + extra_stmts.len}
stmts << Stmt(ModuleStmt{
name: 'foo'
})
for s in extra_stmts {
stmts << s
}
return File{
name: 'inline_minimal.v'
mod: 'foo'
stmts: stmts
}
}
fn make_extra_stmt_a() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'A'
value: Expr(BasicLiteral{
kind: .number
value: '1'
})
},
]
})
}
fn make_extra_stmt_b() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'B'
value: Expr(BasicLiteral{
kind: .number
value: '2'
})
},
]
})
}
// Reference: build a file directly with both extras at construction time.
fn build_reference_with_extras() FlatAst {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts([make_extra_stmt_a(),
make_extra_stmt_b()]))
return b.flat
}
// Subject: build the bare file, emit extras separately into the same builder,
// then splice them in via append_file_stmts.
fn build_subject_with_appended_extras() FlatAst {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts([]))
a_id := b.emit_stmt(make_extra_stmt_a())
b_id := b.emit_stmt(make_extra_stmt_b())
b.append_file_stmts(0, [a_id, b_id])
return b.flat
}
fn test_append_file_stmts_signature_matches_reference() {
ref_sig := build_reference_with_extras().signature()
sub_sig := build_subject_with_appended_extras().signature()
assert ref_sig == sub_sig
}
fn test_append_file_stmts_zero_extras_returns_existing_root() {
mut b := new_flat_builder()
original_id := b.append_file(make_minimal_file_with_stmts([]))
returned_id := b.append_file_stmts(0, [])
assert returned_id == original_id
}
fn test_append_file_stmts_invalid_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts([]))
returned_id := b.append_file_stmts(-1, [FlatNodeId(0)])
assert returned_id == invalid_flat_node_id
}
@@ -0,0 +1,210 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.append_flat` — the flat-to-flat merge primitive behind
// the flat parallel transform. It concatenates a whole `src` FlatAst into a
// builder, relocating node ids / edge targets and re-interning strings so the
// merged result decodes identically to `src` standalone. The key correctness
// risks: (1) node-id / edge relocation, (2) name_id string remap across a
// DIFFERENT intern order in the destination, (3) re-interning the three kinds
// whose `extra` slot holds a string id (file mod, stmt_directive value,
// stmt_import alias) while leaving packed-int extras (counts/flags) untouched.
fn make_ident_stmt(name string) Stmt {
return Stmt(ExprStmt{
expr: Expr(Ident{
name: name
})
})
}
fn make_for_with_idents(names []string) Stmt {
mut body := []Stmt{}
for n in names {
body << make_ident_stmt(n)
}
return Stmt(ForStmt{
init: Stmt(EmptyStmt{})
cond: Expr(BasicLiteral{
kind: .number
value: '1'
})
stmts: body
})
}
// build a small flat with a file root whose stmt list is `stmts`.
fn build_named_flat(file_name string, mod string, stmts []Stmt) (FlatBuilder, FlatNodeId) {
mut b := new_flat_builder()
mut ids := []FlatNodeId{}
for s in stmts {
ids << b.emit_stmt(s)
}
file_id := b.append_file_with_stmt_ids(File{
name: file_name
mod: mod
}, ids)
return b, file_id
}
// Seed a destination builder with DIFFERENT strings so the src->dst string
// remap is non-trivial (src ids must not coincide with dst ids).
fn seeded_dst() FlatBuilder {
mut dst := new_flat_builder()
dst.intern('zzz_pre_a')
dst.intern('zzz_pre_b')
dst.intern('zzz_pre_c')
dst.emit_stmt(make_ident_stmt('zzz_pre_d'))
return dst
}
fn test_append_flat_preserves_subtree_structure_and_name_strings() {
mut a, a_file := build_named_flat('a.v', 'main', [
make_for_with_idents(['alpha', 'beta']),
make_ident_stmt('gamma'),
])
// edge 2 of a .file node is the stmts list; rooting the signature there
// avoids the intern-order-dependent .file `extra` slot.
a_stmts := a.flat.child_at(a_file, 2)
a_sig := a.flat.subtree_signature(a_stmts)
mut dst := seeded_dst()
pre_nodes := dst.flat.nodes.len
pre_edges := dst.flat.edges.len
off := dst.append_flat(a.flat)
assert off == pre_nodes
assert dst.flat.nodes.len == pre_nodes + a.flat.nodes.len
assert dst.flat.edges.len == pre_edges + a.flat.edges.len
// Structure + every interned ident/value survives the id+string relocation.
assert dst.flat.subtree_signature(a_stmts + off) == a_sig
}
fn test_append_flat_two_sources_stay_independent() {
mut a, a_file := build_named_flat('a.v', 'main', [
make_for_with_idents(['alpha', 'beta']),
])
mut b, b_file := build_named_flat('b.v', 'other', [make_ident_stmt('delta'),
make_ident_stmt('epsilon')])
a_sig := a.flat.subtree_signature(a.flat.child_at(a_file, 2))
b_sig := b.flat.subtree_signature(b.flat.child_at(b_file, 2))
mut dst := seeded_dst()
off_a := dst.append_flat(a.flat)
off_b := dst.append_flat(b.flat)
assert dst.flat.files.len == 2
assert dst.flat.subtree_signature(a.flat.child_at(a_file, 2) + off_a) == a_sig
assert dst.flat.subtree_signature(b.flat.child_at(b_file, 2) + off_b) == b_sig
// b's offset accounts for everything already present (seed + a).
assert off_b == off_a + a.flat.nodes.len
}
fn test_append_flat_reinterns_string_extra_kinds() {
mut a := new_flat_builder()
imp_id := a.emit_stmt(Stmt(ImportStmt{
name: 'os'
alias: 'myos'
is_aliased: true
}))
dir_id := a.emit_stmt(Stmt(Directive{
name: 'flag'
value: '-lpthread'
}))
file_id := a.append_file_with_stmt_ids(File{
name: 'a.v'
mod: 'mymod'
}, [imp_id, dir_id])
mut dst := new_flat_builder()
// Shift the intern table so src string ids differ from dst string ids.
dst.intern('os')
dst.intern('zzz')
dst.intern('flag_other')
dst.intern('unrelated_mod')
off := dst.append_flat(a.flat)
// stmt_import.alias lives in `extra` — must resolve to the merged table.
merged_imp := Cursor{
flat: &dst.flat
id: imp_id + off
}.stmt() as ImportStmt
assert merged_imp.name == 'os'
assert merged_imp.alias == 'myos'
assert merged_imp.is_aliased
// stmt_directive.value lives in `extra`.
merged_dir := Cursor{
flat: &dst.flat
id: dir_id + off
}.stmt() as Directive
assert merged_dir.name == 'flag'
assert merged_dir.value == '-lpthread'
// file mod is carried both in FlatFile.mod_idx (decoder reads this) and in
// the .file node's `extra` (must also be re-interned for consistency).
mf := dst.flat.files[dst.flat.files.len - 1]
assert dst.flat.string_at(mf.name_idx) == 'a.v'
assert dst.flat.string_at(mf.mod_idx) == 'mymod'
assert dst.flat.string_at(dst.flat.nodes[file_id + off].extra) == 'mymod'
}
fn test_append_flat_empty_source_is_noop() {
mut dst := seeded_dst()
pre_nodes := dst.flat.nodes.len
pre_edges := dst.flat.edges.len
pre_files := dst.flat.files.len
empty := FlatAst{}
off := dst.append_flat(&empty)
assert off == pre_nodes
assert dst.flat.nodes.len == pre_nodes
assert dst.flat.edges.len == pre_edges
assert dst.flat.files.len == pre_files
}
fn test_copy_subtree_from_preserves_stmt_shape_and_strings() {
mut src := new_flat_builder()
imp_id := src.emit_stmt(Stmt(ImportStmt{
name: 'os'
alias: 'myos'
is_aliased: true
}))
dir_id := src.emit_stmt(Stmt(Directive{
name: 'flag'
value: '-lm'
}))
for_id := src.emit_stmt(make_for_with_idents(['alpha', 'beta']))
mut dst := seeded_dst()
copied_imp := dst.copy_subtree_from(&src.flat, imp_id)
copied_dir := dst.copy_subtree_from(&src.flat, dir_id)
copied_for := dst.copy_subtree_from(&src.flat, for_id)
assert dst.flat.subtree_signature(copied_for) == src.flat.subtree_signature(for_id)
merged_imp := Cursor{
flat: &dst.flat
id: copied_imp
}.stmt() as ImportStmt
assert merged_imp.name == 'os'
assert merged_imp.alias == 'myos'
assert merged_imp.is_aliased
merged_dir := Cursor{
flat: &dst.flat
id: copied_dir
}.stmt() as Directive
assert merged_dir.name == 'flag'
assert merged_dir.value == '-lm'
}
fn test_copy_subtree_from_invalid_root_returns_invalid() {
mut src := new_flat_builder()
mut dst := new_flat_builder()
assert dst.copy_subtree_from(&src.flat, -1) == invalid_flat_node_id
assert dst.copy_subtree_from(&src.flat, 100) == invalid_flat_node_id
}
@@ -0,0 +1,222 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
import v2.token
fn field_flags_ident_expr(name string) Expr {
return Expr(Ident{
name: name
})
}
fn field_flags_int_expr() Expr {
return field_flags_ident_expr('int')
}
fn make_field_flags_file() File {
return File{
name: 'field_flags.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
Stmt(StructDecl{
name: 'Config'
fields: [
FieldDecl{
name: 'public_value'
typ: field_flags_int_expr()
is_public: true
},
FieldDecl{
name: 'mutable_value'
typ: field_flags_int_expr()
is_mut: true
},
FieldDecl{
name: 'module_mut_value'
typ: field_flags_int_expr()
is_module_mut: true
},
]
}),
Stmt(InterfaceDecl{
name: 'Runner'
fields: [
FieldDecl{
name: 'run'
typ: field_flags_int_expr()
is_interface_method: true
},
]
}),
]
}
}
fn test_field_decl_flags_survive_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(make_field_flags_file())
files := b.flat.to_files()
assert files.len == 1
assert files[0].stmts.len == 3
assert files[0].stmts[1] is StructDecl
struct_decl := files[0].stmts[1] as StructDecl
assert struct_decl.fields.len == 3
assert struct_decl.fields[0].is_public
assert !struct_decl.fields[0].is_mut
assert !struct_decl.fields[0].is_module_mut
assert !struct_decl.fields[0].is_interface_method
assert !struct_decl.fields[1].is_public
assert struct_decl.fields[1].is_mut
assert !struct_decl.fields[1].is_module_mut
assert !struct_decl.fields[1].is_interface_method
assert !struct_decl.fields[2].is_public
assert !struct_decl.fields[2].is_mut
assert struct_decl.fields[2].is_module_mut
assert !struct_decl.fields[2].is_interface_method
assert files[0].stmts[2] is InterfaceDecl
interface_decl := files[0].stmts[2] as InterfaceDecl
assert interface_decl.fields.len == 1
assert !interface_decl.fields[0].is_public
assert !interface_decl.fields[0].is_mut
assert !interface_decl.fields[0].is_module_mut
assert interface_decl.fields[0].is_interface_method
}
fn test_global_decl_public_flag_survives_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(File{
name: 'global_decl_public.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
Stmt(GlobalDecl{
is_public: true
fields: [
FieldDecl{
name: 'counter'
typ: field_flags_int_expr()
},
]
}),
]
})
files := b.flat.to_files()
assert files[0].stmts.len == 2
assert files[0].stmts[1] is GlobalDecl
global_decl := files[0].stmts[1] as GlobalDecl
assert global_decl.is_public
assert global_decl.fields.len == 1
}
fn test_emit_field_decl_by_ids_preserves_field_flags() {
field := FieldDecl{
name: 'global_counter'
typ: field_flags_int_expr()
is_public: true
is_mut: true
}
mut b := new_flat_builder()
b.append_file(File{
name: 'direct_field_flags.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
]
})
typ_id := b.emit_expr(field.typ)
value_id := b.emit_expr(field.value)
attrs_id := b.emit_attribute_list(field.attributes)
field_id := b.emit_field_decl_by_ids(field, typ_id, value_id, attrs_id)
fields_id := b.emit_aux_list_from_ids([field_id])
decl_attrs_id := b.emit_attribute_list([])
global_id := b.emit_global_decl_by_ids(true, decl_attrs_id, fields_id)
b.append_file_stmts(0, [global_id])
files := b.flat.to_files()
assert files[0].stmts.len == 2
assert files[0].stmts[1] is GlobalDecl
global_decl := files[0].stmts[1] as GlobalDecl
assert global_decl.is_public
assert global_decl.fields.len == 1
assert global_decl.fields[0].is_public
assert global_decl.fields[0].is_mut
assert !global_decl.fields[0].is_module_mut
assert !global_decl.fields[0].is_interface_method
}
fn field_flags_array_update_from_stmt(stmt Stmt) ArrayInitExpr {
assert stmt is ExprStmt
expr_stmt := stmt as ExprStmt
assert expr_stmt.expr is ArrayInitExpr
return expr_stmt.expr as ArrayInitExpr
}
fn test_array_init_update_expr_survives_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(File{
name: 'array_update_expr.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
Stmt(ExprStmt{
expr: Expr(ArrayInitExpr{
typ: field_flags_int_expr()
update_expr: field_flags_ident_expr('base')
exprs: [
field_flags_ident_expr('item'),
]
})
}),
]
})
files := b.flat.to_files()
assert files[0].stmts.len == 2
array_expr := field_flags_array_update_from_stmt(files[0].stmts[1])
assert array_expr.update_expr is Ident
assert (array_expr.update_expr as Ident).name == 'base'
assert array_expr.exprs.len == 1
assert array_expr.exprs[0] is Ident
assert (array_expr.exprs[0] as Ident).name == 'item'
}
fn test_emit_array_init_expr_by_ids_preserves_update_expr() {
mut b := new_flat_builder()
b.append_file(File{
name: 'direct_array_update_expr.v'
mod: 'field_flags'
stmts: [
Stmt(ModuleStmt{
name: 'field_flags'
}),
]
})
typ_id := b.emit_expr(field_flags_int_expr())
init_id := b.emit_expr(empty_expr)
cap_id := b.emit_expr(empty_expr)
len_id := b.emit_expr(empty_expr)
update_expr_id := b.emit_expr(field_flags_ident_expr('base'))
elem_id := b.emit_expr(field_flags_ident_expr('item'))
array_id := b.emit_array_init_expr_by_ids(typ_id, init_id, cap_id, len_id, update_expr_id, [
elem_id,
], token.Pos{})
stmt_id := b.emit_expr_stmt_by_id(array_id)
b.append_file_stmts(0, [stmt_id])
files := b.flat.to_files()
assert files[0].stmts.len == 2
array_expr := field_flags_array_update_from_stmt(files[0].stmts[1])
assert array_expr.update_expr is Ident
assert (array_expr.update_expr as Ident).name == 'base'
assert array_expr.exprs.len == 1
assert array_expr.exprs[0] is Ident
assert (array_expr.exprs[0] as Ident).name == 'item'
}
@@ -0,0 +1,49 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
fn lock_expr_ident(name string) Expr {
return Expr(Ident{
name: name
})
}
fn lock_expr_stmt(value string) Stmt {
return Stmt(ExprStmt{
expr: Expr(BasicLiteral{
kind: .number
value: value
})
})
}
fn test_lock_expr_counts_survive_flat_roundtrip() {
mut b := new_flat_builder()
b.append_file(File{
name: 'lock_expr.v'
mod: 'lock_expr'
stmts: [
Stmt(ModuleStmt{
name: 'lock_expr'
}),
Stmt(ExprStmt{
expr: Expr(LockExpr{
lock_exprs: [lock_expr_ident('a'), lock_expr_ident('b')]
rlock_exprs: [lock_expr_ident('c')]
stmts: [lock_expr_stmt('1'), lock_expr_stmt('2')]
})
}),
]
})
files := b.flat.to_files()
assert files.len == 1
assert files[0].stmts.len == 2
assert files[0].stmts[1] is ExprStmt
expr_stmt := files[0].stmts[1] as ExprStmt
assert expr_stmt.expr is LockExpr
lock_expr := expr_stmt.expr as LockExpr
assert lock_expr.lock_exprs.len == 2
assert lock_expr.rlock_exprs.len == 1
assert lock_expr.stmts.len == 2
}
@@ -0,0 +1,128 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.prepend_file_stmts`. Sibling of `append_file_stmts`
// (s150) — same shape, opposite order. Needed by `inject_live_reload`'s
// file-level prepend where c_decls + global_decls go BEFORE the file's
// existing stmts.
fn make_minimal_file_with_stmts_prepend(extra_stmts []Stmt) File {
mut stmts := []Stmt{cap: 1 + extra_stmts.len}
for s in extra_stmts {
stmts << s
}
stmts << Stmt(ModuleStmt{
name: 'foo'
})
stmts << Stmt(ConstDecl{
fields: [
FieldInit{
name: 'TAIL'
value: Expr(BasicLiteral{
kind: .number
value: '0'
})
},
]
})
return File{
name: 'inline_prepend.v'
mod: 'foo'
stmts: stmts
}
}
fn make_prepend_const_a() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'PA'
value: Expr(BasicLiteral{
kind: .number
value: '11'
})
},
]
})
}
fn make_prepend_const_b() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'PB'
value: Expr(BasicLiteral{
kind: .number
value: '22'
})
},
]
})
}
// Reference: construct the file with both prepended consts BEFORE the module
// stmt at append time.
//
// Why this layout: prepend_file_stmts puts new stmts at the head of the
// stmts list — even before the ModuleStmt — so the reference must mirror
// that ordering at construction time to produce a bit-equal signature.
//
// Why subtree-of-stmts-list instead of full file signature: the file root's
// `extra` slot stores `intern(mod)` as a raw index. When the reference path
// emits the file's stmts in order, the prepended consts intern their strings
// BEFORE 'foo' the module name → 'foo' lands at a later intern slot. The
// subject path interns 'foo' first (via the bare file's ModuleStmt) → 'foo'
// at slot 0. Comparing the stmts-list subtree sidesteps this leak: it covers
// the actual prepended stmts ordering + content (what the primitive controls)
// without including the file root's leaky `extra` field. The s155 multi-file
// test uses the same subtree_signature workaround for the same reason.
fn build_reference_stmts_list_signature() string {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts_prepend([make_prepend_const_a(),
make_prepend_const_b()]))
stmts_list_id := b.flat.child_at(b.flat.files[0].file_id, 2)
return b.flat.subtree_signature(stmts_list_id)
}
// Subject: build the bare file, emit prepended stmts separately, then splice
// them via prepend_file_stmts.
fn build_subject_stmts_list_signature() string {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts_prepend([]))
a_id := b.emit_stmt(make_prepend_const_a())
c_id := b.emit_stmt(make_prepend_const_b())
b.prepend_file_stmts(0, [a_id, c_id])
stmts_list_id := b.flat.child_at(b.flat.files[0].file_id, 2)
return b.flat.subtree_signature(stmts_list_id)
}
fn test_prepend_file_stmts_signature_matches_reference() {
ref_sig := build_reference_stmts_list_signature()
sub_sig := build_subject_stmts_list_signature()
assert ref_sig == sub_sig
}
fn test_prepend_file_stmts_zero_prepended_returns_existing_root() {
mut b := new_flat_builder()
original_id := b.append_file(make_minimal_file_with_stmts_prepend([]))
returned_id := b.prepend_file_stmts(0, [])
assert returned_id == original_id
}
fn test_prepend_file_stmts_invalid_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_stmts_prepend([]))
returned_id := b.prepend_file_stmts(-1, [FlatNodeId(0)])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_file_stmts_updates_files_table() {
mut b := new_flat_builder()
original_file_id := b.append_file(make_minimal_file_with_stmts_prepend([]))
x_id := b.emit_stmt(make_prepend_const_a())
new_file_id := b.prepend_file_stmts(0, [x_id])
assert new_file_id != original_file_id
assert b.flat.files[0].file_id == new_file_id
}
@@ -0,0 +1,110 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.prepend_to_fn_body`. The helper is the seed primitive
// for the remaining prepend-style post_pass mutations
// (`inject_main_runtime_const_init_calls`, `inject_live_reload`): it re-emits
// a stmt_fn_decl with extras prepended to its body, returning the new
// FlatNodeId so callers can rewire the surrounding file/stmt list.
//
// We compare via `subtree_signature(fn_id)` rather than the whole-file
// `signature()` because the file root's `extra=intern(mod)` slot would leak
// intern order between the reference (which builds extras before file mod
// gets re-interned) and subject (which interns extras AFTER file mod) paths.
fn make_fn_decl_with_body(name string, body []Stmt) Stmt {
return Stmt(FnDecl{
name: name
stmts: body
})
}
fn make_const_stmt(name string, value string) Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: name
value: Expr(BasicLiteral{
kind: .number
value: value
})
},
]
})
}
// Reference: emit a FnDecl whose body is already `[extras + old]`.
fn build_reference_fn_with_prepended() (FlatAst, FlatNodeId) {
mut b := new_flat_builder()
fn_id := b.emit_stmt(make_fn_decl_with_body('foo', [
make_const_stmt('A', '1'),
make_const_stmt('B', '2'),
make_const_stmt('C', '3'),
]))
return b.flat, fn_id
}
// Subject: emit a FnDecl whose body is `[old]`, emit extras separately,
// then prepend via the primitive under test.
fn build_subject_fn_with_prepended() (FlatAst, FlatNodeId) {
mut b := new_flat_builder()
base_fn_id := b.emit_stmt(make_fn_decl_with_body('foo', [
make_const_stmt('C', '3'),
]))
a_id := b.emit_stmt(make_const_stmt('A', '1'))
b_id := b.emit_stmt(make_const_stmt('B', '2'))
new_fn_id := b.prepend_to_fn_body(base_fn_id, [a_id, b_id])
return b.flat, new_fn_id
}
fn test_prepend_to_fn_body_signature_matches_reference() {
ref_flat, ref_fn_id := build_reference_fn_with_prepended()
sub_flat, sub_fn_id := build_subject_fn_with_prepended()
ref_sig := ref_flat.subtree_signature(ref_fn_id)
sub_sig := sub_flat.subtree_signature(sub_fn_id)
assert ref_sig == sub_sig
}
fn test_prepend_to_fn_body_zero_extras_returns_existing_id() {
mut b := new_flat_builder()
original_id := b.emit_stmt(make_fn_decl_with_body('foo', [
make_const_stmt('C', '3'),
]))
returned_id := b.prepend_to_fn_body(original_id, [])
assert returned_id == original_id
}
fn test_prepend_to_fn_body_invalid_id_returns_invalid() {
mut b := new_flat_builder()
a_id := b.emit_stmt(make_const_stmt('A', '1'))
returned_id := b.prepend_to_fn_body(FlatNodeId(-1), [a_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_fn_body_non_fn_decl_returns_invalid() {
// Pass a non-FnDecl node id (a ConstDecl) — primitive must refuse.
mut b := new_flat_builder()
const_id := b.emit_stmt(make_const_stmt('C', '3'))
extra_id := b.emit_stmt(make_const_stmt('A', '1'))
returned_id := b.prepend_to_fn_body(const_id, [extra_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_fn_body_empty_original_body() {
// FnDecl with no original body stmts; prepending should still produce a
// new FnDecl whose body is exactly the extras.
ref_flat, ref_fn_id := fn () (FlatAst, FlatNodeId) {
mut b := new_flat_builder()
fn_id := b.emit_stmt(make_fn_decl_with_body('foo', [make_const_stmt('A', '1')]))
return b.flat, fn_id
}()
mut sb := new_flat_builder()
base_fn_id := sb.emit_stmt(make_fn_decl_with_body('foo', []))
a_id := sb.emit_stmt(make_const_stmt('A', '1'))
new_fn_id := sb.prepend_to_fn_body(base_fn_id, [a_id])
assert ref_flat.subtree_signature(ref_fn_id) == sb.flat.subtree_signature(new_fn_id)
}
@@ -0,0 +1,95 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.prepend_to_for_body`. The ForStmt analog of s154's
// `prepend_to_fn_body`. Body-stmt encoding differs: ForStmt encodes body
// stmts as INLINE edges starting at index 3 (no list child), so this
// primitive rebuilds the for's edge list directly rather than rebuilding a
// stmts-list child node.
fn make_for_body_assert_stmt(value string) Stmt {
return Stmt(AssertStmt{
expr: Expr(BasicLiteral{
kind: .number
value: value
})
})
}
// Build a stmt_for via the legacy add_stmt path so the test exercises the
// same encoding that real callsites produce.
fn build_for_stmt(body_stmts []Stmt) (FlatBuilder, FlatNodeId) {
mut b := new_flat_builder()
for_stmt := Stmt(ForStmt{
init: Stmt(EmptyStmt{})
cond: Expr(BasicLiteral{
kind: .number
value: '1'
})
stmts: body_stmts
})
for_id := b.emit_stmt(for_stmt)
return b, for_id
}
fn ref_sig_for_body(body_stmts []Stmt) string {
mut b, for_id := build_for_stmt(body_stmts)
return b.flat.subtree_signature(for_id)
}
fn sub_sig_for_prepend(initial_body []Stmt, extra_values []string) string {
mut b, for_id := build_for_stmt(initial_body)
mut extra_ids := []FlatNodeId{}
for v in extra_values {
extra_ids << b.emit_stmt(make_for_body_assert_stmt(v))
}
new_for_id := b.prepend_to_for_body(for_id, extra_ids)
return b.flat.subtree_signature(new_for_id)
}
fn test_prepend_to_for_body_signature_matches_reference() {
ref_sig := ref_sig_for_body([make_for_body_assert_stmt('99'),
make_for_body_assert_stmt('1')])
sub_sig := sub_sig_for_prepend([make_for_body_assert_stmt('1')], ['99'])
assert ref_sig == sub_sig
}
fn test_prepend_to_for_body_zero_extras_returns_existing_id() {
mut b, for_id := build_for_stmt([make_for_body_assert_stmt('1')])
returned_id := b.prepend_to_for_body(for_id, [])
assert returned_id == for_id
}
fn test_prepend_to_for_body_invalid_id_returns_invalid() {
mut b := new_flat_builder()
extra_id := b.emit_stmt(make_for_body_assert_stmt('99'))
returned_id := b.prepend_to_for_body(FlatNodeId(-1), [extra_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_for_body_non_for_kind_returns_invalid() {
mut b := new_flat_builder()
// stmt_assert is not stmt_for — kind check must reject it.
assert_id := b.emit_stmt(make_for_body_assert_stmt('1'))
extra_id := b.emit_stmt(make_for_body_assert_stmt('99'))
returned_id := b.prepend_to_for_body(assert_id, [extra_id])
assert returned_id == invalid_flat_node_id
}
fn test_prepend_to_for_body_empty_original_body() {
// ForStmt with no body stmts at all — extras become the entire new body.
ref_sig := ref_sig_for_body([make_for_body_assert_stmt('99')])
sub_sig := sub_sig_for_prepend([]Stmt{}, ['99'])
assert ref_sig == sub_sig
}
fn test_prepend_to_for_body_preserves_existing_body_order() {
// Two existing body stmts + one prepended; order must be [extra, b1, b2].
ref_sig := ref_sig_for_body([make_for_body_assert_stmt('99'),
make_for_body_assert_stmt('1'), make_for_body_assert_stmt('2')])
sub_sig := sub_sig_for_prepend([make_for_body_assert_stmt('1'),
make_for_body_assert_stmt('2')], ['99'])
assert ref_sig == sub_sig
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,153 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.replace_file_stmt`. The companion primitive to
// `prepend_to_fn_body`: when a caller swaps a stmt for a re-emitted variant
// (e.g. the new FnDecl returned by `prepend_to_fn_body`), `replace_file_stmt`
// rebuilds the enclosing file root so the file's stmts list references the
// new id. Together they support the prepend-style post_pass mutations
// (`inject_main_runtime_const_init_calls`, s155).
fn make_minimal_file_with_three_consts() File {
return File{
name: 'inline.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'A'
value: Expr(BasicLiteral{
kind: .number
value: '1'
})
},
]
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'B'
value: Expr(BasicLiteral{
kind: .number
value: '2'
})
},
]
}),
Stmt(ConstDecl{
fields: [
FieldInit{
name: 'C'
value: Expr(BasicLiteral{
kind: .number
value: '3'
})
},
]
}),
]
}
}
fn make_replacement_const_stmt() Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: 'Z'
value: Expr(BasicLiteral{
kind: .number
value: '99'
})
},
]
})
}
fn make_const_stmt_named(name string, value string) Stmt {
return Stmt(ConstDecl{
fields: [
FieldInit{
name: name
value: Expr(BasicLiteral{
kind: .number
value: value
})
},
]
})
}
// Reference: build the file with stmt index 2 (the `B` const) already
// replaced by the `Z` const at construction time.
fn build_reference_with_replacement() FlatAst {
mut b := new_flat_builder()
b.append_file(File{
name: 'inline.v'
mod: 'foo'
stmts: [
Stmt(ModuleStmt{
name: 'foo'
}),
make_const_stmt_named('A', '1'),
make_replacement_const_stmt(),
make_const_stmt_named('C', '3'),
]
})
return b.flat
}
// Subject: build the bare three-const file, emit the replacement stmt
// separately, then splice it in via `replace_file_stmt(0, 2, z_id)`.
fn build_subject_with_replacement() FlatAst {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
b.replace_file_stmt(0, 2, z_id)
return b.flat
}
fn test_replace_file_stmt_signature_matches_reference() {
ref_sig := build_reference_with_replacement().signature()
sub_sig := build_subject_with_replacement().signature()
assert ref_sig == sub_sig
}
fn test_replace_file_stmt_invalid_file_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
returned_id := b.replace_file_stmt(-1, 0, z_id)
assert returned_id == invalid_flat_node_id
}
fn test_replace_file_stmt_invalid_stmt_idx_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
// File has 4 stmts (module + 3 consts); idx 99 is out of range.
returned_id := b.replace_file_stmt(0, 99, z_id)
assert returned_id == invalid_flat_node_id
}
fn test_replace_file_stmt_invalid_new_id_returns_invalid() {
mut b := new_flat_builder()
b.append_file(make_minimal_file_with_three_consts())
returned_id := b.replace_file_stmt(0, 0, FlatNodeId(-1))
assert returned_id == invalid_flat_node_id
}
fn test_replace_file_stmt_updates_files_table() {
mut b := new_flat_builder()
original_file_id := b.append_file(make_minimal_file_with_three_consts())
z_id := b.emit_stmt(make_replacement_const_stmt())
new_file_id := b.replace_file_stmt(0, 1, z_id)
// The flat.files[0].file_id must point at the new file root, not the old.
assert new_file_id != original_file_id
assert b.flat.files[0].file_id == new_file_id
}
@@ -0,0 +1,83 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
// Tests for `FlatBuilder.replace_fn_body_stmts`. The companion primitive to
// `prepend_to_fn_body`: replaces the FnDecl's body entirely (vs prepending
// to it). Needed by `inject_live_reload` which builds a fully-rewritten
// FnDecl body in one shot (preamble + rewritten for-stmts + rewritten calls).
fn make_assert_stmt_value(value string) Stmt {
return Stmt(AssertStmt{
expr: Expr(BasicLiteral{
kind: .number
value: value
})
})
}
fn build_fn_decl(body_stmts []Stmt) (FlatBuilder, FlatNodeId) {
mut b := new_flat_builder()
fn_stmt := Stmt(FnDecl{
name: 'f'
stmts: body_stmts
})
fn_id := b.emit_stmt(fn_stmt)
return b, fn_id
}
fn ref_sig_for_fn_body(body_stmts []Stmt) string {
mut b, fn_id := build_fn_decl(body_stmts)
return b.flat.subtree_signature(fn_id)
}
fn sub_sig_for_replace(initial_body []Stmt, new_body_values []string) string {
mut b, fn_id := build_fn_decl(initial_body)
mut new_ids := []FlatNodeId{}
for v in new_body_values {
new_ids << b.emit_stmt(make_assert_stmt_value(v))
}
new_fn_id := b.replace_fn_body_stmts(fn_id, new_ids)
return b.flat.subtree_signature(new_fn_id)
}
fn test_replace_fn_body_stmts_signature_matches_reference() {
ref_sig := ref_sig_for_fn_body([make_assert_stmt_value('7'),
make_assert_stmt_value('8'), make_assert_stmt_value('9')])
sub_sig := sub_sig_for_replace([make_assert_stmt_value('1'),
make_assert_stmt_value('2')], ['7', '8', '9'])
assert ref_sig == sub_sig
}
fn test_replace_fn_body_stmts_empty_new_body() {
// Replacing with an empty body — the fn becomes bodyless.
ref_sig := ref_sig_for_fn_body([]Stmt{})
sub_sig := sub_sig_for_replace([make_assert_stmt_value('1')], []string{})
assert ref_sig == sub_sig
}
fn test_replace_fn_body_stmts_invalid_id_returns_invalid() {
mut b := new_flat_builder()
x_id := b.emit_stmt(make_assert_stmt_value('1'))
returned_id := b.replace_fn_body_stmts(FlatNodeId(-1), [x_id])
assert returned_id == invalid_flat_node_id
}
fn test_replace_fn_body_stmts_non_fn_kind_returns_invalid() {
mut b := new_flat_builder()
// stmt_assert, not stmt_fn_decl — kind check must reject it.
assert_id := b.emit_stmt(make_assert_stmt_value('1'))
x_id := b.emit_stmt(make_assert_stmt_value('2'))
returned_id := b.replace_fn_body_stmts(assert_id, [x_id])
assert returned_id == invalid_flat_node_id
}
fn test_replace_fn_body_stmts_returns_new_id() {
mut b, fn_id := build_fn_decl([make_assert_stmt_value('1')])
x_id := b.emit_stmt(make_assert_stmt_value('2'))
new_fn_id := b.replace_fn_body_stmts(fn_id, [x_id])
// New id must differ from the original — the old fn is unreachable garbage.
assert new_fn_id != fn_id
assert new_fn_id != invalid_flat_node_id
}
@@ -0,0 +1,56 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
import strings
// signature returns a canonical, content-addressed string representation of
// the flat AST. Two flat ASTs produced from the same legacy AST must produce
// identical signatures — this is the round-trip lossless invariant.
//
// The signature includes per-node fields (kind, flags, aux, extra, pos) and
// resolves all interned strings to their text content, so signatures are
// independent of the order in which the intern table was populated.
pub fn (flat &FlatAst) signature() string {
mut sb := strings.new_builder(flat.nodes.len * 16)
for ff in flat.files {
sb.write_string('FILE name=${flat.string_at(ff.name_idx)} mod=${flat.string_at(ff.mod_idx)} root=')
flat.write_node_sig(mut sb, ff.file_id, 0)
sb.write_string('\n')
}
return sb.str()
}
// subtree_signature returns the same canonical, content-addressed string as
// `signature()` but rooted at a single node id rather than the file roots.
// Useful for tests that want to pin the shape of a specific subtree (e.g.
// the `FlatBuilder.prepend_to_fn_body` bit-equality test) without managing
// a file root or worrying about intern-order leakage in the `.file` node's
// `extra` slot.
pub fn (flat &FlatAst) subtree_signature(node_id FlatNodeId) string {
mut sb := strings.new_builder(128)
flat.write_node_sig(mut sb, node_id, 0)
return sb.str()
}
fn (flat &FlatAst) write_node_sig(mut sb strings.Builder, id FlatNodeId, depth int) {
if id < 0 || id >= flat.nodes.len {
sb.write_string('<nil>')
return
}
n := flat.nodes[id]
sb.write_string('(')
sb.write_string(n.kind.str())
sb.write_string(' flags=${n.flags:x} aux=${n.aux:x} extra=${n.extra} pos=${n.pos.offset}:${n.pos.id}')
if n.name_id >= 0 {
sb.write_string(' name="')
sb.write_string(flat.string_at(n.name_id))
sb.write_string('"')
}
for i in 0 .. n.edge_count {
sb.write_string(' ')
flat.write_node_sig(mut sb, flat.edges[n.first_edge + i].child_id, depth + 1)
}
sb.write_string(')')
}
@@ -0,0 +1,83 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module ast
import v2.token
fn string_inter_test_expr() Expr {
return Expr(Ident{
name: 'value'
})
}
fn string_inter_test_literal(width int, precision int) Expr {
return Expr(StringInterLiteral{
kind: .v
values: ['value=', '']
inters: [
StringInter{
format: .decimal
width: width
precision: precision
expr: string_inter_test_expr()
format_expr: empty_expr
resolved_fmt: '%d'
},
]
})
}
fn assert_string_inter_roundtrip(files []File, width int, precision int) {
assert files.len == 1
assert files[0].stmts.len == 2
assert files[0].stmts[1] is ExprStmt
expr_stmt := files[0].stmts[1] as ExprStmt
assert expr_stmt.expr is StringInterLiteral
lit := expr_stmt.expr as StringInterLiteral
assert lit.inters.len == 1
assert lit.inters[0].width == width
assert lit.inters[0].precision == precision
}
fn test_string_inter_width_precision_survive_flat_roundtrip_losslessly() {
width := 1_000_000
precision := -1_000_000
mut b := new_flat_builder()
b.append_file(File{
name: 'string_inter.v'
mod: 'string_inter'
stmts: [
Stmt(ModuleStmt{
name: 'string_inter'
}),
Stmt(ExprStmt{
expr: string_inter_test_literal(width, precision)
}),
]
})
assert_string_inter_roundtrip(b.flat.to_files(), width, precision)
}
fn test_emit_string_inter_by_ids_preserves_large_width_precision() {
width := 900_000
precision := -900_000
mut b := new_flat_builder()
b.append_file(File{
name: 'string_inter_direct.v'
mod: 'string_inter'
stmts: [
Stmt(ModuleStmt{
name: 'string_inter'
}),
]
})
expr_id := b.emit_expr(string_inter_test_expr())
format_expr_id := b.emit_expr(empty_expr)
inter_id :=
b.emit_string_inter_by_ids(.decimal, width, precision, expr_id, format_expr_id, '%d')
lit_id := b.emit_string_inter_literal_by_ids(.v, ['value=', ''], [inter_id], token.Pos{})
stmt_id := b.emit_expr_stmt_by_id(lit_id)
b.append_file_stmts(0, [stmt_id])
assert_string_inter_roundtrip(b.flat.to_files(), width, precision)
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,124 @@
module builder
import os
import v2.ast
fn test_parse_fn_signature_and_return_generic_params_with_space() {
info := parse_fn_signature_and_return('pub fn run_at[A, X](mut global_app A, params RunParams) ! {') or {
assert false
return
}
assert info.signature == 'pub fn run_at[A, X](mut global_app A, params RunParams)'
assert info.return_type == '!'
}
fn test_sanitize_staged_c_source_addresses_channel_semaphore_waits() {
source := [
'sync__Semaphore__wait(ch->writesem);',
'sync__Semaphore__wait(&ch->readsem);',
].join('\n')
sanitized := sanitize_staged_c_source(source)
assert sanitized.contains('sync__Semaphore__wait(&ch->writesem);')
assert sanitized.contains('sync__Semaphore__wait(&ch->readsem);')
assert !sanitized.contains('sync__Semaphore__wait(ch->writesem);')
assert !sanitized.contains('sync__Semaphore__wait(&&ch->readsem);')
}
fn test_sanitize_cached_main_c_source_addresses_channel_semaphore_waits() {
source := [
'sync__Semaphore__wait(ch->writesem);',
'sync__Semaphore__wait(&ch->readsem);',
].join('\n')
sanitized := sanitize_cached_main_c_source(source)
assert sanitized.contains('sync__Semaphore__wait(&ch->writesem);')
assert sanitized.contains('sync__Semaphore__wait(&ch->readsem);')
assert !sanitized.contains('sync__Semaphore__wait(ch->writesem);')
assert !sanitized.contains('sync__Semaphore__wait(&&ch->readsem);')
}
fn test_sanitize_cached_main_c_source_uses_c_typedef_names_for_sync_fields() {
source := [
'struct atomic_uintptr_t write_adr;',
'struct pthread_rwlockattr_t attr;',
'struct pthread_condattr_t attr;',
].join('\n')
sanitized := sanitize_cached_main_c_source(source)
assert sanitized.contains('atomic_uintptr_t write_adr;')
assert sanitized.contains('pthread_rwlockattr_t attr;')
assert sanitized.contains('pthread_condattr_t attr;')
assert !sanitized.contains('struct atomic_uintptr_t write_adr;')
assert !sanitized.contains('struct pthread_rwlockattr_t attr;')
assert !sanitized.contains('struct pthread_condattr_t attr;')
}
fn test_sanitize_cached_object_c_source_uses_c_typedef_names_for_sync_fields() {
source := [
'struct atomic_uintptr_t write_adr;',
'struct atomic_uintptr_t read_adr;',
].join('\n')
sanitized := sanitize_cached_object_c_source(source)
assert sanitized.contains('atomic_uintptr_t write_adr;')
assert sanitized.contains('atomic_uintptr_t read_adr;')
assert !sanitized.contains('struct atomic_uintptr_t write_adr;')
assert !sanitized.contains('struct atomic_uintptr_t read_adr;')
}
fn test_sanitize_cached_source_guards_stdatomic_preamble_for_tcc_compat_header() {
source := [
'#include <stdint.h>',
'#include <stdatomic.h>',
'#include "/tmp/vroot/thirdparty/stdatomic/nix/atomic.h"',
].join('\n')
sanitized := sanitize_cached_object_c_source(source)
assert sanitized.contains('#ifndef __TINYC__\n#include <stdatomic.h>\n#endif')
assert sanitized.contains('#define extern static\n#endif\n#include "/tmp/vroot/thirdparty/stdatomic/nix/atomic.h"')
assert sanitized.contains('#undef extern')
assert sanitized.contains('#include "/tmp/vroot/thirdparty/stdatomic/nix/atomic.h"')
}
fn test_sanitize_cached_source_keeps_stdatomic_preamble_without_compat_header() {
source := [
'#include <stdint.h>',
'#include <stdatomic.h>',
].join('\n')
sanitized := sanitize_cached_object_c_source(source)
assert sanitized == source
}
fn test_join_flag_strings_deduplicates_partial_overlap() {
flags := join_flag_strings('/tmp/sqlite3.c -lm -lpthread',
'/tmp/sqlite3.c -lm -lssl -lcrypto -L/tmp/ssl -lpthread')
assert flags == '/tmp/sqlite3.c -lm -lpthread -lssl -lcrypto -L/tmp/ssl'
}
fn test_join_flag_strings_keeps_distinct_two_token_flags() {
flags := join_flag_strings('-I /tmp/a -framework Cocoa',
'-I /tmp/a -I /tmp/b -framework Foundation')
assert flags == '-I /tmp/a -framework Cocoa -I /tmp/b -framework Foundation'
}
fn test_user_entry_stamp_files_expands_directory_to_parsed_files() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_cache_entry_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_file := os.join_path(tmp_dir, 'main.v')
header_file := os.join_path(tmp_dir, 'main.vh')
mut b := Builder{
pref: unsafe { nil }
user_files: [tmp_dir]
files: [
ast.File{
name: source_file
},
ast.File{
name: header_file
},
ast.File{},
]
}
files := b.user_entry_stamp_files()
assert files == [os.norm_path(source_file)]
assert os.norm_path(tmp_dir) !in files
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,59 @@
module builder
import os
import v2.pref
// Concern 1: the pre-parse self-host fast relink must never fire for a request
// that gen_cleanc() treats as "generation only" — otherwise a warm-cache
// `-o foo.c cmd/v2/v2.v` links an executable into foo.c instead of writing C.
fn test_fast_relink_skips_generation_only_outputs() {
// A `.c` output is generation-only regardless of how it would be built.
b_c := new_builder(&pref.Preferences{ backend: .cleanc })
assert b_c.fast_relink_output_is_generation_only('/tmp/v2.c')
assert b_c.fast_relink_output_is_generation_only('out.c')
// A shared library is generation-only.
b_shared := new_builder(&pref.Preferences{ backend: .cleanc, is_shared_lib: true })
assert b_shared.fast_relink_output_is_generation_only('/tmp/lib')
// A normal local executable build is NOT generation-only — the fast relink
// is allowed to proceed (subject to its cache/freshness checks).
b_exe := new_builder(&pref.Preferences{ backend: .cleanc })
assert b_exe.can_compile_cleanc_locally() // sanity: host target compiles locally
assert !b_exe.fast_relink_output_is_generation_only('/tmp/v3')
}
// Concern 2: the fast relink trusts the cc/cc_flags recorded in main.stamp, so a
// pre-parse fingerprint of the flag inputs that do NOT need parsing must change
// when the compiler / prod-shared mode / env CFLAGS change, and stay stable
// otherwise. A changed fingerprint is what invalidates a would-be stale relink.
fn test_preparse_flag_fingerprint_tracks_flag_settings() {
base := new_builder(&pref.Preferences{ backend: .cleanc })
fp0 := base.preparse_flag_fingerprint()
// Stable for identical settings.
assert new_builder(&pref.Preferences{ backend: .cleanc }).preparse_flag_fingerprint() == fp0
// A different C compiler changes it.
b_cc := new_builder(&pref.Preferences{ backend: .cleanc, ccompiler: 'some-other-cc' })
assert b_cc.preparse_flag_fingerprint() != fp0
// -prod changes it (different optimization flags).
b_prod := new_builder(&pref.Preferences{ backend: .cleanc, is_prod: true })
assert b_prod.preparse_flag_fingerprint() != fp0
// -shared changes it (no -flto, different link mode).
b_shared := new_builder(&pref.Preferences{ backend: .cleanc, is_shared_lib: true })
assert b_shared.preparse_flag_fingerprint() != fp0
// Env CFLAGS (V2CFLAGS) change it.
prev := os.getenv('V2CFLAGS')
os.setenv('V2CFLAGS', '-DV2_FAST_RELINK_TEST', true)
fp_env := new_builder(&pref.Preferences{ backend: .cleanc }).preparse_flag_fingerprint()
if prev == '' {
os.unsetenv('V2CFLAGS')
} else {
os.setenv('V2CFLAGS', prev, true)
}
assert fp_env != fp0
}
@@ -0,0 +1,608 @@
module builder
import os
import v2.ast
import v2.pref
fn collect_cflags_for_test_source(source string, mut prefs pref.Preferences) string {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_flag_target_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_path := os.join_path(tmp_dir, 'main.v')
os.write_file(source_path, source) or { panic(err) }
prefs.skip_builtin = true
mut b := new_builder(&prefs)
b.files = [
ast.File{
name: source_path
},
]
return b.collect_cflags_from_sources()
}
fn collect_cflags_for_flat_only_test_source(source string, mut prefs pref.Preferences) string {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_flag_target_flat_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_path := os.join_path(tmp_dir, 'main.v')
os.write_file(source_path, source) or { panic(err) }
prefs.skip_builtin = true
mut b := new_builder(&prefs)
b.flat = ast.flatten_files([
ast.File{
name: source_path
mod: 'main'
},
])
b.files = []ast.File{}
return b.collect_cflags_from_sources()
}
fn freestanding_test_call_name(name string, user_defines []string) string {
return freestanding_test_call_name_with_hooks(name, user_defines, [])
}
fn freestanding_test_call_name_with_hooks(name string, user_defines []string, hooks []string) string {
return freestanding_restricted_call_in_expr(ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: name
})
}), FreestandingScanContext{
user_defines: user_defines
target_os: 'linux'
freestanding_hooks: hooks
})
}
fn test_flag_directives_match_cross_and_freestanding_preferences() {
mut cross_prefs := pref.new_preferences()
cross_prefs.target_os = 'cross'
cross_prefs.user_defines = ['cross']
cross_flag := parse_flag_directive_line_with_pref('#flag cross -DCROSS_DIRECT',
'/tmp/source.v', &cross_prefs) or { '' }
linux_flag := parse_flag_directive_line_with_pref('#flag linux -DLINUX_DIRECT',
'/tmp/source.v', &cross_prefs) or { '' }
assert cross_flag == '-DCROSS_DIRECT'
assert linux_flag == ''
mut free_prefs := pref.new_preferences()
free_prefs.target_os = 'linux'
free_prefs.user_defines = ['freestanding']
free_flag := parse_flag_directive_line_with_pref('#flag freestanding -DFREE_DIRECT',
'/tmp/source.v', &free_prefs) or { '' }
linux_in_free_flag := parse_flag_directive_line_with_pref('#flag linux -DLINUX_DIRECT',
'/tmp/source.v', &free_prefs) or { '' }
assert free_flag == '-DFREE_DIRECT'
assert linux_in_free_flag == '-DLINUX_DIRECT'
}
fn test_comptime_flag_blocks_match_cross_and_freestanding_preferences() {
mut cross_prefs := pref.new_preferences()
cross_prefs.target_os = 'cross'
cross_prefs.user_defines = ['cross']
assert comptime_cond_matches_with_pref('cross', &cross_prefs)
assert !comptime_cond_matches_with_pref('linux', &cross_prefs)
assert !comptime_cond_matches_with_pref('linux ?', &cross_prefs)
assert comptime_cond_matches_with_pref('cross && !linux', &cross_prefs)
mut optional_linux_prefs := pref.new_preferences()
optional_linux_prefs.target_os = 'cross'
optional_linux_prefs.user_defines = ['cross', 'linux']
optional_linux_prefs.explicit_user_defines = ['linux']
assert comptime_cond_matches_with_pref('linux ?', &optional_linux_prefs)
mut linux_prefs := pref.new_preferences()
linux_prefs.target_os = 'linux'
assert comptime_cond_matches_with_pref('linux', &linux_prefs)
assert !comptime_cond_matches_with_pref('linux ?', &linux_prefs)
assert !comptime_cond_matches_with_pref('linux ? || windows', &linux_prefs)
linux_prefs.user_defines = ['linux']
linux_prefs.explicit_user_defines = ['linux']
assert comptime_cond_matches_with_pref('linux ?', &linux_prefs)
assert comptime_cond_matches_with_pref('linux ? || windows', &linux_prefs)
mut termux_prefs := pref.new_preferences()
termux_prefs.target_os = 'termux'
assert comptime_cond_matches_with_pref('termux', &termux_prefs)
assert comptime_cond_matches_with_pref('linux || termux', &termux_prefs)
assert !comptime_cond_matches_with_pref('android', &termux_prefs)
assert !comptime_cond_matches_with_pref('android && !termux', &termux_prefs)
mut free_prefs := pref.new_preferences()
free_prefs.target_os = 'linux'
free_prefs.user_defines = ['freestanding']
assert comptime_cond_matches_with_pref('freestanding', &free_prefs)
assert !comptime_cond_matches_with_pref('freestanding ?', &free_prefs)
assert comptime_cond_matches_with_pref('linux', &free_prefs)
assert comptime_cond_matches_with_pref('freestanding && linux', &free_prefs)
}
fn test_comptime_pkgconfig_condition_matches_available_package() {
mut prefs := pref.new_preferences()
prefs.target_os = os.user_os()
assert !comptime_cond_matches_with_pref(r"$pkgconfig('__v2_definitely_missing_pkgconfig_test_package__')",
&prefs)
if pref.comptime_pkgconfig_value('sqlite3') {
assert comptime_cond_matches_with_pref(r"$pkgconfig('sqlite3')", &prefs)
}
}
fn test_pkgconfig_directive_collects_flags_for_active_native_branch() {
mut prefs := pref.new_preferences()
prefs.target_os = os.user_os()
if pref.comptime_pkgconfig_value('sqlite3') {
flag := parse_flag_directive_line_with_pref('#pkgconfig sqlite3', '/tmp/source.v', &prefs) or {
''
}
assert flag.contains('sqlite3')
}
mut cross_prefs := pref.new_preferences()
cross_prefs.target_os = 'cross'
cross_prefs.output_cross_c = true
flag := parse_flag_directive_line_with_pref('#pkgconfig sqlite3', '/tmp/source.v', &cross_prefs) or {
''
}
assert flag == ''
}
fn test_comptime_optional_target_mode_flags_require_explicit_define() {
mut cross_prefs := pref.new_preferences()
cross_prefs.target_os = 'cross'
cross_prefs.user_defines = ['cross']
assert comptime_cond_matches_with_pref('cross', &cross_prefs)
assert !comptime_cond_matches_with_pref('cross ?', &cross_prefs)
cross_prefs.explicit_user_defines = ['cross']
assert comptime_cond_matches_with_pref('cross ?', &cross_prefs)
mut free_prefs := pref.new_preferences()
free_prefs.target_os = 'linux'
free_prefs.user_defines = ['freestanding']
assert comptime_cond_matches_with_pref('freestanding', &free_prefs)
assert !comptime_cond_matches_with_pref('freestanding ?', &free_prefs)
free_prefs.explicit_user_defines = ['freestanding']
assert comptime_cond_matches_with_pref('freestanding ?', &free_prefs)
mut none_prefs := pref.new_preferences()
none_prefs.target_os = 'none'
none_prefs.user_defines = ['freestanding']
assert comptime_cond_matches_with_pref('none', &none_prefs)
assert !comptime_cond_matches_with_pref('none ?', &none_prefs)
none_prefs.explicit_user_defines = ['none']
assert comptime_cond_matches_with_pref('none ?', &none_prefs)
}
fn test_comptime_flag_blocks_use_or_and_not_precedence() {
mut linux_prefs := pref.new_preferences()
linux_prefs.target_os = 'linux'
assert comptime_cond_matches_with_pref('!windows && linux', &linux_prefs)
assert comptime_cond_matches_with_pref('linux || windows && macos', &linux_prefs)
mut macos_prefs := pref.new_preferences()
macos_prefs.target_os = 'macos'
assert !comptime_cond_matches_with_pref('!windows && linux', &macos_prefs)
assert !comptime_cond_matches_with_pref('linux || windows && macos', &macos_prefs)
mut windows_prefs := pref.new_preferences()
windows_prefs.target_os = 'windows'
assert !comptime_cond_matches_with_pref('!windows && linux', &windows_prefs)
assert !comptime_cond_matches_with_pref('linux || windows && macos', &windows_prefs)
}
fn test_parser_level_comptime_flags_match_cross_and_accepted_targets() {
assert ast_comptime_flag_matches('cross', [], 'cross')
assert ast_comptime_flag_matches('solaris', [], 'solaris')
assert ast_comptime_flag_matches('qnx', [], 'qnx')
assert ast_comptime_flag_matches('serenity', [], 'serenity')
assert ast_comptime_flag_matches('plan9', [], 'plan9')
assert ast_comptime_flag_matches('vinix', [], 'vinix')
assert ast_comptime_flag_matches('ios', [], 'ios')
assert ast_comptime_flag_matches('termux', [], 'termux')
assert ast_comptime_flag_matches('none', [], 'none')
assert !ast_comptime_flag_matches('linux', [], 'cross')
assert !ast_comptime_flag_matches('linux', ['linux'], 'windows')
assert !ast_comptime_flag_matches('none', ['none'], 'linux')
}
fn test_flag_directives_match_all_accepted_target_os_names() {
for target in ['android', 'termux', 'ios', 'solaris', 'qnx', 'serenity', 'plan9', 'vinix'] {
flag := parse_flag_directive_line('#flag ${target} -D${target.to_upper()}',
'/tmp/source.v', target) or { '' }
assert flag == '-D${target.to_upper()}'
assert comptime_cond_matches(target, target)
}
}
fn test_collect_cflags_from_sources_matches_cross_and_freestanding_blocks() {
cross_source := 'module main
$if cross {
#flag -DCROSS_BLOCK
}
$if cross ? {
#flag -DOPTIONAL_CROSS_BLOCK
}
$if linux {
#flag -DLINUX_BLOCK
}
$if linux ? {
#flag -DOPTIONAL_LINUX_BLOCK
}
$if macos {
#flag -DMACOS_BLOCK
}
$if windows {
#flag -DWINDOWS_BLOCK
}
#flag cross -DCROSS_DIRECT
#flag linux -DLINUX_DIRECT
#flag macos -DMACOS_DIRECT
#flag windows -DWINDOWS_DIRECT
'
mut cross_prefs := pref.new_preferences()
cross_prefs.target_os = 'cross'
cross_prefs.user_defines = ['cross']
cross_flags := collect_cflags_for_test_source(cross_source, mut cross_prefs)
assert cross_flags.contains('-DCROSS_BLOCK')
assert cross_flags.contains('-DCROSS_DIRECT')
assert !cross_flags.contains('-DOPTIONAL_CROSS_BLOCK')
assert !cross_flags.contains('-DOPTIONAL_LINUX_BLOCK')
host_os := normalize_target_os_name(os.user_os())
if host_os in ['linux', 'macos', 'windows'] {
assert cross_flags.contains(match host_os {
'macos' { '-DMACOS_BLOCK' }
'windows' { '-DWINDOWS_BLOCK' }
else { '-DLINUX_BLOCK' }
})
assert cross_flags.contains(match host_os {
'macos' { '-DMACOS_DIRECT' }
'windows' { '-DWINDOWS_DIRECT' }
else { '-DLINUX_DIRECT' }
})
}
if host_os != 'linux' {
assert !cross_flags.contains('-DLINUX_BLOCK')
assert !cross_flags.contains('-DLINUX_DIRECT')
}
if host_os != 'macos' {
assert !cross_flags.contains('-DMACOS_BLOCK')
assert !cross_flags.contains('-DMACOS_DIRECT')
}
if host_os != 'windows' {
assert !cross_flags.contains('-DWINDOWS_BLOCK')
assert !cross_flags.contains('-DWINDOWS_DIRECT')
}
mut cross_with_optional_linux_prefs := pref.new_preferences()
cross_with_optional_linux_prefs.target_os = 'cross'
cross_with_optional_linux_prefs.user_defines = ['cross', 'linux']
cross_with_optional_linux_prefs.explicit_user_defines = ['linux']
cross_with_optional_linux_flags := collect_cflags_for_test_source(cross_source, mut
cross_with_optional_linux_prefs)
assert cross_with_optional_linux_flags.contains('-DOPTIONAL_LINUX_BLOCK')
assert !cross_with_optional_linux_flags.contains('-DOPTIONAL_CROSS_BLOCK')
mut cross_explicit_prefs := pref.new_preferences()
cross_explicit_prefs.target_os = 'cross'
cross_explicit_prefs.user_defines = ['cross']
cross_explicit_prefs.explicit_user_defines = ['cross']
cross_explicit_flags := collect_cflags_for_test_source(cross_source, mut cross_explicit_prefs)
assert cross_explicit_flags.contains('-DOPTIONAL_CROSS_BLOCK')
free_source := 'module main
$if freestanding {
#flag -DFREE_BLOCK
}
$if freestanding ? {
#flag -DOPTIONAL_FREE_BLOCK
}
$if linux {
#flag -DLINUX_BLOCK
}
#flag freestanding -DFREE_DIRECT
#flag linux -DLINUX_DIRECT
'
mut free_prefs := pref.new_preferences()
free_prefs.target_os = 'linux'
free_prefs.user_defines = ['freestanding']
free_flags := collect_cflags_for_test_source(free_source, mut free_prefs)
assert free_flags.contains('-DFREE_BLOCK')
assert !free_flags.contains('-DOPTIONAL_FREE_BLOCK')
assert free_flags.contains('-DFREE_DIRECT')
assert free_flags.contains('-DLINUX_BLOCK')
assert free_flags.contains('-DLINUX_DIRECT')
mut free_explicit_prefs := pref.new_preferences()
free_explicit_prefs.target_os = 'linux'
free_explicit_prefs.user_defines = ['freestanding']
free_explicit_prefs.explicit_user_defines = ['freestanding']
free_explicit_flags := collect_cflags_for_test_source(free_source, mut free_explicit_prefs)
assert free_explicit_flags.contains('-DOPTIONAL_FREE_BLOCK')
}
fn test_collect_cflags_from_sources_reads_flat_file_names() {
source := 'module main
#flag -DFLAT_ONLY_FLAG
'
mut prefs := pref.new_preferences()
flags := collect_cflags_for_flat_only_test_source(source, mut prefs)
assert flags.contains('-DFLAT_ONLY_FLAG')
}
fn test_collect_cflags_from_sources_keeps_optional_os_flags_custom_only() {
source := 'module main
$if linux {
#flag -DLINUX_BLOCK
}
$if linux ? {
#flag -DOPTIONAL_LINUX_BLOCK
}
$if linux ? || windows {
#flag -DOPTIONAL_LINUX_OR_WINDOWS_BLOCK
}
'
mut linux_prefs := pref.new_preferences()
linux_prefs.target_os = 'linux'
linux_flags := collect_cflags_for_test_source(source, mut linux_prefs)
assert linux_flags.contains('-DLINUX_BLOCK')
assert !linux_flags.contains('-DOPTIONAL_LINUX_BLOCK')
assert !linux_flags.contains('-DOPTIONAL_LINUX_OR_WINDOWS_BLOCK')
mut linux_defined_prefs := pref.new_preferences()
linux_defined_prefs.target_os = 'linux'
linux_defined_prefs.user_defines = ['linux']
linux_defined_prefs.explicit_user_defines = ['linux']
linux_defined_flags := collect_cflags_for_test_source(source, mut linux_defined_prefs)
assert linux_defined_flags.contains('-DLINUX_BLOCK')
assert linux_defined_flags.contains('-DOPTIONAL_LINUX_BLOCK')
assert linux_defined_flags.contains('-DOPTIONAL_LINUX_OR_WINDOWS_BLOCK')
mut windows_prefs := pref.new_preferences()
windows_prefs.target_os = 'windows'
windows_flags := collect_cflags_for_test_source(source, mut windows_prefs)
assert !windows_flags.contains('-DLINUX_BLOCK')
assert !windows_flags.contains('-DOPTIONAL_LINUX_BLOCK')
assert windows_flags.contains('-DOPTIONAL_LINUX_OR_WINDOWS_BLOCK')
}
fn test_collect_cflags_from_sources_keeps_optional_none_custom_only() {
source := 'module main
$if none {
#flag -DNONE_BLOCK
}
$if none ? {
#flag -DOPTIONAL_NONE_BLOCK
}
'
mut none_prefs := pref.new_preferences()
none_prefs.target_os = 'none'
none_prefs.user_defines = ['freestanding']
none_flags := collect_cflags_for_test_source(source, mut none_prefs)
assert none_flags.contains('-DNONE_BLOCK')
assert !none_flags.contains('-DOPTIONAL_NONE_BLOCK')
mut none_explicit_prefs := pref.new_preferences()
none_explicit_prefs.target_os = 'none'
none_explicit_prefs.user_defines = ['freestanding']
none_explicit_prefs.explicit_user_defines = ['none']
none_explicit_flags := collect_cflags_for_test_source(source, mut none_explicit_prefs)
assert none_explicit_flags.contains('-DOPTIONAL_NONE_BLOCK')
}
fn test_collect_cflags_from_sources_matches_termux_blocks_and_directives() {
source := 'module main
$if termux {
#flag -DTERMUX_BLOCK
}
$if linux || termux {
#flag -DTERMUX_OR_LINUX_BLOCK
}
$if android && !termux {
#flag -DANDROID_OUTSIDE_TERMUX_BLOCK
}
#flag termux -DTERMUX_DIRECT
#flag android -DANDROID_DIRECT
'
mut termux_prefs := pref.new_preferences()
termux_prefs.target_os = 'termux'
flags := collect_cflags_for_test_source(source, mut termux_prefs)
assert flags.contains('-DTERMUX_BLOCK')
assert flags.contains('-DTERMUX_OR_LINUX_BLOCK')
assert flags.contains('-DTERMUX_DIRECT')
assert !flags.contains('-DANDROID_OUTSIDE_TERMUX_BLOCK')
assert !flags.contains('-DANDROID_DIRECT')
}
fn test_freestanding_diagnostics_gate_direct_builtins_by_capability() {
for name in ['print', 'println', 'eprint', 'eprintln'] {
assert freestanding_test_call_name(name, ['freestanding_hooks']) == name
assert freestanding_test_call_name_with_hooks(name, ['freestanding_hooks',
'freestanding_output'], ['output']) == ''
}
assert freestanding_test_call_name('panic', ['freestanding_hooks']) == 'panic'
assert freestanding_test_call_name_with_hooks('panic', ['freestanding_hooks',
'freestanding_panic'], ['panic']) == ''
}
fn test_freestanding_diagnostics_leave_runtime_helpers_to_cleanc() {
for name in ['_write_buf_to_fd', '_writeln_to_fd', 'flush_stdout', 'flush_stderr'] {
assert freestanding_test_call_name(name, ['freestanding_hooks']) == ''
}
assert freestanding_test_call_name_with_hooks('arguments', ['freestanding_hooks',
'freestanding_output'], ['output']) == ''
assert freestanding_test_call_name_with_hooks('arguments', ['freestanding_hooks',
'freestanding_alloc'], ['alloc']) == ''
assert freestanding_test_call_name('malloc_noscan',
['freestanding_hooks', 'freestanding_alloc']) == ''
assert freestanding_test_call_name_with_hooks('malloc_noscan', [
'freestanding_hooks',
'freestanding_alloc',
], ['alloc']) == ''
assert freestanding_restricted_call_in_expr(ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'println'
})
args: [
ast.Expr(ast.BasicLiteral{
kind: .number
value: '123'
}),
]
}), FreestandingScanContext{
user_defines: ['freestanding_hooks', 'freestanding_output']
target_os: 'linux'
freestanding_hooks: ['output']
}) == ''
assert freestanding_restricted_call_in_expr(ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'println'
})
args: [
ast.Expr(ast.BasicLiteral{
kind: .number
value: '123'
}),
]
}), FreestandingScanContext{
user_defines: ['freestanding_hooks', 'freestanding_output', 'freestanding_alloc']
target_os: 'linux'
freestanding_hooks: ['output', 'alloc']
}) == ''
}
fn test_freestanding_diagnostics_skip_inactive_conditional_functions() {
ctx := FreestandingScanContext{
user_defines: ['freestanding']
target_os: 'linux'
}
hosted_call := ast.Stmt(ast.ExprStmt{
expr: ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'println'
})
})
})
inactive_windows_fn := ast.Stmt(ast.FnDecl{
attributes: [
ast.Attribute{
comptime_cond: ast.Expr(ast.Ident{
name: 'windows'
})
},
]
stmts: [hosted_call]
})
inactive_not_freestanding_fn := ast.Stmt(ast.FnDecl{
attributes: [
ast.Attribute{
comptime_cond: ast.Expr(ast.PrefixExpr{
op: .not
expr: ast.Expr(ast.Ident{
name: 'freestanding'
})
})
},
]
stmts: [hosted_call]
})
active_fn := ast.Stmt(ast.FnDecl{
attributes: [
ast.Attribute{
comptime_cond: ast.Expr(ast.Ident{
name: 'freestanding'
})
},
]
stmts: [hosted_call]
})
assert freestanding_restricted_call_in_stmt(inactive_windows_fn, ctx) == ''
assert freestanding_restricted_call_in_stmt(inactive_not_freestanding_fn, ctx) == ''
assert freestanding_restricted_call_in_stmt(active_fn, ctx) == 'println'
}
fn test_freestanding_diagnostics_leave_array_allocation_to_cleanc() {
ctx := FreestandingScanContext{
user_defines: ['freestanding']
target_os: 'linux'
}
fixed_array := ast.Expr(ast.ArrayInitExpr{
typ: ast.Expr(ast.Type(ast.ArrayFixedType{
len: ast.Expr(ast.BasicLiteral{
kind: .number
value: '3'
})
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
}))
})
dynamic_array := ast.Expr(ast.ArrayInitExpr{
typ: ast.Expr(ast.Type(ast.ArrayType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
}))
})
fixed_literal_array := ast.Expr(ast.ArrayInitExpr{
exprs: [
ast.Expr(ast.BasicLiteral{
kind: .number
value: '1'
}),
ast.Expr(ast.BasicLiteral{
kind: .number
value: '2'
}),
ast.Expr(ast.BasicLiteral{
kind: .number
value: '3'
}),
]
len: ast.Expr(ast.PostfixExpr{
op: .not
expr: ast.empty_expr
})
})
assert freestanding_restricted_call_in_expr(fixed_array, ctx) == ''
assert freestanding_restricted_call_in_expr(fixed_literal_array, ctx) == ''
assert freestanding_restricted_call_in_expr(dynamic_array, ctx) == ''
}
fn test_collect_cflags_from_sources_uses_or_and_not_precedence() {
source := 'module main
$if !windows && linux {
#flag -DNEGATED_AND_LINUX
}
$if linux || windows && macos {
#flag -DOR_AND_LINUX
}
'
mut linux_prefs := pref.new_preferences()
linux_prefs.target_os = 'linux'
linux_flags := collect_cflags_for_test_source(source, mut linux_prefs)
assert linux_flags.contains('-DNEGATED_AND_LINUX')
assert linux_flags.contains('-DOR_AND_LINUX')
mut macos_prefs := pref.new_preferences()
macos_prefs.target_os = 'macos'
macos_flags := collect_cflags_for_test_source(source, mut macos_prefs)
assert !macos_flags.contains('-DNEGATED_AND_LINUX')
assert !macos_flags.contains('-DOR_AND_LINUX')
mut windows_prefs := pref.new_preferences()
windows_prefs.target_os = 'windows'
windows_flags := collect_cflags_for_test_source(source, mut windows_prefs)
assert !windows_flags.contains('-DNEGATED_AND_LINUX')
assert !windows_flags.contains('-DOR_AND_LINUX')
}
@@ -0,0 +1,191 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
// vtest build: macos
module builder
import os
import v2.ast
import v2.parser
import v2.pref
import v2.token
fn parse_source_for_flat(src string) []ast.File {
tmp := '/tmp/v2_flat_roundtrip_${os.getpid()}.v'
os.write_file(tmp, src) or { panic('write_file: ${err}') }
defer {
os.rm(tmp) or {}
}
p := &pref.Preferences{}
mut fs := token.FileSet.new()
mut par := parser.Parser.new(p)
return par.parse_files([tmp], mut fs)
}
fn assert_roundtrip(src string) {
files := parse_source_for_flat(src)
flat_a := ast.flatten_files(files)
rt_files := flat_a.to_files()
flat_b := ast.flatten_files(rt_files)
sig_a := flat_a.signature()
sig_b := flat_b.signature()
if sig_a != sig_b {
mut diff_at := 0
for diff_at < sig_a.len && diff_at < sig_b.len && sig_a[diff_at] == sig_b[diff_at] {
diff_at++
}
from := if diff_at > 80 { diff_at - 80 } else { 0 }
end_a := if diff_at + 200 < sig_a.len { diff_at + 200 } else { sig_a.len }
end_b := if diff_at + 200 < sig_b.len { diff_at + 200 } else { sig_b.len }
eprintln('signature mismatch at offset ${diff_at}')
eprintln('A: ${sig_a[from..end_a]}')
eprintln('B: ${sig_b[from..end_b]}')
}
assert sig_a == sig_b, 'round-trip signature mismatch for source:\n${src}'
}
fn test_roundtrip_module_only() {
assert_roundtrip('module foo\n')
}
fn test_roundtrip_simple_fn() {
src := 'module main
fn add(a int, b int) int {
return a + b
}
'
assert_roundtrip(src)
}
fn test_roundtrip_struct_and_const() {
src := "module main
pub const greeting = 'hello'
pub struct Point {
pub mut:
x int
y int
}
fn (p Point) sum() int {
return p.x + p.y
}
"
assert_roundtrip(src)
}
fn test_roundtrip_control_flow() {
src := 'module main
fn run(n int) int {
mut total := 0
for i := 0; i < n; i++ {
if i % 2 == 0 {
total += i
} else {
total -= i
}
}
return total
}
'
assert_roundtrip(src)
}
fn test_roundtrip_string_inter() {
src := "module main
fn show(name string, age int) string {
return 'name=\${name} age=\${age:04d}'
}
"
assert_roundtrip(src)
}
fn test_roundtrip_match_and_array() {
src := 'module main
fn classify(x int) string {
return match x {
0 { "zero" }
1, 2, 3 { "small" }
else { "big" }
}
}
fn sum_array() int {
nums := [1, 2, 3, 4, 5]
mut total := 0
for n in nums {
total += n
}
return total
}
'
assert_roundtrip(src)
}
fn test_roundtrip_map_and_or() {
src := "module main
fn lookup() int {
m := {'a': 1, 'b': 2}
v := m['a'] or { -1 }
return v
}
"
assert_roundtrip(src)
}
fn test_roundtrip_enum_and_attrs() {
src := 'module main
@[flag]
pub enum Color {
red
green
blue
}
'
assert_roundtrip(src)
}
fn test_roundtrip_interface() {
src := 'module main
pub interface Animal {
name() string
mut:
age int
}
'
assert_roundtrip(src)
}
fn test_roundtrip_generic_fn() {
src := 'module main
fn max[T](a T, b T) T {
if a > b { return a }
return b
}
'
assert_roundtrip(src)
}
fn test_roundtrip_real_source_file() {
// Round-trip a real V2 source file end-to-end. This exercises the full
// surface of parser-produced AST variants.
path := os.real_path('vlib/v2_toberemoved/ast/ast.v')
if !os.exists(path) {
return
}
src := os.read_file(path) or { return }
files := parse_source_for_flat(src)
flat_a := ast.flatten_files(files)
rt_files := flat_a.to_files()
flat_b := ast.flatten_files(rt_files)
assert flat_a.signature() == flat_b.signature(), 'round-trip mismatch for ${path}'
}
@@ -0,0 +1,157 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
// vtest build: macos
module builder
import os
import v2.ast
import v2.parser
import v2.pref
import v2.token
// Phase 2 regression: Parser.parse_files_to_flat must produce a FlatAst whose
// canonical signature is identical to ast.flatten_files(Parser.parse_files()).
// This guards the streaming path while downstream consumers are migrated.
fn write_tmp_file(name string, src string) string {
path := '/tmp/v2_flat_stream_${os.getpid()}_${name}.v'
os.write_file(path, src) or { panic('write_file: ${err}') }
return path
}
fn assert_streaming_matches(srcs map[string]string) {
mut paths := []string{cap: srcs.len}
for name, src in srcs {
paths << write_tmp_file(name, src)
}
defer {
for p in paths {
os.rm(p) or {}
}
}
prefs := &pref.Preferences{}
mut fs_batch := token.FileSet.new()
mut p_batch := parser.Parser.new(prefs)
legacy_files := p_batch.parse_files(paths, mut fs_batch)
flat_batch := ast.flatten_files(legacy_files)
mut fs_stream := token.FileSet.new()
mut p_stream := parser.Parser.new(prefs)
flat_stream := p_stream.parse_files_to_flat(paths, mut fs_stream)
sig_batch := flat_batch.signature()
sig_stream := flat_stream.signature()
if sig_batch != sig_stream {
mut diff_at := 0
for diff_at < sig_batch.len && diff_at < sig_stream.len
&& sig_batch[diff_at] == sig_stream[diff_at] {
diff_at++
}
from := if diff_at > 80 { diff_at - 80 } else { 0 }
end_b := if diff_at + 200 < sig_batch.len { diff_at + 200 } else { sig_batch.len }
end_s := if diff_at + 200 < sig_stream.len { diff_at + 200 } else { sig_stream.len }
eprintln('signature mismatch at offset ${diff_at}')
eprintln('batch: ${sig_batch[from..end_b]}')
eprintln('stream: ${sig_stream[from..end_s]}')
}
assert sig_batch == sig_stream, 'streaming flat AST diverges from batch flatten'
}
fn test_streaming_single_file() {
assert_streaming_matches({
'single': 'module main
fn add(a int, b int) int {
return a + b
}
'
})
}
fn test_streaming_multi_file_order() {
// Two files should produce node ids in parse order regardless of whether
// the conversion is batched or streamed. Strings interned by file 0 must
// be reused by file 1 in both cases.
a := 'module main
pub const greeting = "hello"
fn say() string {
return greeting
}
'
b := 'module main
pub struct Point { x int y int }
fn (p Point) sum() int { return p.x + p.y }
'
assert_streaming_matches({
'a': a
'b': b
})
}
fn test_streaming_real_source_file() {
path := os.real_path('vlib/v2_toberemoved/ast/ast.v')
if !os.exists(path) {
return
}
prefs := &pref.Preferences{}
mut fs_batch := token.FileSet.new()
mut p_batch := parser.Parser.new(prefs)
legacy := p_batch.parse_files([path], mut fs_batch)
flat_batch := ast.flatten_files(legacy)
mut fs_stream := token.FileSet.new()
mut p_stream := parser.Parser.new(prefs)
flat_stream := p_stream.parse_files_to_flat([path], mut fs_stream)
assert flat_batch.signature() == flat_stream.signature(), 'real-source streaming mismatch'
}
fn test_default_flat_parallel_transform_keeps_user_files() {
path := write_tmp_file('parallel_transform_main', 'module main
fn main() {
println("flat parallel ok")
}
')
out_path := '/tmp/v2_flat_parallel_transform_${os.getpid()}.out'
defer {
os.rm(path) or {}
os.rm(out_path) or {}
}
cmd := '${os.quoted_path(@VEXE)} -v2 -nocache -o ${os.quoted_path(out_path)} ${os.quoted_path(path)} 2>&1'
res := os.execute(cmd)
assert res.exit_code == 0, res.output
run_res := os.execute(os.quoted_path(out_path))
assert run_res.exit_code == 0, run_res.output
assert run_res.output.trim_space() == 'flat parallel ok', run_res.output
}
fn test_default_parallel_transform_monomorphizes_generic_calls_for_native_backend() {
path := write_tmp_file('parallel_transform_generic_main', 'module main
fn id[T](x T) T {
return x
}
fn main() {
_ = id(41)
}
')
out_path := '/tmp/v2_parallel_generic_transform_${os.getpid()}.out'
defer {
os.rm(path) or {}
os.rm(out_path) or {}
}
cmd := '${os.quoted_path(@VEXE)} -v2 -nocache -backend arm64 -gc none -o ${os.quoted_path(out_path)} ${os.quoted_path(path)} 2>&1'
res := os.execute(cmd)
assert !res.output.contains('_id_T_int'), res.output
assert res.exit_code == 0, res.output
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,102 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import runtime
import v2.gen.arm64
$if !windows {
struct GenARM64ChunkArgs {
worker voidptr // &arm64.Gen — pre-cloned worker (created on main thread)
start_idx int
end_idx int
}
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
fn C.pthread_attr_init(attr voidptr) int
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
fn C.pthread_attr_destroy(attr voidptr) int
fn gen_arm64_chunk_thread(arg voidptr) voidptr {
a := unsafe { &GenARM64ChunkArgs(arg) }
mut w := unsafe { &arm64.Gen(a.worker) }
for fi := a.start_idx; fi < a.end_idx; fi++ {
w.gen_func(fi)
}
return unsafe { nil }
}
}
fn (mut b Builder) gen_arm64_parallel(mut gen arm64.Gen) {
gen.gen_pre_pass()
n_funcs := gen.mod.funcs.len
n_jobs := runtime.nr_jobs()
$if windows {
for fi := 0; fi < n_funcs; fi++ {
gen.gen_func(fi)
}
gen.gen_post_pass()
return
} $else {
if n_funcs <= 1 || n_jobs <= 1 {
// Fallback to sequential
for fi := 0; fi < n_funcs; fi++ {
gen.gen_func(fi)
}
gen.gen_post_pass()
return
}
// Split functions into chunks
chunk_size := (n_funcs + n_jobs - 1) / n_jobs
mut thread_ids := []C.pthread_t{len: n_jobs}
mut args := []GenARM64ChunkArgs{cap: n_jobs}
// Pre-create all workers on the main thread to avoid concurrent .clone() races.
// Each worker gets its own deep copy of maps/arrays.
mut workers := []voidptr{cap: n_jobs}
mut chunk_idx := 0
mut i := 0
for i < n_funcs {
end := if i + chunk_size < n_funcs { i + chunk_size } else { n_funcs }
w := gen.new_worker_clone()
workers << voidptr(w)
args << GenARM64ChunkArgs{
worker: voidptr(w)
start_idx: i
end_idx: end
}
i = end
chunk_idx++
}
attr_buf := [64]u8{}
attr := unsafe { voidptr(&attr_buf[0]) }
C.pthread_attr_init(attr)
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
for ci := 0; ci < chunk_idx; ci++ {
C.pthread_create(unsafe { &thread_ids[ci] }, attr, gen_arm64_chunk_thread,
unsafe { voidptr(&args[ci]) })
}
C.pthread_attr_destroy(attr)
// Wait for all workers
for ci := 0; ci < chunk_idx; ci++ {
C.pthread_join(thread_ids[ci], unsafe { nil })
}
// Merge worker results in order
for ci := 0; ci < chunk_idx; ci++ {
w := unsafe { &arm64.Gen(workers[ci]) }
gen.merge_worker(w)
}
gen.gen_post_pass()
}
}
@@ -0,0 +1,196 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import runtime
import time
import v2.gen.cleanc
const max_cleanc_pass5_jobs = 16
$if !windows {
struct GenCleancChunkArgs {
worker voidptr // &cleanc.Gen — pre-cloned worker
work_items_ptr voidptr // &[]cleanc.Pass5WorkItem — work items to process
}
@[typedef]
struct C.pthread_t {}
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
fn C.pthread_attr_init(attr voidptr) int
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
fn C.pthread_attr_destroy(attr voidptr) int
fn gen_cleanc_chunk_thread(arg voidptr) voidptr {
a := unsafe { &GenCleancChunkArgs(arg) }
mut w := unsafe { &cleanc.Gen(a.worker) }
items := unsafe { &[]cleanc.Pass5WorkItem(a.work_items_ptr) }
w.gen_pass5_work_items(*items)
return unsafe { nil }
}
}
fn print_cleanc_parallel_step_time(stats_enabled bool, step string, elapsed time.Duration) {
if !stats_enabled {
return
}
println(' - C Gen/full ${step}: ${elapsed.milliseconds()}ms')
}
fn mark_cleanc_parallel_step(stats_enabled bool, mut sw time.StopWatch, stage_start time.Duration, step string) time.Duration {
if !stats_enabled {
return stage_start
}
now := sw.elapsed()
print_cleanc_parallel_step_time(true, step, time.Duration(now - stage_start))
return now
}
fn (mut b Builder) gen_cleanc_parallel(mut gen cleanc.Gen) {
stats_enabled := b.pref != unsafe { nil } && b.pref.stats
mut stats_sw := time.new_stopwatch()
mut stage_start := stats_sw.elapsed()
emit_indices := gen.gen_pass5_pre()
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 pre')
// Split large files into sub-file work items so no single huge file
// (e.g. ssa/builder.v) pins the whole parallel phase.
work_items := gen.build_pass5_work_items(emit_indices)
n_items := work_items.len
n_runtime_jobs := runtime.nr_jobs()
n_jobs := cleanc_parallel_pass5_job_count(n_runtime_jobs, n_items)
$if windows {
gen.gen_pass5_files(emit_indices)
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
'pass 5 files')
gen.gen_pass5_post()
_ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post')
return
} $else {
if n_items <= 1 || n_jobs <= 1 {
// Fallback to sequential
gen.gen_pass5_files(emit_indices)
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
'pass 5 files')
gen.gen_pass5_post()
_ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post')
return
}
mut thread_ids := []C.pthread_t{len: n_jobs}
mut args := []GenCleancChunkArgs{cap: n_jobs}
mut workers := []voidptr{cap: n_jobs}
// chunk_items: work items assigned to each worker.
// chunk_indices: unique file indices each worker touches (for
// new_pass5_worker's owned-file / cross-worker dedup bookkeeping).
mut chunk_items := [][]cleanc.Pass5WorkItem{cap: n_jobs}
mut chunk_indices := [][]int{cap: n_jobs}
mut chunk_file_seen := []map[int]bool{cap: n_jobs}
mut chunk_costs := []int{cap: n_jobs}
mut chunk_idx := n_jobs
if chunk_idx > n_items {
chunk_idx = n_items
}
for ci := 0; ci < chunk_idx; ci++ {
chunk_items << []cleanc.Pass5WorkItem{}
chunk_indices << []int{}
chunk_file_seen << map[int]bool{}
chunk_costs << 0
}
mut sorted_items := work_items.clone()
for i := 1; i < sorted_items.len; i++ {
mut j := i
for j > 0 && sorted_items[j - 1].cost < sorted_items[j].cost {
sorted_items[j - 1], sorted_items[j] = sorted_items[j], sorted_items[j - 1]
j--
}
}
for item in sorted_items {
mut target := 0
for ci := 1; ci < chunk_idx; ci++ {
if chunk_costs[ci] < chunk_costs[target] {
target = ci
}
}
chunk_items[target] << item
// Only the worker that emits a file's globals (a whole-file item, or the
// first slice of a split file — both carry emit_globals) takes file-level
// dedup ownership. A split file's later slices deliberately do NOT, so the
// file's lazily/transitively emitted fns stay blocked in those workers and
// only the owning worker can emit them; the explicit slice still emits via
// the owner-scoped bypass in gen_file_range. This closes the duplicate /
// reordered-emission hole that file-level ownership left open for files
// split across workers.
if item.emit_globals && item.file_idx !in chunk_file_seen[target] {
chunk_file_seen[target][item.file_idx] = true
chunk_indices[target] << item.file_idx
}
chunk_costs[target] += item.cost
}
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
'pass 5 chunk split')
for ci := 0; ci < chunk_idx; ci++ {
w := gen.new_pass5_worker(chunk_indices[ci], ci)
workers << voidptr(w)
}
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
'pass 5 worker setup')
// Set up args after all chunk_items are stable
for ci := 0; ci < chunk_idx; ci++ {
args << GenCleancChunkArgs{
worker: workers[ci]
work_items_ptr: unsafe { voidptr(&chunk_items[ci]) }
}
}
attr_buf := [64]u8{}
attr := unsafe { voidptr(&attr_buf[0]) }
C.pthread_attr_init(attr)
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
for ci := 0; ci < chunk_idx; ci++ {
C.pthread_create(unsafe { &thread_ids[ci] }, attr, gen_cleanc_chunk_thread,
unsafe { voidptr(&args[ci]) })
}
C.pthread_attr_destroy(attr)
// Wait for all workers
for ci := 0; ci < chunk_idx; ci++ {
C.pthread_join(thread_ids[ci], unsafe { nil })
}
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
'pass 5 worker run')
// Merge worker results in order
for ci := 0; ci < chunk_idx; ci++ {
w := unsafe { &cleanc.Gen(workers[ci]) }
gen.merge_pass5_worker(w)
}
stage_start = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start,
'pass 5 merge')
gen.print_pass5_file_times(8)
gen.gen_pass5_post()
_ = mark_cleanc_parallel_step(stats_enabled, mut stats_sw, stage_start, 'pass 5 post')
}
}
fn cleanc_parallel_pass5_job_count(n_runtime_jobs int, n_files int) int {
if n_runtime_jobs <= 0 || n_files <= 0 {
return 0
}
mut n_jobs := n_runtime_jobs
if n_jobs > max_cleanc_pass5_jobs {
n_jobs = max_cleanc_pass5_jobs
}
if n_jobs > n_files {
n_jobs = n_files
}
return n_jobs
}
@@ -0,0 +1,9 @@
module builder
fn test_cleanc_parallel_pass5_job_count_caps_runtime_jobs() {
assert cleanc_parallel_pass5_job_count(128, 1000) == max_cleanc_pass5_jobs
assert cleanc_parallel_pass5_job_count(64, 3) == 3
assert cleanc_parallel_pass5_job_count(2, 1000) == 2
assert cleanc_parallel_pass5_job_count(4, 0) == 0
assert cleanc_parallel_pass5_job_count(0, 4) == 0
}
@@ -0,0 +1,27 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
#include <malloc/malloc.h>
struct C.malloc_statistics_t {
blocks_in_use u32
size_in_use usize
max_size_in_use usize
size_allocated usize
}
fn C.malloc_default_zone() voidptr
fn C.malloc_zone_statistics(zone voidptr, stats &C.malloc_statistics_t)
// darwin_live_malloc_bytes returns (current live malloc bytes, peak live bytes)
// from the default malloc zone. Under `-gc none` (no frees) the current value
// is monotonic across phases, so per-phase deltas are the exact number of bytes
// each phase allocated and never released. This is the reliable counterpart to
// runtime.used_memory(), whose resident_size reading is distorted by OS paging.
fn darwin_live_malloc_bytes() (u64, u64) {
mut st := C.malloc_statistics_t{}
C.malloc_zone_statistics(C.malloc_default_zone(), &st)
return u64(st.size_in_use), u64(st.max_size_in_use)
}
@@ -0,0 +1,403 @@
module builder
import os
import v2.ast
import v2.gen.v as gen_v
import v2.parser
import v2.pref
import v2.token
fn module_storage_cache_tmp_dir(label string) string {
return os.join_path(os.temp_dir(), 'v2_module_storage_cache_${label}_${os.getpid()}')
}
fn module_storage_cached_header_source_for_test() string {
tmp_dir := module_storage_cache_tmp_dir('source')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_file := os.join_path(tmp_dir, 'state', 'state.v')
os.write_file(source_file, 'module state
pub __global mut total = 0
__global mut hidden = 0
') or {
panic(err)
}
mut prefs := pref.new_preferences()
mut b := new_builder(&prefs)
source_files := b.parse_source_files_for_headers([source_file])
header_ast := b.build_module_header_ast(source_files, 'state') or {
panic('missing state header')
}
mut gen := gen_v.new_gen(b.pref)
gen.gen(header_ast)
return sanitize_header_source(gen.output_string(), map[string]string{})
}
fn module_mut_cached_header_source_for_test() string {
tmp_dir := module_storage_cache_tmp_dir('module_mut_source')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_file := os.join_path(tmp_dir, 'state', 'state.v')
os.write_file(source_file, 'module state
pub struct Counter {
pub module_mut:
value int
pub mut:
open int
}
') or {
panic(err)
}
mut prefs := pref.new_preferences()
mut b := new_builder(&prefs)
source_files := b.parse_source_files_for_headers([source_file])
header_ast := b.build_module_header_ast(source_files, 'state') or {
panic('missing state header')
}
mut gen := gen_v.new_gen(b.pref)
gen.gen(header_ast)
return sanitize_header_source(gen.output_string(), map[string]string{})
}
fn module_mut_interface_cached_header_source_for_test() string {
return module_mut_interface_cached_header_source_for_test_with_source_decls(false)
}
fn module_mut_interface_merged_cached_header_source_for_test() string {
return module_mut_interface_cached_header_source_for_test_with_source_decls(true)
}
fn module_mut_interface_cached_header_source_for_test_with_source_decls(include_source_decls bool) string {
tmp_dir := module_storage_cache_tmp_dir('module_mut_interface_source')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_file := os.join_path(tmp_dir, 'state', 'state.v')
os.write_file(source_file, 'module state
pub type Handler = fn (int) bool
pub interface Mutator {
handler Handler
direct fn (int) bool
handle(int) bool
mut:
bump()
reset()
}
pub struct Counter {
pub module_mut:
iface Mutator
}
') or {
panic(err)
}
mut prefs := pref.new_preferences()
mut b := new_builder(&prefs)
source_files := b.parse_source_files_for_headers([source_file])
header_ast := b.build_module_header_ast(source_files, 'state') or {
panic('missing state header')
}
mut gen := gen_v.new_gen(b.pref)
gen.gen(header_ast)
mut header_source := sanitize_header_source(gen.output_string(), map[string]string{})
if include_source_decls {
source_fn_decls := b.source_fn_decls_for_files([source_file])
header_source = merge_missing_source_fn_decls(header_source, source_fn_decls)
}
return header_source
}
fn module_storage_cached_header_global(header_source string, name string) ast.FieldDecl {
tmp_dir := module_storage_cache_tmp_dir('parse')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
header_path := os.join_path(tmp_dir, 'state.vh')
os.write_file(header_path, header_source) or { panic(err) }
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
files := par.parse_files([header_path], mut file_set)
for stmt in files[0].stmts {
if stmt is ast.GlobalDecl {
for field in stmt.fields {
if field.name == name {
return field
}
}
}
}
panic('missing cached header field ${name}')
}
fn module_storage_cached_header_struct(header_source string, name string) ast.StructDecl {
tmp_dir := module_storage_cache_tmp_dir('parse_struct')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
header_path := os.join_path(tmp_dir, 'state.vh')
os.write_file(header_path, header_source) or { panic(err) }
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
files := par.parse_files([header_path], mut file_set)
for stmt in files[0].stmts {
if stmt is ast.StructDecl {
if stmt.name == name {
return stmt
}
}
}
panic('missing cached header struct ${name}')
}
fn module_storage_cached_header_interface(header_source string, name string) ast.InterfaceDecl {
tmp_dir := module_storage_cache_tmp_dir('parse_interface')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
header_path := os.join_path(tmp_dir, 'state.vh')
os.write_file(header_path, header_source) or { panic(err) }
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
files := par.parse_files([header_path], mut file_set)
for stmt in files[0].stmts {
if stmt is ast.InterfaceDecl {
if stmt.name == name {
return stmt
}
}
}
panic('missing cached header interface ${name}')
}
fn module_storage_run_cached_header_check(label string, header_source string, main_source string) (int, string) {
tmp_dir := module_storage_cache_tmp_dir(label)
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(tmp_dir, 'state', 'state.v'), header_source) or { panic(err) }
main_path := os.join_path(tmp_dir, 'main.v')
os.write_file(main_path, main_source) or { panic(err) }
out_path := os.join_path(tmp_dir, 'out.txt')
cmd := '${os.quoted_path(@VEXE)} -v2 -backend v -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1'
res := os.execute(cmd)
return res.exit_code, res.output
}
fn test_module_storage_cached_header_preserves_flags() {
header_source := module_storage_cached_header_source_for_test()
assert header_source.contains('pub __global (')
assert header_source.contains('mut total int')
assert header_source.contains('__global (')
assert header_source.contains('mut hidden int')
public_field := module_storage_cached_header_global(header_source, 'total')
assert public_field.is_public
assert public_field.is_mut
private_field := module_storage_cached_header_global(header_source, 'hidden')
assert !private_field.is_public
assert private_field.is_mut
}
fn test_module_storage_cached_header_preserves_module_mut_struct_fields() {
header_source := module_mut_cached_header_source_for_test()
assert header_source.contains('pub module_mut:')
assert !header_source.contains('@[module_mut]')
counter := module_storage_cached_header_struct(header_source, 'Counter')
assert counter.fields[0].name == 'value'
assert counter.fields[0].is_public
assert counter.fields[0].is_mut
assert counter.fields[0].is_module_mut
assert counter.fields[1].name == 'open'
assert counter.fields[1].is_public
assert counter.fields[1].is_mut
assert !counter.fields[1].is_module_mut
}
fn test_module_storage_cached_header_preserves_mut_interface_methods_for_module_mut_field() {
header_source := module_mut_interface_cached_header_source_for_test()
assert header_source.contains('pub interface Mutator {')
assert header_source.contains('handler Handler')
assert header_source.contains('direct fn(int) bool')
assert header_source.contains('handle(int) bool')
assert header_source.contains('mut:')
assert header_source.contains('bump()')
assert !header_source.contains('direct(int) bool')
assert !header_source.contains('bump fn(')
mutator := module_storage_cached_header_interface(header_source, 'Mutator')
assert mutator.fields[0].name == 'handler'
assert !mutator.fields[0].is_interface_method
assert mutator.fields[1].name == 'direct'
assert !mutator.fields[1].is_interface_method
assert mutator.fields[2].name == 'handle'
assert mutator.fields[2].is_interface_method
assert mutator.fields[3].name == 'bump'
assert mutator.fields[3].is_mut
assert mutator.fields[3].is_interface_method
assert mutator.fields[4].name == 'reset'
assert mutator.fields[4].is_mut
assert mutator.fields[4].is_interface_method
module_mut_code, module_mut_output := module_storage_run_cached_header_check('module_mut_interface_method',
header_source, 'module main
import state
fn main() {
mut c := state.Counter{}
c.iface.bump()
}
')
assert module_mut_code != 0, 'cached module_mut interface method mutation should fail'
assert module_mut_output.contains('cannot mutate module-mutable field `state.Counter.iface` outside module `state`'), module_mut_output
}
fn test_module_storage_cached_header_does_not_synthesize_interface_fn_field_decls() {
header_source := module_mut_interface_merged_cached_header_source_for_test()
assert header_source.contains('pub interface Mutator {')
assert header_source.contains('direct fn(int) bool')
assert header_source.contains('handle(int) bool')
assert header_source.contains('pub fn (it Mutator) handle(int) bool')
assert header_source.contains('pub fn (mut it Mutator) bump()')
assert header_source.contains('pub fn (mut it Mutator) reset()')
assert !header_source.contains('pub fn (it Mutator) direct fn')
assert !header_source.contains('fn (it Mutator) direct fn')
assert !header_source.contains('pub fn (it Mutator) bump()')
module_mut_code, module_mut_output := module_storage_run_cached_header_check('module_mut_interface_merged_method',
header_source, 'module main
import state
fn main() {
mut c := state.Counter{}
c.iface.bump()
}
')
assert module_mut_code != 0, 'merged cached module_mut interface method mutation should fail'
assert module_mut_output.contains('cannot mutate module-mutable field `state.Counter.iface` outside module `state`'), module_mut_output
}
fn test_module_storage_cached_header_visibility_matches_source_module() {
header_source := module_storage_cached_header_source_for_test()
public_code, public_output := module_storage_run_cached_header_check('public', header_source, 'module main
import state
fn main() {
state.total += 1
}
')
assert public_code == 0, public_output
private_code, private_output := module_storage_run_cached_header_check('private',
header_source, 'module main
import state
fn main() {
state.hidden += 1
}
')
assert private_code != 0, 'cached private module storage should fail'
assert private_output.contains('module global `state.hidden` is private'), private_output
}
fn test_module_storage_cached_header_module_mut_visibility_matches_source_module() {
header_source := module_mut_cached_header_source_for_test()
public_code, public_output := module_storage_run_cached_header_check('module_mut_public',
header_source, 'module main
import state
fn main() {
mut c := state.Counter{}
println(c.value)
c.open++
}
')
assert public_code == 0, public_output
module_mut_code, module_mut_output := module_storage_run_cached_header_check('module_mut_private',
header_source, 'module main
import state
fn main() {
mut c := state.Counter{}
c.value++
}
')
assert module_mut_code != 0, 'cached module_mut field mutation should fail'
assert module_mut_output.contains('cannot mutate module-mutable field `state.Counter.value` outside module `state`'), module_mut_output
}
fn test_module_storage_c_extern_import_cache_uses_raw_symbol() {
tmp_dir := module_storage_cache_tmp_dir('c_extern_import')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'ext')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
mut prefs := pref.new_preferences()
mut cache_builder := new_builder(&prefs)
cache_dir := cache_builder.core_cache_dir()
os.rmdir_all(cache_dir) or {}
c_path := os.join_path(tmp_dir, 'raw_status.c')
os.write_file(c_path, 'int raw_status = 7;\n') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'ext', 'ext.v'), 'module ext
@[c_extern]
pub __global raw_status int
pub fn read_status() int {
return raw_status
}
') or {
panic(err)
}
main_path := os.join_path(tmp_dir, 'main.v')
os.write_file(main_path, 'module main
import ext
#flag ${c_path}
fn main() {
_ = ext.raw_status
_ = ext.read_status()
}
') or {
panic(err)
}
out_path := os.join_path(tmp_dir, 'out')
cmd := '${os.quoted_path(@VEXE)} -v -v2 -keepc -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1'
res := os.execute(cmd)
assert res.exit_code == 0, res.output
ext_header := os.read_file(cache_builder.core_header_path('ext')) or { panic(err) }
assert ext_header.contains('@[c_extern]'), ext_header
imports_c := os.read_file(os.join_path(cache_dir, 'imports.c')) or { panic(err) }
assert imports_c.contains('return raw_status;'), imports_c
assert !imports_c.contains('ext__raw_status'), imports_c
}
@@ -0,0 +1,661 @@
module builder
import os
import v2.abi
import v2.ast
import v2.gen.x64
import v2.pref
fn test_native_x64_windows_selects_coff_and_windows_abi() {
assert is_windows_x64_native_target(.x64, 'windows')
assert native_x64_object_format_for_os('windows') == x64.ObjectFormat.coff
assert native_x64_codegen_abi_for_os('windows') == x64.X64Abi.windows
assert native_x64_lowering_abi_for_os('windows') == abi.X64Abi.windows
}
fn test_native_x64_non_windows_keeps_existing_object_formats_and_sysv() {
assert !is_windows_x64_native_target(.x64, 'linux')
assert native_x64_object_format_for_os('linux') == x64.ObjectFormat.elf
assert native_x64_codegen_abi_for_os('linux') == x64.X64Abi.sysv
assert native_x64_lowering_abi_for_os('linux') == abi.X64Abi.sysv
assert !is_windows_x64_native_target(.x64, 'macos')
assert native_x64_object_format_for_os('macos') == x64.ObjectFormat.macho
assert native_x64_codegen_abi_for_os('macos') == x64.X64Abi.sysv
assert native_x64_lowering_abi_for_os('macos') == abi.X64Abi.sysv
assert is_macos_native_target('macos')
assert is_macos_native_target('darwin')
assert native_x64_object_format_for_os('darwin') == x64.ObjectFormat.macho
assert native_x64_codegen_abi_for_os('darwin') == x64.X64Abi.sysv
assert native_x64_lowering_abi_for_os('darwin') == abi.X64Abi.sysv
}
fn test_minimal_x64_runtime_and_macos_tiny_object_targets() {
old_linux_tiny := os.getenv_opt('V2_X64_LINUX_TINY')
os.unsetenv('V2_X64_LINUX_TINY')
defer {
if old := old_linux_tiny {
os.setenv('V2_X64_LINUX_TINY', old, true)
} else {
os.unsetenv('V2_X64_LINUX_TINY')
}
}
mut prefs := pref.new_preferences()
prefs.backend = .x64
prefs.arch = .x64
prefs.target_os = 'linux'
linux_builder := new_builder(&prefs)
assert !linux_builder.uses_minimal_windows_x64_runtime()
assert linux_builder.uses_minimal_linux_x64_runtime()
assert linux_builder.uses_minimal_x64_runtime()
assert !linux_builder.uses_minimal_linux_x64_runtime_roots()
assert !linux_builder.uses_minimal_x64_runtime_roots()
os.setenv('V2_X64_LINUX_TINY', '1', true)
linux_tiny_builder := new_builder(&prefs)
assert linux_tiny_builder.uses_minimal_linux_x64_runtime()
assert linux_tiny_builder.uses_minimal_linux_x64_runtime_roots()
assert linux_tiny_builder.uses_minimal_x64_runtime_roots()
os.unsetenv('V2_X64_LINUX_TINY')
prefs.target_os = 'macos'
macos_builder := new_builder(&prefs)
assert !macos_builder.uses_minimal_windows_x64_runtime()
assert !macos_builder.uses_minimal_linux_x64_runtime()
assert !macos_builder.uses_minimal_x64_runtime()
assert !macos_builder.uses_minimal_x64_runtime_roots()
assert macos_builder.uses_macos_x64_tiny_object(.x64)
assert !macos_builder.uses_macos_x64_tiny_object(.arm64)
macos_tiny_builder := new_builder(&prefs)
assert !macos_tiny_builder.uses_minimal_x64_runtime()
assert !macos_tiny_builder.uses_minimal_x64_runtime_roots()
assert macos_tiny_builder.uses_macos_x64_tiny_object(.x64)
assert !macos_tiny_builder.uses_macos_x64_tiny_object(.arm64)
prefs.macos_tiny = false
macos_no_tiny_builder := new_builder(&prefs)
assert !macos_no_tiny_builder.uses_macos_x64_tiny_object(.x64)
prefs.macos_tiny = true
prefs.arch = .auto
macos_auto_arch_builder := new_builder(&prefs)
assert !macos_auto_arch_builder.uses_minimal_x64_runtime_roots()
assert macos_auto_arch_builder.uses_macos_x64_tiny_object(.x64)
assert !macos_auto_arch_builder.uses_macos_x64_tiny_object(.arm64)
prefs.arch = .x64
prefs.target_os = 'darwin'
darwin_builder := new_builder(&prefs)
assert !darwin_builder.uses_minimal_windows_x64_runtime()
assert !darwin_builder.uses_minimal_linux_x64_runtime()
assert !darwin_builder.uses_minimal_x64_runtime()
assert !darwin_builder.uses_minimal_x64_runtime_roots()
assert darwin_builder.uses_macos_x64_tiny_object(.x64)
prefs.target_os = 'windows'
windows_builder := new_builder(&prefs)
assert windows_builder.uses_minimal_windows_x64_runtime()
assert !windows_builder.uses_minimal_linux_x64_runtime()
assert windows_builder.uses_minimal_x64_runtime()
assert windows_builder.uses_minimal_x64_runtime_roots()
assert !windows_builder.uses_macos_x64_tiny_object(.x64)
}
fn test_macos_tiny_candidate_source_snapshot_is_auto_by_default_with_opt_out() {
mut prefs := pref.new_preferences()
prefs.backend = .x64
prefs.arch = .auto
prefs.target_os = 'macos'
mut hosted_builder := new_builder(&prefs)
hosted_builder.files = [
ast.File{
mod: 'main'
name: 'main.v'
},
]
hosted_builder.prepare_macos_tiny_candidate_source_files()
assert hosted_builder.macos_tiny_candidate_source_files.len == 1
assert hosted_builder.macos_tiny_candidate_source_files[0].name == 'main.v'
prefs.macos_tiny = false
mut opt_out_builder := new_builder(&prefs)
opt_out_builder.files = [
ast.File{
mod: 'main'
name: 'main.v'
},
]
opt_out_builder.prepare_macos_tiny_candidate_source_files()
assert opt_out_builder.macos_tiny_candidate_source_files.len == 0
assert !opt_out_builder.uses_minimal_x64_runtime_roots()
}
fn test_macos_tiny_candidate_source_snapshot_uses_flat_when_enabled() {
mut prefs := pref.new_preferences()
prefs.backend = .x64
prefs.arch = .auto
prefs.target_os = 'macos'
source_files := [
ast.File{
mod: 'main'
name: 'flat_main.v'
stmts: [
ast.Stmt(ast.FnDecl{
name: 'main'
}),
]
},
]
mut tiny_builder := new_builder(&prefs)
tiny_builder.files = [
ast.File{
mod: 'stale'
name: 'stale.v'
},
]
tiny_builder.flat = ast.flatten_files(source_files)
tiny_builder.prepare_macos_tiny_candidate_source_files()
assert tiny_builder.macos_tiny_candidate_source_files.len == 0
assert tiny_builder.macos_tiny_candidate_source_flat.files.len == source_files.len
assert tiny_builder.macos_tiny_candidate_source_flat.file_name(tiny_builder.macos_tiny_candidate_source_flat.files[0]) == 'flat_main.v'
assert tiny_builder.macos_tiny_candidate_source_flat.string_at(tiny_builder.macos_tiny_candidate_source_flat.files[0].mod_idx) == 'main'
}
fn test_macos_tiny_candidate_native_mir_build_policy_is_sequential_only_for_candidate() {
mut prefs := pref.new_preferences()
prefs.backend = .x64
prefs.arch = .x64
prefs.target_os = 'macos'
prefs.no_parallel = false
prefs.hot_fn = ''
builder := new_builder(&prefs)
assert !builder.native_mir_build_sequential('')
assert builder.native_mir_build_sequential(macos_tiny_candidate_graph_label)
prefs.no_parallel = true
no_parallel_builder := new_builder(&prefs)
assert no_parallel_builder.native_mir_build_sequential('')
}
fn test_macos_tiny_link_command_adds_hygiene_only_for_tiny_object() {
normal := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false, '')
tiny := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', true, '')
assert normal == 'ld -o ${os.quoted_path('/tmp/out')} ${os.quoted_path('main.o')} -lSystem -syslibroot ${os.quoted_path('/SDK Path')} -e _main -arch x86_64 -platform_version macos 11.0.0 11.0.0'
assert tiny == '${normal} -dead_strip -x -S'
assert !normal.contains('-dead_strip')
assert !normal.contains(' -x')
assert !normal.contains(' -S')
}
fn test_macos_sdk_path_from_xcrun_output_accepts_simple_sdk_path() {
output := '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk\n'
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk'
}
fn test_macos_sdk_path_from_xcrun_output_ignores_diagnostics_before_sdk_path() {
output :=
'2026-06-07 16:19:11.428 xcodebuild[1002:7567] Requested but did not find extension point\n' +
'2026-06-07 16:19:14.880 xcodebuild[1004:7681] Requested but did not find extension point\n' +
'/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk\n'
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk'
}
fn test_macos_sdk_path_from_xcrun_output_handles_empty_lines_spaces_and_crlf() {
output := ' \r\n\t\r\n /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk \r\n'
assert macos_sdk_path_from_xcrun_output(output)! == '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk'
}
fn test_macos_sdk_path_from_xcrun_output_rejects_missing_sdk_path() {
path := macos_sdk_path_from_xcrun_output('xcodebuild diagnostic only\nnot a path\n') or {
assert err.msg().contains('could not find a clean macOS SDK path')
return
}
assert false, 'expected missing SDK path error, got ${path}'
}
fn test_macos_sdk_path_from_xcrun_output_preserves_spaces_inside_path() {
output := '/Applications/Xcode With Spaces.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk\n'
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/Xcode With Spaces.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk'
}
fn test_macos_sdk_path_from_xcrun_output_rejects_relative_or_non_macos_sdk_lines() {
output := 'relative/MacOSX12.3.sdk\n/Applications/Xcode.app/Contents/Developer/SDKs/iPhoneOS.sdk\n/Applications/Xcode.app/Contents/Developer/SDKs/NotMacOSX.sdk\n'
path := macos_sdk_path_from_xcrun_output(output) or {
assert err.msg().contains('could not find a clean macOS SDK path')
return
}
assert false, 'expected invalid SDK path lines to be rejected, got ${path}'
}
fn test_macos_sdk_path_from_xcrun_output_uses_last_valid_sdk_line() {
output := '/Applications/Old.app/Contents/Developer/SDKs/MacOSX11.0.sdk\n' +
'/Applications/New.app/Contents/Developer/SDKs/MacOSX12.3.sdk\n'
assert macos_sdk_path_from_xcrun_output(output)! == '/Applications/New.app/Contents/Developer/SDKs/MacOSX12.3.sdk'
}
fn test_validate_macos_sdk_path_for_native_link_accepts_libsystem_tbd() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_macos_sdk_tbd_${os.getpid()}')
sdk_path := os.join_path(tmp_dir, 'MacOSX.test.sdk')
lib_dir := os.join_path(sdk_path, 'usr', 'lib')
os.mkdir_all(lib_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(lib_dir, 'libSystem.tbd'), '') or { panic(err) }
validate_macos_sdk_path_for_native_link(sdk_path)!
}
fn test_validate_macos_sdk_path_for_native_link_accepts_libsystem_dylib() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_macos_sdk_dylib_${os.getpid()}')
sdk_path := os.join_path(tmp_dir, 'MacOSX.test.sdk')
lib_dir := os.join_path(sdk_path, 'usr', 'lib')
os.mkdir_all(lib_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(lib_dir, 'libSystem.dylib'), '') or { panic(err) }
validate_macos_sdk_path_for_native_link(sdk_path)!
}
fn test_validate_macos_sdk_path_for_native_link_rejects_sdk_without_libsystem() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_macos_sdk_missing_libsystem_${os.getpid()}')
sdk_path := os.join_path(tmp_dir, 'MacOSX.test.sdk')
os.mkdir_all(os.join_path(sdk_path, 'usr', 'lib')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
validate_macos_sdk_path_for_native_link(sdk_path) or {
assert err.msg().contains('missing usr/lib/libSystem')
return
}
assert false, 'expected SDK without libSystem to be rejected'
}
fn test_validate_macos_sdk_path_for_native_link_rejects_missing_sdk_dir() {
missing_sdk := os.join_path(os.vtmp_dir(), 'v2_missing_macos_sdk_${os.getpid()}',
'MacOSX.test.sdk')
validate_macos_sdk_path_for_native_link(missing_sdk) or {
assert err.msg().contains('does not exist')
return
}
assert false, 'expected missing SDK directory to be rejected'
}
fn test_native_link_commands_append_directive_link_flags() {
linux := linux_native_link_command('cc', '/tmp/out', 'main.o', '-lm')
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false, '-lm')
assert linux == 'cc ${os.quoted_path('main.o')} -o ${os.quoted_path('/tmp/out')} -no-pie -lm'
assert macos == 'ld -o ${os.quoted_path('/tmp/out')} ${os.quoted_path('main.o')} -lm -lSystem -syslibroot ${os.quoted_path('/SDK Path')} -e _main -arch x86_64 -platform_version macos 11.0.0 11.0.0'
}
fn test_linux_native_link_command_uses_configured_driver_and_link_flags() {
cmd := linux_native_link_command('custom-cc', '/tmp/out', 'main.o', '-fopenmp')
assert cmd.starts_with('custom-cc ${os.quoted_path('main.o')}'), cmd
assert cmd.contains('-fopenmp'), cmd
}
fn test_native_link_commands_keep_wl_for_linux_and_translate_for_macos_ld() {
linux := linux_native_link_command('cc', '/tmp/out', 'main.o', '-Wl,-rpath,/tmp/lib')
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false,
'-Wl,-rpath,@loader_path/lib')
assert linux.contains('-Wl,-rpath,/tmp/lib'), linux
assert macos.contains(' -rpath @loader_path/lib '), macos
assert !macos.contains('-Wl,'), macos
}
fn test_native_link_commands_translate_xlinker_for_macos_ld() {
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false,
'-Xlinker -rpath -Xlinker @loader_path/lib')
assert macos.contains(' -rpath @loader_path/lib '), macos
assert !macos.contains('-Xlinker'), macos
}
fn test_macos_native_link_command_keeps_framework_search_paths() {
macos := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false,
'-F /tmp/Fw -F/opt/Fw -framework Foo')
assert macos.contains(' -F /tmp/Fw -F/opt/Fw -framework Foo '), macos
}
fn test_split_compile_and_link_flags_duplicates_dual_use_driver_flags() {
compile_flags, link_flags :=
split_compile_and_link_flags('-I include -fopenmp -fopenmp=libomp -pthread helper.c')
assert compile_flags == '-I include -fopenmp -fopenmp=libomp -pthread'
assert link_flags == '-fopenmp -fopenmp=libomp -pthread helper.c'
}
fn test_split_compile_and_link_flags_keeps_framework_search_paths_dual_use() {
compile_flags, link_flags :=
split_compile_and_link_flags('-F /tmp/Fw -F/opt/Fw -framework Foo helper.m')
assert compile_flags == '-F /tmp/Fw -F/opt/Fw'
assert link_flags == '-F /tmp/Fw -F/opt/Fw -framework Foo helper.m'
}
fn test_macos_native_ld_rejects_dual_use_driver_link_flags() {
validate_macos_native_ld_link_flags('-lm')!
validate_macos_native_ld_link_flags('-fopenmp') or {
assert err.msg().contains('macOS native ld cannot consume driver linker flags'), err.msg()
return
}
assert false, 'expected macOS native ld driver flag diagnostic'
}
fn test_native_linux_tiny_link_allows_runtime_system_libs_when_user_flags_are_empty() {
assert native_link_flags_allow_builtin_linux_tiny('', '')
assert native_link_flags_allow_builtin_linux_tiny(' ', '')
assert native_link_flags_allow_builtin_linux_tiny('-lpthread -lm -ldl -lc', '')
assert native_link_flags_allow_builtin_linux_tiny('-l pthread -l m -l dl -l c', '')
}
fn test_native_linux_tiny_link_blocks_user_external_link_flags() {
for user_link_flags in ['-lm', '-Wl,-rpath,/tmp/lib', '/tmp/helper.c', '-fopenmp', '-pthread',
'-F/tmp/Frameworks'] {
assert !native_link_flags_allow_builtin_linux_tiny('-lpthread -lm -ldl', user_link_flags), user_link_flags
}
}
fn test_native_linux_tiny_link_blocks_external_link_inputs_in_global_flags() {
assert !native_link_flags_allow_builtin_linux_tiny('-Xlinker -rpath', '')
assert !native_link_flags_allow_builtin_linux_tiny('-L/tmp', '')
assert !native_link_flags_allow_builtin_linux_tiny('/tmp/helper.o', '')
assert !native_link_flags_allow_builtin_linux_tiny('/tmp/libhelper.a', '')
assert !native_link_flags_allow_builtin_linux_tiny('-framework Foundation', '')
assert !native_link_flags_allow_builtin_linux_tiny('-lfoo', '')
}
fn test_native_external_link_inputs_replace_sources_with_objects_in_order() {
inputs := native_external_link_inputs('-L/tmp/lib /tmp/foo.c -lfoo /tmp/bar.m /tmp/baz.o',
'/tmp/out')!
assert inputs.source_files == ['/tmp/foo.c', '/tmp/bar.m']
assert inputs.object_files.len == 2
assert inputs.link_flags == '-L/tmp/lib ${os.quoted_path(inputs.object_files[0])} -lfoo ${os.quoted_path(inputs.object_files[1])} /tmp/baz.o'
assert !inputs.link_flags.contains('/tmp/foo.c')
assert !inputs.link_flags.contains('/tmp/bar.m')
}
fn test_native_external_link_inputs_reject_unsupported_c_family_sources() {
for source in ['/tmp/foo.cc', '/tmp/foo.cpp', '/tmp/foo.cxx', '/tmp/foo.mm'] {
native_external_link_inputs(source, '/tmp/out') or {
assert err.msg().starts_with('x64: unsupported backend feature: '), err.msg()
continue
}
assert false, 'expected ${source} source to be rejected'
}
}
fn test_native_external_link_inputs_reject_quoted_source_tokens() {
native_external_link_inputs('"foo.c"', '/tmp/out') or {
assert err.msg().starts_with('x64: unsupported backend feature: quoted #flag source path'), err.msg()
return
}
assert false, 'expected quoted source token to be rejected'
}
fn test_macos_native_link_command_uses_rewritten_external_objects() {
tmp_dir := os.real_path(os.vtmp_dir())
foo_c := os.join_path(tmp_dir, 'foo.c')
bar_m := os.join_path(tmp_dir, 'bar.m')
out_path := os.join_path(tmp_dir, 'out')
inputs := native_external_link_inputs('${foo_c} -framework Foundation ${bar_m}', out_path)!
cmd := macos_native_link_command(out_path, 'main.o', '/SDK Path', 'x86_64', false,
inputs.link_flags)
assert cmd.contains(os.quoted_path(inputs.object_files[0])), cmd
assert cmd.contains(os.quoted_path(inputs.object_files[1])), cmd
assert cmd.contains('-framework Foundation'), cmd
assert !cmd.contains(foo_c), cmd
assert !cmd.contains(bar_m), cmd
}
fn test_native_external_source_compile_command_adds_macos_sdk_and_arch() {
cmd := native_external_source_compile_command('cc', '/tmp/foo.c', '/tmp/foo.o',
'-I /tmp/include -DHELPER', '/SDK Path', 'x86_64', 'macos')
assert cmd.starts_with('cc -c '), cmd
assert cmd.contains('-isysroot ${os.quoted_path('/SDK Path')}'), cmd
assert cmd.contains('-arch x86_64'), cmd
assert cmd.contains('-I /tmp/include -DHELPER'), cmd
assert cmd.contains(os.quoted_path('/tmp/foo.c')), cmd
assert cmd.contains('-o ${os.quoted_path('/tmp/foo.o')}'), cmd
}
fn test_native_external_source_compile_command_keeps_linux_compile_flags() {
cmd := native_external_source_compile_command('cc', '/tmp/foo.c', '/tmp/foo.o',
'-I /tmp/include -DHELPER', '', '', 'linux')
assert cmd.starts_with('cc -c '), cmd
assert !cmd.contains('-isysroot'), cmd
assert !cmd.contains('-arch'), cmd
assert cmd.contains('-I /tmp/include -DHELPER'), cmd
assert cmd.contains(os.quoted_path('/tmp/foo.c')), cmd
assert cmd.contains('-o ${os.quoted_path('/tmp/foo.o')}'), cmd
}
fn test_native_external_source_compiler_uses_pref_ccompiler() {
mut prefs := pref.new_preferences()
prefs.ccompiler = 'custom-native-cc'
mut b := new_builder(&prefs)
assert b.native_external_source_compiler('macos') == 'custom-native-cc'
}
fn test_native_external_source_compiler_uses_v2cc_when_pref_is_empty() {
old_v2cc := os.getenv_opt('V2CC')
os.setenv('V2CC', 'env-native-cc', true)
defer {
if old := old_v2cc {
os.setenv('V2CC', old, true)
} else {
os.unsetenv('V2CC')
}
}
mut prefs := pref.new_preferences()
prefs.ccompiler = ''
mut b := new_builder(&prefs)
assert b.native_external_source_compiler('macos') == 'env-native-cc'
}
fn test_native_external_source_compiler_defaults_to_cc_for_macos() {
old_v2cc := os.getenv_opt('V2CC')
os.unsetenv('V2CC')
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_external_cc_${os.getpid()}')
tcc_dir := os.join_path(tmp_dir, 'thirdparty', 'tcc')
os.mkdir_all(tcc_dir) or { panic(err) }
os.write_file(os.join_path(tcc_dir, 'tcc.exe'), '') or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
if old := old_v2cc {
os.setenv('V2CC', old, true)
} else {
os.unsetenv('V2CC')
}
}
mut prefs := pref.Preferences{
vroot: tmp_dir
}
mut b := new_builder(&prefs)
assert b.native_external_source_compiler('macos') == 'cc'
assert b.native_external_source_compiler('darwin') == 'cc'
}
fn test_native_linux_hosted_link_compiler_pref_v2cc_default_order() {
old_v2cc := os.getenv_opt('V2CC')
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_linux_hosted_cc_${os.getpid()}')
tcc_dir := os.join_path(tmp_dir, 'thirdparty', 'tcc')
os.mkdir_all(tcc_dir) or { panic(err) }
os.write_file(os.join_path(tcc_dir, 'tcc.exe'), '') or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
if old := old_v2cc {
os.setenv('V2CC', old, true)
} else {
os.unsetenv('V2CC')
}
}
os.setenv('V2CC', 'env-hosted-cc', true)
mut pref_prefs := pref.new_preferences()
pref_prefs.ccompiler = 'pref-hosted-cc'
pref_builder := new_builder(&pref_prefs)
assert pref_builder.native_linux_hosted_link_compiler() == 'pref-hosted-cc'
mut env_prefs := pref.new_preferences()
env_prefs.ccompiler = ''
env_builder := new_builder(&env_prefs)
assert env_builder.native_linux_hosted_link_compiler() == 'env-hosted-cc'
os.unsetenv('V2CC')
mut default_prefs := pref.Preferences{
vroot: tmp_dir
}
default_builder := new_builder(&default_prefs)
assert default_builder.native_linux_hosted_link_compiler() == 'cc'
}
fn test_native_link_flags_from_sources_are_not_global() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_link_flags_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
no_flag_path := os.join_path(tmp_dir, 'no_flag.v')
math_flag_path := os.join_path(tmp_dir, 'math_flag.v')
os.write_file(no_flag_path, 'module main\n') or { panic(err) }
os.write_file(math_flag_path, 'module main\n#flag -lm\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.backend = .x64
prefs.arch = .x64
prefs.target_os = 'linux'
prefs.skip_builtin = true
mut no_flag_builder := new_builder(&prefs)
no_flag_builder.files = [
ast.File{
name: no_flag_path
},
]
assert no_flag_builder.native_link_flags_from_sources() == ''
mut math_flag_builder := new_builder(&prefs)
math_flag_builder.files = [
ast.File{
name: math_flag_path
},
]
assert math_flag_builder.native_link_flags_from_sources() == '-lm'
}
fn test_native_user_link_flags_ignore_internal_vlib_runtime_flags() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_user_link_flags_${os.getpid()}')
vlib_os_dir := os.join_path(tmp_dir, 'vlib', 'os')
os.mkdir_all(vlib_os_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
main_path := os.join_path(tmp_dir, 'main.v')
internal_path := os.join_path(vlib_os_dir, 'signal_linux.c.v')
os.write_file(main_path, 'module main\n#flag -lm\n') or { panic(err) }
os.write_file(internal_path, 'module os\n#flag -lpthread\n') or { panic(err) }
mut prefs := pref.Preferences{
backend: .x64
arch: .x64
target_os: 'linux'
vroot: tmp_dir
skip_builtin: true
}
mut b := new_builder(&prefs)
b.user_files = [main_path]
b.files = [
ast.File{
name: main_path
},
ast.File{
name: internal_path
},
]
_, all_link_flags := b.native_compile_and_link_flags_from_sources()
_, user_link_flags := b.native_user_compile_and_link_flags_from_sources()
assert all_link_flags.fields().sorted() == ['-lm', '-lpthread']
assert user_link_flags == '-lm'
assert !native_link_flags_allow_builtin_linux_tiny(all_link_flags, user_link_flags)
b.user_files = []
b.files = [
ast.File{
name: internal_path
},
]
_, internal_only_link_flags := b.native_compile_and_link_flags_from_sources()
_, no_user_link_flags := b.native_user_compile_and_link_flags_from_sources()
assert internal_only_link_flags == '-lpthread'
assert no_user_link_flags == ''
assert native_link_flags_allow_builtin_linux_tiny(internal_only_link_flags, no_user_link_flags)
}
fn test_native_compile_and_link_flags_from_sources_keep_compile_flags_for_source_inputs() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_native_source_flags_${os.getpid()}')
include_dir := os.join_path(tmp_dir, 'include')
os.mkdir_all(include_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
main_path := os.join_path(tmp_dir, 'main.v')
c_path := os.join_path(tmp_dir, 'helper.c')
os.write_file(c_path, 'int helper(void) { return 7; }\n') or { panic(err) }
os.write_file(main_path, 'module main
#flag -I include -DHELPER -fopenmp -pthread helper.c -Wl,-rpath,/tmp/native-lib -lm
') or {
panic(err)
}
mut prefs := pref.new_preferences()
prefs.backend = .x64
prefs.arch = .x64
prefs.target_os = 'linux'
prefs.skip_builtin = true
mut b := new_builder(&prefs)
b.files = [
ast.File{
name: main_path
},
]
compile_flags, link_flags := b.native_compile_and_link_flags_from_sources()
expected_include_dir := os.real_path(include_dir)
expected_c_path := os.real_path(c_path)
assert compile_flags == '-I ${expected_include_dir} -DHELPER -fopenmp -pthread'
assert link_flags == '-fopenmp -pthread ${expected_c_path} -Wl,-rpath,/tmp/native-lib -lm'
}
fn test_native_x64_requires_ssa_optimization() {
mut prefs := pref.new_preferences()
assert prefs.no_optimize
builder := new_builder(&prefs)
assert builder.native_backend_requires_ssa_optimization(.x64)
assert !builder.native_backend_requires_ssa_optimization(.arm64)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,14 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import v2.ast
fn (mut b Builder) parse_files_parallel(files []string) []ast.File {
// FlatAst mode is the only builder parse pipeline. Keep `-parallel`
// parsing serial for now: token.FileSet shares position counters and file
// slices that are not concurrency-safe, and selector_names are keyed by
// those position ids.
return b.parse_files(files)
}
@@ -0,0 +1,795 @@
// vtest build: macos
module builder
import os
import v2.ast
import v2.pref
fn parse_files_for_parse_test(mut b Builder, paths []string) []ast.File {
b.parse_files(paths)
b.flat = b.flat_builder.flat
return b.flat.to_files()
}
fn test_parse_files_keeps_single_file_inputs_isolated() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_single_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(tmp_dir, 'hello.v')
sibling_file := os.join_path(tmp_dir, 'extra.v')
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(sibling_file, 'module main\nfn extra() {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [entry_file])
assert files.len == 1
}
fn test_parse_files_expands_directory_inputs() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(tmp_dir, 'hello.v')
sibling_file := os.join_path(tmp_dir, 'extra.v')
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(sibling_file, 'module main\nfn extra() {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [tmp_dir])
assert files.len == 2
}
fn test_parse_files_expands_same_module_subdirectories() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_recursive_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'user')) or { panic(err) }
os.mkdir_all(os.join_path(tmp_dir, 'api')) or { panic(err) }
os.mkdir_all(os.join_path(tmp_dir, 'nested_app')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(tmp_dir, 'main.v')
user_file := os.join_path(tmp_dir, 'user', 'user.v')
api_file := os.join_path(tmp_dir, 'api', 'api.v')
nested_file := os.join_path(tmp_dir, 'nested_app', 'main.v')
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(user_file, 'module main\nstruct User {}\n') or { panic(err) }
os.write_file(api_file, 'module api\nstruct Payload {}\n') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'nested_app', 'v.mod'), 'Module { name: "nested" }\n') or {
panic(err)
}
os.write_file(nested_file, 'module main\nfn nested_main() {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [tmp_dir])
names := files.map(os.file_name(it.name))
assert files.len == 2
assert 'main.v' in names
assert 'user.v' in names
}
fn test_parse_files_expands_implicit_main_subdirectories() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_implicit_main_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'heroes')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(tmp_dir, 'main.v')
hero_file := os.join_path(tmp_dir, 'heroes', 'hero.v')
os.write_file(entry_file, 'fn main() {}\n') or { panic(err) }
os.write_file(hero_file, 'struct HeroEffect {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [tmp_dir])
names := files.map(os.file_name(it.name))
assert files.len == 2
assert 'main.v' in names
assert 'hero.v' in names
}
fn test_parse_files_skips_block_commented_non_main_subdirectories() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_dir_block_comment_mod_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'api')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(tmp_dir, 'main.v')
api_file := os.join_path(tmp_dir, 'api', 'api.v')
os.write_file(entry_file, 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(api_file, '/* license header */\nmodule api\nstruct Payload {}\n') or {
panic(err)
}
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [tmp_dir])
names := files.map(os.file_name(it.name))
assert files.len == 1
assert 'main.v' in names
assert 'api.v' !in names
}
fn test_virtual_main_modules_skip_groups_with_executable_main_from_paths() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_virtual_paths_main_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'admin')) or { panic(err) }
os.mkdir_all(os.join_path(tmp_dir, 'tests')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
main_file := os.join_path(tmp_dir, 'main.v')
admin_file := os.join_path(tmp_dir, 'admin', 'admin.v')
test_file := os.join_path(tmp_dir, 'tests', 'first_run.v')
os.write_file(main_file, 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(admin_file, 'module main\nfn admin_route() {}\n') or { panic(err) }
os.write_file(test_file, 'module main\nfn main() {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
b.user_files = [tmp_dir]
groups := b.collect_virtual_main_modules_from_paths([main_file, admin_file, test_file])
names := groups.map(it.name)
assert names == ['admin']
}
fn test_virtual_main_modules_skip_groups_with_attributed_executable_main_from_paths() {
tmp_dir := os.join_path(os.temp_dir(),
'v2_builder_virtual_paths_attributed_main_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'admin')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
main_file := os.join_path(tmp_dir, 'main.v')
admin_file := os.join_path(tmp_dir, 'admin', 'admin.v')
os.write_file(main_file, 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(admin_file, 'module main\n@[console] fn main() {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
b.user_files = [tmp_dir]
groups := b.collect_virtual_main_modules_from_paths([main_file, admin_file])
assert groups.len == 0
}
fn test_virtual_main_modules_skip_groups_with_executable_main_from_ast() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_virtual_ast_main_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(os.join_path(tmp_dir, 'admin')) or { panic(err) }
os.mkdir_all(os.join_path(tmp_dir, 'tests')) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(tmp_dir, 'main.v'), 'module main\nfn main() {}\n') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'admin', 'admin.v'), 'module main\nfn admin_route() {}\n') or {
panic(err)
}
os.write_file(os.join_path(tmp_dir, 'tests', 'first_run.v'), 'module main\nfn main() {}\n') or {
panic(err)
}
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
b.user_files = [tmp_dir]
b.files = parse_files_for_parse_test(mut b, [tmp_dir])
groups := b.collect_virtual_main_modules()
names := groups.map(it.name)
assert names == ['admin']
}
fn test_parse_files_expands_non_main_module_files() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_module_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(tmp_dir, 'math_test.v')
sibling_file := os.join_path(tmp_dir, 'math.v')
os.write_file(entry_file, 'module math\nconst x = twice(21)\n') or { panic(err) }
os.write_file(sibling_file, 'module math\nfn twice(v int) int { return v * 2 }\n') or {
panic(err)
}
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
prefs.skip_imports = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [entry_file])
assert files.len == 2
}
fn test_parse_files_resolves_project_root_sibling_import_from_nested_module_file() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_nested_import_${os.getpid()}')
flags_dir := os.join_path(tmp_dir, 'core', 'flags')
ignore_dir := os.join_path(tmp_dir, 'ignore')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(flags_dir) or { panic(err) }
os.mkdir_all(ignore_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(flags_dir, 'complete_test.v')
ignore_file := os.join_path(ignore_dir, 'lib.v')
os.write_file(os.join_path(tmp_dir, 'v.mod'), "Module { name: 'nested_import' }\n") or {
panic(err)
}
os.write_file(entry_file, 'module flags\nimport ignore\nfn test_use_import() {}\n') or {
panic(err)
}
os.write_file(ignore_file, 'module ignore\npub struct Marker {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [entry_file])
names := files.map(os.file_name(it.name))
assert 'complete_test.v' in names
assert 'lib.v' in names
}
fn test_parse_files_prefers_project_root_module_over_vlib_module() {
tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_local_shadow_${os.getpid()}')
app_dir := os.join_path(tmp_dir, 'app')
regex_dir := os.join_path(tmp_dir, 'regex')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(app_dir) or { panic(err) }
os.mkdir_all(regex_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
entry_file := os.join_path(app_dir, 'main.v')
local_regex_file := os.join_path(regex_dir, 'local_regex.v')
os.write_file(os.join_path(tmp_dir, 'v.mod'), "Module { name: 'local_shadow' }\n") or {
panic(err)
}
os.write_file(entry_file, 'module main\nimport regex\nfn main() {}\n') or { panic(err) }
os.write_file(local_regex_file, 'module regex\npub struct RegexMatcher {}\n') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.skip_builtin = true
mut b := new_builder(&prefs)
files := parse_files_for_parse_test(mut b, [entry_file])
assert files.any(it.name == local_regex_file)
}
fn test_active_file_imports_follow_comptime_else_branch() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.ComptimeExpr{
expr: ast.IfExpr{
cond: ast.Ident{
name: 'linux'
}
stmts: [
ast.Stmt(ast.ImportStmt{
name: 'net.openssl'
alias: 'openssl'
}),
]
else_expr: ast.IfExpr{
cond: ast.empty_expr
stmts: [
ast.Stmt(ast.ImportStmt{
name: 'net.mbedtls'
alias: 'mbedtls'
}),
]
}
}
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.len == 1
assert imports[0].name == 'net.mbedtls'
}
fn test_active_file_imports_skips_sync_for_channel_in_inactive_comptime_branch() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.ComptimeExpr{
expr: ast.IfExpr{
cond: ast.Ident{
name: 'windows'
}
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})
}),
]
}
}
}),
]
}
linux_imports := active_file_imports(file, [], 'linux').map(it.name)
windows_imports := active_file_imports(file, [], 'windows').map(it.name)
assert 'sync' !in linux_imports
assert 'sync' in windows_imports
}
fn test_active_file_imports_skips_sync_for_channel_in_sync_module() {
file := ast.File{
mod: 'sync'
name: 'cond.v'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'sync'
}),
ast.Stmt(ast.StructDecl{
name: 'Cond'
fields: [
ast.FieldDecl{
name: 'waiters'
typ: ast.Type(ast.ArrayType{
elem_type: ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'bool'
})
}))
})
},
]
}),
]
}
imports := active_file_imports(file, [], 'mac').map(it.name)
mut flat_builder := ast.new_flat_builder()
flat_builder.append_file(file)
flat_imports := active_file_imports_from_flat(&flat_builder.flat, flat_builder.flat.files[0],
[], [], 'mac').map(it.name)
assert 'sync' !in imports
assert 'sync' !in flat_imports
}
fn test_active_file_imports_adds_sync_for_channel_in_if_condition() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.IfExpr{
cond: ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'takes_chan'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
}
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_for_condition() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ForStmt{
cond: ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'takes_chan'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_assert_expr() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.AssertStmt{
expr: ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'takes_chan'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_array_literal() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.ArrayInitExpr{
exprs: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_match_condition() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.MatchExpr{
expr: ast.Expr(ast.Ident{
name: 'value'
})
branches: [
ast.MatchBranch{
cond: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
},
]
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_nested_channel_in_infix_expr() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.IfExpr{
cond: ast.InfixExpr{
op: .and
lhs: ast.Expr(ast.Ident{
name: 'ok'
})
rhs: ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'takes_chan'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
})
}
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_call_callee_generic_args() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.CallExpr{
lhs: ast.Expr(ast.GenericArgs{
lhs: ast.Expr(ast.Ident{
name: 'make_value'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
})
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_modifier_expr_operand() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'use'
})
args: [
ast.Expr(ast.ModifierExpr{
kind: .key_mut
expr: ast.IndexExpr{
lhs: ast.Expr(ast.Ident{
name: 'arr'
})
expr: ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'idx'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
})
}
}),
]
}
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_assign_lhs() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.AssignStmt{
lhs: [
ast.Expr(ast.IndexExpr{
lhs: ast.Expr(ast.Ident{
name: 'arr'
})
expr: ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'idx'
})
args: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
})
}),
]
rhs: [
ast.Expr(ast.BasicLiteral{
kind: .number
value: '0'
}),
]
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_adds_sync_for_channel_in_labelled_loop() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.LabelStmt{
name: 'outer'
stmt: ast.Stmt(ast.ForStmt{
stmts: [
ast.Stmt(ast.AssignStmt{
lhs: [
ast.Expr(ast.Ident{
name: '_'
}),
]
rhs: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
}),
]
})
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert imports.any(it.name == 'sync')
}
fn test_active_file_imports_ignores_channel_in_inactive_attributed_fn_body() {
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.FnDecl{
name: 'debug'
typ: ast.FnType{}
attributes: [
ast.Attribute{
name: 'if'
comptime_cond: ast.Expr(ast.Ident{
name: 'windows'
})
},
]
stmts: [
ast.Stmt(ast.AssignStmt{
lhs: [
ast.Expr(ast.Ident{
name: '_'
}),
]
rhs: [
ast.Expr(ast.Type(ast.ChannelType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})),
]
}),
]
}),
]
}
imports := active_file_imports(file, [], 'mac')
assert !imports.any(it.name == 'sync')
}
fn test_active_file_imports_skip_pkgconfig_branch_when_disabled() {
if !pref.comptime_pkgconfig_value('sqlite3') {
return
}
file := ast.File{
mod: 'main'
name: 'main.v'
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.ComptimeExpr{
expr: ast.IfExpr{
cond: ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'pkgconfig'
})
args: [
ast.Expr(ast.StringLiteral{
kind: .v
value: "'sqlite3'"
}),
]
}
stmts: [
ast.Stmt(ast.ImportStmt{
name: 'db.sqlite'
}),
]
}
}
}),
]
}
native_imports := active_file_imports_with_options(file, [], [], 'mac', true).map(it.name)
cross_imports := active_file_imports_with_options(file, [], [], 'mac', false).map(it.name)
assert 'db.sqlite' in native_imports
assert 'db.sqlite' !in cross_imports
}
@@ -0,0 +1,204 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import runtime
import v2.ast
import v2.ssa
// FnDeclRef references a function declaration within the files array.
struct FnDeclRef {
file_idx int
stmt_idx int
mod_name string
selective_import_fn_names map[string]string
selective_import_fn_candidates map[string][]string
module_import_aliases map[string]string
}
$if !windows {
struct SSABuildChunkArgs {
worker voidptr // &ssa.Builder (pre-created worker builder)
files voidptr // &[]ast.File
fn_refs voidptr // &[]FnDeclRef
start_idx int
end_idx int
}
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
fn C.pthread_attr_init(attr voidptr) int
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
fn C.pthread_attr_destroy(attr voidptr) int
fn ssa_build_chunk_thread(arg voidptr) voidptr {
a := unsafe { &SSABuildChunkArgs(arg) }
mut worker_b := unsafe { &ssa.Builder(a.worker) }
files := unsafe { &[]ast.File(a.files) }
fn_refs := unsafe { &[]FnDeclRef(a.fn_refs) }
// Build assigned functions.
// Avoid chained array access like files[i].stmts[j] — in ARM64-compiled
// binaries, chained indexing returns copies with potentially corrupted fields.
// Instead, copy to a local first, then access fields.
for fi := a.start_idx; fi < a.end_idx; fi++ {
ref := unsafe { fn_refs[fi] }
worker_b.cur_module = ref.mod_name
worker_b.set_selective_import_fn_names(ref.selective_import_fn_names)
worker_b.set_selective_import_fn_candidates(ref.selective_import_fn_candidates)
worker_b.set_module_import_aliases(ref.module_import_aliases)
file := unsafe { (*files)[ref.file_idx] }
stmt := file.stmts[ref.stmt_idx]
decl := stmt as ast.FnDecl
worker_b.build_fn(decl)
}
return unsafe { nil }
}
}
fn (mut b Builder) ssa_build_parallel(mut ssa_builder ssa.Builder, files []ast.File) {
n_jobs := runtime.nr_jobs()
mut mod := ssa_builder.mod
// Collect all function declarations that need building.
// Avoid chained array access like files[fi].stmts[si] — copy to locals first.
has_markused := ssa_builder.used_fn_keys.len > 0
mut fn_refs := []FnDeclRef{cap: 4096}
for fi in 0 .. files.len {
file := files[fi]
mod_name := ssa.file_module_name(file)
selective_import_fn_names := ssa.selective_import_fn_names_from_imports(file.imports)
selective_import_fn_candidates :=
ssa.selective_import_fn_candidates_from_imports(file.imports)
module_import_aliases := ssa.module_import_aliases_from_imports(file.imports)
nstmts := file.stmts.len
for si in 0 .. nstmts {
stmt := file.stmts[si]
if stmt is ast.FnDecl {
decl := stmt as ast.FnDecl
if decl.language == .c && decl.stmts.len == 0 {
continue
}
if decl.typ.generic_params.len > 0 {
continue
}
// Dead code elimination: skip unreachable functions
if has_markused {
ssa_builder.cur_module = mod_name
if !ssa_builder.should_build_fn(file.name, decl) {
continue
}
}
fn_refs << FnDeclRef{
file_idx: fi
stmt_idx: si
mod_name: mod_name
selective_import_fn_names: selective_import_fn_names.clone()
selective_import_fn_candidates: selective_import_fn_candidates.clone()
module_import_aliases: module_import_aliases.clone()
}
}
}
}
n_fns := fn_refs.len
$if windows {
ssa_builder.build_all_fn_bodies(files)
ssa_builder.generate_referenced_synthetic_runtime_stubs()
return
} $else {
if n_fns <= 1 || n_jobs <= 1 {
// Fallback to sequential
ssa_builder.build_all_fn_bodies(files)
ssa_builder.generate_referenced_synthetic_runtime_stubs()
return
}
// Pre-create all worker modules and builders on the main thread
// to avoid COW races on shared data structures.
chunk_size := (n_fns + n_jobs - 1) / n_jobs
mut actual_chunks := 0
mut i := 0
for i < n_fns {
actual_chunks++
i += chunk_size
}
// Record seed lengths for merge — workers' new data starts beyond these.
seed_values := mod.values.len
seed_instrs := mod.instrs.len
seed_blocks := mod.blocks.len
seed_types := mod.type_store.types.len
seed_funcs := mod.funcs.len
mut workers := []voidptr{cap: actual_chunks}
for ci := 0; ci < actual_chunks; ci++ {
mut worker_mod := mod.new_worker_module()
mut worker_b := ssa_builder.new_worker_clone(worker_mod, ci)
workers << voidptr(worker_b)
}
// Spawn worker threads
mut thread_ids := []C.pthread_t{len: actual_chunks}
mut args := []SSABuildChunkArgs{cap: actual_chunks}
attr_buf := [64]u8{}
attr := unsafe { voidptr(&attr_buf[0]) }
C.pthread_attr_init(attr)
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
mut chunk_idx := 0
i = 0
for i < n_fns {
end := if i + chunk_size < n_fns { i + chunk_size } else { n_fns }
args << SSABuildChunkArgs{
worker: workers[chunk_idx]
files: unsafe { voidptr(&files) }
fn_refs: unsafe { voidptr(&fn_refs) }
start_idx: i
end_idx: end
}
C.pthread_create(unsafe { &thread_ids[chunk_idx] }, attr, ssa_build_chunk_thread,
unsafe { voidptr(&args[chunk_idx]) })
i = end
chunk_idx++
}
C.pthread_attr_destroy(attr)
// Wait for all workers
for ci := 0; ci < chunk_idx; ci++ {
C.pthread_join(thread_ids[ci], unsafe { nil })
}
// Merge worker results in order
for ci := 0; ci < chunk_idx; ci++ {
w := unsafe { &ssa.Builder(workers[ci]) }
w_mod := w.mod
// Collect func_data from worker's modified funcs[].
// Only include functions that were actually built by this worker,
// not pre-seeded synthetic functions (array__eq, wyhash*, etc.)
// whose blocks already exist in the main module from Phase 3.5.
mut func_data := []ssa.FuncSSAData{cap: 512}
for fi2 := 0; fi2 < w_mod.funcs.len; fi2++ {
wf := w_mod.funcs[fi2]
if wf.blocks.len == 0 {
continue
}
// Seeded funcs (fi2 < seed_funcs) that already had blocks before
// workers started are not new work — skip them.
if fi2 < seed_funcs && wf.blocks[0] < seed_blocks {
continue
}
func_data << ssa.FuncSSAData{
func_idx: fi2
blocks: wf.blocks
params: wf.params
}
}
mod.merge_worker_module(w_mod, func_data, seed_values, seed_instrs, seed_blocks,
seed_types, seed_funcs)
}
ssa_builder.generate_referenced_synthetic_runtime_stubs()
}
}
@@ -0,0 +1,153 @@
// vtest build: !windows
module builder
import os
import runtime
import v2.parser
import v2.pref
import v2.ssa
import v2.token
import v2.transformer
import v2.types
fn parallel_ssa_alias_tmp_dir(label string) string {
return os.join_path(os.temp_dir(), 'v2_parallel_ssa_alias_${label}_${os.getpid()}')
}
struct ParallelSSAAliasTestSource {
rel_path string
code string
}
fn parallel_ssa_module_for_test_sources(label string, sources []ParallelSSAAliasTestSource) &ssa.Module {
tmp_dir := parallel_ssa_alias_tmp_dir(label)
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') }
defer {
os.rmdir_all(tmp_dir) or {}
}
mut paths := []string{}
for source in sources {
path := os.join_path(tmp_dir, source.rel_path)
os.mkdir_all(os.dir(path)) or { panic('cannot create ${os.dir(path)}') }
os.write_file(path, source.code) or { panic('cannot write ${path}') }
paths << path
}
prefs := &pref.Preferences{
backend: .x64
arch: .x64
}
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(prefs)
files := par.parse_files(paths, mut file_set)
env := types.Environment.new()
mut checker := types.Checker.new(prefs, file_set, env)
checker.check_files(files)
mut trans := transformer.Transformer.new_with_pref(env, prefs)
trans.set_file_set(file_set)
transformed_files := trans.transform_files(files)
mut mod := ssa.Module.new('parallel_alias_test')
mut ssa_builder := ssa.Builder.new_with_env(mod, env)
ssa_builder.skip_fn_bodies = true
ssa_builder.build_all(transformed_files)
ssa_builder.skip_fn_bodies = false
old_vjobs := os.getenv_opt('VJOBS')
os.setenv('VJOBS', '2', true)
defer {
if old := old_vjobs {
os.setenv('VJOBS', old, true)
} else {
os.unsetenv('VJOBS')
}
}
assert runtime.nr_jobs() == 2
mut outer_builder := new_builder(prefs)
outer_builder.ssa_build_parallel(mut ssa_builder, transformed_files)
return mod
}
fn parallel_ssa_func_names(m &ssa.Module) []string {
mut names := []string{}
for func in m.funcs {
names << func.name
}
return names
}
fn test_parallel_ssa_worker_restores_module_import_aliases() {
m := parallel_ssa_module_for_test_sources('module_alias', [
ParallelSSAAliasTestSource{
rel_path: 'left/left.v'
code: 'module left
pub struct Box {
pub:
value int
}
'
},
ParallelSSAAliasTestSource{
rel_path: 'right/right.v'
code: 'module right
pub struct Box {
pub:
value int
}
'
},
ParallelSSAAliasTestSource{
rel_path: 'a.v'
code: 'module main
import left as dep
fn bump_left() int {
box := dep.Box{
value: 1
}
return keep[dep.Box](box).value
}
'
},
ParallelSSAAliasTestSource{
rel_path: 'b.v'
code: 'module main
import right as dep
fn bump_right() int {
box := dep.Box{
value: 2
}
return keep[dep.Box](box).value
}
'
},
ParallelSSAAliasTestSource{
rel_path: 'main.v'
code: 'module main
fn keep[T](value T) T {
return value
}
fn filler0() int { return 0 }
fn filler1() int { return 1 }
fn filler2() int { return 2 }
fn filler3() int { return 3 }
fn main() {
_ = bump_left()
_ = bump_right()
}
'
},
])
func_names := parallel_ssa_func_names(m)
assert 'keep_T_left_Box' in func_names, func_names.str()
assert 'keep_T_right_Box' in func_names, func_names.str()
assert 'keep_T_dep_Box' !in func_names, func_names.str()
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,178 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import v2.ast
import v2.transformer
import runtime
$if !windows {
fn C.pthread_create(thread &C.pthread_t, attr voidptr, start_routine fn (voidptr) voidptr, arg voidptr) int
fn C.pthread_join(thread C.pthread_t, retval voidptr) int
fn C.pthread_attr_init(attr voidptr) int
fn C.pthread_attr_setstacksize(attr voidptr, stacksize usize) int
fn C.pthread_attr_destroy(attr voidptr) int
// TransformChunkFlatArgs carries one worker's slice of the parallel flat
// transform: the worker emits its file range cursor-native into its own
// FlatBuilder (no legacy ast.File rehydrate) and hands back the resulting
// FlatAst, which the main thread merges via `FlatBuilder.append_flat`.
struct TransformChunkFlatArgs {
t voidptr // &transformer.Transformer
flat &ast.FlatAst = unsafe { nil }
flat_extra_stmts [][]ast.Stmt
flat_start int
flat_end int
result_ptr voidptr // &ast.FlatAst
worker_ptr voidptr
worker_idx int
}
fn transform_chunk_flat_thread(arg voidptr) voidptr {
a := unsafe { &TransformChunkFlatArgs(arg) }
t := unsafe { &transformer.Transformer(a.t) }
mut w := t.new_worker_clone(a.worker_idx)
// Cursor-native: transform one file at a time straight into this
// worker's FlatBuilder. Peak per worker is the cumulative flat for its
// chunk (no legacy ast.File ever materialises).
mut wb := ast.new_flat_builder()
for fi := a.flat_start; fi < a.flat_end; fi++ {
extra := if fi >= 0 && fi < a.flat_extra_stmts.len {
a.flat_extra_stmts[fi]
} else {
[]ast.Stmt{}
}
w.transform_file_index_with_extra_to_flat(a.flat, fi, extra, mut wb)
}
unsafe {
*(&ast.FlatAst(a.result_ptr)) = wb.flat
*(&voidptr(a.worker_ptr)) = voidptr(w)
}
return unsafe { nil }
}
}
fn flat_extra_stmts_by_file(extra_stmts map[int][]ast.Stmt, n_files int) [][]ast.Stmt {
mut out := [][]ast.Stmt{cap: n_files}
for _ in 0 .. n_files {
out << []ast.Stmt{}
}
for fi, stmts in extra_stmts {
if fi >= 0 && fi < n_files {
out[fi] = stmts
}
}
return out
}
// transform_files_parallel_flat_direct is the flat-native parallel transform
// used by every backend. It mirrors the sequential
// `transform_flat_to_flat_direct` (pre_pass -> prepare -> per-file cursor
// transform -> post_pass tail) but fans the per-file loop across worker threads:
// each worker emits its contiguous file range cursor-native into its OWN
// FlatBuilder, then the main thread concatenates them in file order via
// `FlatBuilder.append_flat`. No legacy ast.File is materialised; backends that
// still consume []ast.File (.v/eval) rehydrate from the transformed flat at
// the codegen boundary in builder.v.
//
// Per-worker synth-position disjointness is handled by `new_worker_clone`
// (it offsets synth_pos_counter by `-worker_idx * 100_000`); worker state is
// merged back via `merge_worker` after the join, exactly as the legacy path.
fn (mut b Builder) transform_files_parallel_flat_direct(mut trans transformer.Transformer) ast.FlatAst {
// Pre-pass + generic preparation: sequential, once (same as the legacy path).
trans.pre_pass_from_flat(&b.flat)
mut flat_extra_stmts := [][]ast.Stmt{}
if trans.needs_full_files_for_transform() {
extra_stmts := trans.prepare_flat_for_transform(&b.flat)
flat_extra_stmts = flat_extra_stmts_by_file(extra_stmts, b.flat.files.len)
}
n_jobs := runtime.nr_jobs()
n_files := b.flat.files.len
mut out := ast.new_flat_builder()
$if windows {
for fi in 0 .. n_files {
extra := if fi < flat_extra_stmts.len { flat_extra_stmts[fi] } else { []ast.Stmt{} }
trans.transform_file_index_with_extra_to_flat(&b.flat, fi, extra, mut out)
}
} $else {
if n_files <= 1 || n_jobs <= 1 {
for fi in 0 .. n_files {
extra := if fi < flat_extra_stmts.len { flat_extra_stmts[fi] } else { []ast.Stmt{} }
trans.transform_file_index_with_extra_to_flat(&b.flat, fi, extra, mut out)
}
} else {
// Contiguous [start,end) file ranges, one per worker. Merging the
// worker outputs in spawn order preserves original file order.
mut bucket_indices := [][]int{len: n_jobs}
chunk_size := (n_files + n_jobs - 1) / n_jobs
mut i := 0
mut bw := 0
for i < n_files {
end := if i + chunk_size < n_files { i + chunk_size } else { n_files }
for j in i .. end {
bucket_indices[bw] << j
}
i = end
bw++
}
mut chunk_flats := []ast.FlatAst{len: n_jobs}
mut worker_ptrs := []voidptr{len: n_jobs, init: unsafe { nil }}
mut thread_ids := []C.pthread_t{len: n_jobs}
mut args := []TransformChunkFlatArgs{cap: n_jobs}
// 64 MB worker stacks: ARM64-compiled transform recursion is stack-heavy.
attr_buf := [64]u8{}
attr := unsafe { voidptr(&attr_buf[0]) }
C.pthread_attr_init(attr)
C.pthread_attr_setstacksize(attr, 64 * 1024 * 1024)
mut chunk_idx := 0
for w in 0 .. n_jobs {
idxs := bucket_indices[w]
if idxs.len == 0 {
continue
}
args << TransformChunkFlatArgs{
t: unsafe { voidptr(trans) }
flat: unsafe { &b.flat }
flat_extra_stmts: flat_extra_stmts
flat_start: idxs[0]
flat_end: idxs[idxs.len - 1] + 1
result_ptr: unsafe { voidptr(&chunk_flats[chunk_idx]) }
worker_ptr: unsafe { voidptr(&worker_ptrs[chunk_idx]) }
worker_idx: chunk_idx
}
C.pthread_create(unsafe { &thread_ids[chunk_idx] }, attr,
transform_chunk_flat_thread, unsafe { voidptr(&args[chunk_idx]) })
chunk_idx++
}
C.pthread_attr_destroy(attr)
for ci := 0; ci < chunk_idx; ci++ {
C.pthread_join(thread_ids[ci], unsafe { nil })
}
// Concatenate worker flats in spawn (= file) order, merging worker state.
mut ci := 0
for w in 0 .. n_jobs {
if bucket_indices[w].len == 0 {
continue
}
out.append_flat(chunk_flats[ci])
worker := unsafe { &transformer.Transformer(worker_ptrs[ci]) }
trans.merge_worker(worker)
ci++
}
// Move synth_pos_counter past all worker ranges for the post_pass.
trans.set_synth_pos_counter(-(chunk_idx * 100_000) - 1)
}
}
// Post-pass tail: identical to transform_flat_to_flat_direct's tail.
generated_parts := trans.generated_fns_parts_from_flat(&out.flat)
trans.post_pass_to_flat(mut out, generated_parts)
trans.apply_post_pass_tail_from_flat(&out.flat)
return out.flat
}
@@ -0,0 +1,10 @@
module builder
import v2.types
fn (mut b Builder) type_check_files() &types.Environment {
env := types.Environment.new()
mut checker := types.Checker.new(b.pref, b.file_set, env)
checker.check_flat(&b.flat)
return env
}
@@ -0,0 +1,12 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import v2.types
fn (mut b Builder) type_check_files_parallel() &types.Environment {
// FlatAst-driven type check is sequential today (check_flat). A parallel
// flat pipeline is future work.
return b.type_check_files()
}
@@ -0,0 +1,15 @@
module builder
import v2.types
// Default build (no `-d parallel`): type-checking is sequential. The real
// parallel implementation lives in type_check_d_parallel.v and is only
// included when `-d parallel` is set.
fn (mut b Builder) type_check_files_parallel() &types.Environment {
// Type checking is sequential: the bulk of the time (~90%) is in check_file
// which has complex interdependencies (pending fn bodies, struct fields, etc.)
// that prevent safe parallelization. The pre-registration phases (scopes, types)
// are only ~6% of total time and share mutable Scope objects, making them
// unsafe for concurrent access without major restructuring.
return b.type_check_files()
}
@@ -0,0 +1,59 @@
// vtest build: macos
module builder
import os
fn run_parallel_type_check_fixture(label string, code string) (int, string) {
tmp_dir := os.join_path(os.temp_dir(), 'v2_parallel_type_check_${label}_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') }
defer {
os.rmdir_all(tmp_dir) or {}
}
source_path := os.join_path(tmp_dir, 'main.v')
helper_path := os.join_path(tmp_dir, 'helper.v')
os.write_file(source_path, code) or { panic('cannot write fixture source') }
os.write_file(helper_path, 'import v2.builder
import v2.pref
fn main() {
prefs := &pref.Preferences{
backend: .v
no_cache: true
}
mut b := builder.new_builder(prefs)
b.build(["${source_path}"])
}
') or {
panic('cannot write helper source')
}
cmd := '${os.quoted_path(@VEXE)} -d parallel -gc none run ${os.quoted_path(helper_path)} 2>&1'
res := os.execute(cmd)
return res.exit_code, res.output
}
fn test_parallel_type_check_runs_struct_field_default_checks() {
code, output := run_parallel_type_check_fixture('struct_default', 'module main
struct Config {
value int = missing_default()
}
fn main() {}
')
assert code != 0, output
assert output.contains('missing_default'), output
}
fn test_parallel_type_check_runs_enum_value_checks() {
code, output := run_parallel_type_check_fixture('enum_value', 'module main
enum Mode {
ready = missing_enum_value()
}
fn main() {}
')
assert code != 0, output
assert output.contains('missing_enum_value'), output
}
+146
View File
@@ -0,0 +1,146 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import os
import v2.pref
fn list_dir_entries(path string) []string {
mut entries := os.ls(path) or { []string{} }
entries.sort()
return entries
}
pub fn get_v_files_from_dir(dir string, user_defines []string, target_os string) []string {
if dir == '' {
return []string{}
}
mod_files := list_dir_entries(dir)
mut v_files := []string{}
mut defaults := []string{}
for file in mod_files {
if file == '' {
continue
}
// Include .v files (including .c.v), exclude .js.v and test files
if !file.ends_with('.v') || file.ends_with('.js.v') || file.contains('_test.') {
continue
}
// skip platform-specific files
if file.contains('.arm64.') || file.contains('.arm32.') || file.contains('.amd64.') {
continue
}
// Skip files specialized for a different target OS before parsing/type checking.
if pref.file_has_incompatible_os_suffix(file, target_os) {
continue
}
if file.ends_with('prealloc.c.v') && 'prealloc' !in user_defines {
continue
}
// Conditional compilation files: _d_<feature> included when -d <feature> is set,
// _notd_<feature> included when -d <feature> is NOT set.
if file.contains('_notd_') {
feature := extract_define_feature(file, '_notd_')
if feature.len > 0 && feature in user_defines {
continue
}
} else if file.contains('_d_') {
feature := extract_define_feature(file, '_d_')
if feature.len == 0 || feature !in user_defines {
continue
}
}
path := os.join_path(dir, file)
if path == '' {
continue
}
// Collect _default.c.v files separately; they are only included when
// no platform-specific variant exists (e.g. _darwin.c.v, _linux.c.v).
if file.contains('default.c.v') {
defaults << path
} else {
v_files << path
}
}
// Add _default.c.v files only when no platform-specific variant was selected.
for dfile in defaults {
no_postfix := fname_without_platform_postfix(dfile)
mut has_specialized := false
for vf in v_files {
if fname_without_platform_postfix(vf) == no_postfix {
has_specialized = true
break
}
}
if !has_specialized {
v_files << dfile
}
}
return v_files
}
// fname_without_platform_postfix strips the platform-specific suffix from a file path,
// so that e.g. "free_memory_impl_darwin.c.v" and "free_memory_impl_default.c.v" both
// map to the same key, allowing detection of specialized vs default variants.
fn fname_without_platform_postfix(file string) string {
return file.replace_each([
'default.c.v',
'_',
'windows.c.v',
'_',
'linux.c.v',
'_',
'darwin.c.v',
'_',
'macos.c.v',
'_',
'android.c.v',
'_',
'android_outside_termux.c.v',
'_',
'termux.c.v',
'_',
'ios.c.v',
'_',
'freebsd.c.v',
'_',
'openbsd.c.v',
'_',
'netbsd.c.v',
'_',
'dragonfly.c.v',
'_',
'bsd.c.v',
'_',
'solaris.c.v',
'_',
'qnx.c.v',
'_',
'serenity.c.v',
'_',
'plan9.c.v',
'_',
'vinix.c.v',
'_',
'nix.c.v',
'_',
])
}
// extract_define_feature extracts the feature name from a _d_ or _notd_ filename.
// e.g. "parse_d_parallel.v" with marker "_d_" returns "parallel"
// e.g. "array_notd_gcboehm_opt.v" with marker "_notd_" returns "gcboehm_opt"
fn extract_define_feature(file string, marker string) string {
idx := file.index(marker) or { return '' }
rest := file[idx + marker.len..]
// Strip .v or .c.v suffix
feature := if rest.ends_with('.c.v') {
rest[..rest.len - 4]
} else if rest.ends_with('.v') {
rest[..rest.len - 2]
} else {
rest
}
return feature
}
@@ -0,0 +1,40 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
#include <dirent.h>
fn C.opendir(path &char) &C.DIR
fn C.readdir(dir &C.DIR) &C.dirent
fn C.closedir(dir &C.DIR) int
// list_dir_entries_nix returns one-level directory entries without using
// result-or blocks, so SSA/C bootstrap does not depend on option guard lowering.
@[manualfree]
fn list_dir_entries_nix(path string) []string {
if path.len == 0 {
return []string{}
}
mut res := []string{cap: 50}
dir := unsafe { C.opendir(path.str) }
if isnil(dir) {
return res
}
for {
ent := C.readdir(dir)
if isnil(ent) {
break
}
unsafe {
bptr := &u8(&ent.d_name[0])
if bptr[0] == 0 || (bptr[0] == `.` && bptr[1] == 0)
|| (bptr[0] == `.` && bptr[1] == `.` && bptr[2] == 0) {
continue
}
res << tos_clone(bptr)
}
}
C.closedir(dir)
return res
}
@@ -0,0 +1,249 @@
// vtest build: macos
module builder
import os
import v2.ast
import v2.pref
fn test_get_v_files_from_dir_uses_target_os() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_target_os_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(tmp_dir, 'common.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'platform_linux.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'platform_macos.v'), 'module test') or { panic(err) }
macos_files := get_v_files_from_dir(tmp_dir, []string{}, 'mac')
macos_names := macos_files.map(os.file_name(it))
assert 'common.v' in macos_names
assert 'platform_macos.v' in macos_names
assert 'platform_linux.v' !in macos_names
linux_files := get_v_files_from_dir(tmp_dir, []string{}, 'linux')
linux_names := linux_files.map(os.file_name(it))
assert 'common.v' in linux_names
assert 'platform_linux.v' in linux_names
assert 'platform_macos.v' !in linux_names
}
fn test_get_v_files_from_dir_uses_d_and_notd_suffixes() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_define_suffix_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(tmp_dir, 'common.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'ssl_d_use_openssl.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'ssl_notd_use_openssl.v'), 'module test') or { panic(err) }
default_names := get_v_files_from_dir(tmp_dir, []string{}, 'mac').map(os.file_name(it))
assert 'common.v' in default_names
assert 'ssl_d_use_openssl.v' !in default_names
assert 'ssl_notd_use_openssl.v' in default_names
openssl_names := get_v_files_from_dir(tmp_dir, ['use_openssl'], 'mac').map(os.file_name(it))
assert 'common.v' in openssl_names
assert 'ssl_d_use_openssl.v' in openssl_names
assert 'ssl_notd_use_openssl.v' !in openssl_names
}
fn test_get_v_files_from_dir_skips_prealloc_without_prealloc_flag() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_prealloc_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(tmp_dir, 'common.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'prealloc.c.v'), 'module test') or { panic(err) }
default_names := get_v_files_from_dir(tmp_dir, []string{}, 'mac').map(os.file_name(it))
assert 'common.v' in default_names
assert 'prealloc.c.v' !in default_names
prealloc_names := get_v_files_from_dir(tmp_dir, ['prealloc'], 'mac').map(os.file_name(it))
assert 'common.v' in prealloc_names
assert 'prealloc.c.v' in prealloc_names
}
fn test_get_v_files_from_dir_returns_lexically_sorted_files() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_sorted_v_files_${os.getpid()}')
os.mkdir_all(tmp_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
os.write_file(os.join_path(tmp_dir, 'foo.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'foo.c.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'bar.v'), 'module test') or { panic(err) }
os.write_file(os.join_path(tmp_dir, 'ignore.txt'), 'not v') or { panic(err) }
names := get_v_files_from_dir(tmp_dir, []string{}, 'linux').map(os.file_name(it))
assert names == ['bar.v', 'foo.c.v', 'foo.v']
assert names.index('foo.c.v') or { -1 } < names.index('foo.v') or { -1 }
}
fn test_flag_helpers_use_target_os() {
assert flag_os_matches('macos', 'mac')
assert flag_os_matches('bsd', 'mac')
assert flag_os_matches('bsd', 'freebsd')
assert !flag_os_matches('linux', 'mac')
assert comptime_cond_matches('macos', 'mac')
assert comptime_cond_matches('linux || bsd', 'mac')
assert !comptime_cond_matches('linux', 'mac')
assert comptime_cond_matches('macos && !linux', 'mac')
macos_flag := parse_flag_directive_line('#flag macos -framework Cocoa', '/tmp/source.v', 'mac') or {
''
}
linux_flag := parse_flag_directive_line('#flag linux -lm', '/tmp/source.v', 'mac') or { '' }
assert macos_flag == '-framework Cocoa'
assert linux_flag == ''
}
fn test_flag_helpers_expand_when_first_existing() {
tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_when_first_${os.getpid()}')
existing_dir := os.join_path(tmp_dir, 'openssl_include')
os.mkdir_all(existing_dir) or { panic(err) }
defer {
os.rmdir_all(tmp_dir) or {}
}
missing_dir := os.join_path(tmp_dir, 'missing')
flag := parse_flag_directive_line("#flag darwin -I\$when_first_existing('${missing_dir}','${existing_dir}')", os.join_path(tmp_dir,
'source.v'), 'mac') or { '' }
assert flag == '-I${existing_dir}'
none_flag := parse_flag_directive_line("#flag darwin -I\$when_first_existing('${missing_dir}')", os.join_path(tmp_dir,
'source.v'), 'mac') or { '' }
assert none_flag == ''
}
fn test_split_compile_and_link_flags_moves_c_sources_to_link_step() {
compile_flags, link_flags :=
split_compile_and_link_flags('-I /tmp/include -DMYFLAG thirdparty/sqlite/sqlite3.c -L/tmp/lib -lsqlite3 foo.o')
assert compile_flags == '-I /tmp/include -DMYFLAG'
assert link_flags == 'thirdparty/sqlite/sqlite3.c -L/tmp/lib -lsqlite3 foo.o'
}
fn test_sanitize_header_source_preserves_public_module_storage() {
source := 'module state
pub __global (
mut count int
pub mut shared int
)
pub __global mut total int
'
sanitized := sanitize_header_source(source, map[string]string{})
assert sanitized.contains('pub __global (')
assert sanitized.contains('mut count int')
assert sanitized.contains('pub mut shared int')
assert sanitized.contains('pub __global mut total int')
}
fn test_build_module_header_ast_preserves_module_storage_flags() {
mut prefs := pref.new_preferences()
mut b := new_builder(&prefs)
source_file := ast.File{
mod: 'state'
name: 'state.v'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'state'
}),
ast.Stmt(ast.GlobalDecl{
is_public: true
fields: [
ast.FieldDecl{
name: 'private_count'
typ: ast.Ident{
name: 'int'
}
is_mut: true
is_public: false
},
ast.FieldDecl{
name: 'shared_count'
typ: ast.Ident{
name: 'int'
}
is_mut: true
is_public: true
},
]
}),
]
}
header := b.build_module_header_ast([source_file], 'state') or {
panic('missing module storage header')
}
mut found := false
for stmt in header.stmts {
if stmt is ast.GlobalDecl {
found = true
assert stmt.is_public
assert stmt.fields.len == 2
assert stmt.fields[0].name == 'private_count'
assert stmt.fields[0].is_mut
assert !stmt.fields[0].is_public
assert stmt.fields[1].name == 'shared_count'
assert stmt.fields[1].is_mut
assert stmt.fields[1].is_public
}
}
assert found
}
fn test_cflags_need_objc_mode_only_for_objc_inputs() {
assert cflags_need_objc_mode('-framework Cocoa')
assert cflags_need_objc_mode('/tmp/clipboard_darwin.m -framework Foundation')
assert !cflags_need_objc_mode('-I /tmp/include -lssl -lcrypto /tmp/sqlite3.c')
assert !cflags_need_objc_mode('-I /tmp/project.m/include')
}
fn test_file_has_incompatible_os_suffix_windows() {
assert pref.file_has_incompatible_os_suffix('time_solaris.c.v', 'windows')
assert pref.file_has_incompatible_os_suffix('time_freebsd.c.v', 'windows')
assert pref.file_has_incompatible_os_suffix('time_openbsd.c.v', 'windows')
assert pref.file_has_incompatible_os_suffix('time_netbsd.c.v', 'windows')
assert pref.file_has_incompatible_os_suffix('time_dragonfly.c.v', 'windows')
assert pref.file_has_incompatible_os_suffix('time_nix.c.v', 'windows')
assert !pref.file_has_incompatible_os_suffix('time_windows.c.v', 'windows')
}
fn test_file_has_incompatible_os_suffix_non_windows_targets() {
assert !pref.file_has_incompatible_os_suffix('time_solaris.c.v', 'solaris')
assert !pref.file_has_incompatible_os_suffix('time_freebsd.c.v', 'freebsd')
assert !pref.file_has_incompatible_os_suffix('time_bsd.c.v', 'macos')
assert pref.file_has_incompatible_os_suffix('time_bsd.c.v', 'linux')
assert !pref.file_has_incompatible_os_suffix('time_darwin.c.v', 'macos')
assert !pref.file_has_incompatible_os_suffix('time_macos.c.v', 'darwin')
assert !pref.file_has_incompatible_os_suffix('time_android_outside_termux.c.v', 'android')
assert pref.file_has_incompatible_os_suffix('time_windows.c.v', 'linux')
assert pref.file_has_incompatible_os_suffix('time_android_outside_termux.c.v', 'linux')
}
fn test_default_cc_uses_tcc_when_available() {
vroot := os.dir(os.dir(@FILE))
tcc_path := os.join_path(vroot, 'thirdparty', 'tcc', 'tcc.exe')
cc := default_cc(vroot)
if os.exists(tcc_path) {
assert cc.contains('tcc')
} else {
assert cc == 'cc'
}
}
fn test_cc_recompile_flags_from_cmd_keeps_include_paths() {
cmd := 'cc -I /tmp/include -I/tmp/other -Dfoo=1 -x objective-c -std=gnu11 -fwrapv "/tmp/main.c" -x none "/tmp/lib.o" -L/tmp/lib -lm -o "app"'
flags := cc_recompile_flags_from_cmd(cmd)
assert flags.contains('-I /tmp/include')
assert flags.contains('-I/tmp/other')
assert flags.contains('-Dfoo=1')
assert flags.contains('-x objective-c')
assert flags.contains('-std=gnu11')
assert flags.contains('-fwrapv')
assert !flags.contains('/tmp/lib.o')
assert !flags.contains('-L/tmp/lib')
assert !flags.contains('-lm')
assert !flags.contains('-o')
}
@@ -0,0 +1,44 @@
module errors
import os
import v2.token
// loads the source file and generates the error message including the source
// line and column. offending token is highlighted in the snipped of source code.
// since this is used on errors, loading the source file isnt an issue.
// TODO: probably needs a better name
pub fn details(file &token.File, pos token.Position, row_padding int) string {
if file.name == '' {
return 'at ${pos.line}:${pos.column}'
}
src := os.read_file(file.name) or { return 'at ${pos.line}:${pos.column} in `${file.name}`' }
line_start := if pos.line - row_padding - 1 > 0 {
file.line_start(pos.line - row_padding)
} else {
0
}
mut line_end := pos.offset + 1
for i := 0; line_end < src.len; {
if src[line_end] == `\n` {
i++
if i == row_padding + 1 {
break
}
}
line_end++
}
lines_src := src[line_start..line_end].split('\n')
line_no_start, _ := file.find_line_and_column(line_start)
mut lines_src_formatted := []string{}
for i in 0 .. lines_src.len {
line_no := line_no_start + i
line_src := lines_src[i]
line_spaces := line_src.replace('\t', ' ')
lines_src_formatted << '${line_no:5d} | ' + line_spaces
if line_no == pos.line {
space_diff := line_spaces.len - line_src.len
lines_src_formatted << ' ' + ' '.repeat(space_diff + pos.column - 1) + '^'
}
}
return lines_src_formatted.join('\n')
}
@@ -0,0 +1,32 @@
module errors
import v2.token
pub enum Reporter {
scanner
parser
checker
builder
gen
}
pub struct CompilerMessage {
pub:
message string
details string
file_path string
pos token.Pos
reporter Reporter
}
pub struct Error {
CompilerMessage
}
pub struct Warning {
CompilerMessage
}
pub struct Notice {
CompilerMessage
}
+33
View File
@@ -0,0 +1,33 @@
module errors
import v2.token
import term
pub enum Kind {
warning
notice
error
}
pub fn (e Kind) str() string {
return match e {
.warning { 'warning' }
.notice { 'notice' }
.error { 'error' }
}
}
pub fn (e Kind) color(s string) string {
return match e {
.warning { term.yellow(s) }
.notice { term.blue(s) }
.error { term.red(s) }
}
}
pub fn error(msg string, details string, kind Kind, pos token.Position) {
eprintln(pos.str() + ' -> ' + term.bold(kind.color(kind.str())) + ': ' + msg)
if details.len > 0 {
eprintln(details)
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,827 @@
// vtest build: macos
module eval
import os
import v2.ast
import v2.parser
import v2.pref
import v2.token
import time
const eval_backend_timeout_ms = 60_000
const eval_backend_poll_ms = 50
const eval_backend_term_grace_ms = 1_000
fn testsuite_begin() {
skip_test('v2 eval tests are temporarily disabled')
}
fn eval_backend_tmp_dir() string {
return os.join_path(os.temp_dir(), 'v2_eval_integration_${os.getpid()}')
}
fn ensure_eval_backend_runner(vexe string) !string {
tmp_dir := eval_backend_tmp_dir()
os.mkdir_all(tmp_dir)!
v2_exe := os.join_path(tmp_dir, 'v2_eval_runner')
if os.exists(v2_exe) {
return v2_exe
}
vroot := os.dir(vexe)
build_res := os.execute('${os.quoted_path(vexe)} -o ${os.quoted_path(v2_exe)} ${os.quoted_path(os.join_path(vroot,
'cmd', 'v2', 'v2.v'))}')
if build_res.exit_code != 0 {
return error(build_res.output)
}
return v2_exe
}
fn run_eval_backend(code string) !os.Result {
return run_eval_backend_with_args(code, []string{})
}
fn run_eval_backend_with_args(code string, extra_args []string) !os.Result {
vexe := os.getenv('VEXE')
if vexe == '' {
return error('VEXE is not set')
}
tmp_dir := eval_backend_tmp_dir()
os.mkdir_all(tmp_dir)!
tmp_file := os.join_path(tmp_dir, 'sample_${time.now().unix_micro()}.v')
os.write_file(tmp_file, code)!
defer {
os.rm(tmp_file) or {}
}
v2_exe := ensure_eval_backend_runner(vexe)!
mut p := os.new_process(v2_exe)
mut args := ['-backend', 'eval', tmp_file]
if extra_args.len > 0 {
args << '--'
args << extra_args
}
p.set_args(args)
p.set_redirect_stdio()
p.run()
defer {
p.close()
}
mut waited_ms := 0
for p.is_alive() && waited_ms < eval_backend_timeout_ms {
time.sleep(eval_backend_poll_ms * time.millisecond)
waited_ms += eval_backend_poll_ms
}
if p.is_alive() {
p.signal_term()
mut grace_ms := 0
for p.is_alive() && grace_ms < eval_backend_term_grace_ms {
time.sleep(eval_backend_poll_ms * time.millisecond)
grace_ms += eval_backend_poll_ms
}
if p.is_alive() {
p.signal_kill()
return error('eval backend timed out after ${eval_backend_timeout_ms}ms')
}
}
p.wait()
stdout := p.stdout_slurp()
stderr := p.stderr_slurp()
return os.Result{
exit_code: p.code
output: stdout + stderr
}
}
fn test_eval_function_call_and_for_range() {
mut e := create()
e.run_text('
fn sum(n int) int {
mut acc := 0
for i in 0 .. n {
acc += i
}
return acc
}
fn main() {
println(sum(5))
}
') or {
panic(err)
}
assert e.stdout() == '10\n'
}
fn test_eval_if_expr_value() {
mut e := create()
e.run_text('
fn main() {
x := if 3 > 2 {
41
} else {
0
}
println(x + 1)
}
') or {
panic(err)
}
assert e.stdout() == '42\n'
}
fn test_eval_array_append_and_string_interpolation() {
mut e := create()
e.run_text('
fn main() {
mut arr := []int{}
arr << 7
arr << 9
println("\${arr[0]}:\${arr[1]}:\${arr.len}")
}
') or {
panic(err)
}
assert e.stdout() == '7:9:2\n'
}
fn test_eval_transformed_array_append_and_string_interpolation() {
res := run_eval_backend('
fn main() {
mut arr := []int{}
arr << 7
arr << 9
println("\${arr[0]}:\${arr[1]}:\${arr.len}")
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('7:9:2\n')
}
fn test_eval_runtime_args_follow_separator() {
res := run_eval_backend_with_args('
import os
fn main() {
println(os.args.join("|"))
}
', [
'alpha',
'beta',
]) or { panic(err) }
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('|alpha|beta')
}
fn test_eval_transformed_nested_array_clone() {
res := run_eval_backend("
fn main() {
mut a := [[1, 2], [3]]
mut b := a.clone()
b[0] << 9
println('\${a[0].len}:\${b[0].len}')
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('2:3\n')
}
fn test_eval_transformed_array_append_many() {
res := run_eval_backend("
fn main() {
mut items := [1]
items << [2, 3]
println('\${items[0]}:\${items[1]}:\${items[2]}:\${items.len}')
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('1:2:3:3\n')
}
fn test_eval_transformed_types_init_universe() {
res := run_eval_backend("
import v2.types
fn main() {
_ = types.init_universe()
println('ok')
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('ok\n')
}
fn test_eval_transformed_types_lookup_parent_typ() {
res := run_eval_backend("
import v2.types
fn main() {
u := types.init_universe()
if obj := u.lookup_parent('string', 0) {
_ = obj.typ()
println('ok')
} else {
println('missing')
}
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('ok\n')
}
fn test_eval_transformed_map_if_guard_with_nested_sumtype() {
res := run_eval_backend("
import v2.types
fn lookup(m map[string]types.Object, key string) ?types.Object {
if obj := m[key] {
return obj
}
return none
}
fn main() {
mut m := map[string]types.Object{}
m['x'] = types.Object(types.Type(types.string_))
if obj := lookup(m, 'x') {
_ = obj.typ()
println('ok')
} else {
println('missing')
}
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('ok\n')
}
fn test_eval_transformed_map_builtin_fast_paths() {
res := run_eval_backend("
fn main() {
mut m := map[string]int{}
m['a'] = 1
m['b'] += 2
a_val := m['a']
b_val := m['b']
has_a := 'a' in m
println('\${a_val}:\${b_val}:\${m.keys().len}:\${m.values().len}:\${has_a}')
m.delete('a')
mut cloned := m.clone()
mut moved := cloned.move()
cloned.clear()
has_a_after_delete := 'a' in m
has_b_in_moved := 'b' in moved
moved_b := moved['b'] or { 0 }
println('\${has_a_after_delete}:\${has_b_in_moved}:\${cloned.len}:\${moved_b}')
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('1:2:2:2:true\n')
assert res.output.contains('false:true:0:2\n')
}
fn test_eval_transformed_os_execute() {
res := run_eval_backend("
import os
fn main() {
cmd := if os.user_os() == 'windows' { 'cmd /c echo ok' } else { 'printf ok' }
result := os.execute(cmd)
println('\${result.exit_code}:\${result.output}')
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('0:ok')
}
fn test_eval_transformed_smartcast_assignment() {
res := run_eval_backend('
struct Foo {
mut:
n int
}
struct Bar {}
type W = Bar | Foo
fn main() {
mut w := W(Foo{n: 1})
if w is Foo {
w.n = 2
}
if w is Foo {
println(w.n)
}
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('2\n')
}
fn test_eval_transformed_ast_expr_pos() {
res := run_eval_backend("
import v2.ast
fn main() {
expr := ast.Expr(ast.PrefixExpr{})
_ = expr.pos()
println('ok')
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('ok\n')
}
fn test_eval_array_has_named_struct_field() {
e := create()
attrs := ArrayValue{
values: [
Value(StructValue{
type_name: 'Attr'
fields: {
'name': Value('flag')
}
}),
]
}
assert e.array_has(attrs, 'flag')
assert !e.array_has(attrs, 'other')
}
fn test_eval_sumtype_tag_from_type_value_uses_registered_type_kind() {
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
type_files :=
os.walk_ext(os.join_path(@VMODROOT, 'vlib', 'v2', 'types'), '.v').filter(!it.ends_with('_test.v'))
dummy_main := os.join_path(os.temp_dir(), 'v2_eval_dummy_main_${os.getpid()}.v')
os.write_file(dummy_main, 'module main\nfn main() {}\n') or { panic(err) }
defer {
os.rm(dummy_main) or {}
}
mut files_to_parse := type_files.clone()
files_to_parse << dummy_main
files := par.parse_files(files_to_parse, mut file_set)
mut e := create()
e.register_files(files) or { panic(err) }
info := e.sum_type_info('types.Type') or { panic('missing types.Type sumtype info') }
struct_tag := e.lookup_sumtype_variant_tag(info, 'Struct') or { panic('missing Struct tag') }
alias_tag := e.lookup_sumtype_variant_tag(info, 'Alias') or { panic('missing Alias tag') }
struct_value_tag := e.sumtype_tag_from_value(info, TypeValue{
name: 'types.ObjectCommon'
}) or { panic('missing struct kind tag') }
alias_value_tag := e.sumtype_tag_from_value(info, TypeValue{
name: 'voidptr'
}) or { panic('missing alias kind tag') }
assert struct_value_tag == struct_tag
assert alias_value_tag == alias_tag
}
fn test_eval_type_sum_data_is_nil() {
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
type_files :=
os.walk_ext(os.join_path(@VMODROOT, 'vlib', 'v2', 'types'), '.v').filter(!it.ends_with('_test.v'))
dummy_main := os.join_path(os.temp_dir(), 'v2_eval_dummy_main_${os.getpid()}.v')
os.write_file(dummy_main, 'module main\nfn main() {}\n') or { panic(err) }
defer {
os.rm(dummy_main) or {}
}
mut files_to_parse := type_files.clone()
files_to_parse << dummy_main
files := par.parse_files(files_to_parse, mut file_set)
mut e := create()
e.register_files(files) or { panic(err) }
assert e.type_sum_data_is_nil(e.zero_struct_value('types.Type'))
assert !e.type_sum_data_is_nil(TypeValue{
name: 'types.ObjectCommon'
})
}
fn test_eval_zero_value_for_sumtype_data_field() {
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
ast_files :=
os.walk_ext(os.join_path(@VMODROOT, 'vlib', 'v2', 'ast'), '.v').filter(!it.ends_with('_test.v'))
dummy_main := os.join_path(os.temp_dir(), 'v2_eval_dummy_main_${os.getpid()}.v')
os.write_file(dummy_main, 'module main\nfn main() {}\n') or { panic(err) }
defer {
os.rm(dummy_main) or {}
}
mut files_to_parse := ast_files.clone()
files_to_parse << dummy_main
files := par.parse_files(files_to_parse, mut file_set)
mut e := create()
e.register_files(files) or { panic(err) }
value := e.zero_value_for_sumtype_data_field('ast.Expr._data', '_ArrayInitExpr') or {
panic('missing sumtype data field zero value')
}
assert value is StructValue
assert (value as StructValue).type_name == 'ast.ArrayInitExpr'
}
fn test_eval_selector_on_sumtype_data_void_field_returns_zero_value() {
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
ast_files :=
os.walk_ext(os.join_path(@VMODROOT, 'vlib', 'v2', 'ast'), '.v').filter(!it.ends_with('_test.v'))
dummy_main := os.join_path(os.temp_dir(), 'v2_eval_dummy_main_${os.getpid()}.v')
os.write_file(dummy_main, 'module main\nfn main() {}\n') or { panic(err) }
defer {
os.rm(dummy_main) or {}
}
mut files_to_parse := ast_files.clone()
files_to_parse << dummy_main
files := par.parse_files(files_to_parse, mut file_set)
mut e := create()
e.register_files(files) or { panic(err) }
e.open_scope()
defer {
e.close_scope() or { panic(err) }
}
e.declare_var('data', StructValue{
type_name: 'ast.Expr._data'
fields: {
'_ArrayInitExpr': Value(void_value())
}
})
data_ident := ast.Ident{
name: 'data'
}
data_lhs := ast.Expr(data_ident)
data_rhs := ast.Ident{
name: '_ArrayInitExpr'
}
data_selector := ast.SelectorExpr{
lhs: data_lhs
rhs: data_rhs
}
value := e.eval_selector_expr(data_selector) or { panic(err) }
assert value is StructValue
array_init := value as StructValue
assert array_init.type_name == 'ast.ArrayInitExpr'
assert 'exprs' in array_init.fields
assert array_init.fields['exprs'] or { panic('missing exprs field') } is ArrayValue
}
fn test_eval_selector_on_sumtype_wrapper_variant_field_returns_zero_value() {
mut prefs := pref.new_preferences()
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(&prefs)
ast_files :=
os.walk_ext(os.join_path(@VMODROOT, 'vlib', 'v2', 'ast'), '.v').filter(!it.ends_with('_test.v'))
dummy_main := os.join_path(os.temp_dir(), 'v2_eval_dummy_main_${os.getpid()}.v')
os.write_file(dummy_main, 'module main\nfn main() {}\n') or { panic(err) }
defer {
os.rm(dummy_main) or {}
}
mut files_to_parse := ast_files.clone()
files_to_parse << dummy_main
files := par.parse_files(files_to_parse, mut file_set)
mut e := create()
e.register_files(files) or { panic(err) }
info := e.sum_type_info('ast.Expr') or { panic('missing ast.Expr sum type info') }
ident_tag := e.lookup_sumtype_variant_tag(info, 'ast.Ident') or {
panic('missing ast.Ident sumtype tag')
}
expr_value := e.build_sumtype_wrapper('ast.Expr', info, ident_tag, StructValue{
type_name: 'ast.Ident'
fields: {
'name': Value('')
}
})
e.open_scope()
defer {
e.close_scope() or { panic(err) }
}
e.declare_var('expr', expr_value)
expr_ident := ast.Ident{
name: 'expr'
}
expr_lhs := ast.Expr(expr_ident)
expr_rhs := ast.Ident{
name: '_ArrayInitExpr'
}
expr_selector := ast.SelectorExpr{
lhs: expr_lhs
rhs: expr_rhs
}
value := e.eval_selector_expr(expr_selector) or { panic(err) }
assert value is StructValue
assert (value as StructValue).type_name == 'ast.ArrayInitExpr'
}
fn test_eval_lookup_parent_through_parent_scope() {
res := run_eval_backend("
import v2.types
fn main() {
mut parent := types.new_scope(unsafe { nil })
parent.insert('Error', types.Object(types.Type(types.Struct{name: 'builtin__Error'})))
mut child := types.new_scope(parent)
if obj := child.lookup_parent('Error', 0) {
if obj is types.Type {
println(obj.name())
}
}
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('builtin__Error\n')
}
fn test_eval_flag_enum_defaults() {
res := run_eval_backend('
@[flag]
enum Properties {
boolean
float
integer
unsigned
untyped
}
fn main() {
println(int(Properties.integer))
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('4\n')
}
fn test_eval_option_typed_struct_field() {
res := run_eval_backend('
import v2.types
fn main() {
println(types.Type(types.Channel{elem_type: types.int_}).name())
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('chan int\n')
}
fn test_eval_transformed_type_name_for_primitive_variants() {
res := run_eval_backend('
import v2.types
fn main() {
println(types.Type(types.bool_).name())
println(types.Type(types.int_).name())
println(types.Type(types.f64_).name())
println(types.Type(types.voidptr_).name())
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('bool\nint\nf64\nvoidptr\n')
}
fn test_eval_transformed_imported_struct_field_assignment_keeps_declaring_module_type() {
res := run_eval_backend('
import v2.types
fn main() {
mut m := types.Map{
key_type: types.Type(types.int_)
value_type: types.Type(types.int_)
}
m.key_type = types.int_
println(m.key_type.name())
println(m.key_type.base_type().name())
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('int\nint\n')
}
fn test_eval_transformed_typed_array_index_assignment_keeps_sumtype_wrapper() {
res := run_eval_backend('
import v2.types
fn main() {
mut items := []types.Type{len: 1, init: types.Type(types.int_)}
items[0] = types.int_
println(items[0].name())
println(items[0].base_type().name())
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('int\nint\n')
}
fn test_eval_transformed_typed_map_assignment_keeps_sumtype_wrapper() {
res := run_eval_backend("
import v2.types
fn main() {
mut m := map[string]types.Type{}
m['x'] = types.int_
println(m['x'].name())
println(m['x'].base_type().name())
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('int\nint\n')
}
fn test_eval_transformed_object_module_roundtrip_keeps_wrapper() {
res := run_eval_backend("
import v2.types
fn id(obj types.Object) types.Object {
return obj
}
fn main() {
mut mod_scope := types.new_scope(unsafe { nil })
mod_scope.insert('File', types.Object(types.Struct{
name: 'File'
}))
obj := types.Object(types.Module{
name: 'ast'
scope: mod_scope
})
println(obj.type_name())
println(id(obj).type_name())
mut arr := []types.Object{}
arr << obj
println(arr[0].type_name())
mut m := map[string]types.Object{}
m['ast'] = obj
println(m['ast'].type_name())
if got := m['ast'] {
println(got.type_name())
if got is types.Module {
println(got.name)
if file_obj := got.scope.lookup_parent('File', 0) {
println(file_obj.type_name())
println(file_obj.typ().name())
}
}
}
}
") or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('types.Module\ntypes.Module\ntypes.Module\ntypes.Module\ntypes.Module\n')
assert res.output.contains('ast\ntypes.Type\nFile\n')
}
fn test_eval_transformed_array_parameter_keeps_array_shape() {
res := run_eval_backend('
import v2.ast
fn stmt_list(stmts []ast.Stmt) {
println(stmts.len)
}
fn main() {
items := []ast.Stmt{}
stmt_list(items)
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('0\n')
}
fn test_eval_transformed_array_first_and_last() {
res := run_eval_backend('
fn main() {
items := [1, 2, 3]
println(items.first())
println(items.last())
}
') or {
panic(err)
}
if res.exit_code != 0 {
panic(res.output)
}
assert res.output.contains('1\n3\n')
}
fn test_eval_prefix_bit_not() {
mut e := create()
e.run_text('
fn main() {
println(~1)
}
') or { panic(err) }
assert e.stdout() == '-2\n'
}
fn test_eval_token_and_byte_helpers() {
mut e := create()
e.run_text("
fn main() {
for i := 0; i < 2; i++ {
println(i)
}
println(r'raw')
println(u8(`A`).is_alnum())
println(u8(`A`).is_capital())
println(u8(` `).is_space())
}
") or {
panic(err)
}
assert e.stdout() == '0\n1\nraw\ntrue\ntrue\ntrue\n'
}
fn test_eval_string_int_conversion() {
mut e := create()
e.run_text("
fn main() {
println('42'.int())
println('84'.i64())
}
") or { panic(err) }
assert e.stdout() == '42\n84\n'
}
@[noreturn]
fn skip_test(reason string) {
println('skipping test, because ${reason} .')
exit(0)
}
File diff suppressed because it is too large. Load diff
+635
View File
@@ -0,0 +1,635 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module arm64
// ARM64 Instruction Encoding Helpers
// These functions provide type-safe instruction encoding for the ARM64 backend.
// Register type for type safety
type Reg = int
// Common register constants
const xzr = Reg(31) // Zero register
const sp = Reg(31) // Stack pointer (context-dependent)
const fp = Reg(29) // Frame pointer
const lr = Reg(30) // Link register
// === Prologue/Epilogue ===
// stp fp, lr, [sp, -16]! (pre-indexed)
fn asm_stp_fp_lr_pre() u32 {
return 0xA9BF7BFD
}
// mov fp, sp
fn asm_mov_fp_sp() u32 {
return 0x910003FD
}
// ldp fp, lr, [sp], 16 (post-indexed)
fn asm_ldp_fp_lr_post() u32 {
return 0xA8C17BFD
}
// ret
fn asm_ret() u32 {
return 0xD65F03C0
}
// === Store/Load Pair ===
// stp r1, r2, [sp, -16]! (callee-saved pair)
fn asm_stp_pair_pre(r1 Reg, r2 Reg) u32 {
return 0xA9BF0000 | (u32(r2) << 10) | (31 << 5) | u32(r1)
}
// ldp r1, r2, [sp], 16 (restore callee-saved pair)
fn asm_ldp_pair_post(r1 Reg, r2 Reg) u32 {
return 0xA8C10000 | (u32(r2) << 10) | (31 << 5) | u32(r1)
}
// === Arithmetic ===
// add rd, rn, #imm12
fn asm_add_imm(rd Reg, rn Reg, imm u32) u32 {
return 0x91000000 | (imm << 10) | (u32(rn) << 5) | u32(rd)
}
// add rd, rn, #imm, lsl #12
fn asm_add_imm_lsl12(rd Reg, rn Reg, imm u32) u32 {
return 0x91400000 | (imm << 10) | (u32(rn) << 5) | u32(rd)
}
// add rd, rn, rm
fn asm_add_reg(rd Reg, rn Reg, rm Reg) u32 {
return 0x8B000000 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// add rd, rn, rm, lsl #3 (for GEP: index * 8)
fn asm_add_reg_lsl3(rd Reg, rn Reg, rm Reg) u32 {
return 0x8B000C00 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// sub rd, rn, #imm12
fn asm_sub_imm(rd Reg, rn Reg, imm u32) u32 {
return 0xD1000000 | (imm << 10) | (u32(rn) << 5) | u32(rd)
}
// sub rd, rn, #imm12, lsl #12
fn asm_sub_imm_lsl12(rd Reg, rn Reg, imm u32) u32 {
return 0xD1400000 | (imm << 10) | (u32(rn) << 5) | u32(rd)
}
// sub rd, rn, rm
fn asm_sub_reg(rd Reg, rn Reg, rm Reg) u32 {
return 0xCB000000 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// madd rd, rn, rm, xzr (multiply: rd = rn * rm)
fn asm_mul(rd Reg, rn Reg, rm Reg) u32 {
return 0x9B007C00 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// sdiv rd, rn, rm
fn asm_sdiv(rd Reg, rn Reg, rm Reg) u32 {
return 0x9AC00C00 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// udiv rd, rn, rm
fn asm_udiv(rd Reg, rn Reg, rm Reg) u32 {
return 0x9AC00800 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// msub rd, rn, rm, ra (rd = ra - rn * rm)
fn asm_msub(rd Reg, rn Reg, rm Reg, ra Reg) u32 {
return 0x9B008000 | (u32(rm) << 16) | (u32(ra) << 10) | (u32(rn) << 5) | u32(rd)
}
// === Logical ===
// and rd, rn, rm
fn asm_and(rd Reg, rn Reg, rm Reg) u32 {
return 0x8A000000 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// and rd, rn, #1 — truncate to 1 bit (AND Xd, Xn, #1: N=1, immr=0, imms=0x00)
fn asm_and_imm_1(rd Reg, rn Reg) u32 {
return 0x92400000 | (u32(rn) << 5) | u32(rd)
}
// and rd, rn, #0xFF — zero-extend u8 (AND Xd, Xn, #0xFF: N=1, immr=0, imms=0x07)
fn asm_and_imm_0xff(rd Reg, rn Reg) u32 {
return 0x92401C00 | (u32(rn) << 5) | u32(rd)
}
// and rd, rn, #0xFFFF — zero-extend u16 (AND Xd, Xn, #0xFFFF: N=1, immr=0, imms=0x0F)
fn asm_and_imm_0xffff(rd Reg, rn Reg) u32 {
return 0x92403C00 | (u32(rn) << 5) | u32(rd)
}
// and rd, rn, #0xFFFFFFFF — zero-extend u32 (AND Xd, Xn, #0xFFFFFFFF: N=1, immr=0, imms=0x1F)
fn asm_and_imm_0xffffffff(rd Reg, rn Reg) u32 {
return 0x92407C00 | (u32(rn) << 5) | u32(rd)
}
// orr rd, rn, rm
fn asm_orr(rd Reg, rn Reg, rm Reg) u32 {
return 0xAA000000 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// eor rd, rn, rm (xor)
fn asm_eor(rd Reg, rn Reg, rm Reg) u32 {
return 0xCA000000 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// orr rd, xzr, rm (mov rd, rm)
fn asm_mov_reg(rd Reg, rm Reg) u32 {
return 0xAA0003E0 | (u32(rm) << 16) | u32(rd)
}
// === Shifts ===
// lslv rd, rn, rm (logical shift left variable)
fn asm_lslv(rd Reg, rn Reg, rm Reg) u32 {
return 0x9AC02000 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// asrv rd, rn, rm (arithmetic shift right variable)
fn asm_asrv(rd Reg, rn Reg, rm Reg) u32 {
return 0x9AC02800 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// lsrv rd, rn, rm (logical shift right variable)
fn asm_lsrv(rd Reg, rn Reg, rm Reg) u32 {
return 0x9AC02400 | (u32(rm) << 16) | (u32(rn) << 5) | u32(rd)
}
// lsr xd, xn, #shift (logical shift right immediate, 64-bit)
// Alias for UBFM Xd, Xn, #shift, #63
fn asm_lsr_imm(rd Reg, rn Reg, shift u32) u32 {
return 0xD340FC00 | ((shift & 0x3F) << 16) | (u32(rn) << 5) | u32(rd)
}
// ubfx xd, xn, #0, #width (unsigned bitfield extract, zero-extend lower bits)
// Alias for UBFM Xd, Xn, #0, #(width-1)
fn asm_ubfx_lower(rd Reg, rn Reg, width u32) u32 {
imms := (width - 1) & 0x3F
return 0xD3400000 | (imms << 10) | (u32(rn) << 5) | u32(rd)
}
// sxtw rd, rn — sign-extend 32-bit value to 64-bit (SBFM Xd, Xn, #0, #31)
fn asm_sxtw(rd Reg, rn Reg) u32 {
return 0x93407C00 | (u32(rn) << 5) | u32(rd)
}
// sxth rd, rn — sign-extend 16-bit value to 64-bit (SBFM Xd, Xn, #0, #15)
fn asm_sxth(rd Reg, rn Reg) u32 {
return 0x93403C00 | (u32(rn) << 5) | u32(rd)
}
// sxtb rd, rn — sign-extend 8-bit value to 64-bit (SBFM Xd, Xn, #0, #7)
fn asm_sxtb(rd Reg, rn Reg) u32 {
return 0x93401C00 | (u32(rn) << 5) | u32(rd)
}
// === Compare ===
// cmp rn, rm (subs xzr, rn, rm) — 64-bit
fn asm_cmp_reg(rn Reg, rm Reg) u32 {
return 0xEB00001F | (u32(rm) << 16) | (u32(rn) << 5)
}
// cmp xn, #imm12 (subs xzr, xn, #imm12) — 64-bit immediate compare
fn asm_cmp_imm(rn Reg, imm12 u32) u32 {
return 0xF100001F | (imm12 << 10) | (u32(rn) << 5)
}
// cmp wn, #imm12 (subs wzr, wn, #imm12) — 32-bit immediate compare
fn asm_cmp_imm_w(rn Reg, imm12 u32) u32 {
return 0x7100001F | (imm12 << 10) | (u32(rn) << 5)
}
// cmp wn, wm (subs wzr, wn, wm) — 32-bit, sign-aware for i32
fn asm_cmp_reg_w(rn Reg, rm Reg) u32 {
return 0x6B00001F | (u32(rm) << 16) | (u32(rn) << 5)
}
// === Conditional Set ===
// cset rd, eq
fn asm_cset_eq(rd Reg) u32 {
return 0x9A9F17E0 | u32(rd)
}
// cset rd, ne
fn asm_cset_ne(rd Reg) u32 {
return 0x9A9F07E0 | u32(rd)
}
// cset rd, lt
fn asm_cset_lt(rd Reg) u32 {
return 0x9A9FA7E0 | u32(rd)
}
// cset rd, gt
fn asm_cset_gt(rd Reg) u32 {
return 0x9A9FD7E0 | u32(rd)
}
// cset rd, le
fn asm_cset_le(rd Reg) u32 {
return 0x9A9FC7E0 | u32(rd)
}
// cset rd, ge
fn asm_cset_ge(rd Reg) u32 {
return 0x9A9FB7E0 | u32(rd)
}
// cset rd, hi (unsigned greater than)
fn asm_cset_hi(rd Reg) u32 {
return 0x9A9F97E0 | u32(rd)
}
// cset rd, hs (unsigned greater or equal)
fn asm_cset_hs(rd Reg) u32 {
return 0x9A9F37E0 | u32(rd)
}
// cset rd, lo (unsigned less than)
fn asm_cset_lo(rd Reg) u32 {
return 0x9A9F27E0 | u32(rd)
}
// cset rd, ls (unsigned less or equal)
fn asm_cset_ls(rd Reg) u32 {
return 0x9A9F87E0 | u32(rd)
}
// === Float Compare ===
// fcmp dn, dm (compare two double-precision floats, sets NZCV)
fn asm_fcmp_d(dn Reg, dm Reg) u32 {
return 0x1E602000 | (u32(dm) << 16) | (u32(dn) << 5)
}
// === Memory ===
// str rt, [rn] (store 64-bit)
fn asm_str(rt Reg, rn Reg) u32 {
return 0xF9000000 | (u32(rn) << 5) | u32(rt)
}
// str wt, [rn] (store 32-bit)
fn asm_str_w(rt Reg, rn Reg) u32 {
return 0xB9000000 | (u32(rn) << 5) | u32(rt)
}
// strh wt, [rn] (store 16-bit)
fn asm_str_h(rt Reg, rn Reg) u32 {
return 0x79000000 | (u32(rn) << 5) | u32(rt)
}
// strb wt, [rn] (store 8-bit)
fn asm_str_b(rt Reg, rn Reg) u32 {
return 0x39000000 | (u32(rn) << 5) | u32(rt)
}
// str rt, [rn, #imm12] (scaled by 8)
// imm12 is the byte offset divided by 8; range 0..32760 step 8
fn asm_str_imm(rt Reg, rn Reg, imm12 u32) u32 {
return 0xF9000000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// str wt, [rn, #imm12] (32-bit store, unsigned offset scaled by 4)
// imm12 is the byte offset divided by 4; range 0..16380 step 4
fn asm_str_imm_w(rt Reg, rn Reg, imm12 u32) u32 {
return 0xB9000000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// strh wt, [rn, #imm12] (16-bit store, unsigned offset scaled by 2)
// imm12 is the byte offset divided by 2; range 0..8190 step 2
fn asm_str_imm_h(rt Reg, rn Reg, imm12 u32) u32 {
return 0x79000000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// strb wt, [rn, #imm12] (8-bit store, unsigned offset)
// imm12 is the byte offset; range 0..4095
fn asm_str_imm_b(rt Reg, rn Reg, imm12 u32) u32 {
return 0x39000000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// stp rt, rt2, [rn, #simm7] (store pair, signed offset scaled by 8)
// simm7 is byte offset / 8; range -512..504 step 8
fn asm_stp_offset(rt Reg, rt2 Reg, rn Reg, simm7 i32) u32 {
return 0xA9000000 | (u32(simm7 & 0x7F) << 15) | (u32(rt2) << 10) | (u32(rn) << 5) | u32(rt)
}
// ldp rt, rt2, [rn, #simm7] (load pair, signed offset scaled by 8)
// simm7 is byte offset / 8; range -512..504 step 8
fn asm_ldp_offset(rt Reg, rt2 Reg, rn Reg, simm7 i32) u32 {
return 0xA9400000 | (u32(simm7 & 0x7F) << 15) | (u32(rt2) << 10) | (u32(rn) << 5) | u32(rt)
}
// stur xt, [xn, #simm9] (unscaled 64-bit store)
fn asm_stur(rt Reg, rn Reg, simm9 i32) u32 {
return 0xF8000000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// stur wt, [xn, #simm9] (unscaled 32-bit store)
fn asm_stur_w(rt Reg, rn Reg, simm9 i32) u32 {
return 0xB8000000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// sturh wt, [xn, #simm9] (unscaled 16-bit store)
fn asm_stur_h(rt Reg, rn Reg, simm9 i32) u32 {
return 0x78000000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// sturb wt, [xn, #simm9] (unscaled 8-bit store)
fn asm_stur_b(rt Reg, rn Reg, simm9 i32) u32 {
return 0x38000000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// ldr rt, [rn] (load 64-bit)
fn asm_ldr(rt Reg, rn Reg) u32 {
return 0xF9400000 | (u32(rn) << 5) | u32(rt)
}
// ldr wt, [rn] (load 32-bit, zero-extend to x)
fn asm_ldr_w(rt Reg, rn Reg) u32 {
return 0xB9400000 | (u32(rn) << 5) | u32(rt)
}
// ldrsw xt, [rn] (load 32-bit, sign-extend to 64-bit x)
// Used for signed i32 loads to preserve negative values in 64-bit registers.
fn asm_ldrsw(rt Reg, rn Reg) u32 {
return 0xB9800000 | (u32(rn) << 5) | u32(rt)
}
// ldrh wt, [rn] (load 16-bit, zero-extend to x)
fn asm_ldr_h(rt Reg, rn Reg) u32 {
return 0x79400000 | (u32(rn) << 5) | u32(rt)
}
// ldrb wt, [rn] (load 8-bit, zero-extend to x)
fn asm_ldr_b(rt Reg, rn Reg) u32 {
return 0x39400000 | (u32(rn) << 5) | u32(rt)
}
// ldr rt, [rn, #imm12] (load 64-bit with unsigned scaled offset)
// imm12 is the byte offset divided by 8; range 0..32760 step 8
fn asm_ldr_imm(rt Reg, rn Reg, imm12 u32) u32 {
return 0xF9400000 | ((imm12 & 0xFFF) << 10) | (u32(rn) << 5) | u32(rt)
}
// ldr wt, [rn, #imm12] (32-bit load, unsigned offset scaled by 4)
// imm12 is the byte offset divided by 4; range 0..16380 step 4
fn asm_ldr_imm_w(rt Reg, rn Reg, imm12 u32) u32 {
return 0xB9400000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// ldrh wt, [rn, #imm12] (16-bit load, unsigned offset scaled by 2)
// imm12 is the byte offset divided by 2; range 0..8190 step 2
fn asm_ldr_imm_h(rt Reg, rn Reg, imm12 u32) u32 {
return 0x79400000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// ldrb wt, [rn, #imm12] (8-bit load, unsigned offset)
// imm12 is the byte offset; range 0..4095
fn asm_ldr_imm_b(rt Reg, rn Reg, imm12 u32) u32 {
return 0x39400000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// ldrsw xt, [rn, #imm12] (32-bit sign-extending load, unsigned offset scaled by 4)
fn asm_ldrsw_imm(rt Reg, rn Reg, imm12 u32) u32 {
return 0xB9800000 | (imm12 << 10) | (u32(rn) << 5) | u32(rt)
}
// ldur xt, [xn, #simm9] (unscaled 64-bit load)
fn asm_ldur(rt Reg, rn Reg, simm9 i32) u32 {
return 0xF8400000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// ldur wt, [xn, #simm9] (unscaled 32-bit load, zero-extend)
fn asm_ldur_w(rt Reg, rn Reg, simm9 i32) u32 {
return 0xB8400000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// ldurh wt, [xn, #simm9] (unscaled 16-bit load, zero-extend)
fn asm_ldur_h(rt Reg, rn Reg, simm9 i32) u32 {
return 0x78400000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// ldurb wt, [xn, #simm9] (unscaled 8-bit load, zero-extend)
fn asm_ldur_b(rt Reg, rn Reg, simm9 i32) u32 {
return 0x38400000 | (u32(simm9 & 0x1FF) << 12) | (u32(rn) << 5) | u32(rt)
}
// === Branches ===
// b #imm26 (unconditional branch)
fn asm_b(rel26 i32) u32 {
return 0x14000000 | (u32(rel26) & 0x3FFFFFF)
}
// bl #imm26 (branch with link / call)
fn asm_bl(rel26 i32) u32 {
return 0x94000000 | (u32(rel26) & 0x3FFFFFF)
}
// bl (placeholder for relocation)
fn asm_bl_reloc() u32 {
return 0x94000000
}
// blr rn (branch with link to register)
fn asm_blr(rn Reg) u32 {
return 0xD63F0000 | (u32(rn) << 5)
}
// cbnz rt, #imm19
fn asm_cbnz(rt Reg, rel19 i32) u32 {
return 0xB5000000 | ((u32(rel19) & 0x7FFFF) << 5) | u32(rt)
}
// cbz rt, #imm19
fn asm_cbz(rt Reg, rel19 i32) u32 {
return 0xB4000000 | ((u32(rel19) & 0x7FFFF) << 5) | u32(rt)
}
// b.cond #imm19 (conditional branch)
fn asm_b_cond(cond u32, rel19 i32) u32 {
return 0x54000000 | ((u32(rel19) & 0x7FFFF) << 5) | cond
}
// Condition codes for b.cond
const cond_eq = u32(0x0) // equal
const cond_ne = u32(0x1) // not equal
const cond_lt = u32(0xB) // less than (signed)
const cond_gt = u32(0xC) // greater than (signed)
const cond_le = u32(0xD) // less or equal (signed)
const cond_ge = u32(0xA) // greater or equal (signed)
// === Move Immediate ===
// movz rd, #imm16 (move wide with zero)
fn asm_movz(rd Reg, imm16 u32) u32 {
return 0xD2800000 | (imm16 << 5) | u32(rd)
}
// movk rd, #imm16, lsl #shift (move wide with keep)
fn asm_movk(rd Reg, imm16 u32, shift int) u32 {
hw := u32(shift / 16)
return 0xF2800000 | (hw << 21) | (imm16 << 5) | u32(rd)
}
// movn rd, #imm16 (move wide with NOT)
fn asm_movn(rd Reg, imm16 u32) u32 {
return 0x92800000 | (imm16 << 5) | u32(rd)
}
// === PC-Relative Addressing ===
// adrp rd, #imm21 (load page address - placeholder for reloc)
fn asm_adrp(rd Reg) u32 {
return 0x90000000 | u32(rd)
}
// add rd, rd, #imm12 (add page offset - typically follows adrp)
fn asm_add_pageoff(rd Reg) u32 {
return 0x91000000 | u32(rd) | (u32(rd) << 5)
}
// ldr rd, [rd, #imm12] (load from page offset - for GOT access, typically follows adrp)
fn asm_ldr_pageoff(rd Reg) u32 {
return 0xF9400000 | u32(rd) | (u32(rd) << 5)
}
// === Stack Operations ===
// sub sp, sp, x10
fn asm_sub_sp_reg(rm Reg) u32 {
return 0xCB000000 | (u32(rm) << 16) | (31 << 5) | 31
}
// add sp, sp, x10
fn asm_add_sp_reg(rm Reg) u32 {
return 0x8B000000 | (u32(rm) << 16) | (31 << 5) | 31
}
// sub x10, x29, x10
fn asm_sub_fp_to_reg(rd Reg, rm Reg) u32 {
return 0xCB0003A0 | (u32(rm) << 16) | u32(rd)
}
// === Float Operations ===
// fadd dd, dn, dm
fn asm_fadd_d(dd int, dn int, dm int) u32 {
return 0x1E602800 | (u32(dm) << 16) | (u32(dn) << 5) | u32(dd)
}
// fadd d0, d0, d1 (common case)
fn asm_fadd_d0_d0_d1() u32 {
return 0x1E612800
}
// fsub d0, d0, d1
fn asm_fsub_d0_d0_d1() u32 {
return 0x1E613800
}
// fmul d0, d0, d1
fn asm_fmul_d0_d0_d1() u32 {
return 0x1E610800
}
// fdiv d0, d0, d1
fn asm_fdiv_d0_d0_d1() u32 {
return 0x1E611800
}
// fdiv d2, d0, d1
fn asm_fdiv_d2_d0_d1() u32 {
return 0x1E611802
}
// frintz d2, d2 (truncate to integer)
fn asm_frintz_d2() u32 {
return 0x1E65C042
}
// fnmsub d0, d2, d1, d0 (d0 = d0 - d2*d1)
fn asm_fnmsub_d0_d2_d1_d0() u32 {
return 0x1F618000
}
// === Float Conversions ===
// fmov xd, dn (copy float bits to integer)
fn asm_fmov_x_d(xd Reg, dn int) u32 {
return 0x9E660000 | (u32(dn) << 5) | u32(xd)
}
// fmov dd, xn (copy integer bits to float)
fn asm_fmov_d_x(dd int, xn Reg) u32 {
return 0x9E670000 | (u32(xn) << 5) | u32(dd)
}
// fcvtzs xd, dn (float to signed int, truncate toward zero)
fn asm_fcvtzs_x_d(xd Reg, dn int) u32 {
return 0x9E780000 | (u32(dn) << 5) | u32(xd)
}
// scvtf dd, xn (signed int to float)
fn asm_scvtf_d_x(dd int, xn Reg) u32 {
return 0x9E620000 | (u32(xn) << 5) | u32(dd)
}
// ucvtf dd, xn (unsigned int to float)
fn asm_ucvtf_d_x(dd int, xn Reg) u32 {
return 0x9E630000 | (u32(xn) << 5) | u32(dd)
}
// fcvtzu xd, dn (float to unsigned int, truncate toward zero)
fn asm_fcvtzu_x_d(xd Reg, dn int) u32 {
return 0x9E790000 | (u32(dn) << 5) | u32(xd)
}
// fmov sd, wn (copy 32-bit integer to single-precision float register)
fn asm_fmov_s_w(sd int, wn Reg) u32 {
return 0x1E270000 | (u32(wn) << 5) | u32(sd)
}
// fmov wd, sn (copy single-precision float to 32-bit integer register)
fn asm_fmov_w_s(wd Reg, sn int) u32 {
return 0x1E260000 | (u32(sn) << 5) | u32(wd)
}
// fcvt dd, sn (convert single-precision to double-precision float)
fn asm_fcvt_d_s(dd int, sn int) u32 {
return 0x1E22C000 | (u32(sn) << 5) | u32(dd)
}
// fcvt sd, dn (convert double-precision to single-precision float)
fn asm_fcvt_s_d(sd int, dn int) u32 {
return 0x1E624000 | (u32(dn) << 5) | u32(sd)
}
// === Special ===
// udf #0 (undefined - trap)
fn asm_udf() u32 {
return 0x00000000
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,285 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module arm64
// import encoding.binary
import os
// Mach-O Constants for ARM64
const mh_magic_64 = u32(0xfeedfacf)
const cpu_type_arm64 = 0x0100000c
const cpu_subtype_arm64_all = 0
const lc_segment_64 = 0x19
const lc_symtab = 0x2
const arm64_reloc_branch26 = 2
const arm64_reloc_page21 = 3
const arm64_reloc_pageoff12 = 4
const arm64_reloc_got_load_page21 = 5
const arm64_reloc_got_load_pageoff12 = 6
pub struct MachOObject {
pub mut:
text_data []u8
str_data []u8
data_data []u8
relocs []RelocationInfo
symbols []Symbol
str_table []u8
sym_by_name map[string]int // symbol name → index in symbols
}
struct RelocationInfo {
addr int
sym_idx int
pcrel bool
length int
extern bool
type_ int
}
struct Symbol {
mut:
name string
type_ u8
sect u8
desc u16
value u64
name_off int
}
pub fn MachOObject.new() &MachOObject {
mut m := &MachOObject{
str_table: [u8(0)]
}
return m
}
pub fn (mut m MachOObject) add_symbol(name string, addr u64, is_ext bool, sect u8) int {
typ := if is_ext { u8(0x0f) } else { u8(0x0e) } // N_SECT | N_EXT : N_SECT
// Check if symbol already exists (e.g., was added as undefined earlier)
if i := m.sym_by_name[name] {
mut s := &m.symbols[i]
s.type_ = typ
s.sect = sect
s.value = addr
return i
}
// Add new symbol
idx := m.symbols.len
name_off := m.str_table.len
unsafe { m.str_table.push_many(name.str, name.len) }
m.str_table << 0
m.symbols << Symbol{
name: name
type_: typ
sect: sect
desc: 0
value: addr
name_off: name_off
}
m.sym_by_name[name] = idx
return idx
}
pub fn (mut m MachOObject) add_undefined(name string) int {
// Check for any existing symbol with this name (defined or undefined)
if i := m.sym_by_name[name] {
return i
}
idx := m.symbols.len
name_off := m.str_table.len
unsafe { m.str_table.push_many(name.str, name.len) }
m.str_table << 0
m.symbols << Symbol{
name: name
type_: 0x01 // N_UNDF | N_EXT
sect: 0
desc: 0
value: 0
name_off: name_off
}
m.sym_by_name[name] = idx
return idx
}
pub fn (mut m MachOObject) add_reloc(addr int, sym_idx int, typ int, pcrel bool) {
m.relocs << RelocationInfo{
addr: addr
sym_idx: sym_idx // 0-based symbol table index
type_: typ
pcrel: pcrel
length: 2
extern: true
}
}
pub fn (mut m MachOObject) write(path string) {
mut buf := []u8{}
n_sects := 3
header_size := 32
seg_cmd_size := 72 + (80 * n_sects)
symtab_cmd_size := 24
load_cmds_size := seg_cmd_size + symtab_cmd_size
text_off := header_size + load_cmds_size
text_len := m.text_data.len
cstring_off := text_off + text_len
cstring_len := m.str_data.len
data_off := cstring_off + cstring_len
data_len := m.data_data.len
reloc_off := data_off + data_len
sym_off := reloc_off + (m.relocs.len * 8)
sym_len := m.symbols.len * 16
str_off := sym_off + sym_len
str_size := m.str_table.len
// 1. Header
write_u32_le(mut buf, mh_magic_64)
write_u32_le(mut buf, u32(cpu_type_arm64))
write_u32_le(mut buf, u32(cpu_subtype_arm64_all))
write_u32_le(mut buf, 1) // MH_OBJECT
write_u32_le(mut buf, 2) // ncmds
write_u32_le(mut buf, u32(load_cmds_size))
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
// 2. LC_SEGMENT_64
write_u32_le(mut buf, u32(lc_segment_64))
write_u32_le(mut buf, u32(seg_cmd_size))
for _ in 0 .. 16 {
buf << 0
}
write_u64_le(mut buf, 0)
write_u64_le(mut buf, u64(text_len + cstring_len + data_len))
write_u64_le(mut buf, u64(text_off))
write_u64_le(mut buf, u64(text_len + cstring_len + data_len))
write_u32_le(mut buf, 7)
write_u32_le(mut buf, 7)
write_u32_le(mut buf, u32(n_sects))
write_u32_le(mut buf, 0)
// Section 1: __text
write_string_fixed(mut buf, '__text', 16)
write_string_fixed(mut buf, '__TEXT', 16)
write_u64_le(mut buf, 0) // Addr 0
write_u64_le(mut buf, u64(text_len))
write_u32_le(mut buf, u32(text_off))
write_u32_le(mut buf, 4)
write_u32_le(mut buf, u32(reloc_off))
write_u32_le(mut buf, u32(m.relocs.len))
write_u32_le(mut buf, u32(0x80000400))
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
// Section 2: __cstring
write_string_fixed(mut buf, '__cstring', 16)
write_string_fixed(mut buf, '__TEXT', 16)
write_u64_le(mut buf, u64(text_len)) // Addr = text_len
write_u64_le(mut buf, u64(cstring_len))
write_u32_le(mut buf, u32(cstring_off))
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 2) // S_CSTRING_LITERALS
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
// Section 3: __data
write_string_fixed(mut buf, '__data', 16)
write_string_fixed(mut buf, '__DATA', 16)
write_u64_le(mut buf, u64(text_len + cstring_len)) // Addr = text_len + cstring_len
write_u64_le(mut buf, u64(data_len))
write_u32_le(mut buf, u32(data_off))
write_u32_le(mut buf, 3) // align 8
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
write_u32_le(mut buf, 0)
// 3. LC_SYMTAB
write_u32_le(mut buf, u32(lc_symtab))
write_u32_le(mut buf, u32(symtab_cmd_size))
write_u32_le(mut buf, u32(sym_off))
write_u32_le(mut buf, u32(m.symbols.len))
write_u32_le(mut buf, u32(str_off))
write_u32_le(mut buf, u32(str_size))
buf << m.text_data
buf << m.str_data
buf << m.data_data
for r in m.relocs {
write_u32_le(mut buf, u32(r.addr))
mut info := u32(r.sym_idx)
if r.pcrel {
info |= (1 << 24)
}
info |= (u32(r.length) << 25)
if r.extern {
info |= (1 << 27)
}
info |= (u32(r.type_) << 28)
write_u32_le(mut buf, info)
}
for s in m.symbols {
write_u32_le(mut buf, u32(s.name_off))
buf << s.type_
buf << s.sect
write_u16_le(mut buf, s.desc)
write_u64_le(mut buf, s.value)
}
buf << m.str_table
os.write_file_array(path, buf) or { panic(err) }
}
fn write_u32_le(mut b []u8, v u32) {
b << u8(v)
b << u8(v >> 8)
b << u8(v >> 16)
b << u8(v >> 24)
}
fn write_u64_le(mut b []u8, v u64) {
b << u8(v)
b << u8(v >> 8)
b << u8(v >> 16)
b << u8(v >> 24)
b << u8(v >> 32)
b << u8(v >> 40)
b << u8(v >> 48)
b << u8(v >> 56)
}
fn write_u16_le(mut b []u8, v u16) {
b << u8(v)
b << u8(v >> 8)
}
fn write_string_fixed(mut b []u8, s string, len int) {
mut bytes := s.bytes()
for bytes.len < len {
bytes << 0
}
for i in 0 .. len {
b << bytes[i]
}
}
@@ -0,0 +1,8 @@
import os
fn main() {
println(os.args.len > 0)
if os.args.len > 0 {
println(os.args[0].len > 0)
}
}
@@ -0,0 +1,33 @@
struct FnType {
x int
}
struct Void {}
type Type = FnType | Void
struct Fn {
name string
typ Type
}
fn lookup(methods []&Fn, method_name string) bool {
for method in methods {
if method.name == method_name {
return method.typ is FnType
}
}
return false
}
fn main() {
method := &Fn{
name: 'write_string'
typ: FnType{
x: 1
}
}
mut methods := []&Fn{}
methods << method
println(lookup(methods, 'write_string'))
}
@@ -0,0 +1,13 @@
fn main() {
mut matrix := [][]int{len: 4}
mut worklist := []int{}
worklist << 2
idx := worklist.pop()
matrix[idx] << 10
matrix[idx] << 5
mut total := 0
for value in matrix[2] {
total += value
}
println(total)
}
@@ -0,0 +1,46 @@
module main
struct Entry {
name string
initial_data []u8
size int
}
fn collect(entries []Entry) []u8 {
mut out := []u8{}
for entry in entries {
if entry.initial_data.len > 0 {
out << entry.initial_data
continue
}
for _ in 0 .. entry.size {
out << u8(0)
}
}
return out
}
fn main() {
entries := [
Entry{
name: 'a'
initial_data: [u8(1), 2, 3]
size: 0
},
Entry{
name: 'b'
initial_data: []u8{}
size: 4
},
Entry{
name: 'c'
initial_data: [u8(9)]
size: 0
},
]
out := collect(entries)
println(out.len)
println(out[0])
println(out[5])
println(out[7])
}
@@ -0,0 +1,53 @@
type ValueID = int
type TypeID = int
struct Value {
id int
typ TypeID
index int
}
struct Function {
id int
name string
typ TypeID
mut:
blocks []int
params []ValueID
}
struct Module {
mut:
values []Value
funcs []Function
}
fn sum_param_types(mut m Module) int {
mut out := 0
params := m.funcs[0].params.clone()
for i in [0, 1] {
if i < params.len {
param := params[i]
out += int(m.values[param].typ)
}
}
return out
}
fn main() {
mut m := Module{}
for i := 0; i < 200; i++ {
m.values << Value{
id: i
typ: TypeID(i + 10)
index: i
}
}
m.funcs << Function{
id: 0
name: 'f'
typ: TypeID(0)
params: [ValueID(134), ValueID(135)]
}
println(sum_param_types(mut m))
}
@@ -0,0 +1,17 @@
fn main() {
pairs := [
[1, 2]!,
[2, 3]!,
[1, 0]!,
]
mut got := []int{}
for pair in pairs {
got << pair[0]
got << pair[1]
}
println(got)
for i := 0; i < pairs.len; i++ {
pair := pairs[i]
println('${pair[0]},${pair[1]}')
}
}
@@ -0,0 +1,3 @@
module main
fn main() {}
@@ -0,0 +1,14 @@
module main
struct DirLike {
mut:
d_name [256]u8
}
fn main() {
mut d := DirLike{}
d.d_name[255] = 7
assert d.d_name[255] == 7
assert d.d_name.len == 256
println('ok')
}
@@ -0,0 +1,29 @@
import v2.ast
fn main() {
file := ast.File{
name: @FILE
mod: 'main'
imports: [
ast.ImportStmt{
name: 'v2.pref'
alias: 'pref'
},
]
stmts: [
ast.Stmt(ast.ImportStmt{
name: 'v2.pref'
alias: 'pref'
}),
]
}
flat := ast.flatten_files([file])
file_node := flat.nodes[flat.files[0].file_id]
imports := flat.file_cursor(0).imports().import_stmts()
println(file_node.edge_count)
println(imports.len)
if imports.len > 0 {
println(imports[0].name)
println(imports[0].alias)
}
}
@@ -0,0 +1,45 @@
module main
const page_size = 64
@[direct_array_access]
fn fill_hash(data &u8, data_len int, out &u8) {
for i in 0 .. 32 {
unsafe {
out[i] = if i < data_len { data[i] + 1 } else { u8(i) }
}
}
}
fn hash_pages_shape(data &u8, mut hashes []u8, page_start int, page_end int, code_limit int) {
mut hash_buf := [32]u8{}
for page := page_start; page < page_end; page++ {
start := page * page_size
mut end := start + page_size
if end > code_limit {
end = code_limit
}
unsafe {
fill_hash(data + start, end - start, &hash_buf[0])
}
hash_offset := page * 32
for i in 0 .. 32 {
hashes[hash_offset + i] = hash_buf[i]
}
}
}
fn main() {
mut data := []u8{len: 150}
for i in 0 .. data.len {
data[i] = u8(i % 251)
}
mut hashes := []u8{len: 96}
unsafe {
hash_pages_shape(data.data, mut hashes, 0, 3, data.len)
}
println(hashes[0])
println(hashes[32])
println(hashes[64])
println(hashes[95])
}
@@ -0,0 +1,24 @@
module main
struct GenShape {
mut:
cache []int
stack []bool
}
fn new_gen_shape(n int) &GenShape {
return &GenShape{
cache: []int{len: n}
stack: []bool{len: n}
}
}
fn main() {
g := new_gen_shape(256)
mut sum := 0
for i := 0; i < g.cache.len; i++ {
sum += g.cache[i]
}
println(sum)
println(g.cache.len)
}
@@ -0,0 +1,14 @@
module main
fn sum_cache(n int) int {
cache := []int{len: n}
mut sum := 0
for i := 0; i < cache.len; i++ {
sum += cache[i]
}
return sum
}
fn main() {
println(sum_cache(256))
}
@@ -0,0 +1,5 @@
module main
fn main() {
println(2147483647)
}
@@ -0,0 +1,36 @@
module main
struct TypeShape {
kind int
width int
elem_type int
len int
fields []int
field_names []string
params []int
ret_type int
is_c_struct bool
is_union bool
is_unsigned bool
}
fn fixed_array_size(types []TypeShape, typ_id int) int {
typ := types[typ_id]
if typ.kind == 4 {
return typ.len * 4
}
return 0
}
fn main() {
types := [
TypeShape{
kind: 4
elem_type: 1
len: 128
fields: []int{}
params: []int{}
},
]
println(fixed_array_size(types, 0))
}
@@ -0,0 +1,42 @@
module main
enum TypeKindShape {
void_t
int_t
float_t
ptr_t
array_t
struct_t
func_t
label_t
metadata_t
}
struct TypeShape {
kind TypeKindShape
width int
elem_type int
len int
fields []int
field_names []string
params []int
ret_type int
is_c_struct bool
is_union bool
is_unsigned bool
}
fn main() {
mut types := []TypeShape{}
for i := 0; i < 400; i++ {
types << TypeShape{
kind: .array_t
elem_type: 1
len: 64 + i
}
}
println(types[0].len)
println(types[63].len)
println(types[128].len)
println(types[399].len)
}
@@ -0,0 +1,57 @@
module main
enum TypeKindShape {
void_t
int_t
float_t
ptr_t
array_t
struct_t
func_t
label_t
metadata_t
}
struct TypeShape {
kind TypeKindShape
width int
elem_type int
len int
fields []int
field_names []string
params []int
ret_type int
is_c_struct bool
is_union bool
is_unsigned bool
}
struct TypeStoreShape {
mut:
types []TypeShape
cache map[string]int
}
fn (mut ts TypeStoreShape) register(t TypeShape) int {
id := ts.types.len
ts.types << t
return id
}
fn (mut ts TypeStoreShape) get_array(elem int, length int) int {
return ts.register(TypeShape{
kind: .array_t
elem_type: elem
len: length
})
}
fn main() {
mut ts := TypeStoreShape{}
ts.cache = map[string]int{}
id := ts.get_array(1, 128)
typ := ts.types[id]
println(typ.len)
println(typ.elem_type)
println(int(typ.kind))
}
@@ -0,0 +1,18 @@
fn push_name(mut names []string, name string) {
names << name
println(names.len)
}
fn wrap(mut names []string, name string) {
push_name(mut names, name)
println(names.len)
}
fn main() {
mut names := []string{}
wrap(mut names, 'abc')
println(names.len)
if names.len > 0 {
println(names[0])
}
}
@@ -0,0 +1,18 @@
module main
fn main() {
mut lists := [][]int{len: 4}
for i := 0; i < lists.len; i++ {
lists[i] << (i + 10)
lists[i] << (i + 20)
}
mut total := 0
for i := 0; i < lists.len; i++ {
total += lists[i].len
total += lists[i][0]
total += lists[i][1]
}
println(total)
println(lists[3][0])
println(lists[3][1])
}
@@ -0,0 +1,95 @@
module main
struct EmptyExpr {
}
struct BasicLiteral {
value string
}
struct Ident {
name string
}
type Type = ArrayFixedType | ArrayType
type Expr = ArrayInitExpr | BasicLiteral | EmptyExpr | Ident | Type
struct ArrayType {
elem_type Expr
}
struct ArrayFixedType {
len Expr
elem_type Expr
}
struct ArrayInitExpr {
typ Expr
exprs []Expr
}
struct FieldDecl {
name string
value Expr
}
fn parse_digits(s string) int {
mut out := 0
for i := 0; i < s.len; i++ {
out = out * 10 + int(s[i] - `0`)
}
return out
}
fn fixed_len_from_type(typ Type) int {
return match typ {
ArrayFixedType {
if typ.len is BasicLiteral {
parse_digits(typ.len.value)
} else {
-2
}
}
ArrayType {
-3
}
}
}
fn type_to_len(expr Expr) int {
return match expr {
Type {
fixed_len_from_type(expr)
}
else {
-1
}
}
}
fn field_len(field FieldDecl) int {
if field.value is ArrayInitExpr {
arr := field.value
return type_to_len(arr.typ)
}
return -4
}
fn main() {
field := FieldDecl{
name: 'buf'
value: Expr(ArrayInitExpr{
typ: Expr(Type(ArrayFixedType{
len: Expr(BasicLiteral{
value: '128'
})
elem_type: Expr(Ident{
name: 'u8'
})
}))
exprs: []Expr{}
})
}
println(field_len(field))
}
@@ -0,0 +1,87 @@
module main
struct EmptyExpr {
}
struct BasicLiteral {
value int
}
struct Ident {
name string
}
type Type = ArrayFixedType | ArrayType
type Expr = ArrayInitExpr | BasicLiteral | EmptyExpr | Ident | Type
struct ArrayType {
elem_type Expr
}
struct ArrayFixedType {
len Expr
elem_type Expr
}
struct ArrayInitExpr {
typ Expr
exprs []Expr
}
struct FieldDecl {
name string
value Expr
}
fn fixed_len_from_type(typ Type) int {
return match typ {
ArrayFixedType {
if typ.len is BasicLiteral {
typ.len.value
} else {
-2
}
}
ArrayType {
-3
}
}
}
fn type_to_len(expr Expr) int {
return match expr {
Type {
fixed_len_from_type(expr)
}
else {
-1
}
}
}
fn field_len(field FieldDecl) int {
if field.value is ArrayInitExpr {
arr := field.value
return type_to_len(arr.typ)
}
return -4
}
fn main() {
field := FieldDecl{
name: 'buf'
value: Expr(ArrayInitExpr{
typ: Expr(Type(ArrayFixedType{
len: Expr(BasicLiteral{
value: 128
})
elem_type: Expr(Ident{
name: 'u8'
})
}))
exprs: []Expr{}
})
}
println(field_len(field))
}
@@ -0,0 +1,54 @@
import v2.ast
struct Parameter {
typ ast.Expr
}
struct FnType {
params []Parameter
}
fn classify(expr ast.Expr) int {
match expr {
ast.Type {
if expr is ast.ArrayType {
return 11
}
return 7
}
else {
return 3
}
}
}
fn classify_param_types(fn_type FnType) int {
mut out := 0
idx := 0
first := fn_type.params[idx]
out += classify(first.typ)
for i := 0; i < fn_type.params.len; i++ {
param := fn_type.params[i]
out += classify(param.typ)
}
for _, param in fn_type.params {
out += classify(param.typ)
}
return out
}
fn main() {
typ := ast.Type(ast.ArrayType{
elem_type: ast.Expr(ast.Ident{
name: 'int'
})
})
fn_type := FnType{
params: [
Parameter{
typ: ast.Expr(typ)
},
]
}
println(classify_param_types(fn_type))
}
@@ -0,0 +1,7 @@
module main
fn main() {
assert 0o755 == 493
assert 0b1010_0101 == 165
println('ok')
}
@@ -0,0 +1,30 @@
module main
fn to_i64_direct(x u64) i64 {
return i64(x)
}
fn to_i64_if_expr(x u64, neg bool) i64 {
return if neg { -i64(x) } else { i64(x) }
}
fn to_i64_result_direct(x u64) !i64 {
return i64(x)
}
fn to_i64_result_if_expr(x u64, neg bool) !i64 {
return if neg { -i64(x) } else { i64(x) }
}
fn to_i64_result_if_var(x u64, neg bool) !i64 {
y := if neg { -i64(x) } else { i64(x) }
return y
}
fn main() {
println(to_i64_direct(6))
println(to_i64_if_expr(6, false))
println(to_i64_result_direct(6) or { 99 })
println(to_i64_result_if_expr(6, false) or { 99 })
println(to_i64_result_if_var(6, false) or { 99 })
}
@@ -0,0 +1,28 @@
import os
struct Sample {
a voidptr
b int
c bool
}
fn mk(path string) !Sample {
cfile := os.vfopen(path, 'wb')!
fd := os.fileno(cfile)
return Sample{
a: cfile
b: fd
c: true
}
}
fn main() {
s := mk('/tmp/v2_result_postfix_unwrap.txt') or {
println('err')
return
}
println(s.b > 0)
println(s.c)
}
// v2-clobber-test
@@ -0,0 +1,101 @@
// Test runner for ARM64 backend tests
// Run with: ./v run vlib/v2_toberemoved/gen/arm64/tests/run_tests.v
// Or: cd vlib/v2_toberemoved/gen/arm64/tests && v run run_tests.v
module main
import os
import time
fn main() {
t0 := time.now()
vroot := os.dir(@VEXE)
v2_source := os.join_path(vroot, 'cmd', 'v2', 'v2.v')
v2_binary := os.join_path(vroot, 'cmd', 'v2', 'v2')
tests_dir := os.join_path(vroot, 'vlib', 'v2', 'gen', 'arm64', 'tests')
// Build v2 compiler
println('[*] Building v2...')
build_res := os.execute('${@VEXE} ${v2_source} -o ${v2_binary}')
if build_res.exit_code != 0 {
eprintln('Error: Failed to build v2')
eprintln(build_res.output)
exit(1)
}
// Find all test files (*.v except run_tests.v)
test_files := os.ls(tests_dir) or {
eprintln('Error: Cannot list tests directory')
exit(1)
}
mut passed := 0
mut failed := 0
mut test_count := 0
for file in test_files {
if !file.ends_with('.v') || file == 'run_tests.v' {
continue
}
test_count++
test_path := os.join_path(tests_dir, file)
test_name := file.replace('.v', '')
println('\n[*] Testing: ${file}')
// Run v2 with ARM64 backend
v2_output := os.join_path(tests_dir, 'test_${test_name}')
v2_cmd := '${v2_binary} -backend arm64 ${test_path} -o ${v2_output}'
v2_res := os.execute(v2_cmd)
if v2_res.exit_code != 0 {
eprintln(' [FAIL] v2 compilation failed')
eprintln(v2_res.output)
failed++
continue
}
// Run the generated binary
gen_res := os.execute(v2_output)
if gen_res.exit_code != 0 {
eprintln(' [FAIL] Generated binary exited with code ${gen_res.exit_code}')
eprintln(gen_res.output)
failed++
continue
}
// Run reference compiler
ref_res := os.execute('${@VEXE} -n -w -enable-globals run ${test_path}')
if ref_res.exit_code != 0 {
eprintln(' [FAIL] Reference compilation failed')
eprintln(ref_res.output)
failed++
continue
}
// Compare outputs
expected := ref_res.output.trim_space().replace('\r\n', '\n')
actual := gen_res.output.trim_space().replace('\r\n', '\n')
if expected == actual {
println(' [PASS] Output matches reference')
passed++
} else {
println(' [FAIL] Output mismatch')
println(' Expected:\n${expected}')
println(' Got:\n${actual}')
failed++
}
// Clean up
os.rm(v2_output) or {}
}
println('\n========================================')
println('Results: ${passed}/${test_count} tests passed')
if failed > 0 {
println('${failed} tests FAILED')
exit(1)
}
println('Total time: ${time.since(t0)}')
}
@@ -0,0 +1 @@
println('script main')
@@ -0,0 +1,35 @@
struct IntInfo {
value int
}
struct StructInfo {
name string
value int
}
type TypeInfo = IntInfo | StructInfo
fn name_contains_dot(name string) bool {
return name.contains('.')
}
fn has_dot(info TypeInfo) bool {
mut cur := info
if cur is StructInfo {
return name_contains_dot(cur.name)
}
return false
}
fn main() {
with_dot := TypeInfo(StructInfo{
name: 'foo.bar'
value: 1
})
without_dot := TypeInfo(StructInfo{
name: 'foobar'
value: 2
})
println(has_dot(with_dot))
println(has_dot(without_dot))
}
@@ -0,0 +1,13 @@
module main
fn main() {
assert '6'.int() == 6
assert '10'.int() == 10
assert '13'.int() == 13
assert '-1'.int() == -1
assert '10'.i32() == i32(10)
println('32'.u64())
println('32'.i64())
println('-32'.i64())
println('0x20'.u64())
}
@@ -0,0 +1,113 @@
// Tests for ARM64 backend large struct returns (> 16 bytes)
// This tests the fix from commit 9919ddeb5 which handles:
// - Saving/restoring x8 (indirect return pointer) for large struct returns
// - Proper stack frame layout with callee-saved registers
// - Large struct parameter copying
// - String operations (string is 24 bytes: ptr + len + is_lit)
fn print_str(s string) {
C.write(1, s.str, s.len)
C.write(1, c'\n', 1)
}
// ===================== FUNCTIONS RETURNING STRINGS =====================
// Test 1: Return a string literal (24 bytes indirect return)
fn get_hello() string {
return 'hello'
}
// Test 2: Return concatenated string
fn get_greeting(name string) string {
return 'Hello, ' + name
}
// Test 3: Nested string returns
fn get_formal_greeting(name string) string {
greeting := get_greeting(name)
return greeting + '!'
}
// Test 4: Multiple string concatenations
fn concat_three(a string, b string, c string) string {
return a + b + c
}
// Test 5: String function calling another string function
fn get_full_message() string {
greeting := get_hello()
suffix := ' world'
return greeting + suffix
}
// Test 6: Chain of string returns
fn chain_a() string {
return 'A'
}
fn chain_b() string {
return chain_a() + 'B'
}
fn chain_c() string {
return chain_b() + 'C'
}
// Test 7: Conditional string return
fn conditional_string(flag bool) string {
if flag {
return 'true_case'
}
return 'false_case'
}
// ===================== MAIN =====================
fn main() {
print_str('--- String Return Tests (Large Struct > 16 bytes) ---')
// Test 1: Basic string literal return
s1 := get_hello()
print_str(s1)
// Test 2: String concatenation return
s2 := get_greeting('Alice')
print_str(s2)
// Test 3: Nested string function calls
s3 := get_formal_greeting('Bob')
print_str(s3)
// Test 4: Multiple parameters concatenation
s4 := concat_three('One', '-Two-', 'Three')
print_str(s4)
// Test 5: Function calling function returning strings
s5 := get_full_message()
print_str(s5)
// Test 6: Chain of string returns
s6 := chain_c()
print_str(s6)
// Test 7: Conditional string returns
s7a := conditional_string(true)
s7b := conditional_string(false)
print_str(s7a)
print_str(s7b)
// Test 8: Direct string operations
direct := 'Direct' + ' ' + 'Test'
print_str(direct)
// Test 9: Interpolation with string return
name := 'Claude'
interp := 'Name: ${name}'
print_str(interp)
// Test 10: Chained string concat expression
chained := 'A' + 'B' + 'C' + 'D' + 'E'
print_str(chained)
print_str('--- All tests completed ---')
}
@@ -0,0 +1,19 @@
module main
type EmptyExpr = u8
struct Node {
value int
}
type Expr = EmptyExpr | Node
const empty_expr = Expr(EmptyExpr(0))
fn main() {
if empty_expr is EmptyExpr {
println('ok')
return
}
println('bad')
}
@@ -0,0 +1,84 @@
module main
type Type = ArrayFixed | Primitive
struct Primitive {
size int
}
struct ArrayFixed {
len int
elem_type Type
}
struct ObjectCommon {
name string
typ Type
}
struct Const {
ObjectCommon
}
struct Global {
ObjectCommon
}
type Object = Const | Global | Type
struct Scope {
mut:
objects map[string]Object
}
fn (s &Scope) lookup(name string) ?Object {
if obj := s.objects[name] {
return obj
}
return none
}
fn (obj &Object) typ() Type {
match obj {
Const {
return obj.typ
}
Global {
return obj.typ
}
Type {
return obj
}
}
}
fn array_len(t Type) int {
return match t {
ArrayFixed {
t.len
}
Primitive {
-1
}
}
}
fn main() {
mut s := Scope{
objects: map[string]Object{}
}
s.objects['buf'] = Object(Global{
name: 'buf'
typ: Type(ArrayFixed{
len: 128
elem_type: Type(Primitive{
size: 8
})
})
})
if obj := s.lookup('buf') {
println(array_len(obj.typ()))
return
}
println(-2)
}
@@ -0,0 +1,102 @@
module main
enum TypeKindShape {
void_t
int_t
float_t
ptr_t
array_t
struct_t
func_t
label_t
metadata_t
}
struct TypeShape {
kind TypeKindShape
width int
elem_type int
len int
fields []int
field_names []string
params []int
ret_type int
is_c_struct bool
is_union bool
is_unsigned bool
}
struct TypeStoreShape {
mut:
types []TypeShape
cache map[string]int
}
struct TargetShape {
ptr_size int
endian_little bool
}
struct ModuleShape {
mut:
name string
target TargetShape
ssa_mod &int = unsafe { nil }
env &int = unsafe { nil }
type_store TypeStoreShape
values []int
instrs []int
blocks []int
funcs []int
globals []int
}
fn lower(src &ModuleShape) ModuleShape {
return ModuleShape{
name: src.name
target: src.target
ssa_mod: src.ssa_mod
env: src.env
type_store: src.type_store
values: src.values.clone()
instrs: src.instrs.clone()
blocks: src.blocks.clone()
funcs: src.funcs.clone()
globals: src.globals.clone()
}
}
fn fixed_array_size(m &ModuleShape, typ_id int) int {
typ := m.type_store.types[typ_id]
if typ.kind == .array_t {
return typ.len * 4
}
return 0
}
fn main() {
mut src := ModuleShape{}
src.name = 'main'
src.target = TargetShape{
ptr_size: 8
endian_little: true
}
src.type_store.cache = map[string]int{}
src.type_store.types = [
TypeShape{
kind: .array_t
elem_type: 1
len: 128
fields: []int{}
params: []int{}
},
]
src.values = [1, 2, 3]
src.instrs = [4, 5]
src.blocks = [6]
src.funcs = [7]
src.globals = [8]
src.type_store.cache['x'] = 0
dst := lower(&src)
println(fixed_array_size(&dst, 0))
}
@@ -0,0 +1,77 @@
module main
enum TypeKindShape {
void_t
int_t
float_t
ptr_t
array_t
struct_t
func_t
label_t
metadata_t
}
struct TypeShape {
kind TypeKindShape
width int
elem_type int
len int
fields []int
field_names []string
params []int
ret_type int
is_c_struct bool
is_union bool
is_unsigned bool
}
struct TypeStoreShape {
mut:
types []TypeShape
cache map[string]int
}
struct ModuleShape {
mut:
type_store TypeStoreShape
}
fn (mut ts TypeStoreShape) register(t TypeShape) int {
id := ts.types.len
ts.types << t
return id
}
fn (mut ts TypeStoreShape) get_array(elem int, length int) int {
key := 'a${elem}_${length}'
if id := ts.cache[key] {
return id
}
id := ts.register(TypeShape{
kind: .array_t
elem_type: elem
len: length
})
ts.cache[key] = id
return id
}
fn main() {
mut ts := TypeStoreShape{
cache: map[string]int{}
}
for i := 0; i < 400; i++ {
ts.get_array(1, 64 + i)
}
src := ModuleShape{
type_store: ts
}
dst := ModuleShape{
type_store: src.type_store
}
println(dst.type_store.types[0].len)
println(dst.type_store.types[63].len)
println(dst.type_store.types[128].len)
println(dst.type_store.types[399].len)
}
@@ -0,0 +1,23 @@
module main
fn read_u32_le_like(data []u8, off int) u32 {
return u32(data[off]) | (u32(data[off + 1]) << 8) | (u32(data[off + 2]) << 16) | (u32(data[
off + 3]) << 24)
}
fn main() {
instr := u32(0x910000a5)
result := (instr & u32(0xffc003ff)) | (u32(0xa0) << 10)
assert result == u32(0x910280a5)
sym_addr := u64(0x1000240e0)
page_off := sym_addr & 0xfff
result_from_sym := (instr & u32(0xffc003ff)) | (u32(page_off) << 10)
assert result_from_sym == u32(0x910380a5)
bytes := [u8(0xa5), 0, 0, 0x91]
instr_from_bytes := read_u32_le_like(bytes, 0)
assert instr_from_bytes == u32(0x910000a5)
result_from_bytes := (instr_from_bytes & u32(0xffc003ff)) | (u32(page_off) << 10)
assert result_from_bytes == u32(0x910380a5)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,628 @@
module main
struct Point {
mut:
x int
y int
}
struct Rect {
origin Point
width int
height int
}
enum Color {
red
green
blue
}
enum Direction {
up
down
left
right
}
fn add(a int, b int) int {
return a + b
}
fn multiply(a int, b int) int {
return a * b
}
fn (p Point) sum() int {
return p.x + p.y
}
fn (p Point) scale(factor int) Point {
return Point{
x: p.x * factor
y: p.y * factor
}
}
fn (r Rect) area() int {
return r.width * r.height
}
fn max(a int, b int) int {
if a > b {
return a
}
return b
}
fn abs(n int) int {
if n < 0 {
return -n
}
return n
}
fn fibonacci(n int) int {
if n <= 1 {
return n
}
mut a := 0
mut b := 1
for i := 2; i <= n; i++ {
c := a + b
a = b
b = c
}
return b
}
fn color_name(c Color) string {
if int(c) == 0 {
return 'red'
} else if int(c) == 1 {
return 'green'
} else if int(c) == 2 {
return 'blue'
}
return 'unknown'
}
fn collatz_steps(start int) int {
mut n := start
mut steps := 0
for n > 1 {
if n % 2 == 0 {
n = n / 2
} else {
n = n * 3 + 1
}
steps = steps + 1
}
return steps
}
fn sum_range(start int, end int) int {
mut total := 0
for i := start; i < end; i++ {
total = total + i
}
return total
}
fn is_even(n int) bool {
return n % 2 == 0
}
fn count_even(limit int) int {
mut c := 0
for i := 0; i < limit; i++ {
if is_even(i) {
c = c + 1
}
}
return c
}
fn direction_dx(d Direction) int {
if int(d) == 2 {
return -1
} else if int(d) == 3 {
return 1
}
return 0
}
fn direction_dy(d Direction) int {
if int(d) == 0 {
return -1
} else if int(d) == 1 {
return 1
}
return 0
}
__global g_val int
fn fib_recursive(n int) int {
if n <= 1 {
return n
}
return fib_recursive(n - 1) + fib_recursive(n - 2)
}
fn factorial_recursive(n int) int {
if n <= 1 {
return 1
}
return n * factorial_recursive(n - 1)
}
fn gcd(a int, b int) int {
if b == 0 {
return a
}
return gcd(b, a % b)
}
fn power(base int, exp int) int {
mut result := 1
for i := 0; i < exp; i++ {
result = result * base
}
return result
}
fn swap_point(mut p Point) {
tmp := p.x
p.x = p.y
p.y = tmp
}
fn scale_point(mut p Point, factor int) {
p.x = p.x * factor
p.y = p.y * factor
}
fn nested_return(x int) int {
if x < 10 {
return 100
} else {
if x < 20 {
return 200
} else {
return 300
}
}
}
fn sum_many(a int, b int, c int, d int, e int, f int, g int, h int) int {
return a + b + c + d + e + f + g + h
}
fn clamp(x int, lo int, hi int) int {
if x < lo {
return lo
} else if x > hi {
return hi
}
return x
}
fn print_str(s string) {
C.puts(s.str)
}
fn print_int(n int) {
C.printf(c'%d\n', n)
}
fn main() {
// 1. Basic arithmetic
x := 10
y := 20
z := add(x, y)
if z > 25 {
print_str('big')
} else {
print_str('small')
}
// 2. For loop with sum
mut sum := 0
for i := 0; i < 5; i++ {
sum = sum + i
}
if sum == 10 {
print_str('sum ok')
}
// 3. Struct and method
p := Point{
x: 3
y: 4
}
ps := p.sum()
if ps == 7 {
print_str('struct ok')
}
// 4. While loop
mut count := 3
for count > 0 {
count = count - 1
}
if count == 0 {
print_str('loop ok2')
}
// 5. Print the sum result
print_int(z)
// 6. Nested function calls
result := add(multiply(3, 4), multiply(5, 6))
if result == 42 {
print_str('nested ok')
}
// 7. Multiple comparisons and boolean logic
a := 15
b := 25
if a < b && b < 30 {
print_str('logic ok')
}
if a == 15 || b == 99 {
print_str('or ok')
}
// 8. Negation and abs
neg := -42
pos := abs(neg)
if pos == 42 {
print_str('abs ok')
}
// 9. Nested if/else
val := 50
if val < 0 {
print_str('negative')
} else if val == 0 {
print_str('zero')
} else if val < 100 {
print_str('medium')
} else {
print_str('large')
}
// 10. Chained arithmetic
chain := (10 + 20) * 3 - 5
if chain == 85 {
print_str('chain ok')
}
// 11. Bitwise operations
bw := 0xFF & 0x0F
if bw == 15 {
print_str('bit and ok')
}
bw2 := 0xA0 | 0x05
if bw2 == 0xA5 {
print_str('bit or ok')
}
bw3 := 1 << 4
if bw3 == 16 {
print_str('shift ok')
}
// 12. Struct method returning struct
p2 := Point{
x: 2
y: 3
}
p3 := p2.scale(10)
if p3.x == 20 && p3.y == 30 {
print_str('scale ok')
}
// 13. Nested struct
r := Rect{
origin: Point{
x: 1
y: 2
}
width: 10
height: 5
}
if r.area() == 50 {
print_str('rect ok')
}
if r.origin.x == 1 && r.origin.y == 2 {
print_str('nested struct ok')
}
// 14. Fibonacci
fib := fibonacci(10)
if fib == 55 {
print_str('fib ok')
}
// 15. Max function
m := max(33, 77)
if m == 77 {
print_str('max ok')
}
// 16. Multiple mutable updates
mut acc := 1
for j := 1; j <= 5; j++ {
acc = acc * j
}
if acc == 120 {
print_str('factorial ok')
}
// 17. Comparison operators
if 10 >= 10 {
print_str('ge ok')
}
if 9 <= 10 {
print_str('le ok')
}
if 10 != 11 {
print_str('ne ok')
}
// 18. Modulo
rem := 17 % 5
if rem == 2 {
print_str('mod ok')
}
// 19. Mutable struct fields
mut mp := Point{
x: 10
y: 20
}
mp.x = 100
mp.y = mp.x + 50
if mp.x == 100 && mp.y == 150 {
print_str('mut struct ok')
}
// 20. Division
div := 100 / 3
if div == 33 {
print_str('div ok')
}
// 21. Enum values
c1 := Color.red
c2 := Color.blue
if int(c1) == 0 && int(c2) == 2 {
print_str('enum ok')
}
// 22. Enum in function
name := color_name(Color.green)
C.printf(c'color: %s\n', name.str)
// 23. Bool return value
if is_even(42) {
print_str('even ok')
}
if !is_even(7) {
print_str('odd ok')
}
// 24. Nested loops
mut total := 0
for i := 0; i < 3; i++ {
for j := 0; j < 4; j++ {
total = total + 1
}
}
if total == 12 {
print_str('nested loops ok')
}
// 25. Break in loop
mut found := -1
for i := 0; i < 100; i++ {
if i * i > 50 {
found = i
break
}
}
if found == 8 {
print_str('break ok')
}
// 26. Continue in loop
mut odd_sum := 0
for i := 0; i < 10; i++ {
if is_even(i) {
continue
}
odd_sum = odd_sum + i
}
if odd_sum == 25 {
print_str('continue ok')
}
// 27. Enum as direction
dx := direction_dx(Direction.right)
dy := direction_dy(Direction.down)
if dx == 1 && dy == 1 {
print_str('direction ok')
}
// 28. Compound assignment operators
mut ca := 10
ca += 5
ca -= 3
ca *= 2
if ca == 24 {
print_str('compound ok')
}
// 29. XOR
xr := 0xFF ^ 0x0F
if xr == 0xF0 {
print_str('xor ok')
}
// 30. Right shift
rs := 256 >> 3
if rs == 32 {
print_str('rshift ok')
}
// 31. Collatz conjecture (complex loop logic)
steps := collatz_steps(27)
if steps == 111 {
print_str('collatz ok')
}
// 32. count_even (function call in loop condition body)
ev := count_even(10)
if ev == 5 {
print_str('count ok')
}
// 33. Struct with zero fields
origin := Point{}
if origin.x == 0 && origin.y == 0 {
print_str('zero struct ok')
}
// 34. Multiple early returns
m2 := max(max(10, 20), max(15, 5))
if m2 == 20 {
print_str('multi ret ok')
}
// 35. sum_range
sr := sum_range(1, 11)
if sr == 55 {
print_str('range sum ok')
}
// 36. Recursive fibonacci
rf := fib_recursive(10)
if rf == 55 {
print_str('rec fib ok')
}
// 37. Recursive factorial
fact := factorial_recursive(6)
if fact == 720 {
print_str('rec fact ok')
}
// 38. GCD
g := gcd(48, 18)
if g == 6 {
print_str('gcd ok')
}
// 39. Power
pw := power(2, 10)
if pw == 1024 {
print_str('power ok')
}
// 40. Heap allocation (&Point{})
hp := &Point{
x: 42
y: 84
}
if hp.x == 42 && hp.y == 84 {
print_str('heap ok')
}
// 41. Mut struct parameter (swap)
mut sp := Point{
x: 10
y: 20
}
swap_point(mut sp)
if sp.x == 20 && sp.y == 10 {
print_str('swap ok')
}
// 42. Mut struct parameter (scale)
mut sc := Point{
x: 5
y: 3
}
scale_point(mut sc, 10)
if sc.x == 50 && sc.y == 30 {
print_str('mut param ok')
}
// 43. Nested return (all branches return)
nr1 := nested_return(5)
nr2 := nested_return(15)
nr3 := nested_return(25)
if nr1 == 100 && nr2 == 200 && nr3 == 300 {
print_str('nested ret ok')
}
// 44. Global variable
g_val = 100
g_val += 50
g_val -= 20
if g_val == 130 {
print_str('global ok')
}
// 45. Many arguments (8 args)
sm := sum_many(1, 2, 3, 4, 5, 6, 7, 8)
if sm == 36 {
print_str('8 args ok')
}
// 46. Infinite loop with break
mut inf := 0
for {
inf++
if inf == 10 {
break
}
}
if inf == 10 {
print_str('inf loop ok')
}
// 47. Assert
assert 2 + 2 == 4
assert 10 > 5
print_str('assert ok')
// 48. Double negation
dn := !!true
if dn {
print_str('double neg ok')
}
// 49. Clamp
cl1 := clamp(5, 0, 10)
cl2 := clamp(-5, 0, 10)
cl3 := clamp(15, 0, 10)
if cl1 == 5 && cl2 == 0 && cl3 == 10 {
print_str('clamp ok')
}
// 50. Heap struct method call
hp2 := &Point{
x: 7
y: 8
}
hs := hp2.sum()
if hs == 15 {
print_str('heap method ok')
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,485 @@
// vtest build: macos
module cleanc
import os
import v2.parser
import v2.pref as vpref
import v2.token
import v2.transformer
import v2.types
fn generate_array_append_c_for_test(code string) string {
return generate_array_append_c_for_test_files([code])
}
fn generate_array_append_c_for_test_files(sources []string) string {
tmp_dir := os.join_path(os.temp_dir(), 'v2_array_append_codegen_test_${os.getpid()}')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_dir) or { panic('failed to create temp dir') }
defer {
os.rmdir_all(tmp_dir) or {}
}
mut paths := []string{cap: sources.len}
for i, code in sources {
tmp_file := os.join_path(tmp_dir, 'file_${i}.v')
os.write_file(tmp_file, code) or { panic('failed to write temp file') }
paths << tmp_file
}
prefs := &vpref.Preferences{
backend: .cleanc
no_parallel: true
}
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(prefs)
files := par.parse_files(paths, mut file_set)
env := types.Environment.new()
mut checker := types.Checker.new(prefs, file_set, env)
checker.check_files(files)
mut trans := transformer.Transformer.new_with_pref(env, prefs)
mut gen := Gen.new_with_env_and_pref(trans.transform_files(files), env, prefs)
return gen.gen()
}
fn test_generate_c_uses_push_many_for_array_slice_append() {
csrc := generate_array_append_c_for_test('
fn main() {
mut dst := []u8{}
haystack := [u8(1), 2, 3]
dst << haystack[1..3]
}
')
assert csrc.contains('array__push_many(')
assert !csrc.contains('&(u8[1]){array__slice(haystack, 1, 3)}')
}
fn test_generate_c_uses_push_many_for_array_literal_append() {
csrc := generate_array_append_c_for_test('
fn main() {
mut dst := []u8{}
dst << [u8(`$`)]
}
')
assert csrc.contains('array__push_many(')
assert !csrc.contains('&(u8[1]){new_array_from_c_array(')
}
fn test_generate_c_uses_push_many_for_local_array_append() {
csrc := generate_array_append_c_for_test('
fn source() []u8 {
return [u8(1), 2]
}
fn main() {
mut dst := []u8{}
src := source()
dst << src
}
')
assert csrc.contains('array__push_many(')
assert !csrc.contains('&(u8[1]){src}')
}
fn test_generate_c_push_many_to_mut_array_param_uses_param_pointer() {
csrc := generate_array_append_c_for_test('
fn extend(mut dst []usize, src []usize) {
dst << src
}
')
assert csrc.contains('array__push_many(')
assert csrc.contains('array__push_many(((array*)(dst)),')
assert !csrc.contains('array__push_many((array*)&dst,')
assert !csrc.contains('array__push_many(((array*)(&dst)),')
}
fn test_generate_c_lowered_push_many_to_mut_array_param_uses_param_pointer() {
csrc := generate_array_append_c_for_test('
struct Index {
entries map[string][]usize
}
fn (idx Index) matches_into(candidate string, mut matches []usize) {
if hits := idx.entries[candidate] {
matches << hits
}
}
')
assert csrc.contains('array__push_many(')
assert csrc.contains('array__push_many(((array*)(matches)),')
assert !csrc.contains('array__push_many((array*)&matches,')
assert !csrc.contains('array__push_many(((array*)(&matches)),')
}
fn test_generate_c_uses_push_many_for_fn_pointer_array_return_append() {
csrc := generate_array_append_c_for_test('
fn source(data []u8) []u8 {
return data
}
fn consume(hash_func fn ([]u8) []u8) {
mut dst := []u8{}
src := hash_func(dst)
dst << src
}
fn main() {
consume(source)
}
')
assert csrc.contains('array__push_many(')
assert !csrc.contains('&(u8[1]){src}')
}
fn test_generate_c_keeps_bitwise_or_inside_array_append_value() {
csrc := generate_array_append_c_for_test('
fn main() {
mut bytes := []u8{}
n1 := u8(1)
n0 := u8(2)
bytes << (n1 << 4) | n0
}
')
assert csrc.contains('array__push(')
assert csrc.contains('| n0')
assert !csrc.contains('array__push_many(')
assert !csrc.contains('_arr_push_many_tmp_')
assert !csrc.contains('}) | n0')
}
fn test_generate_c_wraps_mut_u8_array_param_bitwise_append_value() {
csrc := generate_array_append_c_for_test('
fn write_int(mut out []u8, value u64, high_bits u8) {
out << high_bits | u8(value)
}
')
assert csrc.contains('array__push(((array*)(out)), &(u8[1]){')
assert !csrc.contains('array__push(((array*)(out)), (high_bits | ((u8)(value))))')
}
fn test_generate_c_appends_for_mut_pointer_value_to_array() {
csrc := generate_array_append_c_for_test('
struct PullRequest {
mut:
repo_name string
}
fn copy(mut prs []PullRequest) []PullRequest {
mut out := []PullRequest{}
for mut pr in prs {
pr.repo_name = "x"
out << pr
}
return out
}
')
assert csrc.contains('array__push(((array*)(&out)), pr);')
assert !csrc.contains('array__push(((array*)(&out)), &(PullRequest[1]){pr});')
}
fn test_generate_c_indexes_local_array_that_shadows_function_name() {
csrc := generate_array_append_c_for_test('
fn bytes() []u8 {
return []u8{}
}
fn main() {
mut bytes := [u8(1)]
bytes[0] &= u8(3)
}
')
assert csrc.contains('((u8*)bytes.data)[((int)(0))] &= ((u8)(3));')
assert !csrc.contains('bytes &= ((u8)(3));')
}
fn test_generate_c_uses_typed_zero_for_array_tuple_if_declarations() {
csrc := generate_array_append_c_for_test('
fn pick(operand_a []u64, operand_b []u64) {
mut a, mut b := if operand_a.len >= operand_b.len {
operand_a, operand_b
} else {
operand_b, operand_a
}
_ = a
_ = b
}
')
assert csrc.contains('Array_u64 a = ((Array_u64){0});')
assert csrc.contains('Array_u64 b = ((Array_u64){0});')
assert !csrc.contains('Array_u64 a = 0;')
assert !csrc.contains('Array_u64 b = 0;')
}
fn test_generate_c_uses_default_init_for_empty_array_struct_fields() {
csrc := generate_array_append_c_for_test('
struct Pool {
items []voidptr
}
fn main() {
pool := Pool{
items: []
}
_ = pool.items.len
}
')
assert csrc.contains('.items = __new_array_with_default_noscan(0, 0, sizeof(void*)')
assert !csrc.contains('.items = new_array_from_c_array(0, 0, sizeof(void*)')
}
fn test_generate_c_specializes_generic_pointer_array_push_literal() {
csrc := generate_array_append_c_for_test('
struct Foo {}
fn get_results_ref[T](items []voidptr) []&T {
mut res := []&T{cap: items.len}
for i in 0 .. items.len {
res << unsafe { &T(items[i]) }
}
return res
}
fn main() {
items := []voidptr{}
res := get_results_ref[Foo](items)
_ = res.len
}
')
assert csrc.contains('(Foo*[1]){((Foo*)')
assert !csrc.contains('(T*[1])')
assert !csrc.contains('stdatomic__T')
}
fn test_generate_c_uses_captured_mut_array_pointer_for_closure_push() {
csrc := generate_array_append_c_for_test('
fn consume(cb fn (int) bool) {
_ = cb(1)
}
fn main() {
mut values := []int{}
consume(fn [mut values] (value int) bool {
values << value
return true
})
}
')
assert csrc.contains('Array_int* values = _anon_fn_')
assert csrc.contains('array__push(((array*)(values)),')
assert !csrc.contains('array__push(((array*)(&values)),')
}
fn test_generate_c_updates_captured_mut_scalar_for_closure_postfix() {
csrc := generate_array_append_c_for_test('
fn consume(cb fn () bool) {
_ = cb()
}
fn main() {
mut count := u64(0)
consume(fn [mut count] () bool {
count++
return true
})
}
')
assert csrc.contains('u64* count = _anon_fn_')
assert csrc.contains('(*count)++')
assert !csrc.contains('count++;')
}
fn test_generate_c_evaluates_generic_comptime_is_against_implements() {
csrc := generate_array_append_c_for_test('
interface Writer {
write([]u8) !int
}
struct Buffer implements Writer {}
fn (mut b Buffer) write(buf []u8) !int {
_ = b
return buf.len
}
fn choose[T]() int {
$if T is Writer {
return 1
} $else {
return 2
}
}
fn main() {
_ = choose[Buffer]()
}
')
assert csrc.contains('return 1;')
assert !csrc.contains('return 2;')
}
fn test_generate_c_specializes_comptime_pointer_field_generic_call_by_field_type() {
csrc := generate_array_append_c_for_test('
fn struct_field_should_encode[T](val T) bool {
_ = val
return true
}
fn encode_fields[T](val T) bool {
mut ok := true
$for field in T.fields {
ok = struct_field_should_encode(val.$(field.name))
}
return ok
}
struct Node {
next &Node = unsafe { nil }
}
fn main() {
_ = encode_fields(Node{})
}
')
assert csrc.contains('struct_field_should_encode_T_Nodeptr(val.next)')
assert !csrc.contains('struct_field_should_encode_T_Node((*val.next))')
}
fn test_generate_c_specializes_comptime_generic_pointer_field_by_field_type() {
csrc := generate_array_append_c_for_test('
module json2
struct ValueInfo {}
struct Node[T] {
value T
next &Node[T] = unsafe { nil }
}
struct Decoder {
current_node &Node[ValueInfo] = unsafe { nil }
}
struct EncoderFieldInfo {}
struct Encoder {}
fn (mut encoder Encoder) encode_value[T](val T) {
$if T is $pointer {
encoder.encode_value(*val)
} $else $if T is $struct {
_ = encoder.encode_fields[T](val)
}
}
fn (mut encoder Encoder) cached_field_infos[T]() []EncoderFieldInfo {
return []EncoderFieldInfo{}
}
fn (mut encoder Encoder) encode_struct_field_value[T](val T) {
$if T.indirections == 1 {
encoder.encode_value(*val)
} $else {
encoder.encode_value(val)
}
}
fn struct_field_should_encode[T](field_info EncoderFieldInfo, val T) bool {
_ = field_info
_ = val
return true
}
fn (mut encoder Encoder) encode_fields[T](val T) bool {
field_infos := encoder.cached_field_infos[T]()
mut ok := true
mut i := 0
$for field in T.fields {
field_info := field_infos[i]
ok = struct_field_should_encode(field_info, val.$(field.name))
encoder.encode_struct_field_value(val.$(field.name))
i++
}
return ok
}
fn use_encoder() {
mut encoder := Encoder{}
_ = encoder.encode_fields(Decoder{})
_ = encoder.encode_fields(Node[ValueInfo]{})
}
')
assert csrc.contains('json2__struct_field_should_encode_T_json2_Nodeptr(field_info, val.current_node)')
assert csrc.contains('json2__Encoder__encode_struct_field_value_T_json2_Nodeptr(encoder, val.current_node)')
assert csrc.contains('json2__Encoder__cached_field_infos_T_json2_Decoder(encoder)')
assert csrc.contains('Array_json2__EncoderFieldInfo json2__Encoder__cached_field_infos_T_json2_Node(json2__Encoder* encoder)')
assert !csrc.contains('json2__struct_field_should_encode_T_json2_Node(field_info, (*val.current_node))')
assert !csrc.contains('json2__Encoder__encode_struct_field_value_T_json2_Node(encoder, (*val.current_node))')
}
fn test_generate_c_extracts_array_sum_variant_for_generic_array_param() {
csrc := generate_array_append_c_for_test_files([
'
module orm
pub struct InfixType {
right int
}
pub type Primitive = InfixType | []InfixType | int
pub fn tenant_filter_array_primitive_type[T](value []T) int {
return value.len
}
pub fn tenant_filter_primitive_type(value Primitive) int {
return match value {
InfixType {
2
}
[]InfixType {
tenant_filter_array_primitive_type(value)
}
int {
3
}
}
}
',
'
module main
import orm
fn main() {
_ = orm.tenant_filter_primitive_type([]orm.InfixType{})
}
',
])
assert csrc.contains('orm__tenant_filter_array_primitive_type_T_orm_InfixType(((((Array_orm__InfixType*)(value._data._Array_orm__InfixType))')
assert !csrc.contains('orm__tenant_filter_array_primitive_type_T_orm_InfixType(((((orm__InfixType*)(value._data._InfixType))')
}
fn test_generate_c_lowers_global_interface_generic_method_call() {
csrc := generate_array_append_c_for_test('
interface Rng {
intn(max int) !int
}
__global default_rng &Rng
fn (mut rng Rng) element[T](array []T) !T {
if array.len == 0 {
return error("empty")
}
return array[rng.intn(array.len)!]
}
fn element[T](array []T) !T {
return default_rng.element[T](array)
}
fn main() {
_ = element[u8]([u8(1)]) or { u8(0) }
}
')
assert csrc.contains('Rng__element_T_u8(default_rng, _v_array)')
assert !csrc.contains('u8 _val = (_v_array)')
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 11957 files, more files were not shown because too many files have changed in this diff. Show more