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

@@ -0,0 +1,537 @@
module transformer
import os
import v2.ast
import v2.parser
import v2.pref as vpref
import v2.token
import v2.types
fn auto_str_parse_files_for_test(label string, source string) ([]ast.File, ast.FlatAst, &types.Environment, &vpref.Preferences) {
tmp_file := os.join_path(os.vtmp_dir(), 'v2_auto_str_generation_${label}_${os.getpid()}.v')
os.write_file(tmp_file, source) or { panic('failed to write temp file') }
defer {
os.rm(tmp_file) or {}
}
mut prefs := &vpref.Preferences{
backend: .x64
no_parallel: true
}
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(prefs)
files := par.parse_files([tmp_file], mut file_set)
mut env := types.Environment.new()
mut checker := types.Checker.new(prefs, file_set, env)
checker.check_files(files)
flat := ast.flatten_files(files)
return files, flat, env, prefs
}
fn auto_str_legacy_generated_files(label string, source string) []ast.File {
files, _, env, prefs := auto_str_parse_files_for_test(label, source)
mut trans := Transformer.new_with_pref(env, prefs)
return trans.transform_files(files)
}
fn auto_str_flat_generated_files(label string, source string) []ast.File {
files, flat, env, prefs := auto_str_parse_files_for_test(label, source)
mut trans := Transformer.new_with_pref(env, prefs)
transformed_flat, _ := trans.transform_files_to_flat_via_driver(&flat, files)
return transformed_flat.to_files()
}
fn auto_str_generated_fn_names(files []ast.File) []string {
mut names := []string{}
for file in files {
for stmt in file.stmts {
if stmt is ast.FnDecl && stmt.name.ends_with('__str') {
names << stmt.name
}
}
}
names.sort()
return names
}
fn assert_auto_str_names(files []ast.File, expected []string) {
names := auto_str_generated_fn_names(files)
for name in expected {
assert name in names, '${name} missing from generated names ${names}'
}
}
fn auto_str_generated_fn(files []ast.File, name string) ?ast.FnDecl {
for file in files {
for stmt in file.stmts {
if stmt is ast.FnDecl && stmt.name == name {
return stmt
}
}
}
return none
}
fn auto_str_collect_stmt_string_literals(stmt ast.Stmt, mut values []string) {
match stmt {
ast.AssignStmt {
for expr in stmt.lhs {
auto_str_collect_expr_string_literals(expr, mut values)
}
for expr in stmt.rhs {
auto_str_collect_expr_string_literals(expr, mut values)
}
}
ast.ExprStmt {
auto_str_collect_expr_string_literals(stmt.expr, mut values)
}
ast.ReturnStmt {
for expr in stmt.exprs {
auto_str_collect_expr_string_literals(expr, mut values)
}
}
else {}
}
}
fn auto_str_collect_expr_string_literals(expr ast.Expr, mut values []string) {
match expr {
ast.AsCastExpr {
auto_str_collect_expr_string_literals(expr.expr, mut values)
auto_str_collect_expr_string_literals(expr.typ, mut values)
}
ast.CallExpr {
for arg in expr.args {
auto_str_collect_expr_string_literals(arg, mut values)
}
}
ast.CastExpr {
auto_str_collect_expr_string_literals(expr.expr, mut values)
auto_str_collect_expr_string_literals(expr.typ, mut values)
}
ast.IfExpr {
auto_str_collect_expr_string_literals(expr.cond, mut values)
for nested in expr.stmts {
auto_str_collect_stmt_string_literals(nested, mut values)
}
auto_str_collect_expr_string_literals(expr.else_expr, mut values)
}
ast.InfixExpr {
auto_str_collect_expr_string_literals(expr.lhs, mut values)
auto_str_collect_expr_string_literals(expr.rhs, mut values)
}
ast.ModifierExpr {
auto_str_collect_expr_string_literals(expr.expr, mut values)
}
ast.ParenExpr {
auto_str_collect_expr_string_literals(expr.expr, mut values)
}
ast.PrefixExpr {
auto_str_collect_expr_string_literals(expr.expr, mut values)
}
ast.SelectorExpr {
auto_str_collect_expr_string_literals(expr.lhs, mut values)
}
ast.StringLiteral {
values << expr.value
}
else {}
}
}
fn auto_str_fn_string_literals(files []ast.File, fn_name string) []string {
fn_decl := auto_str_generated_fn(files, fn_name) or {
assert false, 'missing generated function ${fn_name}'
return []string{}
}
mut values := []string{}
for stmt in fn_decl.stmts {
auto_str_collect_stmt_string_literals(stmt, mut values)
}
return values
}
fn auto_str_has_literal_prefix(values []string, prefix string) bool {
for value in values {
if value.starts_with(prefix) {
return true
}
}
return false
}
fn auto_str_collect_stmt_call_names(stmt ast.Stmt, mut names []string) {
match stmt {
ast.AssignStmt {
for expr in stmt.lhs {
auto_str_collect_expr_call_names(expr, mut names)
}
for expr in stmt.rhs {
auto_str_collect_expr_call_names(expr, mut names)
}
}
ast.ExprStmt {
auto_str_collect_expr_call_names(stmt.expr, mut names)
}
ast.ForStmt {
auto_str_collect_stmt_call_names(stmt.init, mut names)
auto_str_collect_expr_call_names(stmt.cond, mut names)
auto_str_collect_stmt_call_names(stmt.post, mut names)
for nested in stmt.stmts {
auto_str_collect_stmt_call_names(nested, mut names)
}
}
ast.ReturnStmt {
for expr in stmt.exprs {
auto_str_collect_expr_call_names(expr, mut names)
}
}
else {}
}
}
fn auto_str_collect_expr_call_names(expr ast.Expr, mut names []string) {
match expr {
ast.AsCastExpr {
auto_str_collect_expr_call_names(expr.expr, mut names)
auto_str_collect_expr_call_names(expr.typ, mut names)
}
ast.CallExpr {
if expr.lhs is ast.Ident {
lhs := expr.lhs as ast.Ident
names << lhs.name
}
for arg in expr.args {
auto_str_collect_expr_call_names(arg, mut names)
}
}
ast.CastExpr {
auto_str_collect_expr_call_names(expr.expr, mut names)
auto_str_collect_expr_call_names(expr.typ, mut names)
}
ast.IfExpr {
auto_str_collect_expr_call_names(expr.cond, mut names)
for nested in expr.stmts {
auto_str_collect_stmt_call_names(nested, mut names)
}
auto_str_collect_expr_call_names(expr.else_expr, mut names)
}
ast.InfixExpr {
auto_str_collect_expr_call_names(expr.lhs, mut names)
auto_str_collect_expr_call_names(expr.rhs, mut names)
}
ast.ModifierExpr {
auto_str_collect_expr_call_names(expr.expr, mut names)
}
ast.ParenExpr {
auto_str_collect_expr_call_names(expr.expr, mut names)
}
ast.PrefixExpr {
auto_str_collect_expr_call_names(expr.expr, mut names)
}
ast.SelectorExpr {
auto_str_collect_expr_call_names(expr.lhs, mut names)
}
ast.StringInterLiteral {
for inter in expr.inters {
auto_str_collect_expr_call_names(inter.expr, mut names)
auto_str_collect_expr_call_names(inter.format_expr, mut names)
}
}
else {}
}
}
fn auto_str_fn_call_names(files []ast.File, fn_name string) []string {
fn_decl := auto_str_generated_fn(files, fn_name) or {
assert false, 'missing generated function ${fn_name}'
return []string{}
}
mut names := []string{}
for stmt in fn_decl.stmts {
auto_str_collect_stmt_call_names(stmt, mut names)
}
return names
}
fn test_array_str_generation_uses_helper_name_for_elem_type_when_metadata_is_stale() {
env := types.Environment.new()
mut trans := Transformer.new_with_pref(env, &vpref.Preferences{})
stmt := trans.generate_array_str_fn('Array_int_str', 'ast__KeywordOperator__str')
if stmt is ast.FnDecl {
mut calls := []string{}
for body_stmt in stmt.stmts {
auto_str_collect_stmt_call_names(body_stmt, mut calls)
}
assert 'int__str' in calls, calls.str()
assert 'ast__KeywordOperator__str__str' !in calls, calls.str()
return
}
assert false, 'expected generated Array_int_str function'
}
fn auto_str_collect_stmt_selector_names(stmt ast.Stmt, mut names []string) {
match stmt {
ast.AssignStmt {
for expr in stmt.lhs {
auto_str_collect_expr_selector_names(expr, mut names)
}
for expr in stmt.rhs {
auto_str_collect_expr_selector_names(expr, mut names)
}
}
ast.ExprStmt {
auto_str_collect_expr_selector_names(stmt.expr, mut names)
}
ast.ReturnStmt {
for expr in stmt.exprs {
auto_str_collect_expr_selector_names(expr, mut names)
}
}
else {}
}
}
fn auto_str_collect_expr_selector_names(expr ast.Expr, mut names []string) {
match expr {
ast.AsCastExpr {
auto_str_collect_expr_selector_names(expr.expr, mut names)
auto_str_collect_expr_selector_names(expr.typ, mut names)
}
ast.CallExpr {
for arg in expr.args {
auto_str_collect_expr_selector_names(arg, mut names)
}
}
ast.CastExpr {
auto_str_collect_expr_selector_names(expr.expr, mut names)
auto_str_collect_expr_selector_names(expr.typ, mut names)
}
ast.IfExpr {
auto_str_collect_expr_selector_names(expr.cond, mut names)
for nested in expr.stmts {
auto_str_collect_stmt_selector_names(nested, mut names)
}
auto_str_collect_expr_selector_names(expr.else_expr, mut names)
}
ast.InfixExpr {
auto_str_collect_expr_selector_names(expr.lhs, mut names)
auto_str_collect_expr_selector_names(expr.rhs, mut names)
}
ast.ModifierExpr {
auto_str_collect_expr_selector_names(expr.expr, mut names)
}
ast.ParenExpr {
auto_str_collect_expr_selector_names(expr.expr, mut names)
}
ast.PrefixExpr {
auto_str_collect_expr_selector_names(expr.expr, mut names)
}
ast.SelectorExpr {
names << expr.rhs.name
auto_str_collect_expr_selector_names(expr.lhs, mut names)
}
ast.StringInterLiteral {
for inter in expr.inters {
auto_str_collect_expr_selector_names(inter.expr, mut names)
auto_str_collect_expr_selector_names(inter.format_expr, mut names)
}
}
else {}
}
}
fn auto_str_fn_selector_names(files []ast.File, fn_name string) []string {
fn_decl := auto_str_generated_fn(files, fn_name) or {
assert false, 'missing generated function ${fn_name}'
return []string{}
}
mut names := []string{}
for stmt in fn_decl.stmts {
auto_str_collect_stmt_selector_names(stmt, mut names)
}
return names
}
fn assert_auto_str_simple_point_body(files []ast.File) {
literals := auto_str_fn_string_literals(files, 'Point__str')
calls := auto_str_fn_call_names(files, 'Point__str')
assert 'Point{}' !in literals
assert auto_str_has_literal_prefix(literals, 'Point{')
assert ' x: ' in literals
assert ' y: ' in literals
assert 'strings__Builder__write_string' in calls
assert 'strings__Builder__str' in calls
}
fn assert_auto_str_recursive_node_body(files []ast.File) {
literals := auto_str_fn_string_literals(files, 'Node__str')
calls := auto_str_fn_call_names(files, 'Node__str')
assert 'Node{}' !in literals
assert auto_str_has_literal_prefix(literals, 'Node{')
assert ' value: ' in literals
assert ' left: ' in literals
assert ' right: ' in literals
assert 'Tree__str' in calls
assert 'string__replace' in calls
}
fn assert_auto_str_tree_sumtype_body(files []ast.File) {
literals := auto_str_fn_string_literals(files, 'Tree__str')
calls := auto_str_fn_call_names(files, 'Tree__str')
selectors := auto_str_fn_selector_names(files, 'Tree__str')
assert 'Tree(' in literals
assert ')' in literals
assert 'Tree{}' in literals
assert 'Empty__str' in calls
assert 'Node__str' in calls
assert '_tag' in selectors
}
fn assert_auto_str_main_uses_node_str(files []ast.File) {
calls := auto_str_fn_call_names(files, 'main')
assert 'Node__str' in calls
}
fn test_auto_struct_str_generates_field_struct_helper_from_legacy_and_flat() {
source := '
module main
struct Point {
x int
y int
}
fn show(point Point) string {
return "\${point}"
}
'
legacy := auto_str_legacy_generated_files('simple_struct_legacy', source)
flat := auto_str_flat_generated_files('simple_struct_flat', source)
assert_auto_str_names(legacy, ['Point__str'])
assert_auto_str_names(flat, ['Point__str'])
assert_auto_str_simple_point_body(legacy)
assert_auto_str_simple_point_body(flat)
}
fn test_auto_sumtype_str_generates_wrapper_and_variant_helpers_from_legacy_and_flat() {
source := '
module main
type Tree = Empty | Node
struct Empty {}
struct Node {
value int
}
fn show(tree Tree) string {
return "\${tree}"
}
'
legacy := auto_str_legacy_generated_files('sumtype_legacy', source)
flat := auto_str_flat_generated_files('sumtype_flat', source)
assert_auto_str_names(legacy, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_names(flat, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_tree_sumtype_body(legacy)
assert_auto_str_tree_sumtype_body(flat)
}
fn test_auto_recursive_struct_sumtype_str_generates_helpers_from_legacy_and_flat() {
source := '
module main
type Tree = Empty | Node
struct Empty {}
struct Node {
value int
left Tree
right Tree
}
fn show(node Node) string {
return "\${node}"
}
'
legacy := auto_str_legacy_generated_files('recursive_legacy', source)
flat := auto_str_flat_generated_files('recursive_flat', source)
assert_auto_str_names(legacy, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_names(flat, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_recursive_node_body(legacy)
assert_auto_str_recursive_node_body(flat)
assert_auto_str_tree_sumtype_body(legacy)
assert_auto_str_tree_sumtype_body(flat)
}
fn test_auto_recursive_struct_sumtype_str_from_call_arg_interpolation_legacy_and_flat() {
source := '
module main
type Tree = Empty | Node
struct Empty {}
struct Node {
value int
left Tree
right Tree
}
fn sink(s string) {
_ = s
}
fn main() {
leaf := Node{7, Empty{}, Empty{}}
root := Node{9, leaf, Empty{}}
sink("\${root}")
}
'
legacy := auto_str_legacy_generated_files('recursive_call_arg_legacy', source)
flat := auto_str_flat_generated_files('recursive_call_arg_flat', source)
assert_auto_str_names(legacy, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_names(flat, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_main_uses_node_str(legacy)
assert_auto_str_main_uses_node_str(flat)
assert_auto_str_recursive_node_body(legacy)
assert_auto_str_recursive_node_body(flat)
assert_auto_str_tree_sumtype_body(legacy)
assert_auto_str_tree_sumtype_body(flat)
}
fn test_auto_recursive_struct_sumtype_str_from_implicit_main_call_arg_legacy_and_flat() {
source := '
type Tree = Empty | Node
struct Empty {}
struct Node {
value int
left Tree
right Tree
}
fn sink(s string) {
_ = s
}
fn main() {
node1 := Node{30, Empty{}, Empty{}}
node2 := Node{20, Empty{}, Empty{}}
tree := Node{10, node1, node2}
sink("\${tree}")
}
'
legacy := auto_str_legacy_generated_files('recursive_implicit_main_call_arg_legacy', source)
flat := auto_str_flat_generated_files('recursive_implicit_main_call_arg_flat', source)
assert_auto_str_names(legacy, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_names(flat, ['Empty__str', 'Node__str', 'Tree__str'])
assert_auto_str_main_uses_node_str(legacy)
assert_auto_str_main_uses_node_str(flat)
assert_auto_str_recursive_node_body(legacy)
assert_auto_str_recursive_node_body(flat)
assert_auto_str_tree_sumtype_body(legacy)
assert_auto_str_tree_sumtype_body(flat)
}
@@ -0,0 +1,167 @@
// 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 transformer
import os
import v2.ast
import v2.parser
import v2.pref as vpref
import v2.token
import v2.types
fn parse_optional_test_code(code string, target_os string, defines []string) []ast.File {
tmp_file := '/tmp/v2_parser_optional_cond_test_${os.getpid()}.v'
os.write_file(tmp_file, code) or { panic('failed to write temp file') }
defer {
os.rm(tmp_file) or {}
}
prefs := &vpref.Preferences{
backend: .cleanc
no_parallel: true
target_os: target_os
user_defines: defines
explicit_user_defines: defines.clone()
}
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(prefs)
return par.parse_files([tmp_file], mut file_set)
}
fn transform_optional_test_code(code string, target_os string, defines []string) []ast.File {
tmp_file := '/tmp/v2_transformer_optional_cond_test_${os.getpid()}.v'
os.write_file(tmp_file, code) or { panic('failed to write temp file') }
defer {
os.rm(tmp_file) or {}
}
prefs := &vpref.Preferences{
backend: .cleanc
no_parallel: true
target_os: target_os
user_defines: defines
explicit_user_defines: defines.clone()
}
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(prefs)
files := par.parse_files([tmp_file], mut file_set)
mut env := types.Environment.new()
mut checker := types.Checker.new(prefs, file_set, env)
checker.check_files(files)
mut trans := Transformer.new_with_pref(env, prefs)
return trans.transform_files(files)
}
fn optional_test_struct_field_names(file ast.File, struct_name string) []string {
for stmt in file.stmts {
if stmt is ast.StructDecl && stmt.name == struct_name {
mut names := []string{cap: stmt.fields.len}
for field in stmt.fields {
names << field.name
}
return names
}
}
return []string{}
}
fn optional_test_fn_stmt_count(files []ast.File, name string) int {
for file in files {
for stmt in file.stmts {
if stmt is ast.FnDecl && stmt.name == name {
return stmt.stmts.len
}
}
}
return -1
}
fn optional_test_first_return_number(files []ast.File, name string) string {
for file in files {
for stmt in file.stmts {
if stmt is ast.FnDecl && stmt.name == name {
for nested in stmt.stmts {
if nested is ast.ReturnStmt && nested.exprs.len == 1
&& nested.exprs[0] is ast.BasicLiteral {
return (nested.exprs[0] as ast.BasicLiteral).value
}
}
}
}
}
return ''
}
fn test_parser_struct_field_attr_optional_os_requires_user_define() {
source := '
module main
struct Container {
name string
plain_linux int @[if linux]
optional_linux int @[if linux ?]
}
'
no_define_files := parse_optional_test_code(source, 'linux', [])
assert no_define_files.len == 1
assert optional_test_struct_field_names(no_define_files[0], 'Container') == [
'name',
'plain_linux',
]
defined_files := parse_optional_test_code(source, 'linux', ['linux'])
assert defined_files.len == 1
assert optional_test_struct_field_names(defined_files[0], 'Container') == ['name', 'plain_linux',
'optional_linux']
}
fn test_transformer_comptime_optional_os_requires_user_define() {
source := '
module main
fn optional_branch() int {
$if linux ? {
return 1
} $else {
return 2
}
}
fn plain_os_branch() int {
$if linux {
return 3
} $else {
return 4
}
}
'
no_define_files := transform_optional_test_code(source, 'linux', [])
assert optional_test_first_return_number(no_define_files, 'optional_branch') == '2'
assert optional_test_first_return_number(no_define_files, 'plain_os_branch') == '3'
defined_files := transform_optional_test_code(source, 'linux', ['linux'])
assert optional_test_first_return_number(defined_files, 'optional_branch') == '1'
assert optional_test_first_return_number(defined_files, 'plain_os_branch') == '3'
}
fn test_transformer_function_attr_optional_os_requires_user_define() {
source := '
module main
@[if linux ?]
fn optional_fn() int {
return 1
}
@[if linux]
fn plain_os_fn() int {
return 2
}
'
no_define_files := transform_optional_test_code(source, 'linux', [])
assert optional_test_fn_stmt_count(no_define_files, 'optional_fn') == 0
assert optional_test_fn_stmt_count(no_define_files, 'plain_os_fn') > 0
defined_files := transform_optional_test_code(source, 'linux', ['linux'])
assert optional_test_fn_stmt_count(defined_files, 'optional_fn') > 0
assert optional_test_fn_stmt_count(defined_files, 'plain_os_fn') > 0
}
@@ -0,0 +1,227 @@
// 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
//
// Bit-equality pin for s165: `explicit_str_method_fn_names_from_flat`
// (FlatAst input) must produce the same `map[string]bool` as the legacy
// `explicit_str_method_fn_names([]ast.File)` across the module-prefix
// branches: main excluded, builtin excluded, module-prefixed name short-
// circuit (already-`__`), no-method skip, and non-str method skip.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
fn create_str_fn_names_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
}
}
fn make_str_method_decl(recv_name string, fn_name string) ast.Stmt {
return ast.Stmt(ast.FnDecl{
name: fn_name
is_method: true
receiver: ast.Parameter{
name: 'r'
typ: ast.Expr(ast.Ident{
name: recv_name
})
}
typ: ast.FnType{
return_type: ast.Ident{
name: 'string'
}
}
})
}
fn make_plain_fn_decl(fn_name string) ast.Stmt {
return ast.Stmt(ast.FnDecl{
name: fn_name
is_method: false
typ: ast.FnType{}
})
}
fn assert_maps_equal(legacy map[string]bool, fromflat map[string]bool) {
assert legacy.len == fromflat.len
for k, v in legacy {
assert fromflat[k] == v
}
for k, v in fromflat {
assert legacy[k] == v
}
}
fn run_str_fn_names_parity(files []ast.File) {
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_main_module_no_prefix() {
// In `main` the recv_name is NOT module-prefixed.
files := [
ast.File{
name: 'main.v'
mod: 'main'
stmts: [make_str_method_decl('Foo', 'str')]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 1
assert legacy['Foo__str']
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_builtin_module_no_prefix() {
// In `builtin` the recv_name is NOT module-prefixed either.
files := [
ast.File{
name: 'builtin.v'
mod: 'builtin'
stmts: [make_str_method_decl('string', 'str')]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 1
assert legacy['string__str']
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_named_module_gets_prefix() {
// In a non-main/non-builtin module the recv_name is module-prefixed.
files := [
ast.File{
name: 'time.v'
mod: 'time'
stmts: [make_str_method_decl('Duration', 'str')]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 1
assert legacy['time__Duration__str']
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_dotted_module_replaced() {
// Module 'os.windows' -> recv_name prefix 'os__windows__'.
files := [
ast.File{
name: 'win.v'
mod: 'os.windows'
stmts: [make_str_method_decl('Handle', 'str')]
},
]
run_str_fn_names_parity(files)
}
fn test_explicit_str_method_fn_names_from_flat_already_prefixed_skips_prefix() {
// recv_name already containing `__` does NOT get a second prefix.
files := [
ast.File{
name: 'time.v'
mod: 'time'
stmts: [make_str_method_decl('time__Duration', 'str')]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 1
assert legacy['time__Duration__str']
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_non_str_method_ignored() {
// is_method && name != 'str' must NOT be picked up.
files := [
ast.File{
name: 'main.v'
mod: 'main'
stmts: [make_str_method_decl('Foo', 'other')]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 0
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_plain_fn_str_ignored() {
// A plain (non-method) `fn str()` must NOT be picked up.
files := [
ast.File{
name: 'main.v'
mod: 'main'
stmts: [make_plain_fn_decl('str')]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 0
assert_maps_equal(legacy, fromflat)
}
fn test_explicit_str_method_fn_names_from_flat_multi_file_collects_all() {
// Multiple files / modules — both paths must collect the same set.
files := [
ast.File{
name: 'main.v'
mod: 'main'
stmts: [
make_str_method_decl('Foo', 'str'),
make_str_method_decl('Bar', 'other'),
]
},
ast.File{
name: 'time.v'
mod: 'time'
stmts: [
make_str_method_decl('Duration', 'str'),
]
},
ast.File{
name: 'builtin.v'
mod: 'builtin'
stmts: [
make_str_method_decl('string', 'str'),
]
},
]
t := create_str_fn_names_test_transformer()
flat := ast.flatten_files(files)
legacy := t.explicit_str_method_fn_names(files)
fromflat := t.explicit_str_method_fn_names_from_flat(&flat)
assert legacy.len == 3
assert legacy['Foo__str']
assert legacy['time__Duration__str']
assert legacy['string__str']
assert_maps_equal(legacy, fromflat)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,183 @@
module transformer
import os
import v2.ast
import v2.parser
import v2.pref as vpref
import v2.token
import v2.types
fn transform_fixed_array_slice_code_for_test(code string) []ast.File {
tmp_file := os.join_path(os.temp_dir(), 'v2_transformer_fixed_slice_${os.getpid()}.v')
os.write_file(tmp_file, code) or { panic('failed to write temp file') }
defer {
os.rm(tmp_file) or {}
}
prefs := &vpref.Preferences{
backend: .cleanc
no_parallel: true
}
mut file_set := token.FileSet.new()
mut par := parser.Parser.new(prefs)
files := par.parse_files([tmp_file], mut file_set)
mut env := types.Environment.new()
mut checker := types.Checker.new(prefs, file_set, env)
checker.check_files(files)
mut transformer := Transformer.new_with_pref(env, prefs)
return transformer.transform_files(files)
}
fn count_fixed_slice_call_in_expr(expr ast.Expr, name string) int {
match expr {
ast.ArrayInitExpr {
mut count := count_fixed_slice_call_in_expr(expr.typ, name)
for item in expr.exprs {
count += count_fixed_slice_call_in_expr(item, name)
}
count += count_fixed_slice_call_in_expr(expr.init, name)
count += count_fixed_slice_call_in_expr(expr.cap, name)
count += count_fixed_slice_call_in_expr(expr.len, name)
count += count_fixed_slice_call_in_expr(expr.update_expr, name)
return count
}
ast.CallExpr {
mut count := if expr.lhs is ast.Ident && expr.lhs.name == name { 1 } else { 0 }
count += count_fixed_slice_call_in_expr(expr.lhs, name)
for arg in expr.args {
count += count_fixed_slice_call_in_expr(arg, name)
}
return count
}
ast.CastExpr {
return count_fixed_slice_call_in_expr(expr.typ, name) +
count_fixed_slice_call_in_expr(expr.expr, name)
}
ast.IndexExpr {
return count_fixed_slice_call_in_expr(expr.lhs, name) +
count_fixed_slice_call_in_expr(expr.expr, name)
}
ast.InfixExpr {
return count_fixed_slice_call_in_expr(expr.lhs, name) +
count_fixed_slice_call_in_expr(expr.rhs, name)
}
ast.InitExpr {
mut count := count_fixed_slice_call_in_expr(expr.typ, name)
for field in expr.fields {
count += count_fixed_slice_call_in_expr(field.value, name)
}
return count
}
ast.KeywordOperator {
mut count := 0
for value in expr.exprs {
count += count_fixed_slice_call_in_expr(value, name)
}
return count
}
ast.MapInitExpr {
mut count := count_fixed_slice_call_in_expr(expr.typ, name)
for key in expr.keys {
count += count_fixed_slice_call_in_expr(key, name)
}
for value in expr.vals {
count += count_fixed_slice_call_in_expr(value, name)
}
return count
}
ast.ModifierExpr {
return count_fixed_slice_call_in_expr(expr.expr, name)
}
ast.ParenExpr {
return count_fixed_slice_call_in_expr(expr.expr, name)
}
ast.PrefixExpr {
return count_fixed_slice_call_in_expr(expr.expr, name)
}
ast.RangeExpr {
return count_fixed_slice_call_in_expr(expr.start, name) +
count_fixed_slice_call_in_expr(expr.end, name)
}
ast.SelectorExpr {
return count_fixed_slice_call_in_expr(expr.lhs, name)
}
ast.Tuple {
mut count := 0
for value in expr.exprs {
count += count_fixed_slice_call_in_expr(value, name)
}
return count
}
ast.UnsafeExpr {
return count_fixed_slice_call_in_stmts(expr.stmts, name)
}
else {
return 0
}
}
}
fn count_fixed_slice_call_in_stmt(stmt ast.Stmt, name string) int {
match stmt {
ast.AssignStmt {
mut count := 0
for lhs in stmt.lhs {
count += count_fixed_slice_call_in_expr(lhs, name)
}
for rhs in stmt.rhs {
count += count_fixed_slice_call_in_expr(rhs, name)
}
return count
}
ast.ExprStmt {
return count_fixed_slice_call_in_expr(stmt.expr, name)
}
else {
return 0
}
}
}
fn count_fixed_slice_call_in_stmts(stmts []ast.Stmt, name string) int {
mut count := 0
for stmt in stmts {
count += count_fixed_slice_call_in_stmt(stmt, name)
}
return count
}
fn transformed_fixed_slice_fn(files []ast.File, name string) ?ast.FnDecl {
for file in files {
for stmt in file.stmts {
if stmt is ast.FnDecl && stmt.name == name {
return stmt
}
}
}
return none
}
fn test_fixed_array_slice_bounds_are_materialized_once() {
files := transform_fixed_array_slice_code_for_test('
fn next() int {
return 1
}
fn stop() int {
return 3
}
fn main() {
arr := [4]int{}
part := arr[next()..stop()]
_ = part
}
')
main_decl := transformed_fixed_slice_fn(files, 'main') or {
assert false, 'missing transformed main'
return
}
assert count_fixed_slice_call_in_stmts(main_decl.stmts, 'next') == 1
assert count_fixed_slice_call_in_stmts(main_decl.stmts, 'stop') == 1
assert count_fixed_slice_call_in_stmts(main_decl.stmts, 'new_array_from_c_array') == 1
}
@@ -0,0 +1,210 @@
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
fn test_flat_direct_filter_skips_open_generic_fn_decls_only() {
env := &types.Environment{}
t := &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
}
file := ast.File{
mod: 'main'
name: 'generic_filter.v'
selector_names: {
101: 'main.Counter.value'
202: 'main.Queue.is_empty'
303: 'datatypes.Queue.is_empty'
}
stmts: [
ast.Stmt(ast.FnDecl{
name: 'BSTree__insert'
is_method: true
receiver: ast.Parameter{
name: 'tree'
typ: ast.Type(ast.GenericType{
name: ast.Ident{
name: 'BSTree'
}
params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
})
}
}),
ast.Stmt(ast.FnDecl{
name: 'Queue__is_empty'
is_method: true
receiver: ast.Parameter{
name: 'queue'
typ: ast.Type(ast.GenericType{
name: ast.Ident{
name: 'Queue'
}
params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
})
}
}),
ast.Stmt(ast.FnDecl{
name: 'datatypes__Queue__is_empty'
is_method: true
receiver: ast.Parameter{
name: 'queue'
typ: ast.Type(ast.GenericType{
name: ast.Ident{
name: 'datatypes__Queue'
}
params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
})
}
}),
ast.Stmt(ast.FnDecl{
name: 'Queue_T_Array_string__is_empty'
is_method: true
receiver: ast.Parameter{
name: 'queue'
typ: ast.Ident{
name: 'Queue_T_Array_string'
}
}
}),
ast.Stmt(ast.FnDecl{
name: 'Counter__value'
is_method: true
receiver: ast.Parameter{
name: 'counter'
typ: ast.Ident{
name: 'Counter'
}
}
}),
ast.Stmt(ast.FnDecl{
name: 'identity'
typ: ast.FnType{
generic_params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
}
}),
ast.Stmt(ast.FnDecl{
name: 'main'
}),
]
}
filtered := t.file_without_open_generic_fn_decls(file)
assert filtered.selector_names[101] == 'main.Counter.value'
assert filtered.selector_names[202] == 'main.Queue.is_empty'
assert filtered.selector_names[303] == 'datatypes.Queue.is_empty'
assert filtered.selector_names.len == 3
mut kept_names := []string{}
for stmt in filtered.stmts {
assert stmt is ast.FnDecl
kept_names << (stmt as ast.FnDecl).name
}
assert kept_names == ['Queue_T_Array_string__is_empty', 'Counter__value', 'main']
assert 'Queue__is_empty' !in kept_names
assert 'datatypes__Queue__is_empty' !in kept_names
}
fn test_flat_input_direct_skips_open_generic_fn_decls_only() {
env := &types.Environment{}
mut t := Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
}
file := ast.File{
mod: 'main'
name: 'generic_filter_flat_input.v'
stmts: [
ast.Stmt(ast.FnDecl{
name: 'Queue__is_empty'
is_method: true
receiver: ast.Parameter{
name: 'queue'
typ: ast.Type(ast.GenericType{
name: ast.Ident{
name: 'Queue'
}
params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
})
}
}),
ast.Stmt(ast.FnDecl{
name: 'datatypes__Queue__is_empty'
is_method: true
receiver: ast.Parameter{
name: 'queue'
typ: ast.Type(ast.GenericType{
name: ast.Ident{
name: 'datatypes__Queue'
}
params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
})
}
}),
ast.Stmt(ast.FnDecl{
name: 'identity'
typ: ast.FnType{
generic_params: [
ast.Expr(ast.Ident{
name: 'T'
}),
]
}
}),
ast.Stmt(ast.FnDecl{
name: 'Queue_T_Array_string__is_empty'
is_method: true
receiver: ast.Parameter{
name: 'queue'
typ: ast.Ident{
name: 'Queue_T_Array_string'
}
}
}),
ast.Stmt(ast.FnDecl{
name: 'main'
}),
]
}
flat := ast.flatten_files([file])
out_files := t.transform_flat_to_flat_direct(&flat, []).to_files()
mut fn_names := []string{}
for out_file in out_files {
for stmt in out_file.stmts {
if stmt is ast.FnDecl {
fn_names << stmt.name
}
}
}
assert 'Queue_T_Array_string__is_empty' in fn_names
assert 'main' in fn_names
assert 'Queue__is_empty' !in fn_names
assert 'datatypes__Queue__is_empty' !in fn_names
assert 'identity' !in fn_names
}
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,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.
// vtest build: macos
//
// Bit-equality pin for s164: `generated_fn_module_from_flat` (FlatAst input)
// must produce the same string as `generated_fn_module` (legacy []ast.File
// input) across the routing prefixes: __sort_cmp_, Array_, generic
// module-prefixed names, and the non-module fallback set.
module transformer
import v2.ast
fn make_files_for_routing_test() []ast.File {
return [
ast.File{
name: 'main.v'
mod: 'main'
},
ast.File{
name: 'time_lib.v'
mod: 'time'
},
ast.File{
name: 'os_lib.v'
mod: 'os'
},
]
}
fn make_flat_for_routing_test() ast.FlatAst {
return ast.flatten_files(make_files_for_routing_test())
}
fn test_generated_fn_module_from_flat_matches_legacy_sort_cmp_module() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
name := '__sort_cmp_time__FormatDate'
assert generated_fn_module(name, files) == 'time'
assert generated_fn_module_from_flat(name, &flat) == 'time'
}
fn test_generated_fn_module_from_flat_matches_legacy_sort_cmp_unknown() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
// 'somethingelse' is not a registered file mod — both paths return ''
name := '__sort_cmp_somethingelse__Frob'
assert generated_fn_module(name, files) == ''
assert generated_fn_module_from_flat(name, &flat) == ''
}
fn test_generated_fn_module_from_flat_matches_legacy_array_prefix_module() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
name := 'Array_os__File_str'
assert generated_fn_module(name, files) == 'os'
assert generated_fn_module_from_flat(name, &flat) == 'os'
}
fn test_generated_fn_module_from_flat_matches_legacy_module_prefixed() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
name := 'time__FormatDate__str'
assert generated_fn_module(name, files) == 'time'
assert generated_fn_module_from_flat(name, &flat) == 'time'
}
fn test_generated_fn_module_from_flat_matches_legacy_no_module_prefix() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
// fn name with no __ separator falls through to ''
name := 'plainname'
assert generated_fn_module(name, files) == ''
assert generated_fn_module_from_flat(name, &flat) == ''
}
fn test_generated_fn_module_from_flat_matches_legacy_filtered_prefix() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
// 'int' is in the filtered prefix set — both paths skip and return ''
name := 'int__custom_helper'
assert generated_fn_module(name, files) == ''
assert generated_fn_module_from_flat(name, &flat) == ''
}
fn test_generated_fn_module_from_flat_matches_legacy_main_excluded() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
// 'main' is excluded from prefix module routing — both paths return ''
name := 'main__Helper__str'
assert generated_fn_module(name, files) == ''
assert generated_fn_module_from_flat(name, &flat) == ''
}
fn test_generated_fn_module_from_flat_matches_legacy_unknown_module() {
files := make_files_for_routing_test()
flat := make_flat_for_routing_test()
// 'nonexistent' isn't a registered file mod — both paths return ''
name := 'nonexistent__Type__str'
assert generated_fn_module(name, files) == ''
assert generated_fn_module_from_flat(name, &flat) == ''
}
@@ -0,0 +1,223 @@
// 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
//
// Bit-equality pin for s166: `generated_fns_parts_from_flat` must produce
// a `GeneratedFnsParts` whose buckets contain the same fn names in the
// same order as the legacy `generated_fns_parts([]ast.File)`. The two
// flat-input helpers (`explicit_str_method_fn_names_from_flat` from s165
// and `generated_fn_module_from_flat` from s164) are individually pinned;
// this test pins their assembly.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
fn create_parts_from_flat_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
}
}
fn fn_names_from_stmts(stmts []ast.Stmt) []string {
mut names := []string{cap: stmts.len}
for s in stmts {
if s is ast.FnDecl {
names << s.name
}
}
return names
}
fn assert_parts_equal(legacy GeneratedFnsParts, fromflat GeneratedFnsParts) {
legacy_core := fn_names_from_stmts(legacy.core_fns)
flat_core := fn_names_from_stmts(fromflat.core_fns)
assert legacy_core == flat_core
legacy_user := fn_names_from_stmts(legacy.user_fns)
flat_user := fn_names_from_stmts(fromflat.user_fns)
assert legacy_user == flat_user
assert legacy.module_fns.len == fromflat.module_fns.len
for mod, _ in legacy.module_fns {
legacy_mod := fn_names_from_stmts(legacy.module_fns[mod])
flat_mod := fn_names_from_stmts(fromflat.module_fns[mod])
assert legacy_mod == flat_mod
}
}
// none branch: no needed_* maps populated → both return `none`.
fn test_generated_fns_parts_from_flat_none_when_no_generators_fire() {
files := [ast.File{
name: 'main.v'
mod: 'main'
}]
flat := ast.flatten_files(files)
mut t_legacy := create_parts_from_flat_test_transformer()
mut t_flat := create_parts_from_flat_test_transformer()
legacy := t_legacy.generated_fns_parts(files) or {
assert true
assert t_flat.generated_fns_parts_from_flat(&flat) == none
return
}
_ = legacy
assert false, 'expected none'
}
// str-fn routed to a registered module: Duration__str → 'time' bucket.
fn test_generated_fns_parts_from_flat_str_fn_routed_to_module() {
// Two files: main + time. Both register `time__Duration__str` as needed.
files := [
ast.File{
name: 'main.v'
mod: 'main'
},
ast.File{
name: 'time.v'
mod: 'time'
},
]
flat := ast.flatten_files(files)
mut t_legacy := create_parts_from_flat_test_transformer()
mut t_flat := create_parts_from_flat_test_transformer()
t_legacy.needed_str_fns['time__Duration__str'] = ''
t_flat.needed_str_fns['time__Duration__str'] = ''
legacy := t_legacy.generated_fns_parts(files) or {
assert false, 'legacy returned none'
return
}
fromflat := t_flat.generated_fns_parts_from_flat(&flat) or {
assert false, 'flat returned none'
return
}
assert legacy.module_fns['time'].len == 1
assert fromflat.module_fns['time'].len == 1
assert_parts_equal(legacy, fromflat)
}
// core_fn (int_str) routed to core bucket regardless of files.
fn test_generated_fns_parts_from_flat_core_fn_routed_to_core() {
files := [ast.File{
name: 'main.v'
mod: 'main'
}]
flat := ast.flatten_files(files)
mut t_legacy := create_parts_from_flat_test_transformer()
mut t_flat := create_parts_from_flat_test_transformer()
// int__str is core (is_core_generated_fn matches it). Use a typed
// non-existent str entry that gets generated but goes to the core bucket
// via the prefix list. We rely on the existing `int__str` path.
t_legacy.needed_str_fns['int__str'] = ''
t_flat.needed_str_fns['int__str'] = ''
legacy := t_legacy.generated_fns_parts(files) or {
// int__str may short-circuit to skip generation; if so just assert
// both paths agree by returning none.
assert t_flat.generated_fns_parts_from_flat(&flat) == none
return
}
fromflat := t_flat.generated_fns_parts_from_flat(&flat) or {
assert false, 'flat returned none while legacy returned parts'
return
}
assert_parts_equal(legacy, fromflat)
}
// user_fn (Array_int_str) routed to user bucket (not core, no module prefix).
fn test_generated_fns_parts_from_flat_array_str_fn_routed_to_user() {
files := [ast.File{
name: 'main.v'
mod: 'main'
}]
flat := ast.flatten_files(files)
mut t_legacy := create_parts_from_flat_test_transformer()
mut t_flat := create_parts_from_flat_test_transformer()
t_legacy.needed_str_fns['Array_int_str'] = 'int'
t_flat.needed_str_fns['Array_int_str'] = 'int'
legacy := t_legacy.generated_fns_parts(files) or {
assert false, 'legacy returned none'
return
}
fromflat := t_flat.generated_fns_parts_from_flat(&flat) or {
assert false, 'flat returned none'
return
}
assert_parts_equal(legacy, fromflat)
}
// mixed buckets: time__Duration__str → 'time' module, Array_int_str → user.
fn test_generated_fns_parts_from_flat_mixed_buckets_match() {
files := [
ast.File{
name: 'main.v'
mod: 'main'
},
ast.File{
name: 'time.v'
mod: 'time'
},
]
flat := ast.flatten_files(files)
mut t_legacy := create_parts_from_flat_test_transformer()
mut t_flat := create_parts_from_flat_test_transformer()
t_legacy.needed_str_fns['time__Duration__str'] = ''
t_legacy.needed_str_fns['Array_int_str'] = 'int'
t_flat.needed_str_fns['time__Duration__str'] = ''
t_flat.needed_str_fns['Array_int_str'] = 'int'
legacy := t_legacy.generated_fns_parts(files) or {
assert false, 'legacy returned none'
return
}
fromflat := t_flat.generated_fns_parts_from_flat(&flat) or {
assert false, 'flat returned none'
return
}
assert_parts_equal(legacy, fromflat)
}
// explicit str method skips generation: file has `fn (d Duration) str()` for
// time module, and `needed_str_fns` requests `time__Duration__str` — both
// paths must skip the generator (explicit_str_fns contains the name).
fn test_generated_fns_parts_from_flat_explicit_str_method_skips_generation() {
files := [
ast.File{
name: 'time.v'
mod: 'time'
stmts: [
ast.Stmt(ast.FnDecl{
name: 'str'
is_method: true
receiver: ast.Parameter{
name: 'd'
typ: ast.Expr(ast.Ident{
name: 'Duration'
})
}
typ: ast.FnType{
return_type: ast.Ident{
name: 'string'
}
}
}),
]
},
]
flat := ast.flatten_files(files)
mut t_legacy := create_parts_from_flat_test_transformer()
mut t_flat := create_parts_from_flat_test_transformer()
t_legacy.needed_str_fns['time__Duration__str'] = ''
t_flat.needed_str_fns['time__Duration__str'] = ''
// Generator should skip because explicit fn exists; both paths return none.
legacy_opt := t_legacy.generated_fns_parts(files)
flat_opt := t_flat.generated_fns_parts_from_flat(&flat)
assert legacy_opt == none
assert flat_opt == none
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,63 @@
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
fn import_alias_test_transformer(aliases map[string]string) &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
cur_import_aliases: aliases.clone()
}
}
fn test_selector_type_name_resolves_nested_import_aliases() {
t := import_alias_test_transformer({
'bar': 'foo.bar'
'baz': 'foo.bar'
})
bar_leaf := ast.SelectorExpr{
lhs: ast.Expr(ast.Ident{
name: 'bar'
})
rhs: ast.Ident{
name: 'Leaf'
}
}
baz_leaf := ast.SelectorExpr{
lhs: ast.Expr(ast.Ident{
name: 'baz'
})
rhs: ast.Ident{
name: 'Leaf'
}
}
assert t.selector_type_name(bar_leaf, false) == 'Leaf'
assert t.selector_type_name(bar_leaf, true) == 'foo__bar__Leaf'
assert t.selector_type_name(baz_leaf, true) == 'foo__bar__Leaf'
}
fn test_generic_match_branch_variant_info_resolves_nested_import_alias() {
t := import_alias_test_transformer({
'bar': 'foo.bar'
})
lhs := ast.Expr(ast.SelectorExpr{
lhs: ast.Expr(ast.Ident{
name: 'bar'
})
rhs: ast.Ident{
name: 'Leaf'
}
})
base_name, variant_full, variant_module, ok := t.generic_match_branch_variant_info(lhs, [
ast.Expr(ast.Ident{
name: 'int'
}),
])
assert ok
assert base_name == 'Leaf'
assert variant_full == 'foo__bar__Leaf_T_int'
assert variant_module == 'foo__bar'
}
@@ -0,0 +1,111 @@
// 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
//
// Bit-equality pin for the first flat-aware post_pass port (s151):
// `inject_embed_file_helper_to_flat` must produce a FlatAst whose
// `signature()` matches what the legacy `inject_embed_file_helper`
// (mut []ast.File) + `flatten_files` pair produces. The s150 primitive
// `append_file_stmts` is the splice mechanism under the hood.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation, so
// `create_test_transformer` from `transformer_test.v` is not visible here.
fn create_embed_to_flat_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
}
}
fn make_minimal_builtin_file_with_stmts(extra []ast.Stmt) ast.File {
mut stmts := []ast.Stmt{cap: 1 + extra.len}
stmts << ast.Stmt(ast.ModuleStmt{
name: 'builtin'
})
for s in extra {
stmts << s
}
return ast.File{
name: 'builtin_min.v'
mod: 'builtin'
stmts: stmts
}
}
fn make_minimal_main_file() ast.File {
return ast.File{
name: 'main_min.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
]
}
}
// Reference: run legacy mutator on []ast.File then flatten.
//
// Why single-file (no non-builtin sibling): the file-root node's `extra`
// slot stores `intern(mod)` (raw intern index), and the legacy path
// interns all helper-stmt strings BEFORE it interns the next file's
// mod string, while the subject path interns the next file's mod BEFORE
// the helper-stmt strings. With only the builtin file present, both
// paths intern 'builtin' as idx 0 first, so the file root's `extra=0`
// matches in both signatures.
fn build_reference_signature_with_helper() string {
mut files := [make_minimal_builtin_file_with_stmts([])]
mut t := create_embed_to_flat_test_transformer()
t.inject_embed_file_helper(mut files)
return ast.flatten_files(files).signature()
}
// Subject: append bare builtin file to FlatBuilder, then run flat-aware splice.
fn build_subject_signature_with_helper() string {
mut b := ast.new_flat_builder()
b.append_file(make_minimal_builtin_file_with_stmts([]))
mut t := create_embed_to_flat_test_transformer()
t.inject_embed_file_helper_to_flat(mut b)
return b.flat.signature()
}
fn test_inject_embed_file_helper_to_flat_signature_matches_legacy() {
ref_sig := build_reference_signature_with_helper()
sub_sig := build_subject_signature_with_helper()
assert ref_sig == sub_sig
}
fn test_inject_embed_file_helper_to_flat_no_builtin_is_noop() {
mut b := ast.new_flat_builder()
b.append_file(make_minimal_main_file())
baseline_sig := b.flat.signature()
mut t := create_embed_to_flat_test_transformer()
t.inject_embed_file_helper_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_embed_file_helper_to_flat_already_present_is_noop() {
// Pre-seed the builtin file with a StructDecl whose name matches the
// helper type — the re-entry guard must skip the splice.
preseed := ast.Stmt(ast.StructDecl{
name: embed_file_helper_type_name
})
mut b := ast.new_flat_builder()
b.append_file(make_minimal_builtin_file_with_stmts([preseed]))
baseline_sig := b.flat.signature()
mut t := create_embed_to_flat_test_transformer()
t.inject_embed_file_helper_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
@@ -0,0 +1,127 @@
// 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
//
// Bit-equality pin for the third flat-aware post_pass port (s153):
// `inject_generated_fns_to_flat` must produce a FlatAst whose
// `signature()` matches what `inject_generated_fns_to_files` (the
// extracted legacy splice) + `flatten_files` produces. Each bucket
// (core_fns / module_fns[mod] / user_fns) is exercised in isolation
// against a single-file fixture — the multi-file intern-order quirk
// (same as s151/s152) makes multi-file signature comparison brittle, so
// we test the routing fallbacks of each bucket independently.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation, so
// `create_test_transformer` from `transformer_test.v` is not visible here.
fn create_generated_fns_to_flat_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
}
}
fn make_file_for_mod(mod string) ast.File {
return ast.File{
name: '${mod}_min.v'
mod: mod
stmts: [
ast.Stmt(ast.ModuleStmt{
name: mod
}),
]
}
}
fn make_fn_decl(name string) ast.Stmt {
return ast.Stmt(ast.FnDecl{
name: name
})
}
// Bucket 1: core_fns → builtin file.
fn test_inject_generated_fns_to_flat_core_fns_into_builtin() {
parts := GeneratedFnsParts{
core_fns: [make_fn_decl('int_str'), make_fn_decl('f64_str')]
module_fns: map[string][]ast.Stmt{}
user_fns: []ast.Stmt{}
}
mut ref_files := [make_file_for_mod('builtin')]
inject_generated_fns_to_files(mut ref_files, parts)
ref_sig := ast.flatten_files(ref_files).signature()
mut b := ast.new_flat_builder()
b.append_file(make_file_for_mod('builtin'))
mut t := create_generated_fns_to_flat_test_transformer()
t.inject_generated_fns_to_flat(mut b, parts)
assert b.flat.signature() == ref_sig
}
// Bucket 2: module_fns[mod] → mod file.
fn test_inject_generated_fns_to_flat_module_fns_into_mod() {
mut module_fns := map[string][]ast.Stmt{}
module_fns['time'] = [make_fn_decl('time__FormatDate__str')]
parts := GeneratedFnsParts{
core_fns: []ast.Stmt{}
module_fns: module_fns
user_fns: []ast.Stmt{}
}
mut ref_files := [make_file_for_mod('time')]
inject_generated_fns_to_files(mut ref_files, parts)
ref_sig := ast.flatten_files(ref_files).signature()
mut b := ast.new_flat_builder()
b.append_file(make_file_for_mod('time'))
mut t := create_generated_fns_to_flat_test_transformer()
t.inject_generated_fns_to_flat(mut b, parts)
assert b.flat.signature() == ref_sig
}
// Bucket 3: user_fns → main file.
fn test_inject_generated_fns_to_flat_user_fns_into_main() {
parts := GeneratedFnsParts{
core_fns: []ast.Stmt{}
module_fns: map[string][]ast.Stmt{}
user_fns: [make_fn_decl('Array_Test2_str'), make_fn_decl('Map_int_string_str')]
}
mut ref_files := [make_file_for_mod('main')]
inject_generated_fns_to_files(mut ref_files, parts)
ref_sig := ast.flatten_files(ref_files).signature()
mut b := ast.new_flat_builder()
b.append_file(make_file_for_mod('main'))
mut t := create_generated_fns_to_flat_test_transformer()
t.inject_generated_fns_to_flat(mut b, parts)
assert b.flat.signature() == ref_sig
}
// Fallback: core_fns lands in user_fns when no builtin file exists.
// With a single main file, user_fns → main → both legacy and flat-aware
// should splice core_fns into main via the user_fns route.
fn test_inject_generated_fns_to_flat_core_fns_fallback_no_builtin() {
parts := GeneratedFnsParts{
core_fns: [make_fn_decl('int_str')]
module_fns: map[string][]ast.Stmt{}
user_fns: []ast.Stmt{}
}
mut ref_files := [make_file_for_mod('main')]
inject_generated_fns_to_files(mut ref_files, parts)
ref_sig := ast.flatten_files(ref_files).signature()
mut b := ast.new_flat_builder()
b.append_file(make_file_for_mod('main'))
mut t := create_generated_fns_to_flat_test_transformer()
t.inject_generated_fns_to_flat(mut b, parts)
assert b.flat.signature() == ref_sig
}
@@ -0,0 +1,148 @@
// 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
//
// Bit-equality pin for the fifth flat-aware post_pass port (s159):
// `inject_live_reload_to_flat` must produce a FlatAst whose signature
// matches what the legacy `inject_live_reload` + `flatten_files` pair
// produces. Uses all six FlatBuilder primitives needed by live_reload:
// `replace_fn_body_stmts` (s158), `replace_file_stmt` (s155),
// `prepend_file_stmts` (s156), plus the legacy `inject_live_into_stmts`
// helper after decoding only the matching FnDecls from cursors.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation so the
// shared transformer-test scaffold isn't visible here. Seeded with two
// optional @[live] fn entries (controlled by `with_live`) and a stable
// source_file path so both paths see identical LiveReloadParts.
fn create_live_reload_to_flat_test_transformer(with_live bool) &Transformer {
env := &types.Environment{}
mut t := &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
live_source_file: '/tmp/_live_reload_to_flat_test.v'
}
if with_live {
t.live_fns << LiveFn{
decl_name: 'tick'
mangled_name: 'tick'
is_method: false
}
}
return t
}
fn make_main_file_with_main_and_for() ast.File {
return ast.File{
name: 'main_live.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.FnDecl{
name: 'tick'
stmts: [
ast.Stmt(ast.AssertStmt{
expr: ast.Expr(ast.BasicLiteral{
kind: .number
value: '1'
})
}),
]
}),
ast.Stmt(ast.FnDecl{
name: 'main'
stmts: [
ast.Stmt(ast.ForStmt{
cond: ast.Expr(ast.BasicLiteral{
kind: .number
value: '1'
})
stmts: [
ast.Stmt(ast.ExprStmt{
expr: ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'tick'
})
})
}),
]
}),
]
}),
]
}
}
fn test_inject_live_reload_to_flat_empty_live_fns_is_noop() {
mut b := ast.new_flat_builder()
b.append_file(make_main_file_with_main_and_for())
baseline_sig := b.flat.signature()
mut t := create_live_reload_to_flat_test_transformer(false)
t.inject_live_reload_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_live_reload_to_flat_no_main_fn_is_noop() {
// File with no main fn — locator returns none even though live_fns is
// populated; signature must be unchanged.
module_only_file := ast.File{
name: 'lib_live.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.FnDecl{
name: 'tick'
stmts: []ast.Stmt{}
}),
]
}
mut b := ast.new_flat_builder()
b.append_file(module_only_file)
baseline_sig := b.flat.signature()
mut t := create_live_reload_to_flat_test_transformer(true)
t.inject_live_reload_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_live_reload_to_flat_signature_matches_legacy() {
// Populated live_fns + main fn — splice happens. Compare the stmts-list
// subtree of the spliced file in both paths via `subtree_signature` on
// the file root's edge 2.
//
// Why subtree-of-stmts-list instead of full signature: the .file node's
// `extra` slot stores `intern(mod)` as a raw index. The legacy path
// flattens an already-mutated file (c_decls + global_decls + body
// emitted in declaration order → 'main' interned LATE) vs the flat
// path (file appended first → 'main' interned at slot 0 → splice
// re-emits the file root reusing saved mod_idx). Comparing the stmts
// list subtree skips the leaky `extra` field.
mut ref_files := [make_main_file_with_main_and_for()]
mut t_ref := create_live_reload_to_flat_test_transformer(true)
t_ref.inject_live_reload(mut ref_files)
ref_flat := ast.flatten_files(ref_files)
ref_stmts_list_id := ref_flat.child_at(ref_flat.files[0].file_id, 2)
ref_sub_sig := ref_flat.subtree_signature(ref_stmts_list_id)
mut b := ast.new_flat_builder()
b.append_file(make_main_file_with_main_and_for())
mut t_sub := create_live_reload_to_flat_test_transformer(true)
t_sub.inject_live_reload_to_flat(mut b)
sub_stmts_list_id := b.flat.child_at(b.flat.files[0].file_id, 2)
sub_sub_sig := b.flat.subtree_signature(sub_stmts_list_id)
assert ref_sub_sig == sub_sub_sig
}
@@ -0,0 +1,161 @@
// 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
//
// Bit-equality pin for the fourth flat-aware post_pass port (s155):
// `inject_main_runtime_const_init_to_flat` must produce a FlatAst whose
// `signature()` matches what the legacy `inject_main_runtime_const_init_calls`
// (mut []ast.File) + `flatten_files` pair produces. Uses the s154 primitives
// `prepend_to_fn_body` (rebuilds main's FnDecl with init calls prepended)
// and `replace_file_stmt` (rewires the file's stmts list to the new fn id).
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation, so the
// shared `create_test_transformer` is not visible here. Seeded with the two
// runtime-const fields the legacy + flat-aware paths both read off
// `Transformer` state.
fn create_main_init_to_flat_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
runtime_const_modules: ['mymod']
runtime_const_init_fn_name: {
'mymod': '__v_init_consts_mymod'
}
}
}
fn make_main_file_with_main_fn(body []ast.Stmt) ast.File {
return ast.File{
name: 'main_min.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.FnDecl{
name: 'main'
stmts: body
}),
]
}
}
fn make_assert_one_stmt() ast.Stmt {
return ast.Stmt(ast.AssertStmt{
expr: ast.Expr(ast.BasicLiteral{
kind: .number
value: '1'
})
})
}
// Reference: run legacy mutator on []ast.File then flatten.
//
// Why single-file: the file root's `extra` slot stores `intern(mod)` as a
// raw intern index; multi-file fixtures leak intern order between the
// reference (which interns init-call strings BEFORE the next file's mod) and
// subject (interns next file's mod BEFORE init-call strings) paths. Same
// quirk as s151/s152/s153 single-file fixtures.
fn build_reference_signature_with_main_init() string {
mut files := [make_main_file_with_main_fn([make_assert_one_stmt()])]
mut t := create_main_init_to_flat_test_transformer()
t.inject_main_runtime_const_init_calls(mut files)
return ast.flatten_files(files).signature()
}
// Subject: append bare main file to FlatBuilder, then run flat-aware splice.
fn build_subject_signature_with_main_init() string {
mut b := ast.new_flat_builder()
b.append_file(make_main_file_with_main_fn([make_assert_one_stmt()]))
mut t := create_main_init_to_flat_test_transformer()
t.inject_main_runtime_const_init_to_flat(mut b)
return b.flat.signature()
}
fn test_inject_main_runtime_const_init_to_flat_signature_matches_legacy() {
ref_sig := build_reference_signature_with_main_init()
sub_sig := build_subject_signature_with_main_init()
assert ref_sig == sub_sig
}
fn test_inject_main_runtime_const_init_to_flat_no_init_calls_is_noop() {
// Transformer with empty runtime_const_modules → init_calls is empty →
// no splice; flat signature must be identical to the baseline.
mut b := ast.new_flat_builder()
b.append_file(make_main_file_with_main_fn([make_assert_one_stmt()]))
baseline_sig := b.flat.signature()
env := &types.Environment{}
mut t := &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
runtime_const_modules: []string{}
runtime_const_init_fn_name: map[string]string{}
}
t.inject_main_runtime_const_init_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_main_runtime_const_init_to_flat_no_main_fn_is_noop() {
// File has no `fn main()` — locator returns none; signature unchanged
// even though init_calls is non-empty.
module_only_file := ast.File{
name: 'lib.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
]
}
mut b := ast.new_flat_builder()
b.append_file(module_only_file)
baseline_sig := b.flat.signature()
mut t := create_main_init_to_flat_test_transformer()
t.inject_main_runtime_const_init_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_main_runtime_const_init_to_flat_picks_first_main() {
// Two files, both contain `fn main()`. Legacy picks the FIRST match
// (loop breaks on first changed file). The flat-aware port must
// replicate this — only the first file's main gets the init prepended.
//
// Note: this is multi-file, which leaks intern-order through the .file
// node's `extra=intern(mod)` slot. We compare ONLY the FnDecl subtree of
// the first file's main, which has identical content in both paths.
mut ref_files := [
make_main_file_with_main_fn([make_assert_one_stmt()]),
make_main_file_with_main_fn([make_assert_one_stmt()]),
]
mut t_ref := create_main_init_to_flat_test_transformer()
t_ref.inject_main_runtime_const_init_calls(mut ref_files)
ref_flat := ast.flatten_files(ref_files)
ref_main_fn_id := ref_flat.child_at(ref_flat.child_at(ref_flat.files[0].file_id, 2), 1)
ref_sub_sig := ref_flat.subtree_signature(ref_main_fn_id)
mut b := ast.new_flat_builder()
b.append_file(make_main_file_with_main_fn([make_assert_one_stmt()]))
b.append_file(make_main_file_with_main_fn([make_assert_one_stmt()]))
mut t_sub := create_main_init_to_flat_test_transformer()
t_sub.inject_main_runtime_const_init_to_flat(mut b)
sub_main_fn_id := b.flat.child_at(b.flat.child_at(b.flat.files[0].file_id, 2), 1)
sub_sub_sig := b.flat.subtree_signature(sub_main_fn_id)
assert ref_sub_sig == sub_sub_sig
}
@@ -0,0 +1,209 @@
// 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
//
// Bit-equality pin for the sixth flat-aware post_pass port (s160):
// `inject_runtime_const_init_fns_to_flat` must produce a FlatAst whose
// `signature()` matches what the legacy `inject_runtime_const_init_fns`
// (mut []ast.File) + `flatten_files` pair produces. Uses s150's
// `append_file_stmts` to splice the synthesised `__v_init_consts_<mod>`
// FnDecl into the matching module file. Closes the post_pass port arc:
// every file-mutating step in legacy `post_pass` now has a flat-aware
// `_to_flat` variant — s151/s152/s153/s155/s159/s160.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation.
// Seeds `runtime_const_modules` + `runtime_const_inits_by_mod` so
// `runtime_const_init_fn_stmts_parts` produces a non-empty (mod, fn_stmt)
// map. The init expr is a trivial integer literal — the legacy splice
// transforms it via `transform_expr_in_module` (identity for literals),
// so both paths produce identical FnDecl bodies.
fn create_runtime_init_fns_to_flat_test_transformer() &Transformer {
env := &types.Environment{}
mut t := &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
runtime_const_modules: ['mymod']
runtime_const_inits_by_mod: map[string][]RuntimeConstInit{}
runtime_const_init_fn_name: map[string]string{}
}
t.runtime_const_inits_by_mod['mymod'] = [
RuntimeConstInit{
name: 'mymod__answer'
expr: ast.Expr(ast.BasicLiteral{
kind: .number
value: '42'
})
},
]
return t
}
fn make_mymod_file() ast.File {
return ast.File{
name: 'mymod.v'
mod: 'mymod'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'mymod'
}),
]
}
}
fn make_other_file() ast.File {
return ast.File{
name: 'other.v'
mod: 'other'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'other'
}),
]
}
}
// Reference: run legacy mutator on []ast.File then flatten.
//
// Why single-file: the file root's `extra` slot stores `intern(mod)` as a
// raw intern index; the legacy path interns the synthesised fn's strings
// BEFORE the next file's mod string, but the subject path interns the
// next file's mod string FIRST (during the initial bare-file append) and
// THEN the synthesised fn's strings. With one file present, both paths
// intern 'mymod' as idx 0 first — `extra=0` matches in both signatures.
fn build_reference_signature_with_init_fn() string {
mut files := [make_mymod_file()]
mut t := create_runtime_init_fns_to_flat_test_transformer()
t.inject_runtime_const_init_fns(mut files)
return ast.flatten_files(files).signature()
}
fn build_subject_signature_with_init_fn() string {
mut b := ast.new_flat_builder()
b.append_file(make_mymod_file())
mut t := create_runtime_init_fns_to_flat_test_transformer()
t.inject_runtime_const_init_fns_to_flat(mut b)
return b.flat.signature()
}
fn test_inject_runtime_const_init_fns_to_flat_signature_matches_legacy() {
ref_sig := build_reference_signature_with_init_fn()
sub_sig := build_subject_signature_with_init_fn()
assert ref_sig == sub_sig
}
fn test_inject_runtime_const_init_fns_legacy_path_decodes_cursor_backed_inits() {
mut source_builder := ast.new_flat_builder()
source_builder.append_file(ast.File{
name: 'mymod.v'
mod: 'mymod'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'mymod'
}),
ast.Stmt(ast.ConstDecl{
fields: [
ast.FieldInit{
name: 'answer'
value: ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'make_value'
})
})
},
]
}),
]
})
field := source_builder.flat.file_cursor(0).stmts().at(1).list_at(0).at(0)
mut t := create_runtime_init_fns_to_flat_test_transformer()
t.runtime_const_inits_by_mod['mymod'] = [
RuntimeConstInit{
name: 'mymod__answer'
expr_cursor: field.edge(0)
},
]
mut files := [make_mymod_file()]
t.inject_runtime_const_init_fns(mut files)
assert files[0].stmts.len == 2
fn_decl := files[0].stmts[1] as ast.FnDecl
assert fn_decl.name == '__v_init_consts_mymod'
assert fn_decl.stmts.len == 1
assign := fn_decl.stmts[0] as ast.AssignStmt
assert assign.rhs.len == 1
call := assign.rhs[0] as ast.CallExpr
assert call.lhs is ast.Ident
assert (call.lhs as ast.Ident).name == 'make_value'
}
fn test_inject_runtime_const_init_fns_to_flat_no_matching_file_is_noop() {
// runtime_const_modules names 'mymod' but no file has that mod — the
// inner loop never finds a match, so the splice is a no-op.
mut b := ast.new_flat_builder()
b.append_file(make_other_file())
baseline_sig := b.flat.signature()
mut t := create_runtime_init_fns_to_flat_test_transformer()
t.inject_runtime_const_init_fns_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_runtime_const_init_fns_to_flat_empty_modules_is_noop() {
// Transformer with empty runtime_const_modules → parts map is empty →
// no splice.
env := &types.Environment{}
mut t := &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
runtime_const_modules: []string{}
runtime_const_inits_by_mod: map[string][]RuntimeConstInit{}
runtime_const_init_fn_name: map[string]string{}
}
mut b := ast.new_flat_builder()
b.append_file(make_mymod_file())
baseline_sig := b.flat.signature()
t.inject_runtime_const_init_fns_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_runtime_const_init_fns_to_flat_picks_first_matching_file() {
// Two files with mod='mymod'. Legacy picks the FIRST match (loop breaks
// on first append). The flat-aware port must replicate this — only the
// first 'mymod' file gets the synthesised fn appended.
//
// Multi-file leaks intern-order through `.file extra=intern(mod)`, so
// we compare ONLY the FnDecl subtree (last stmt of file 0's stmts list),
// which has identical content in both paths.
mut ref_files := [make_mymod_file(), make_mymod_file()]
mut t_ref := create_runtime_init_fns_to_flat_test_transformer()
t_ref.inject_runtime_const_init_fns(mut ref_files)
ref_flat := ast.flatten_files(ref_files)
ref_stmts_list := ref_flat.child_at(ref_flat.files[0].file_id, 2)
// After append, the stmts list has 2 children: [ModuleStmt, appended FnDecl].
ref_fn_id := ref_flat.child_at(ref_stmts_list, 1)
ref_sub_sig := ref_flat.subtree_signature(ref_fn_id)
mut b := ast.new_flat_builder()
b.append_file(make_mymod_file())
b.append_file(make_mymod_file())
mut t_sub := create_runtime_init_fns_to_flat_test_transformer()
t_sub.inject_runtime_const_init_fns_to_flat(mut b)
sub_stmts_list := b.flat.child_at(b.flat.files[0].file_id, 2)
sub_fn_id := b.flat.child_at(sub_stmts_list, 1)
sub_sub_sig := b.flat.subtree_signature(sub_fn_id)
assert ref_sub_sig == sub_sub_sig
}
@@ -0,0 +1,111 @@
// 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
//
// Bit-equality pin for the second flat-aware post_pass port (s152):
// `inject_test_main_to_flat` must produce a FlatAst whose `signature()`
// matches what the legacy `inject_test_main` (mut []ast.File) +
// `flatten_files` pair produces. The s150 primitive `append_file_stmts`
// is the splice mechanism; the synthesised main payload is built by the
// shared `synthesise_test_main_fn` helper so both paths produce
// identical FnDecl content by construction.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation, so
// `create_test_transformer` from `transformer_test.v` is not visible here.
fn create_test_main_to_flat_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
}
}
fn make_test_file_with_stmts(extra []ast.Stmt) ast.File {
mut stmts := []ast.Stmt{cap: 1 + extra.len}
stmts << ast.Stmt(ast.ModuleStmt{
name: 'main'
})
for s in extra {
stmts << s
}
return ast.File{
name: 'inline_test.v'
mod: 'main'
stmts: stmts
}
}
fn make_test_fn_decl(name string) ast.Stmt {
return ast.Stmt(ast.FnDecl{
name: name
})
}
// Reference: run legacy mutator on []ast.File then flatten.
//
// Why single-file (no sibling): the file-root node's `extra` slot stores
// `intern(mod)` as a raw intern index, and `signature()` prints it
// unresolved. Legacy `flatten_files` and the subject splice path intern
// strings in different orders when multiple files are present, so a
// single-file fixture is the apples-to-apples comparison. (Same reason
// as s151's `inject_embed_file_helper_to_flat_test.v`.)
fn build_reference_signature_with_test_main() string {
mut files := [
make_test_file_with_stmts([make_test_fn_decl('test_first'),
make_test_fn_decl('test_second')]),
]
mut t := create_test_main_to_flat_test_transformer()
t.inject_test_main(mut files)
return ast.flatten_files(files).signature()
}
// Subject: append bare test file to FlatBuilder, then run flat-aware splice.
fn build_subject_signature_with_test_main() string {
mut b := ast.new_flat_builder()
b.append_file(make_test_file_with_stmts([make_test_fn_decl('test_first'),
make_test_fn_decl('test_second')]))
mut t := create_test_main_to_flat_test_transformer()
t.inject_test_main_to_flat(mut b)
return b.flat.signature()
}
fn test_inject_test_main_to_flat_signature_matches_legacy() {
ref_sig := build_reference_signature_with_test_main()
sub_sig := build_subject_signature_with_test_main()
assert ref_sig == sub_sig
}
fn test_inject_test_main_to_flat_no_test_fns_is_noop() {
mut b := ast.new_flat_builder()
b.append_file(make_test_file_with_stmts([]))
baseline_sig := b.flat.signature()
mut t := create_test_main_to_flat_test_transformer()
t.inject_test_main_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
fn test_inject_test_main_to_flat_existing_main_is_noop() {
// Pre-seed the file with a top-level non-method FnDecl named `main` —
// the user-main detection must skip the splice even though there's a
// `test_*` fn present.
preseeded_main := ast.Stmt(ast.FnDecl{
name: 'main'
})
mut b := ast.new_flat_builder()
b.append_file(make_test_file_with_stmts([preseeded_main, make_test_fn_decl('test_first')]))
baseline_sig := b.flat.signature()
mut t := create_test_main_to_flat_test_transformer()
t.inject_test_main_to_flat(mut b)
assert b.flat.signature() == baseline_sig
}
@@ -0,0 +1,323 @@
// 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 transformer
import v2.ast
import v2.types
// is_interface_var checks if a variable is an interface type by looking up its type in scope
fn (t &Transformer) is_interface_var(name string) bool {
// Special case: 'err' in or-blocks is always IError interface
if name == 'err' {
return true
}
typ := t.lookup_var_type(name) or { return false }
return typ is types.Interface
}
// is_interface_receiver checks if an expression's type is an interface type
fn (t &Transformer) is_interface_receiver(expr ast.Expr) bool {
match expr {
ast.Ident {
return t.is_interface_var(expr.name)
}
ast.ParenExpr {
return t.is_interface_receiver(expr.expr)
}
ast.ModifierExpr {
return t.is_interface_receiver(expr.expr)
}
else {}
}
if typ := t.get_interface_receiver_type(expr) {
base := t.unwrap_alias_and_pointer_type(typ)
return base is types.Interface
}
return false
}
fn (t &Transformer) get_interface_receiver_type(expr ast.Expr) ?types.Type {
if typ := t.get_expr_type(expr) {
return typ
}
if typ := t.resolve_expr_type(expr) {
return typ
}
if expr is ast.IndexExpr {
if expr.expr is ast.RangeExpr {
return none
}
if container_type := t.get_expr_type(expr.lhs) {
base := t.unwrap_alias_and_pointer_type(container_type)
if base is types.Array {
return base.elem_type
}
if base is types.ArrayFixed {
return base.elem_type
}
if base is types.Map {
return base.value_type
}
}
}
return none
}
// is_interface_cast checks if an expression is an interface cast like Calculator(value)
fn (t &Transformer) is_interface_cast(expr ast.Expr) bool {
if t.get_interface_cast_inner_expr(expr) != none {
return true
}
return false
}
// get_expr_type_name returns the concrete type name for an expression using the type checker env
fn (t &Transformer) get_expr_type_name(expr ast.Expr) ?string {
if typ := t.get_expr_type(expr) {
name := t.type_to_c_name(typ)
if name != '' {
return name
}
}
// For simple identifiers, try scope lookup
if expr is ast.Ident {
if typ := t.lookup_var_type(expr.name) {
name := t.type_to_c_name(typ)
if name != '' {
return name
}
}
}
return none
}
fn (t &Transformer) get_interface_cast_concrete_type(expr ast.Expr) ?string {
match expr {
ast.ParenExpr {
return t.get_interface_cast_concrete_type(expr.expr)
}
ast.CallOrCastExpr {
if t.interface_type_expr_is_interface(expr.lhs) {
if concrete := t.get_expr_type_name(expr.expr) {
return concrete
}
}
}
ast.CallExpr {
if expr.args.len == 1 && t.interface_type_expr_is_interface(expr.lhs) {
if concrete := t.get_expr_type_name(expr.args[0]) {
return concrete
}
}
}
ast.CastExpr {
if t.interface_type_expr_is_interface(expr.typ) {
if concrete := t.get_expr_type_name(expr.expr) {
return concrete
}
}
}
else {}
}
return none
}
fn (t &Transformer) get_interface_cast_inner_expr(expr ast.Expr) ?ast.Expr {
match expr {
ast.ParenExpr {
return t.get_interface_cast_inner_expr(expr.expr)
}
ast.CallOrCastExpr {
if t.interface_type_expr_is_interface(expr.lhs) {
return expr.expr
}
}
ast.CallExpr {
if expr.args.len == 1 && t.interface_type_expr_is_interface(expr.lhs) {
return expr.args[0]
}
}
ast.CastExpr {
if t.interface_type_expr_is_interface(expr.typ) {
return expr.expr
}
}
else {}
}
return none
}
fn (t &Transformer) interface_type_expr_is_interface(expr ast.Expr) bool {
type_name := t.expr_to_type_name(expr)
if type_name != '' {
if typ := t.lookup_type(type_name) {
return typ is types.Interface
}
}
if expr is ast.Ident {
mut scope := t.get_current_scope() or { return false }
obj := scope.lookup_parent(expr.name, 0) or { return false }
if obj is types.Type {
return obj is types.Interface
}
}
return false
}
fn (t &Transformer) get_interface_array_init_concrete_type(expr &ast.ArrayInitExpr) ?string {
if !expr_array_has_valid_data(expr.exprs) {
return none
}
mut concrete_type := ''
for e in expr.exprs {
concrete := t.get_interface_cast_concrete_type(e) or { return none }
if concrete_type == '' {
concrete_type = concrete
} else if concrete_type != concrete {
return none
}
}
if concrete_type != '' {
return concrete_type
}
return none
}
fn (t &Transformer) get_interface_array_expr_concrete_type(expr ast.Expr) ?string {
match expr {
ast.Ident {
return t.get_interface_concrete_type(expr.name)
}
ast.ParenExpr {
return t.get_interface_array_expr_concrete_type(expr.expr)
}
ast.ArrayInitExpr {
return t.get_interface_array_init_concrete_type(&expr)
}
ast.CallExpr {
if expr.lhs is ast.SelectorExpr {
sel := expr.lhs as ast.SelectorExpr
if sel.rhs.name in ['repeat', 'clone'] {
return t.get_interface_array_expr_concrete_type(sel.lhs)
}
}
}
ast.CallOrCastExpr {
if expr.lhs is ast.SelectorExpr {
sel := expr.lhs as ast.SelectorExpr
if sel.rhs.name in ['repeat', 'clone'] {
return t.get_interface_array_expr_concrete_type(sel.lhs)
}
}
}
else {}
}
return none
}
fn (t &Transformer) get_interface_assignment_concrete_type(expr ast.Expr) ?string {
if concrete := t.get_interface_cast_concrete_type(expr) {
return concrete
}
if concrete := t.get_interface_array_expr_concrete_type(expr) {
return concrete
}
if expr is ast.Ident {
return t.get_interface_concrete_type(expr.name)
}
if expr is ast.ParenExpr {
return t.get_interface_assignment_concrete_type(expr.expr)
}
return none
}
fn (t &Transformer) get_interface_concrete_type_for_expr(expr ast.Expr) ?string {
match expr {
ast.Ident {
return t.get_interface_concrete_type(expr.name)
}
ast.ParenExpr {
return t.get_interface_concrete_type_for_expr(expr.expr)
}
ast.ModifierExpr {
return t.get_interface_concrete_type_for_expr(expr.expr)
}
ast.IndexExpr {
return t.get_interface_array_expr_concrete_type(expr.lhs)
}
else {}
}
return none
}
fn (t &Transformer) get_native_default_interface_concrete_type(expr ast.Expr, method_name string) ?string {
prng_methods := ['seed', 'u8', 'u16', 'u32', 'u64', 'block_size', 'free']
if method_name !in prng_methods {
return none
}
if t.cur_module == 'rand' {
return 'wyrand__WyRandRNG'
}
receiver_type := t.get_interface_receiver_type(expr) or { return none }
base := t.unwrap_alias_and_pointer_type(receiver_type)
if base !is types.Interface {
return none
}
iface_name := t.type_to_c_name(base)
if iface_name !in ['PRNG', 'rand__PRNG'] {
return none
}
return 'wyrand__WyRandRNG'
}
fn (mut t Transformer) native_interface_receiver_arg(expr ast.Expr, concrete string) ast.Expr {
concrete_type := t.c_name_to_type(concrete) or { return expr }
return t.native_interface_receiver_arg_with_type(expr, concrete_type)
}
fn (mut t Transformer) native_interface_receiver_arg_with_type(expr ast.Expr, concrete_type types.Type) ast.Expr {
match expr {
ast.IndexExpr {
pos := t.next_synth_pos()
t.register_synth_type(pos, concrete_type)
return ast.Expr(ast.IndexExpr{
lhs: expr.lhs
expr: expr.expr
is_gated: expr.is_gated
pos: pos
})
}
ast.ParenExpr {
return ast.Expr(ast.ParenExpr{
expr: t.native_interface_receiver_arg_with_type(expr.expr, concrete_type)
pos: expr.pos
})
}
ast.ModifierExpr {
return ast.Expr(ast.ModifierExpr{
kind: expr.kind
expr: t.native_interface_receiver_arg_with_type(expr.expr, concrete_type)
pos: expr.pos
})
}
else {}
}
return expr
}
// get_interface_concrete_type returns the concrete type name for an interface variable
// if it was tracked during interface cast lowering (native backend only)
fn (t &Transformer) get_interface_concrete_type(name string) ?string {
if concrete := t.interface_concrete_types[name] {
return concrete
}
return none
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,22 @@
// 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 transformer
import os
// t_print_mem reports live malloc bytes at a transformer sub-phase boundary.
// Gated on V2_MEM. Under -gc none the value is monotonic on macOS, so deltas
// between stages are the exact bytes each sub-phase allocated. Defined for all
// platforms (transform_files calls it everywhere); the malloc-statistics probe
// is macOS-only, so other platforms just print the stage marker.
fn t_print_mem(stage string) {
if os.getenv('V2_MEM') == '' {
return
}
$if macos {
eprintln(' [mem] transform/${stage}: live ${darwin_transform_live_mb()} MB')
} $else {
eprintln(' [mem] transform/${stage}')
}
}
@@ -0,0 +1,25 @@
// 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 transformer
#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_transform_live_mb returns the process's currently live malloc bytes
// (in MB) on macOS. Used by t_print_mem; only referenced from its `$if macos`
// branch, so the macOS-only C calls here never reach other platforms.
fn darwin_transform_live_mb() u64 {
mut st := C.malloc_statistics_t{}
C.malloc_zone_statistics(C.malloc_default_zone(), &st)
return u64(st.size_in_use) / (1024 * 1024)
}
@@ -0,0 +1,227 @@
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
fn transformer_with_x64_target(target_os string) &Transformer {
env := &types.Environment{}
prefs := &vpref.Preferences{
backend: .x64
arch: .x64
target_os: target_os
}
return Transformer.new_with_pref(env, prefs)
}
fn transformer_with_macos_tiny_candidate_graph() &Transformer {
mut t := transformer_with_x64_target('macos')
t.enable_macos_tiny_candidate_graph()
return t
}
fn runtime_init_call_names(stmts []ast.Stmt) []string {
mut names := []string{}
for stmt in stmts {
if stmt is ast.ExprStmt {
if stmt.expr is ast.CallExpr {
if stmt.expr.lhs is ast.Ident {
names << stmt.expr.lhs.name
}
}
}
}
return names
}
fn runtime_init_call_names_from_flat(mut t Transformer, files []ast.File) []string {
flat := ast.flatten_files(files)
return runtime_init_call_names(t.runtime_const_init_main_calls_parts_from_flat(&flat))
}
fn runtime_init_test_files(main_imports []ast.ImportStmt) []ast.File {
return [
ast.File{
mod: 'main'
imports: main_imports
stmts: [
ast.Stmt(ast.FnDecl{
name: 'main'
}),
]
},
ast.File{
mod: 'os'
stmts: [
ast.Stmt(ast.FnDecl{
name: 'init'
}),
]
},
]
}
fn runtime_init_test_files_with_main_extra_stmts(extra_stmts []ast.Stmt) []ast.File {
mut files := runtime_init_test_files([]ast.ImportStmt{})
mut main_stmts := []ast.Stmt{cap: extra_stmts.len + files[0].stmts.len}
main_stmts << extra_stmts
main_stmts << files[0].stmts
files[0] = ast.File{
mod: files[0].mod
stmts: main_stmts
}
return files
}
fn runtime_init_import_stmt(name string) ast.Stmt {
return ast.Stmt(ast.ImportStmt{
name: name
})
}
fn runtime_init_comptime_if_import_stmt(cond_name string, import_name string) ast.Stmt {
return ast.Stmt(ast.ExprStmt{
expr: ast.Expr(ast.ComptimeExpr{
expr: ast.Expr(ast.IfExpr{
cond: ast.Expr(ast.Ident{
name: cond_name
})
stmts: [runtime_init_import_stmt(import_name)]
})
})
})
}
fn runtime_init_comptime_else_import_stmt(cond_name string, import_name string) ast.Stmt {
return ast.Stmt(ast.ExprStmt{
expr: ast.Expr(ast.ComptimeExpr{
expr: ast.Expr(ast.IfExpr{
cond: ast.Expr(ast.Ident{
name: cond_name
})
else_expr: ast.Expr(ast.IfExpr{
cond: ast.empty_expr
stmts: [runtime_init_import_stmt(import_name)]
})
})
})
})
}
fn seed_runtime_const_init_names(mut t Transformer) {
t.runtime_const_modules = ['os', 'main']
t.runtime_const_init_fn_name['os'] = '__v_init_consts_os'
t.runtime_const_init_fn_name['main'] = '__v_init_consts_main'
}
fn test_linux_minimal_runtime_filters_unimported_runtime_init_calls() {
mut t := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t)
names :=
runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([]ast.ImportStmt{})))
assert names == ['__v_init_consts_main']
}
fn test_linux_minimal_runtime_keeps_imported_runtime_init_calls() {
mut t := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t)
names := runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([
ast.ImportStmt{
name: 'os'
},
])))
assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
}
fn test_linux_minimal_runtime_filters_unimported_runtime_init_calls_from_flat() {
mut t := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t)
names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([]ast.ImportStmt{}))
assert names == ['__v_init_consts_main']
}
fn test_linux_minimal_runtime_keeps_imported_runtime_init_calls_from_flat() {
mut t := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t)
names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([
ast.ImportStmt{
name: 'os'
},
]))
assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
}
fn test_macos_tiny_candidate_filters_unimported_runtime_init_calls() {
mut t := transformer_with_macos_tiny_candidate_graph()
seed_runtime_const_init_names(mut t)
names :=
runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([]ast.ImportStmt{})))
assert names == ['__v_init_consts_main']
}
fn test_macos_tiny_candidate_keeps_imported_runtime_init_calls() {
mut t := transformer_with_macos_tiny_candidate_graph()
seed_runtime_const_init_names(mut t)
names := runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([
ast.ImportStmt{
name: 'os'
},
])))
assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
}
fn test_linux_minimal_runtime_keeps_active_comptime_imports_from_flat() {
files := runtime_init_test_files_with_main_extra_stmts([
runtime_init_comptime_if_import_stmt('linux', 'os'),
])
mut t_legacy := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t_legacy)
mut t_flat := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t_flat)
legacy_names := runtime_init_call_names(t_legacy.runtime_const_init_main_calls_parts(files))
flat_names := runtime_init_call_names_from_flat(mut t_flat, files)
assert legacy_names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
assert flat_names == legacy_names
}
fn test_linux_minimal_runtime_keeps_comptime_else_imports_from_flat() {
files := runtime_init_test_files_with_main_extra_stmts([
runtime_init_comptime_else_import_stmt('windows', 'os'),
])
mut t_legacy := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t_legacy)
mut t_flat := transformer_with_x64_target('linux')
seed_runtime_const_init_names(mut t_flat)
legacy_names := runtime_init_call_names(t_legacy.runtime_const_init_main_calls_parts(files))
flat_names := runtime_init_call_names_from_flat(mut t_flat, files)
assert legacy_names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
assert flat_names == legacy_names
}
fn test_macos_tiny_candidate_filters_unimported_runtime_init_calls_from_flat() {
mut t := transformer_with_macos_tiny_candidate_graph()
seed_runtime_const_init_names(mut t)
names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([]ast.ImportStmt{}))
assert names == ['__v_init_consts_main']
}
fn test_macos_tiny_candidate_keeps_imported_runtime_init_calls_from_flat() {
mut t := transformer_with_macos_tiny_candidate_graph()
seed_runtime_const_init_names(mut t)
names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([
ast.ImportStmt{
name: 'os'
},
]))
assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
}
fn test_non_minimal_x64_runtime_keeps_existing_init_behavior() {
mut t := transformer_with_x64_target('macos')
seed_runtime_const_init_names(mut t)
names :=
runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([]ast.ImportStmt{})))
assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main']
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,433 @@
// 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 transformer
import v2.ast
import v2.token
import v2.types
fn (mut t Transformer) transform_sql_expr(expr ast.SqlExpr) ast.Expr {
if !expr.is_create {
return ast.Expr(ast.SqlExpr{
expr: t.transform_expr(expr.expr)
table_name: expr.table_name
is_count: expr.is_count
is_create: expr.is_create
pos: expr.pos
})
}
if lowered := t.lower_sql_create_expr(expr) {
return lowered
}
return ast.Expr(ast.SqlExpr{
expr: t.transform_expr(expr.expr)
table_name: expr.table_name
is_count: expr.is_count
is_create: expr.is_create
pos: expr.pos
})
}
fn (mut t Transformer) lower_sql_create_expr(expr ast.SqlExpr) ?ast.Expr {
table := t.lookup_sql_table_struct(expr.table_name) or { return none }
call_pos := t.next_synth_pos()
call := ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.SelectorExpr{
lhs: expr.expr
rhs: ast.Ident{
name: 'create'
pos: call_pos
}
pos: call_pos
})
args: [
t.sql_orm_table_expr(table.name, expr.pos),
t.sql_orm_table_fields_expr(table.fields, expr.pos),
]
pos: call_pos
})
t.register_synth_type(call_pos, types.Type(types.ResultType{
base_type: types.Type(types.void_)
}))
return t.transform_expr(call)
}
fn (t &Transformer) lookup_sql_table_struct(name string) ?types.Struct {
if typ := t.lookup_type(name) {
base := t.sql_unwrap_alias_pointer_type(typ)
if base is types.Struct {
return base
}
}
return t.lookup_struct_type_any_module(name)
}
fn (t &Transformer) sql_unwrap_alias_pointer_type(typ types.Type) types.Type {
mut cur := typ
for {
match cur {
types.Alias {
cur = cur.base_type
continue
}
types.Pointer {
cur = cur.base_type
continue
}
else {}
}
break
}
return cur
}
fn (mut t Transformer) sql_orm_table_expr(type_name string, pos token.Pos) ast.Expr {
init_pos := t.next_synth_pos()
return ast.Expr(ast.InitExpr{
typ: t.sql_orm_type_expr('Table', pos)
fields: [
ast.FieldInit{
name: 'name'
value: t.sql_string_expr(type_name.to_lower(), pos)
},
ast.FieldInit{
name: 'attrs'
value: t.sql_vattribute_array_expr([], pos)
},
]
pos: init_pos
})
}
fn (mut t Transformer) sql_orm_table_fields_expr(fields []types.Field, pos token.Pos) ast.Expr {
mut exprs := []ast.Expr{cap: fields.len}
for field in fields {
if field_expr := t.sql_orm_table_field_expr(field, pos) {
exprs << field_expr
}
}
array_pos := t.next_synth_pos()
return ast.Expr(ast.ArrayInitExpr{
typ: ast.Expr(ast.Type(ast.ArrayType{
elem_type: t.sql_orm_type_expr('TableField', pos)
}))
exprs: exprs
pos: array_pos
})
}
fn (mut t Transformer) sql_orm_table_field_expr(field types.Field, pos token.Pos) ?ast.Expr {
if sql_field_is_skipped(field.attributes) {
return none
}
field_type, nullable, is_arr := t.sql_field_storage_type(field.typ)
init_pos := t.next_synth_pos()
return ast.Expr(ast.InitExpr{
typ: t.sql_orm_type_expr('TableField', pos)
fields: [
ast.FieldInit{
name: 'name'
value: t.sql_string_expr(field.name, pos)
},
ast.FieldInit{
name: 'typ'
value: t.sql_orm_type_idx_expr(field_type, pos)
},
ast.FieldInit{
name: 'nullable'
value: t.sql_bool_expr(nullable, pos)
},
ast.FieldInit{
name: 'default_val'
value: t.sql_string_expr('', pos)
},
ast.FieldInit{
name: 'attrs'
value: t.sql_vattribute_array_expr(field.attributes, pos)
},
ast.FieldInit{
name: 'is_arr'
value: t.sql_bool_expr(is_arr, pos)
},
]
pos: init_pos
})
}
fn (t &Transformer) sql_field_storage_type(typ types.Type) (types.Type, bool, bool) {
mut cur := typ
mut nullable := false
mut is_arr := false
for {
match cur {
types.OptionType {
nullable = true
cur = cur.base_type
continue
}
types.Alias {
cur = cur.base_type
continue
}
types.Pointer {
cur = cur.base_type
continue
}
types.Array {
is_arr = true
cur = cur.elem_type
continue
}
else {}
}
break
}
return cur, nullable, is_arr
}
fn (mut t Transformer) sql_orm_type_idx_expr(typ types.Type, pos token.Pos) ast.Expr {
type_name := t.type_to_c_name(typ)
if type_name == 'time__Time' || type_name == 'time.Time' {
return t.sql_int_expr(-2, pos)
}
match typ {
types.Enum {
return t.sql_int_expr(-3, pos)
}
types.String {
return t.sql_int_expr(typeof_type_idx('string'), pos)
}
types.Primitive {
return t.sql_int_expr(typeof_type_idx(type_name), pos)
}
else {}
}
if type_name == 'string' {
return t.sql_int_expr(typeof_type_idx('string'), pos)
}
return t.sql_int_expr(typeof_type_idx(type_name), pos)
}
fn sql_field_is_skipped(attrs []ast.Attribute) bool {
if attrs.has('skip') {
return true
}
for attr in attrs {
if attr.name == 'sql' && sql_attribute_value_string(attr.value) == '-' {
return true
}
}
return false
}
fn (mut t Transformer) sql_vattribute_array_expr(attrs []ast.Attribute, pos token.Pos) ast.Expr {
mut exprs := []ast.Expr{cap: attrs.len}
for attr in attrs {
if attr_expr := t.sql_vattribute_expr(attr, pos) {
exprs << attr_expr
}
}
array_pos := t.next_synth_pos()
elem_pos := t.next_synth_pos()
return ast.Expr(ast.ArrayInitExpr{
typ: ast.Expr(ast.Type(ast.ArrayType{
elem_type: ast.Expr(ast.Ident{
name: 'VAttribute'
pos: elem_pos
})
}))
exprs: exprs
pos: array_pos
})
}
fn (mut t Transformer) sql_vattribute_expr(attr ast.Attribute, pos token.Pos) ?ast.Expr {
mut name := attr.name
mut has_arg := attr.name != ''
mut arg := ''
mut kind := 'AttributeKind__plain'
if name == '' {
name = sql_attribute_value_string(attr.value)
has_arg = false
_, kind = sql_attribute_arg_and_kind(attr.value)
if kind != 'AttributeKind__string' {
kind = 'AttributeKind__plain'
}
} else {
arg, kind = sql_attribute_arg_and_kind(attr.value)
}
if name == '' {
return none
}
init_pos := t.next_synth_pos()
typ_pos := t.next_synth_pos()
kind_pos := t.next_synth_pos()
return ast.Expr(ast.InitExpr{
typ: ast.Expr(ast.Ident{
name: 'VAttribute'
pos: typ_pos
})
fields: [
ast.FieldInit{
name: 'name'
value: t.sql_string_expr(name, pos)
},
ast.FieldInit{
name: 'has_arg'
value: t.sql_bool_expr(has_arg, pos)
},
ast.FieldInit{
name: 'arg'
value: t.sql_string_expr(arg, pos)
},
ast.FieldInit{
name: 'kind'
value: ast.Expr(ast.Ident{
name: kind
pos: kind_pos
})
},
]
pos: init_pos
})
}
fn sql_attribute_arg_and_kind(expr ast.Expr) (string, string) {
match expr {
ast.StringLiteral {
return sql_unquote_literal(expr.value), 'AttributeKind__string'
}
ast.BasicLiteral {
if expr.kind in [.key_true, .key_false] {
return expr.value, 'AttributeKind__bool'
}
return expr.value, 'AttributeKind__number'
}
ast.Ident {
kind := if expr.name in sql_plain_attribute_args {
'AttributeKind__plain'
} else {
'AttributeKind__string'
}
return expr.name, kind
}
ast.PrefixExpr {
if expr.op == .minus && expr.expr is ast.BasicLiteral {
inner := expr.expr as ast.BasicLiteral
return '-' + inner.value, 'AttributeKind__number'
}
}
else {}
}
return expr.name(), 'AttributeKind__plain'
}
fn sql_attribute_value_string(expr ast.Expr) string {
match expr {
ast.StringLiteral {
return sql_unquote_literal(expr.value)
}
ast.BasicLiteral {
return expr.value
}
ast.Ident {
return expr.name
}
ast.PrefixExpr {
if expr.op == .minus && expr.expr is ast.BasicLiteral {
inner := expr.expr as ast.BasicLiteral
return '-' + inner.value
}
}
else {}
}
return expr.name()
}
fn sql_unquote_literal(raw string) string {
if raw.len < 2 {
return raw
}
first := raw[0]
last := raw[raw.len - 1]
if first == last && first in [`'`, `"`] {
return raw[1..raw.len - 1]
}
return raw
}
const sql_plain_attribute_args = ['serial', 'i8', 'i16', 'int', 'i64', 'u8', 'u16', 'u32', 'u64',
'f32', 'f64', 'bool', 'string']
fn (mut t Transformer) sql_orm_type_expr(name string, pos token.Pos) ast.Expr {
_ = pos
expr_pos := t.next_synth_pos()
lhs_pos := t.next_synth_pos()
rhs_pos := t.next_synth_pos()
return ast.Expr(ast.SelectorExpr{
lhs: ast.Expr(ast.Ident{
name: 'orm'
pos: lhs_pos
})
rhs: ast.Ident{
name: name
pos: rhs_pos
}
pos: expr_pos
})
}
fn (mut t Transformer) sql_orm_const_expr(name string, pos token.Pos) ast.Expr {
_ = pos
expr_pos := t.next_synth_pos()
lhs_pos := t.next_synth_pos()
rhs_pos := t.next_synth_pos()
return ast.Expr(ast.SelectorExpr{
lhs: ast.Expr(ast.Ident{
name: 'orm'
pos: lhs_pos
})
rhs: ast.Ident{
name: name
pos: rhs_pos
}
pos: expr_pos
})
}
fn (mut t Transformer) sql_string_expr(value string, pos token.Pos) ast.Expr {
_ = pos
lit_pos := t.next_synth_pos()
return ast.Expr(ast.StringLiteral{
kind: .v
value: value
pos: lit_pos
})
}
fn (mut t Transformer) sql_int_expr(value int, pos token.Pos) ast.Expr {
_ = pos
lit_pos := t.next_synth_pos()
return ast.Expr(ast.BasicLiteral{
kind: .number
value: value.str()
pos: lit_pos
})
}
fn (mut t Transformer) sql_bool_expr(value bool, pos token.Pos) ast.Expr {
_ = pos
lit_pos := t.next_synth_pos()
return ast.Expr(ast.BasicLiteral{
kind: if value { token.Token.key_true } else { token.Token.key_false }
value: if value { 'true' } else { 'false' }
pos: lit_pos
})
}
@@ -0,0 +1,180 @@
// 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
//
// Bit-equality pin for s161: the `post_pass_to_flat` driver must run
// the six flat-aware post_pass steps in the same order as legacy
// `post_pass`, with the same gating, producing a FlatAst whose
// `signature()` matches what legacy `post_pass` + `flatten_files`
// produces. Pins the driver assembly that wires together
// s151/s152/s153/s155/s159/s160's `_to_flat` variants.
module transformer
import v2.ast
import v2.pref as vpref
import v2.types
// Local helper — `v test` compiles each `_test.v` file in isolation.
fn create_post_pass_to_flat_test_transformer() &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
runtime_const_inits_by_mod: map[string][]RuntimeConstInit{}
runtime_const_init_fn_name: map[string]string{}
}
}
fn make_minimal_main_file_with_main_fn() ast.File {
return ast.File{
name: 'main.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.FnDecl{
name: 'main'
stmts: []ast.Stmt{}
}),
]
}
}
fn make_mymod_file_for_driver() ast.File {
return ast.File{
name: 'mymod.v'
mod: 'mymod'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'mymod'
}),
]
}
}
// All gates off → driver is a near-no-op (only `inject_runtime_const_init_fns_to_flat`
// and `inject_main_runtime_const_init_to_flat` and `inject_test_main_to_flat`
// have unconditional entry, but they short-circuit because state is empty).
// Bit-equality with legacy `post_pass + flatten_files` proves the driver
// preserves the post_pass control-flow tree even when all steps no-op.
fn test_post_pass_to_flat_all_gates_off_matches_legacy() {
mut ref_files := [make_minimal_main_file_with_main_fn()]
mut t_ref := create_post_pass_to_flat_test_transformer()
t_ref.post_pass(mut ref_files)
ref_sig := ast.flatten_files(ref_files).signature()
mut b := ast.new_flat_builder()
b.append_file(make_minimal_main_file_with_main_fn())
mut t_sub := create_post_pass_to_flat_test_transformer()
t_sub.post_pass_to_flat(mut b, none)
assert b.flat.signature() == ref_sig
}
// Active runtime-const-init gate exercises s160 (generate the
// `__v_init_consts_mymod` fn into the mymod file) AND s155 (prepend init
// call to main's body). Two-mod fixture leaks intern order through
// `.file.extra=intern(mod)`, so compare per-file via subtree_signature.
fn test_post_pass_to_flat_runtime_const_init_matches_legacy() {
mut t_ref := create_post_pass_to_flat_test_transformer()
t_ref.runtime_const_modules = ['mymod']
t_ref.runtime_const_inits_by_mod['mymod'] = [
RuntimeConstInit{
name: 'mymod__answer'
expr: ast.Expr(ast.BasicLiteral{
kind: .number
value: '42'
})
},
]
mut ref_files := [make_minimal_main_file_with_main_fn(), make_mymod_file_for_driver()]
t_ref.post_pass(mut ref_files)
ref_flat := ast.flatten_files(ref_files)
// File 0 (main): edge 2 = stmts list — main's body should be prepended
// with init call.
ref_main_stmts := ref_flat.child_at(ref_flat.files[0].file_id, 2)
ref_main_sub_sig := ref_flat.subtree_signature(ref_main_stmts)
// File 1 (mymod): edge 2 = stmts list — should contain the generated
// `__v_init_consts_mymod` fn.
ref_mymod_stmts := ref_flat.child_at(ref_flat.files[1].file_id, 2)
ref_mymod_sub_sig := ref_flat.subtree_signature(ref_mymod_stmts)
mut t_sub := create_post_pass_to_flat_test_transformer()
t_sub.runtime_const_modules = ['mymod']
t_sub.runtime_const_inits_by_mod['mymod'] = [
RuntimeConstInit{
name: 'mymod__answer'
expr: ast.Expr(ast.BasicLiteral{
kind: .number
value: '42'
})
},
]
mut b := ast.new_flat_builder()
b.append_file(make_minimal_main_file_with_main_fn())
b.append_file(make_mymod_file_for_driver())
t_sub.post_pass_to_flat(mut b, none)
sub_main_stmts := b.flat.child_at(b.flat.files[0].file_id, 2)
sub_main_sub_sig := b.flat.subtree_signature(sub_main_stmts)
sub_mymod_stmts := b.flat.child_at(b.flat.files[1].file_id, 2)
sub_mymod_sub_sig := b.flat.subtree_signature(sub_mymod_stmts)
assert ref_main_sub_sig == sub_main_sub_sig
assert ref_mymod_sub_sig == sub_mymod_sub_sig
}
// Backend gating: cleanc must skip inject_test_main, eval must skip
// inject_runtime_const_init_fns + inject_main_runtime_const_init_calls.
// We don't fully exercise eval backend here (no test_ fns, no native
// backend), but cleanc gating is observable: with a `test_*` fn, default
// backend produces `fn main()` (synthesised), cleanc skips that. The
// driver must match legacy on both.
fn test_post_pass_to_flat_cleanc_skips_test_main() {
mut t_ref := create_post_pass_to_flat_test_transformer()
t_ref.pref = &vpref.Preferences{
backend: .cleanc
}
test_file := ast.File{
name: 'foo_test.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.FnDecl{
name: 'test_one'
stmts: []ast.Stmt{}
}),
]
}
mut ref_files := [test_file]
t_ref.post_pass(mut ref_files)
ref_sig := ast.flatten_files(ref_files).signature()
mut t_sub := create_post_pass_to_flat_test_transformer()
t_sub.pref = &vpref.Preferences{
backend: .cleanc
}
mut b := ast.new_flat_builder()
b.append_file(ast.File{
name: 'foo_test.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.FnDecl{
name: 'test_one'
stmts: []ast.Stmt{}
}),
]
})
t_sub.post_pass_to_flat(mut b, none)
assert b.flat.signature() == ref_sig
}
@@ -0,0 +1,255 @@
// 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
//
// Bit-equality pin for s167: `propagate_types_from_flat` walks flat files
// directly and must produce the same `t.env` mutations as `propagate_types`.
// Also pins `apply_post_pass_tail_from_flat` against `apply_post_pass_tail`.
//
// The flat path walks stmt/expression cursors directly. Behavior should match
// legacy provided flattening preserves the bits propagate_types reads
// (pos.id, stmt/expr shape).
module transformer
import v2.ast
import v2.pref as vpref
import v2.token
import v2.types
fn create_propagate_test_transformer(backend vpref.Backend) &Transformer {
env := &types.Environment{}
return &Transformer{
pref: &vpref.Preferences{
backend: backend
}
env: unsafe { env }
needed_clone_fns: map[string]string{}
needed_array_contains_fns: map[string]ArrayMethodInfo{}
needed_array_index_fns: map[string]ArrayMethodInfo{}
needed_array_last_index_fns: map[string]ArrayMethodInfo{}
local_decl_types: map[string]types.Type{}
}
}
fn make_propagate_test_files() []ast.File {
return [
ast.File{
name: 'main.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
]
},
ast.File{
name: 'time.v'
mod: 'time'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'time'
}),
]
},
]
}
fn propagate_test_pos(id int) token.Pos {
return token.Pos{
offset: id
id: id
}
}
fn make_propagate_expr_test_files() []ast.File {
arr_ident := ast.Expr(ast.Ident{
name: 'arr'
pos: propagate_test_pos(1)
})
arr_len := ast.Expr(ast.SelectorExpr{
lhs: arr_ident
rhs: ast.Ident{
name: 'len'
pos: propagate_test_pos(2)
}
pos: propagate_test_pos(3)
})
literal := ast.Expr(ast.BasicLiteral{
kind: .number
value: '7'
pos: propagate_test_pos(6)
})
clone_call := ast.Expr(ast.CallExpr{
lhs: ast.Expr(ast.Ident{
name: 'string__clone'
pos: propagate_test_pos(7)
})
args: [
ast.Expr(ast.StringLiteral{
kind: .v
value: 'x'
pos: propagate_test_pos(8)
}),
]
pos: propagate_test_pos(9)
})
return [
ast.File{
name: 'expr.v'
mod: 'main'
stmts: [
ast.Stmt(ast.ModuleStmt{
name: 'main'
}),
ast.Stmt(ast.AssignStmt{
op: .decl_assign
lhs: [
ast.Expr(ast.Ident{
name: 'n'
pos: propagate_test_pos(4)
}),
]
rhs: [
arr_len,
]
}),
ast.Stmt(ast.AssignStmt{
op: .decl_assign
lhs: [
ast.Expr(ast.Ident{
name: 'm'
pos: propagate_test_pos(5)
}),
]
rhs: [
literal,
]
}),
ast.Stmt(ast.AssignStmt{
op: .decl_assign
lhs: [
ast.Expr(ast.Ident{
name: 's'
pos: propagate_test_pos(10)
}),
]
rhs: [
clone_call,
]
}),
]
},
]
}
fn seed_propagate_expr_scope(mut t Transformer) {
mut main_scope := types.new_scope(unsafe { nil })
main_scope.insert('arr', types.Type(types.Array{
elem_type: types.Type(types.int_)
}))
t.cached_scopes['main'] = main_scope
}
// Calling propagate_types_from_flat on an empty flat must not crash and
// must produce no env mutations.
fn test_propagate_types_from_flat_empty_flat_is_noop() {
mut t := create_propagate_test_transformer(.cleanc)
flat := ast.FlatAst{}
pre := t.env.expr_type_count()
t.propagate_types_from_flat(&flat)
assert t.env.expr_type_count() == pre
}
// On a non-empty flat, propagate_types_from_flat visits the same files as
// propagate_types(files) and produces the same env state.
fn test_propagate_types_from_flat_matches_legacy_for_simple_files() {
files := make_propagate_test_files()
flat := ast.flatten_files(files)
mut t_legacy := create_propagate_test_transformer(.cleanc)
mut t_flat := create_propagate_test_transformer(.cleanc)
t_legacy.propagate_types(files)
t_flat.propagate_types_from_flat(&flat)
// Both transformers should end with identical env state. With no
// expressions to propagate, both expression type maps stay empty.
assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count()
// cur_module is set to the last visited file's mod on both paths.
assert t_legacy.cur_module == t_flat.cur_module
}
fn test_propagate_types_from_flat_matches_legacy_for_expression_shapes() {
files := make_propagate_expr_test_files()
flat := ast.flatten_files(files)
mut t_legacy := create_propagate_test_transformer(.cleanc)
mut t_flat := create_propagate_test_transformer(.cleanc)
seed_propagate_expr_scope(mut t_legacy)
seed_propagate_expr_scope(mut t_flat)
t_legacy.propagate_types(files)
t_flat.propagate_types_from_flat(&flat)
assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count()
for id in [3, 4, 5, 6, 8, 9, 10] {
legacy_type := t_legacy.env.get_expr_type(id) or { panic('missing legacy type ${id}') }
flat_type := t_flat.env.get_expr_type(id) or { panic('missing flat type ${id}') }
assert legacy_type.name() == flat_type.name()
}
}
// apply_post_pass_tail_from_flat must match apply_post_pass_tail for the
// same input (synth_types + cached_fn_scopes seeded identically).
fn test_apply_post_pass_tail_from_flat_matches_legacy_arm64_skip() {
// On arm64 the propagate_types step is skipped — both helpers reduce to
// synth_types + fn_scopes pushes which don't touch the file/flat input.
files := make_propagate_test_files()
flat := ast.flatten_files(files)
mut t_legacy := create_propagate_test_transformer(.arm64)
mut t_flat := create_propagate_test_transformer(.arm64)
// Seed both with one synth_types entry and one fn_scope.
t_legacy.synth_types[42] = types.Type(types.Primitive{
size: 4
})
t_flat.synth_types[42] = types.Type(types.Primitive{
size: 4
})
t_legacy.cached_fn_scopes['Foo__bar'] = &types.Scope{}
t_flat.cached_fn_scopes['Foo__bar'] = &types.Scope{}
t_legacy.apply_post_pass_tail(files)
t_flat.apply_post_pass_tail_from_flat(&flat)
// Expression type maps must contain the synth_types[42] entry on both.
assert t_legacy.env.has_expr_type(42)
assert t_flat.env.has_expr_type(42)
assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count()
// fn_scopes must have the seeded key on both.
lock t_legacy.env.fn_scopes {
assert 'Foo__bar' in t_legacy.env.fn_scopes
}
lock t_flat.env.fn_scopes {
assert 'Foo__bar' in t_flat.env.fn_scopes
}
}
// apply_post_pass_tail_from_flat on non-arm64 must run propagate path.
fn test_apply_post_pass_tail_from_flat_matches_legacy_cleanc() {
files := make_propagate_test_files()
flat := ast.flatten_files(files)
mut t_legacy := create_propagate_test_transformer(.cleanc)
mut t_flat := create_propagate_test_transformer(.cleanc)
t_legacy.apply_post_pass_tail(files)
t_flat.apply_post_pass_tail_from_flat(&flat)
// Both paths should end with identical env state. With a stmt-free
// fixture, propagate_types is a no-op and expression type maps stay empty.
assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count()
assert t_legacy.cur_module == t_flat.cur_module
}
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
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,515 @@
// 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 transformer
import os
import v2.ast
import v2.parser
import v2.pref
import v2.token
import v2.types
// Integration test: run the full pipeline (parse → type check → transform) on
// cmd/v2/v2.v and verify that every expression with a valid position carries a
// type in the environment after transformation.
struct ExprTypeChecker {
env &types.Environment
file_set &token.FileSet
mut:
total int
missing int
details []string
by_kind map[string]int
in_generic_fn bool
generic_miss int
cur_fn_name string
fn_miss map[string]int
seen_expr_ids map[int]bool
}
fn test_v2_transformer_all_exprs_have_types() {
vroot := detect_vroot()
v2_dir := os.join_path(vroot, 'cmd', 'v2')
assert os.is_dir(v2_dir), 'cmd/v2 directory not found at ${v2_dir}'
prefs := &pref.Preferences{
backend: .cleanc
vroot: vroot
no_parallel: true
}
// --- Parse ---
mut p := parser.Parser.new(prefs)
mut file_set := token.FileSet.new()
// Parse core modules (same order as builder)
core_module_paths := [
'builtin',
'strconv',
'strings',
'hash',
'math.bits',
'os',
'time',
'term',
'term.termios',
'os.cmdline',
'encoding.binary',
'crypto.sha256',
'strings.textscanner',
]
mut ast_files := []ast.File{}
for mod_path in core_module_paths {
module_dir := prefs.get_vlib_module_path(mod_path)
module_files := get_v_files_from_dir(module_dir)
parsed := p.parse_files(module_files, mut file_set)
ast_files << parsed
}
// Parse user files (only v2.v, not test files in the same directory)
user_files := [os.join_path(v2_dir, 'v2.v')]
parsed_user := p.parse_files(user_files, mut file_set)
ast_files << parsed_user
// Parse imports
mut parsed_imports := []string{}
parsed_imports << core_module_paths
for afi := 0; afi < ast_files.len; afi++ {
ast_file := ast_files[afi]
for mod in ast_file.imports {
if mod.name in parsed_imports {
continue
}
mod_dir := prefs.get_module_path(mod.name, ast_file.name)
module_files := get_v_files_from_dir(mod_dir)
parsed := p.parse_files(module_files, mut file_set)
ast_files << parsed
parsed_imports << mod.name
}
}
assert ast_files.len > 0, 'no files parsed'
// --- Type Check ---
env := types.Environment.new()
mut checker := types.Checker.new(prefs, file_set, env)
checker.check_files(ast_files)
// --- Transform ---
mut trans := Transformer.new_with_pref(env, prefs)
transformed := trans.transform_files(ast_files)
// --- Verify: every expression with a valid pos must have a type ---
mut etc := ExprTypeChecker{
env: env
file_set: file_set
seen_expr_ids: map[int]bool{}
}
for file in transformed {
for stmt in file.stmts {
etc.check_stmt(stmt)
}
}
// Allow a small number of missing types from transformer-generated synthetic
// expressions (temp variables, lowered operator calls, etc.) that don't go
// through the checker. Track this threshold and reduce it as coverage improves.
max_missing := 1701
if etc.missing > max_missing {
mut msg := '${etc.missing} of ${etc.total} expressions missing types (max allowed: ${max_missing}).\n'
msg += 'breakdown by kind:\n'
for kind, count in etc.by_kind {
msg += ' ${kind}: ${count}\n'
}
msg += 'by function (${etc.fn_miss.len} fns):\n'
mut fn_counts := []int{}
mut fn_names := []string{}
for fn_name, count in etc.fn_miss {
fn_counts << count
fn_names << fn_name
}
for i := 0; i < fn_counts.len; i++ {
for j := i + 1; j < fn_counts.len; j++ {
if fn_counts[j] > fn_counts[i] {
fn_counts[i], fn_counts[j] = fn_counts[j], fn_counts[i]
fn_names[i], fn_names[j] = fn_names[j], fn_names[i]
}
}
}
fn_limit := if fn_counts.len < 50 { fn_counts.len } else { 50 }
for i := 0; i < fn_limit; i++ {
msg += ' ${fn_names[i]}: ${fn_counts[i]}\n'
}
limit := if etc.details.len < 100 { etc.details.len } else { 100 }
msg += 'first ${limit} missing:\n'
for detail in etc.details[..limit] {
msg += ' ${detail}\n'
}
assert false, msg
}
assert etc.total > 0, 'no expressions found in transformed AST'
}
// --- Helpers ---
fn detect_vroot() string {
mut dir := os.getwd()
for _ in 0 .. 8 {
if os.is_dir(os.join_path(dir, 'vlib', 'builtin')) {
return dir
}
dir = os.dir(dir)
}
home_vroot := os.join_path(os.home_dir(), 'code', 'v')
if os.is_dir(os.join_path(home_vroot, 'vlib', 'builtin')) {
return home_vroot
}
panic('cannot detect vroot')
}
fn get_v_files_from_dir(dir string) []string {
entries := os.ls(dir) or { []string{} }
mut v_files := []string{}
for file in entries {
if !file.ends_with('.v') || file.ends_with('.js.v') || file.contains('_test.') {
continue
}
if file.contains('.arm64.') || file.contains('.arm32.') || file.contains('.amd64.') {
continue
}
if pref.file_has_incompatible_os_suffix(file, os.user_os()) {
continue
}
if file.ends_with('prealloc.c.v') {
continue
}
if file.contains('_d_') {
continue
}
v_files << os.join_path(dir, file)
}
return v_files
}
// --- AST walkers ---
// has_type checks whether the environment has a type set for the given expression ID.
// This checks directly against the Void(1) sentinel (meaning "unset") rather than
// filtering all Void types, so expressions explicitly typed as void (Void(0)) are
// correctly recognized as having a type.
fn (c &ExprTypeChecker) has_type(id int) bool {
return c.env.has_expr_type(id)
}
fn (mut c ExprTypeChecker) check_expr(expr ast.Expr) {
pos := expr.pos()
if pos.is_valid() {
if pos.id in c.seen_expr_ids {
return
}
c.seen_expr_ids[pos.id] = true
c.total++
if c.has_type(pos.id) {
// ok
} else {
if c.in_generic_fn {
c.generic_miss++
}
c.missing++
c.fn_miss[c.cur_fn_name] = c.fn_miss[c.cur_fn_name] + 1
kind := expr.type_name()
c.by_kind[kind] = c.by_kind[kind] + 1
if c.details.len < 100 {
file := c.file_set.file(pos)
position := file.position(pos)
extra := match expr {
ast.Ident { ' name="${expr.name}"' }
ast.BasicLiteral { ' val="${expr.value}"' }
ast.StringLiteral { ' val="${expr.value}"' }
ast.SelectorExpr { ' .sel' }
ast.CallExpr { ' call' }
ast.InfixExpr { ' op=${expr.op}' }
ast.IndexExpr { ' idx' }
ast.CastExpr { ' cast' }
ast.PrefixExpr { ' op=${expr.op}' }
ast.ParenExpr { ' paren' }
ast.ModifierExpr { ' mod=${expr.kind}' }
ast.KeywordOperator { ' kw' }
ast.PostfixExpr { ' op=${expr.op}' }
ast.IfExpr { ' if' }
else { '' }
}
c.details << '${position} id=${pos.id} kind=${kind}${extra}'
}
}
}
// Recurse into sub-expressions
match expr {
ast.ArrayInitExpr {
c.check_expr(expr.typ)
for e in expr.exprs {
c.check_expr(e)
}
c.check_expr(expr.init)
c.check_expr(expr.cap)
c.check_expr(expr.len)
}
ast.AsCastExpr {
c.check_expr(expr.expr)
c.check_expr(expr.typ)
}
ast.AssocExpr {
c.check_expr(expr.typ)
c.check_expr(expr.expr)
for f in expr.fields {
c.check_expr(f.value)
}
}
ast.BasicLiteral {}
ast.CallExpr {
c.check_expr(expr.lhs)
for arg in expr.args {
c.check_expr(arg)
}
}
ast.CallOrCastExpr {
c.check_expr(expr.lhs)
c.check_expr(expr.expr)
}
ast.CastExpr {
c.check_expr(expr.typ)
c.check_expr(expr.expr)
}
ast.ComptimeExpr {
c.check_expr(expr.expr)
}
ast.FnLiteral {
for cv in expr.captured_vars {
c.check_expr(cv)
}
for s in expr.stmts {
c.check_stmt(s)
}
}
ast.GenericArgs {
c.check_expr(expr.lhs)
for arg in expr.args {
c.check_expr(arg)
}
}
ast.GenericArgOrIndexExpr {
c.check_expr(expr.lhs)
c.check_expr(expr.expr)
}
ast.Ident {}
ast.IfExpr {
c.check_expr(expr.cond)
for s in expr.stmts {
c.check_stmt(s)
}
c.check_expr(expr.else_expr)
}
ast.IfGuardExpr {
c.check_stmt(ast.Stmt(expr.stmt))
}
ast.InfixExpr {
c.check_expr(expr.lhs)
c.check_expr(expr.rhs)
}
ast.IndexExpr {
c.check_expr(expr.lhs)
c.check_expr(expr.expr)
}
ast.InitExpr {
c.check_expr(expr.typ)
for f in expr.fields {
c.check_expr(f.value)
}
}
ast.KeywordOperator {
for e in expr.exprs {
c.check_expr(e)
}
}
ast.LambdaExpr {
c.check_expr(expr.expr)
}
ast.LockExpr {
for e in expr.lock_exprs {
c.check_expr(e)
}
for e in expr.rlock_exprs {
c.check_expr(e)
}
for s in expr.stmts {
c.check_stmt(s)
}
}
ast.MapInitExpr {
c.check_expr(expr.typ)
for k in expr.keys {
c.check_expr(k)
}
for v in expr.vals {
c.check_expr(v)
}
}
ast.MatchExpr {
c.check_expr(expr.expr)
for br in expr.branches {
for cond in br.cond {
c.check_expr(cond)
}
for s in br.stmts {
c.check_stmt(s)
}
}
}
ast.ModifierExpr {
c.check_expr(expr.expr)
}
ast.OrExpr {
c.check_expr(expr.expr)
for s in expr.stmts {
c.check_stmt(s)
}
}
ast.ParenExpr {
c.check_expr(expr.expr)
}
ast.PostfixExpr {
c.check_expr(expr.expr)
}
ast.PrefixExpr {
c.check_expr(expr.expr)
}
ast.RangeExpr {
c.check_expr(expr.start)
c.check_expr(expr.end)
}
ast.SelectExpr {
c.check_stmt(expr.stmt)
for s in expr.stmts {
c.check_stmt(s)
}
c.check_expr(expr.next)
}
ast.SelectorExpr {
c.check_expr(expr.lhs)
}
ast.SqlExpr {
c.check_expr(expr.expr)
}
ast.StringInterLiteral {
for inter in expr.inters {
c.check_expr(inter.expr)
c.check_expr(inter.format_expr)
}
}
ast.StringLiteral {}
ast.Tuple {
for e in expr.exprs {
c.check_expr(e)
}
}
ast.UnsafeExpr {
for s in expr.stmts {
c.check_stmt(s)
}
}
else {}
}
}
fn (mut c ExprTypeChecker) check_stmt(stmt ast.Stmt) {
match stmt {
ast.AssertStmt {
c.check_expr(stmt.expr)
c.check_expr(stmt.extra)
}
ast.AssignStmt {
for e in stmt.lhs {
c.check_expr(e)
}
for e in stmt.rhs {
c.check_expr(e)
}
}
ast.BlockStmt {
for s in stmt.stmts {
c.check_stmt(s)
}
}
ast.ComptimeStmt {
c.check_stmt(stmt.stmt)
}
ast.ConstDecl {
for f in stmt.fields {
c.check_expr(f.value)
}
}
ast.DeferStmt {
for s in stmt.stmts {
c.check_stmt(s)
}
}
ast.ExprStmt {
c.check_expr(stmt.expr)
}
ast.FnDecl {
prev_generic := c.in_generic_fn
prev_fn := c.cur_fn_name
c.cur_fn_name = stmt.name
if stmt.typ.generic_params.len > 0 {
c.in_generic_fn = true
}
for s in stmt.stmts {
c.check_stmt(s)
}
c.in_generic_fn = prev_generic
c.cur_fn_name = prev_fn
}
ast.ForStmt {
c.check_stmt(stmt.init)
c.check_expr(stmt.cond)
c.check_stmt(stmt.post)
for s in stmt.stmts {
c.check_stmt(s)
}
}
ast.ForInStmt {
c.check_expr(stmt.key)
c.check_expr(stmt.value)
c.check_expr(stmt.expr)
}
ast.LabelStmt {
c.check_stmt(stmt.stmt)
}
ast.ReturnStmt {
for e in stmt.exprs {
c.check_expr(e)
}
}
ast.EnumDecl {
for f in stmt.fields {
c.check_expr(f.value)
}
}
ast.GlobalDecl {
for f in stmt.fields {
c.check_expr(f.value)
}
}
ast.InterfaceDecl {}
ast.StructDecl {}
ast.TypeDecl {}
else {}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff