Initial project import
This commit is contained in:
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
|
||||
}
|
||||
+227
@@ -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
|
||||
}
|
||||
+161
@@ -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
Reference in new issue
Block a user