From 3dd96de45339a469cb83ffacc0e50b017468e032 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 7 Jun 2026 04:38:57 +0300 Subject: [PATCH] v2: finish flat AST migration (#27372) --- vlib/v2/README.md | 4 +- vlib/v2/ast/cursor.v | 33 ++ vlib/v2/ast/flat_reader.v | 18 +- vlib/v2/builder/builder.v | 134 +++-- vlib/v2/builder/flag_target_test.v | 30 ++ vlib/v2/builder/flat_streaming_test.v | 4 +- vlib/v2/builder/parse.v | 59 ++- vlib/v2/builder/parse_d_parallel.v | 39 +- vlib/v2/builder/parse_test.v | 27 +- vlib/v2/builder/target_os_test.v | 67 +++ vlib/v2/gen/cleanc/cleanc_test.v | 65 +++ vlib/v2/gen/cleanc/for.v | 86 +++ .../inject_live_reload_to_flat_test.v | 2 +- vlib/v2/transformer/live.v | 33 +- .../transformer/minimal_runtime_init_test.v | 77 +++ vlib/v2/transformer/monomorphize.v | 496 ++++++++++++++++++ .../propagate_types_from_flat_test.v | 14 +- vlib/v2/transformer/transformer.v | 323 ++++++++++-- .../transformer/transformer_flat_diff_test.v | 51 ++ vlib/v2/transformer/type_propagation.v | 37 +- vlib/v2/transformer/types.v | 6 + vlib/v2/types/checker.v | 50 +- vlib/v2/types/checker_flat_test.v | 26 + 23 files changed, 1465 insertions(+), 216 deletions(-) diff --git a/vlib/v2/README.md b/vlib/v2/README.md index b83b64415..553b0d02b 100644 --- a/vlib/v2/README.md +++ b/vlib/v2/README.md @@ -4,8 +4,10 @@ ## Flat AST -`v2.ast` now includes an index-based flat AST graph for tooling and profiling: +`v2.ast` is the default index-based AST graph used by the v2 builder for +parsing, type checking, markused, and native SSA input: - `ast.flatten_files(files)` builds `ast.FlatAst` with contiguous `nodes` and `edges`. - `ast.legacy_ast_stats(files)` estimates memory/shape metrics for recursive AST files. - `flat.stats()` and `flat.count_reachable_nodes()` report flat graph size and reachability. +- `V2_LEGACY_AST=1` keeps the old recursive-AST pipeline available for comparison. diff --git a/vlib/v2/ast/cursor.v b/vlib/v2/ast/cursor.v index 3a36e3f3a..40a404445 100644 --- a/vlib/v2/ast/cursor.v +++ b/vlib/v2/ast/cursor.v @@ -73,6 +73,39 @@ pub fn (c Cursor) name_id() int { return c.flat.nodes[c.id].name_id } +// ident reads an expr_ident cursor directly into an Ident. +pub fn (c Cursor) ident() Ident { + if !c.is_valid() || c.kind() != .expr_ident { + return Ident{} + } + return Ident{ + pos: c.pos() + name: c.name() + } +} + +// import_stmt reads a stmt_import cursor directly into an ImportStmt. +pub fn (c Cursor) import_stmt() ImportStmt { + if !c.is_valid() || c.kind() != .stmt_import { + return ImportStmt{} + } + mut symbols := []Expr{cap: c.edge_count()} + for i in 0 .. c.edge_count() { + sym := c.edge(i) + if sym.kind() == .expr_ident { + symbols << Expr(sym.ident()) + } else if sym.is_valid() { + symbols << c.flat.decode_expr(sym.id) + } + } + return ImportStmt{ + name: c.name() + alias: c.extra_str() + is_aliased: c.flag(flag_is_aliased) + symbols: symbols + } +} + @[inline] pub fn (c Cursor) edge_count() int { return c.flat.nodes[c.id].edge_count diff --git a/vlib/v2/ast/flat_reader.v b/vlib/v2/ast/flat_reader.v index 15707e4d5..91ecf93aa 100644 --- a/vlib/v2/ast/flat_reader.v +++ b/vlib/v2/ast/flat_reader.v @@ -93,9 +93,12 @@ fn (r &FlatReader) read_file(ff FlatFile) File { } mut imports := []ImportStmt{} for cid in r.list_children(imports_id) { - s := r.read_stmt(cid) - if s is ImportStmt { - imports << s + c := Cursor{ + flat: r.flat + id: cid + } + if c.kind() == .stmt_import { + imports << c.import_stmt() } } mut stmts := []Stmt{} @@ -123,9 +126,12 @@ pub fn (flat &FlatAst) read_file_imports(ff FlatFile) []ImportStmt { imports_id := r.edge(n, 1) mut imports := []ImportStmt{} for cid in r.list_children(imports_id) { - s := r.read_stmt(cid) - if s is ImportStmt { - imports << s + c := Cursor{ + flat: r.flat + id: cid + } + if c.kind() == .stmt_import { + imports << c.import_stmt() } } return imports diff --git a/vlib/v2/builder/builder.v b/vlib/v2/builder/builder.v index f95fed576..0622bd09a 100644 --- a/vlib/v2/builder/builder.v +++ b/vlib/v2/builder/builder.v @@ -48,10 +48,10 @@ mut: used_vh_for_parse bool used_import_vh_for_parse bool used_virtual_vh_for_parse bool - flat_roundtrip_enabled bool // V2_FLAT_ROUNDTRIP=1: route parses through streaming + to_files() - flat_check_enabled bool // V2_CHECK_FLAT=1: route type-check through Checker.check_flat - markused_flat_enabled bool // V2_MARKUSED_FLAT=1: route markused through mark_used_flat shim - flat_ssa_enabled bool // V2_FLAT_SSA=1: route the (sequential) native SSA build through build_all_from_flat on the post-transform b.flat. Requires V2_CHECK_FLAT + V2_MARKUSED_FLAT so b.flat is post-transform-populated. Default off. + flat_roundtrip_enabled bool // V2_FLAT_ROUNDTRIP=1: legacy comparison mode; route parses through streaming + to_files(). + flat_check_enabled bool // Default on: stream parse/type-check through FlatAst. V2_LEGACY_AST=1 disables it unless V2_CHECK_FLAT=1 is set. + markused_flat_enabled bool // Default on: route markused through mark_used_flat. + flat_ssa_enabled bool // Default on: route SSA codegen through build_all_from_flat on the post-transform b.flat. // flat caches the FlatAst representation of b.files. When // flat_check_enabled is set, parse_batch streams directly into // flat_builder so b.flat is built incrementally during parsing rather @@ -71,15 +71,17 @@ mut: } pub fn new_builder(prefs &pref.Preferences) &Builder { + legacy_ast_enabled := os.getenv('V2_LEGACY_AST') != '' + flat_default_enabled := !legacy_ast_enabled unsafe { return &Builder{ pref: prefs used_fn_keys: map[string]bool{} cached_called_fn_names: map[string]bool{} flat_roundtrip_enabled: os.getenv('V2_FLAT_ROUNDTRIP') != '' - flat_check_enabled: os.getenv('V2_CHECK_FLAT') != '' - markused_flat_enabled: os.getenv('V2_MARKUSED_FLAT') != '' - flat_ssa_enabled: os.getenv('V2_FLAT_SSA') != '' + flat_check_enabled: flat_default_enabled || os.getenv('V2_CHECK_FLAT') != '' + markused_flat_enabled: flat_default_enabled || os.getenv('V2_MARKUSED_FLAT') != '' + flat_ssa_enabled: flat_default_enabled || os.getenv('V2_FLAT_SSA') != '' } } } @@ -108,6 +110,20 @@ fn (b &Builder) backend_uses_markused_pruning() bool { return b.pref.backend != .arm64 } +fn (b &Builder) should_build_ssa_from_flat() bool { + return b.flat.files.len > 0 && b.flat_ssa_enabled && b.markused_flat_enabled + && b.flat_check_enabled +} + +fn (b &Builder) should_keep_flat_for_codegen() bool { + flat_ssa_codegen := b.flat_ssa_enabled && b.markused_flat_enabled && b.flat_check_enabled + return match b.pref.backend { + .c { flat_ssa_codegen } + .x64, .arm64 { flat_ssa_codegen || b.native_flat_pipeline_enabled } + else { false } + } +} + fn (b &Builder) can_compile_cleanc_locally() bool { if b.pref == unsafe { nil } { return true @@ -263,20 +279,26 @@ pub fn (mut b Builder) build(files []string) { // transform_files_from_flat, parallel streams per-worker via // to_files_range. No up-front full rehydration needed in either case. // - // When V2_MARKUSED_FLAT is also enabled, both transform paths route - // through their *_to_flat wedge so the post-transform flatten lives - // inside the transformer call (one round-trip), avoiding a separate - // flatten_files() pass before mark_used_flat. + // Flat markused routes transform through flat-output wedges. Backends that + // can consume flat codegen use the direct flat-output path and drop the + // transformed []ast.File result; legacy backends keep the compatibility + // wedge that returns both flat and files. mut flat_populated_by_transform := false + transform_flat_only := b.should_keep_flat_for_codegen() if sequential_transform { if use_native_flat_pipeline && !b.flat_check_enabled { b.flat = trans.transform_files_to_flat_direct(b.files) b.files = []ast.File{} flat_populated_by_transform = true } else if use_flat_markused { - new_flat, files_out := trans.transform_files_to_flat(&b.flat, b.files) - b.flat = new_flat - b.files = files_out + if transform_flat_only { + b.flat = trans.transform_flat_to_flat_direct(&b.flat, b.files) + b.files = []ast.File{} + } else { + new_flat, files_out := trans.transform_files_to_flat(&b.flat, b.files) + b.flat = new_flat + b.files = files_out + } flat_populated_by_transform = true } else if b.flat_check_enabled { b.files = trans.transform_files_from_flat(&b.flat, b.files) @@ -289,9 +311,14 @@ pub fn (mut b Builder) build(files []string) { b.files = []ast.File{} flat_populated_by_transform = true } else if use_flat_markused { - new_flat, files_out := b.transform_files_parallel_to_flat(mut trans) - b.flat = new_flat - b.files = files_out + if transform_flat_only { + b.flat = trans.transform_flat_to_flat_direct(&b.flat, b.files) + b.files = []ast.File{} + } else { + new_flat, files_out := b.transform_files_parallel_to_flat(mut trans) + b.flat = new_flat + b.files = files_out + } flat_populated_by_transform = true } else if b.flat_check_enabled { b.files = b.transform_files_parallel_from_flat(mut trans) @@ -308,17 +335,15 @@ pub fn (mut b Builder) build(files []string) { b.used_fn_keys = map[string]bool{} } else { mark_used_start := sw.elapsed() - // V2_MARKUSED_FLAT only takes effect when V2_CHECK_FLAT is also on, - // since b.flat is only populated when flat_check_enabled streams - // parses into flat_builder. Without that, b.flat is empty and the - // shim would walk nothing. + // Flat markused consumes the post-transform FlatAst. Legacy comparison + // mode (`V2_LEGACY_AST=1`) can still reach the AST walker unless one of + // the flat env flags explicitly re-enables this path. // - // The transformer mutates b.files but does not write back into - // b.flat. Both sequential and parallel paths now populate b.flat - // as part of their *_to_flat wedge when V2_MARKUSED_FLAT is on, - // so the separate flatten_files() pass is gone. The branch below - // remains as a defensive fallback for any future code path that - // reaches markused without having set flat_populated_by_transform. + // The transformer mutates b.files but does not write back into b.flat. + // Both sequential and parallel paths now populate b.flat as part of + // their *_to_flat wedge, so the separate flatten_files() pass is gone. + // The branch below remains as a defensive fallback for any future code + // path that reaches markused without setting flat_populated_by_transform. if use_flat_markused && !flat_populated_by_transform { b.flat = ast.flatten_files(b.files) } @@ -340,16 +365,11 @@ pub fn (mut b Builder) build(files []string) { } mark_used_time := time.Duration(sw.elapsed() - mark_used_start) print_time('Mark Used', mark_used_time) - // b.flat is unused by the legacy codegen path. Under -gc none this is a - // no-op for peak memory, but keep the lifetime explicit for readers. - // When V2_FLAT_SSA or the native flat pipeline is on, the native SSA - // build consumes b.flat directly (build_all_from_flat), so keep it alive - // through codegen. - if !b.flat_ssa_enabled && !b.native_flat_pipeline_enabled { - b.flat = ast.FlatAst{} - } print_rss('after markused') } + if b.flat_check_enabled && !b.should_keep_flat_for_codegen() { + b.flat = ast.FlatAst{} + } // Generate output based on backend match b.pref.backend { @@ -601,7 +621,13 @@ fn (mut b Builder) gen_ssa_c() { ssa_builder.target_os = b.pref.target_os_or_host() mut stage_start := sw.elapsed() - ssa_builder.build_all(b.files) + mut built_from_flat := false + if b.should_build_ssa_from_flat() { + ssa_builder.build_all_from_flat(&b.flat) + built_from_flat = true + } else { + ssa_builder.build_all(b.files) + } print_time('SSA Build', time.Duration(sw.elapsed() - stage_start)) // TODO: re-enable SSA optimization once the new builder is mature @@ -618,6 +644,9 @@ fn (mut b Builder) gen_ssa_c() { } if output_name.ends_with('.c') { + if built_from_flat { + b.flat = ast.FlatAst{} + } stage_start = sw.elapsed() mut gen := c.new_gen(mod) c_source := gen.gen() @@ -633,6 +662,12 @@ fn (mut b Builder) gen_ssa_c() { cc := if b.pref.ccompiler.len > 0 { b.pref.ccompiler } else { configured_cc(b.pref.vroot) } directive_flags := b.collect_cflags_from_sources() + if built_from_flat { + // SSA has copied the program into MIR, and directive scanning has read + // source names from the FlatAst. Keep the later C generator/compiler + // working sets clear of the transformed FlatAst. + b.flat = ast.FlatAst{} + } mut cc_flag_parts := []string{} env_flags := configured_cflags() if env_flags.trim_space() != '' { @@ -1684,7 +1719,7 @@ fn (mut b Builder) prepare_macos_tiny_candidate_source_files() { if !b.uses_macos_x64_tiny_object(.x64) { return } - b.macos_tiny_candidate_source_files = if b.flat_check_enabled { + b.macos_tiny_candidate_source_files = if b.flat_check_enabled && b.flat.files.len > 0 { b.flat.to_files() } else { b.files.clone() @@ -1998,6 +2033,12 @@ fn (b &Builder) collect_cflags_from_sources() string { scan_paths << file.name } } + for ff in b.flat.files { + name := b.flat.file_name(ff) + if name != '' { + scan_paths << name + } + } cflags_target_os := b.cflags_target_os_for_local_compile() if !b.pref.skip_builtin { target_os := cflags_target_os @@ -2567,18 +2608,15 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, } mut stage_start := native_sw.elapsed() - // V2_FLAT_SSA: route the whole SSA build through the cursor-native - // build_all_from_flat on the post-transform b.flat (kept alive above). - // Sequential only (build_all_from_flat builds fn bodies in-phase). Default off. + // Route the whole SSA build through the cursor-native build_all_from_flat + // on the post-transform b.flat (kept alive above). Sequential only + // (build_all_from_flat builds fn bodies in-phase). // - // b.flat is only POST-TRANSFORM when either V2_MARKUSED_FLAT has routed - // transform through transform_files_to_flat, or the native flat pipeline has - // emitted transform output directly into FlatAst. With V2_CHECK_FLAT but NOT - // V2_MARKUSED_FLAT, b.flat stays parse-time, so feeding it here would skip - // transformer rewrites. - build_from_flat := b.flat.files.len > 0 - && ((b.flat_ssa_enabled && b.markused_flat_enabled && b.flat_check_enabled) - || (b.native_flat_pipeline_enabled && label == '')) + // b.flat is only POST-TRANSFORM when flat markused has routed transform + // through transform_files_to_flat, or the direct native flat pipeline has + // emitted transform output directly into FlatAst. + build_from_flat := b.should_build_ssa_from_flat() + || (b.flat.files.len > 0 && b.native_flat_pipeline_enabled && label == '') if build_from_flat { ssa_builder.build_all_from_flat(&b.flat) // SSA has copied the program into MIR; keep the FlatAst lifetime out of diff --git a/vlib/v2/builder/flag_target_test.v b/vlib/v2/builder/flag_target_test.v index f340e3c49..353d423b6 100644 --- a/vlib/v2/builder/flag_target_test.v +++ b/vlib/v2/builder/flag_target_test.v @@ -22,6 +22,26 @@ fn collect_cflags_for_test_source(source string, mut prefs pref.Preferences) str return b.collect_cflags_from_sources() } +fn collect_cflags_for_flat_only_test_source(source string, mut prefs pref.Preferences) string { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_builder_flag_target_flat_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source_path := os.join_path(tmp_dir, 'main.v') + os.write_file(source_path, source) or { panic(err) } + prefs.skip_builtin = true + mut b := new_builder(&prefs) + b.flat = ast.flatten_files([ + ast.File{ + name: source_path + mod: 'main' + }, + ]) + b.files = []ast.File{} + return b.collect_cflags_from_sources() +} + fn freestanding_test_call_name(name string, user_defines []string) string { return freestanding_test_call_name_with_hooks(name, user_defines, []) } @@ -304,6 +324,16 @@ $if linux { assert free_explicit_flags.contains('-DOPTIONAL_FREE_BLOCK') } +fn test_collect_cflags_from_sources_reads_flat_file_names() { + source := 'module main + +#flag -DFLAT_ONLY_FLAG +' + mut prefs := pref.new_preferences() + flags := collect_cflags_for_flat_only_test_source(source, mut prefs) + assert flags.contains('-DFLAT_ONLY_FLAG') +} + fn test_collect_cflags_from_sources_keeps_optional_os_flags_custom_only() { source := 'module main diff --git a/vlib/v2/builder/flat_streaming_test.v b/vlib/v2/builder/flat_streaming_test.v index 7ade3a2d8..a7d88bf35 100644 --- a/vlib/v2/builder/flat_streaming_test.v +++ b/vlib/v2/builder/flat_streaming_test.v @@ -114,7 +114,7 @@ fn test_streaming_real_source_file() { assert flat_batch.signature() == flat_stream.signature(), 'real-source streaming mismatch' } -fn test_check_flat_default_parallel_transform_keeps_user_files() { +fn test_default_flat_parallel_transform_keeps_user_files() { path := write_tmp_file('parallel_transform_main', 'module main fn main() { @@ -126,7 +126,7 @@ fn main() { os.rm(path) or {} os.rm(out_path) or {} } - cmd := 'V2_CHECK_FLAT=1 ${os.quoted_path(@VEXE)} -v2 -nocache -o ${os.quoted_path(out_path)} ${os.quoted_path(path)} 2>&1' + cmd := '${os.quoted_path(@VEXE)} -v2 -nocache -o ${os.quoted_path(out_path)} ${os.quoted_path(path)} 2>&1' res := os.execute(cmd) assert res.exit_code == 0, res.output run_res := os.execute(os.quoted_path(out_path)) diff --git a/vlib/v2/builder/parse.v b/vlib/v2/builder/parse.v index 98fdc8cb2..3ae07ac3a 100644 --- a/vlib/v2/builder/parse.v +++ b/vlib/v2/builder/parse.v @@ -7,6 +7,7 @@ import os import v2.ast import v2.parser import v2.pref as vpref +import v2.token fn should_expand_single_file_input(input string) bool { if os.file_name(input).ends_with('_test.v') { @@ -822,12 +823,7 @@ fn flat_collect_active_imports(stmts ast.CursorList, options ChannelScanOptions, fn flat_collect_active_imports_stmt(s ast.Cursor, options ChannelScanOptions, mut imports []ast.ImportStmt) { match s.kind() { .stmt_import { - // Import nodes are tiny (name/alias/symbols); decoding this one node is - // safe and matches the legacy `imports << stmt` arm. - imp := s.flat.decode_stmt(s.id) - if imp is ast.ImportStmt { - imports << imp - } + imports << s.import_stmt() } .stmt_expr { inner := s.edge(0) @@ -967,17 +963,54 @@ fn flat_fn_decl_body_active_for_channel_scan(fn_c ast.Cursor, options ChannelSca return true } -// flat_comptime_cond_matches evaluates a comptime condition. Conditions are tiny -// exprs (`windows`, `!tinyc`, `a && b`, `pkgconfig('x')`, `x?`), so decoding just -// this sub-expr is cheap and lets us reuse the exact legacy evaluator — the -// whole-file decode this scan replaces is what was unsafe, not a leaf condition. +// flat_comptime_cond_matches evaluates a comptime condition from FlatAst +// cursors. Call/pkgconfig conditions still fall back to the legacy expression +// evaluator until call argument cursors are ported. fn flat_comptime_cond_matches(cond ast.Cursor, options ChannelScanOptions) bool { if !cond.is_valid() { return false } - decoded := cond.flat.decode_expr(cond.id) - return ast_comptime_cond_matches_with_options(decoded, options.user_defines, - options.explicit_user_defines, options.target_os, options.allow_pkgconfig) + match cond.kind() { + .expr_ident { + return ast_comptime_flag_matches(cond.name(), options.user_defines, options.target_os) + } + .expr_comptime, .expr_paren { + return flat_comptime_cond_matches(cond.edge(0), options) + } + .expr_prefix { + op := unsafe { token.Token(int(cond.aux())) } + if op == .not { + return !flat_comptime_cond_matches(cond.edge(0), options) + } + } + .expr_infix { + op := unsafe { token.Token(int(cond.aux())) } + if op == .and { + return flat_comptime_cond_matches(cond.edge(0), options) + && flat_comptime_cond_matches(cond.edge(1), options) + } + if op == .logical_or { + return flat_comptime_cond_matches(cond.edge(0), options) + || flat_comptime_cond_matches(cond.edge(1), options) + } + } + .expr_postfix { + op := unsafe { token.Token(int(cond.aux())) } + inner := cond.edge(0) + if op == .question && inner.kind() == .expr_ident { + return vpref.comptime_optional_define_value(inner.name(), options.user_defines, + options.explicit_user_defines) + } + } + .expr_call, .expr_call_or_cast { + decoded := cond.flat.decode_expr(cond.id) + return ast_comptime_cond_matches_with_options(decoded, options.user_defines, + options.explicit_user_defines, options.target_os, options.allow_pkgconfig) + } + else {} + } + + return false } // parse_batch routes a parser.parse_files() call through either the direct diff --git a/vlib/v2/builder/parse_d_parallel.v b/vlib/v2/builder/parse_d_parallel.v index a5565f2fc..08bd7e5c2 100644 --- a/vlib/v2/builder/parse_d_parallel.v +++ b/vlib/v2/builder/parse_d_parallel.v @@ -15,8 +15,8 @@ struct ParsingSharedState { mut: file_set &token.FileSet parsed_modules shared []string - // flat_enabled drives the under-lock append into flat_builder. When - // false (V2_CHECK_FLAT unset), workers skip the lock entirely. + // flat_enabled is reserved for the future thread-safe flat parallel parser. + // The default flat pipeline currently returns before spawning workers. flat_enabled bool flat_mu &sync.Mutex = unsafe { nil } flat_builder &ast.FlatBuilder = unsafe { nil } @@ -78,32 +78,14 @@ fn worker(mut wp util.WorkerPool[string, ast.File], mut pstate ParsingSharedStat fn (mut b Builder) parse_files_parallel(files []string) []ast.File { if b.flat_check_enabled { - // Pre-size the streaming builder from total source bytes (user + - // core source dirs) so workers don't trip the geometric realloc - // path inside the mutex. - mut pre_size_paths := []string{} - pre_size_paths << files - if !b.pref.skip_builtin { - if b.can_use_cached_core_headers_for_parse() { - pre_size_paths << b.core_cached_parse_paths() - } else { - target_os := b.pref.target_os_or_host() - for module_path in core_cached_module_paths { - pre_size_paths << get_v_files_from_dir(b.pref.get_vlib_module_path(module_path), - b.pref.user_defines, target_os) - } - } - } - b.init_flat_builder_for_paths(pre_size_paths) + // FlatAst mode is the default pipeline. Keep parsing serial for now: + // token.FileSet shares position counters and file slices that are not + // concurrency-safe, and selector_names are keyed by those position ids. + return b.parse_files(files) } mut pstate := &ParsingSharedState{ file_set: b.file_set } - if b.flat_check_enabled { - pstate.flat_enabled = true - pstate.flat_mu = sync.new_mutex() - pstate.flat_builder = unsafe { &b.flat_builder } - } // mut worker_pool := util.WorkerPool.new[string, ast.File](mut ch_in, mut ch_out) mut worker_pool := util.WorkerPool.new[string, ast.File]() @@ -135,14 +117,5 @@ fn (mut b Builder) parse_files_parallel(files []string) []ast.File { worker_pool.queue_jobs(files) results := worker_pool.wait_for_results() - if b.flat_check_enabled { - // Stream-into-builder is done; downstream code reads from b.flat, - // so flip the inited flag to skip the fallback flatten_files() pass. - // `results` is a slice of empty File{} sentinels — drop it; build() - // derives b.files from b.flat once the FlatAst is finalized. - _ = results - b.flat_builder_inited = true - return []ast.File{} - } return results } diff --git a/vlib/v2/builder/parse_test.v b/vlib/v2/builder/parse_test.v index b3f67a64b..c041c232b 100644 --- a/vlib/v2/builder/parse_test.v +++ b/vlib/v2/builder/parse_test.v @@ -5,6 +5,15 @@ import os import v2.ast import v2.pref +fn parse_files_for_parse_test(mut b Builder, paths []string) []ast.File { + files := b.parse_files(paths) + if b.flat_check_enabled { + b.flat = b.flat_builder.flat + return b.flat.to_files() + } + return files +} + fn test_parse_files_keeps_single_file_inputs_isolated() { tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_single_${os.getpid()}') os.rmdir_all(tmp_dir) or {} @@ -22,7 +31,7 @@ fn test_parse_files_keeps_single_file_inputs_isolated() { prefs.skip_builtin = true prefs.skip_imports = true mut b := new_builder(&prefs) - files := b.parse_files([entry_file]) + files := parse_files_for_parse_test(mut b, [entry_file]) assert files.len == 1 } @@ -44,7 +53,7 @@ fn test_parse_files_expands_directory_inputs() { prefs.skip_builtin = true prefs.skip_imports = true mut b := new_builder(&prefs) - files := b.parse_files([tmp_dir]) + files := parse_files_for_parse_test(mut b, [tmp_dir]) assert files.len == 2 } @@ -75,7 +84,7 @@ fn test_parse_files_expands_same_module_subdirectories() { prefs.skip_builtin = true prefs.skip_imports = true mut b := new_builder(&prefs) - files := b.parse_files([tmp_dir]) + files := parse_files_for_parse_test(mut b, [tmp_dir]) names := files.map(os.file_name(it.name)) assert files.len == 2 @@ -100,7 +109,7 @@ fn test_parse_files_expands_implicit_main_subdirectories() { prefs.skip_builtin = true prefs.skip_imports = true mut b := new_builder(&prefs) - files := b.parse_files([tmp_dir]) + files := parse_files_for_parse_test(mut b, [tmp_dir]) names := files.map(os.file_name(it.name)) assert files.len == 2 @@ -127,7 +136,7 @@ fn test_parse_files_skips_block_commented_non_main_subdirectories() { prefs.skip_builtin = true prefs.skip_imports = true mut b := new_builder(&prefs) - files := b.parse_files([tmp_dir]) + files := parse_files_for_parse_test(mut b, [tmp_dir]) names := files.map(os.file_name(it.name)) assert files.len == 1 @@ -208,7 +217,7 @@ fn test_virtual_main_modules_skip_groups_with_executable_main_from_ast() { prefs.skip_imports = true mut b := new_builder(&prefs) b.user_files = [tmp_dir] - b.files = b.parse_files([tmp_dir]) + b.files = parse_files_for_parse_test(mut b, [tmp_dir]) groups := b.collect_virtual_main_modules() names := groups.map(it.name) @@ -234,7 +243,7 @@ fn test_parse_files_expands_non_main_module_files() { prefs.skip_builtin = true prefs.skip_imports = true mut b := new_builder(&prefs) - files := b.parse_files([entry_file]) + files := parse_files_for_parse_test(mut b, [entry_file]) assert files.len == 2 } @@ -263,7 +272,7 @@ fn test_parse_files_resolves_project_root_sibling_import_from_nested_module_file mut prefs := pref.new_preferences() prefs.skip_builtin = true mut b := new_builder(&prefs) - files := b.parse_files([entry_file]) + files := parse_files_for_parse_test(mut b, [entry_file]) names := files.map(os.file_name(it.name)) assert 'complete_test.v' in names @@ -292,7 +301,7 @@ fn test_parse_files_prefers_project_root_module_over_vlib_module() { mut prefs := pref.new_preferences() prefs.skip_builtin = true mut b := new_builder(&prefs) - files := b.parse_files([entry_file]) + files := parse_files_for_parse_test(mut b, [entry_file]) assert files.any(it.name == local_regex_file) } diff --git a/vlib/v2/builder/target_os_test.v b/vlib/v2/builder/target_os_test.v index cb953e111..3e788eed7 100644 --- a/vlib/v2/builder/target_os_test.v +++ b/vlib/v2/builder/target_os_test.v @@ -66,6 +66,73 @@ fn build_test_ssa(mut b Builder, mut ssa_builder ssa.Builder) { ssa_builder.build_all(b.files) } +fn test_ssa_c_backend_keeps_flat_for_codegen() { + mut prefs := pref.Preferences{ + backend: .c + } + mut b := Builder{ + pref: &prefs + flat_check_enabled: true + markused_flat_enabled: true + flat_ssa_enabled: true + } + + assert b.should_keep_flat_for_codegen() + b.flat = ast.FlatAst{ + files: [ast.FlatFile{}] + } + assert b.should_build_ssa_from_flat() + + prefs.backend = .cleanc + assert !b.should_keep_flat_for_codegen() + prefs.backend = .v + assert !b.should_keep_flat_for_codegen() + prefs.backend = .x64 + assert b.should_keep_flat_for_codegen() +} + +fn test_gen_ssa_c_consumes_flat_codegen_input() { + tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_ssa_c_flat_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + + output_path := os.join_path(tmp_dir, 'ssa_flat.c') + files := [ + ast.File{ + name: 'main.v' + mod: 'main' + stmts: [ + ast.Stmt(ast.ModuleStmt{ + name: 'main' + }), + ] + }, + ] + mut prefs := pref.Preferences{ + backend: .c + output_file: output_path + } + mut b := Builder{ + pref: &prefs + files: files + env: types.Environment.new() + flat: ast.flatten_files(files) + flat_check_enabled: true + markused_flat_enabled: true + flat_ssa_enabled: true + } + + b.gen_ssa_c() + + assert b.flat.files.len == 0 + assert os.exists(output_path) + c_source := os.read_file(output_path) or { panic(err) } + assert c_source.len > 0 +} + fn test_get_v_files_from_dir_uses_windows_target_os() { tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_filter_windows_${os.getpid()}') os.rmdir_all(tmp_dir) or {} diff --git a/vlib/v2/gen/cleanc/cleanc_test.v b/vlib/v2/gen/cleanc/cleanc_test.v index 23722bb1c..58a945492 100644 --- a/vlib/v2/gen/cleanc/cleanc_test.v +++ b/vlib/v2/gen/cleanc/cleanc_test.v @@ -3346,6 +3346,71 @@ fn test_fixed_array_contains_call_is_inlined() { assert !out.contains('Array_fixed_rune_4_contains(') } +fn test_raw_array_for_in_stmt_uses_typed_runtime_local() { + mut gen := Gen.new([]) + gen.remember_runtime_local_type('items', 'Array_int') + gen.remember_runtime_local_type('total', 'int') + gen.gen_for_stmt(ast.ForStmt{ + init: ast.ForInStmt{ + value: ast.Expr(ast.Ident{ + name: 'item' + }) + expr: ast.Expr(ast.Ident{ + name: 'items' + }) + } + stmts: [ + ast.Stmt(ast.AssignStmt{ + op: .plus_assign + lhs: [ast.Expr(ast.Ident{ + name: 'total' + })] + rhs: [ast.Expr(ast.Ident{ + name: 'item' + })] + }), + ] + }) + out := gen.sb.str() + assert out.contains('Array_int _arr_iter_') + assert out.contains('int item = ((int*)_arr_iter_') + assert out.contains('total += item;') + assert !out.contains('for (;;) {') +} + +fn test_local_map_or_lowers_without_result_temp() { + csrc := cleanc_csrc_for_test_source('local_map_or', ' +fn local_map_defaults() int { + mut indegree := map[int]int{} + mut dependents := map[int][]int{} + dep := dependents[0] or { []int{} } + indegree[0] = (indegree[0] or { 0 }) + 1 + return dep.len + (indegree[0] or { 0 }) +} +') + assert csrc.contains('map__get') + assert !csrc.contains('Array_int _or_t') + assert !csrc.contains('.is_error') + assert !csrc.contains('IError err = _or_t') +} + +fn test_nested_array_for_in_from_map_value_lowers_without_infinite_loop() { + csrc := cleanc_csrc_for_test_source('nested_map_array_for_in', ' +fn sum_stores(alloca_stores map[int][]int) int { + mut total := 0 + for _, store_ids in alloca_stores { + for store_id in store_ids { + total += store_id + } + } + return total +} +') + assert csrc.contains('Array_int store_ids') + assert csrc.contains('int store_id') + assert !csrc.contains('for (;;) {') +} + fn test_new_array_from_c_array_elem_type_uses_sizeof_arg() { mut gen := Gen.new([]) elem := gen.infer_array_elem_type_from_expr(ast.Expr(ast.CallExpr{ diff --git a/vlib/v2/gen/cleanc/for.v b/vlib/v2/gen/cleanc/for.v index 3e9d4ca84..511efb9f8 100644 --- a/vlib/v2/gen/cleanc/for.v +++ b/vlib/v2/gen/cleanc/for.v @@ -12,6 +12,9 @@ fn (mut g Gen) gen_for_stmt(node ast.ForStmt) { if g.gen_map_for_in_stmt(node, node.init) { return } + if g.gen_array_for_in_stmt(node, node.init) { + return + } } if g.gen_transformed_untyped_map_for_in_stmt(node) { return @@ -146,6 +149,89 @@ fn (mut g Gen) gen_map_for_in_stmt(node ast.ForStmt, for_in ast.ForInStmt) bool return true } +fn cleanc_for_in_ident_name(expr ast.Expr) (string, bool) { + if expr is ast.Ident { + return expr.name, false + } + if expr is ast.ModifierExpr { + if expr.expr is ast.Ident { + return expr.expr.name, expr.kind == .key_mut + } + } + return '', false +} + +fn (mut g Gen) gen_array_for_in_stmt(node ast.ForStmt, for_in ast.ForInStmt) bool { + mut array_type := g.get_expr_type(for_in.expr).trim_space().trim_right('*') + if (array_type == '' || array_type == 'int') && for_in.expr is ast.Ident { + array_type = + (g.get_local_var_c_type(for_in.expr.name) or { '' }).trim_space().trim_right('*') + } + if array_type == '' { + return false + } + mut array_decl_type := array_type + if !c_type_is_array_value(array_decl_type) { + alias_base := g.array_alias_base_type(array_decl_type) + if alias_base != '' { + array_decl_type = alias_base + } + } + if !c_type_is_array_value(array_decl_type) { + return false + } + elem_type := g.array_alias_elem_type_from_c_type(array_decl_type) + if elem_type == '' { + return false + } + id := g.tmp_counter + g.tmp_counter++ + array_tmp := '_arr_iter_${id}' + idx_tmp := '_arr_idx_${id}' + mut expr_sb := strings.new_builder(64) + saved_sb := g.sb + g.sb = expr_sb + g.expr(for_in.expr) + array_expr := g.sb.str() + g.sb = saved_sb + g.write_indent() + g.sb.writeln('${array_decl_type} ${array_tmp} = ${array_expr};') + g.write_indent() + g.sb.writeln('for (int ${idx_tmp} = 0; (${idx_tmp} < ${array_tmp}.len); ${idx_tmp} = (${idx_tmp} + 1)) {') + g.indent++ + key_name, _ := cleanc_for_in_ident_name(for_in.key) + value_name, value_is_mut := cleanc_for_in_ident_name(for_in.value) + saved_local_types := g.runtime_local_types.clone() + saved_decl_types := g.runtime_decl_types.clone() + if key_name != '' && key_name != '_' { + g.write_indent() + g.sb.writeln('int ${key_name} = ${idx_tmp};') + g.remember_runtime_local_type(key_name, 'int') + } + if value_name != '' && value_name != '_' { + g.write_indent() + if value_is_mut { + g.sb.writeln('${elem_type}* ${value_name} = &(((${elem_type}*)${array_tmp}.data)[${idx_tmp}]);') + g.remember_runtime_local_type(value_name, '${elem_type}*') + } else { + g.sb.writeln('${elem_type} ${value_name} = ((${elem_type}*)${array_tmp}.data)[${idx_tmp}];') + if elem_type == 'string' { + g.write_indent() + g.sb.writeln('${value_name} = string__clone(${value_name});') + } + g.remember_runtime_local_type(value_name, elem_type) + } + } + g.gen_stmts(node.stmts) + g.runtime_local_types = saved_local_types.clone() + g.runtime_decl_types = saved_decl_types.clone() + g.not_local_var_cache.clear() + g.indent-- + g.write_indent() + g.sb.writeln('}') + return true +} + fn (mut g Gen) gen_transformed_untyped_map_for_in_stmt(node ast.ForStmt) bool { if node.init !is ast.AssignStmt || node.stmts.len == 0 { return false diff --git a/vlib/v2/transformer/inject_live_reload_to_flat_test.v b/vlib/v2/transformer/inject_live_reload_to_flat_test.v index edf0b7112..7d4a0a454 100644 --- a/vlib/v2/transformer/inject_live_reload_to_flat_test.v +++ b/vlib/v2/transformer/inject_live_reload_to_flat_test.v @@ -9,7 +9,7 @@ // 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 hydrating each FnDecl's body via `to_files_range`. +// helper after decoding only the matching FnDecls from cursors. module transformer import v2.ast diff --git a/vlib/v2/transformer/live.v b/vlib/v2/transformer/live.v index a14db9f73..df846836a 100644 --- a/vlib/v2/transformer/live.v +++ b/vlib/v2/transformer/live.v @@ -82,21 +82,20 @@ pub fn (mut t Transformer) inject_live_reload_parts_from_flat(flat &ast.FlatAst) // inject_live_reload_to_flat is the FlatBuilder-side splice counterpart to // the legacy `inject_live_reload(mut []ast.File)`. Locates the main file -// via `inject_live_reload_parts_from_flat`, rehydrates that single file's -// FnDecl bodies to legacy `ast.Stmt` (via `to_files_range`) so the legacy -// `inject_live_into_stmts` helper can do the per-stmt call rewriting, then -// uses the FlatBuilder primitives to splice the result back: +// via `inject_live_reload_parts_from_flat`, decodes only each target FnDecl so +// the legacy `inject_live_into_stmts` helper can do the per-stmt call +// rewriting, then uses the FlatBuilder primitives to splice the result back: // - `replace_fn_body_stmts(old_fn_id, new_body_ids)` for each rewritten FnDecl // - `replace_file_stmt(file_idx, stmt_idx, new_fn_id)` to rewire the file // - `prepend_file_stmts(file_idx, c_decl_ids + global_decl_ids)` for the // file-level C-extern + GlobalDecl prepend // -// Hybrid hydrate approach: the per-stmt call-rewriting logic +// Hybrid cursor/decode approach: the per-stmt call-rewriting logic // (`rewrite_live_call_in_stmt_b`) stays legacy — porting the recursive // CallExpr/Ident/SelectorExpr walk to cursors is a much bigger lift and -// outside the scope of `inject_live_reload`'s purpose (the rewriting is a -// stable, well-tested helper). Only the FILE-LEVEL and FN-LEVEL splice -// (which is what the new primitives address) runs through the flat path. +// outside the scope of `inject_live_reload`'s purpose. Only the matching +// FnDecl bodies are decoded; the file-level and fn-level splice runs through +// the flat path. // // Bit-equal w.r.t. `signature()` to running legacy // `inject_live_reload(mut files)` followed by `ast.flatten_files(files)`. @@ -105,14 +104,6 @@ pub fn (mut t Transformer) inject_live_reload_to_flat(mut out ast.FlatBuilder) { file_idx := flat_parts.file_idx parts := flat_parts.parts - // Rehydrate the target file's FnDecls to legacy ast.Stmt so we can run - // legacy inject_live_into_stmts (which handles call rewriting) on each. - files_range := out.flat.to_files_range(file_idx, file_idx + 1) - if files_range.len == 0 { - return - } - legacy_file := files_range[0] - // Per-FnDecl rewrite: capture (stmt_idx, new_fn_id) replacements; apply // them AFTER the loop while stmt indices are still stable (prepend // happens after all replaces). file_stmts is built from the OLD file @@ -125,13 +116,11 @@ pub fn (mut t Transformer) inject_live_reload_to_flat(mut out ast.FlatBuilder) { mut replacement_ids := []ast.FlatNodeId{} for j in 0 .. file_stmts.len() { - if j >= legacy_file.stmts.len { - break - } - legacy_stmt := legacy_file.stmts[j] - if legacy_stmt !is ast.FnDecl { + stmt_cursor := file_stmts.at(j) + if !stmt_cursor.is_valid() || stmt_cursor.kind() != .stmt_fn_decl { continue } + legacy_stmt := out.flat.decode_stmt(stmt_cursor.id) decl := legacy_stmt as ast.FnDecl is_main := !decl.is_method && decl.name == 'main' mut new_body := []ast.Stmt{} @@ -162,7 +151,7 @@ pub fn (mut t Transformer) inject_live_reload_to_flat(mut out ast.FlatBuilder) { for ns in new_body { new_body_ids << out.emit_stmt(ns) } - old_fn_id := file_stmts.at(j).id + old_fn_id := stmt_cursor.id new_fn_id := out.replace_fn_body_stmts(old_fn_id, new_body_ids) replacement_idxs << j replacement_ids << new_fn_id diff --git a/vlib/v2/transformer/minimal_runtime_init_test.v b/vlib/v2/transformer/minimal_runtime_init_test.v index 11b46a4e8..86aed0501 100644 --- a/vlib/v2/transformer/minimal_runtime_init_test.v +++ b/vlib/v2/transformer/minimal_runtime_init_test.v @@ -61,6 +61,53 @@ fn runtime_init_test_files(main_imports []ast.ImportStmt) []ast.File { ] } +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' @@ -123,6 +170,36 @@ fn test_macos_tiny_candidate_keeps_imported_runtime_init_calls() { 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) diff --git a/vlib/v2/transformer/monomorphize.v b/vlib/v2/transformer/monomorphize.v index ed925836f..dc72a1369 100644 --- a/vlib/v2/transformer/monomorphize.v +++ b/vlib/v2/transformer/monomorphize.v @@ -123,6 +123,59 @@ pub fn (mut t Transformer) prepare_files_for_transform(files []ast.File) []ast.F return prepared } +// prepare_flat_for_transform performs the same whole-program generic +// preparation as prepare_files_for_transform, but keeps the original source in +// FlatAst form. Monomorphization and generic struct specialization are +// append-only, so the flat path stores just the per-file appended statements and +// lets the caller stream each original file through a one-file decode. +fn (mut t Transformer) prepare_flat_for_transform(flat &ast.FlatAst) map[int][]ast.Stmt { + t.env.generic_types = map[string][]map[string]types.Type{} + mut extra_stmts := map[int][]ast.Stmt{} + t.collect_declared_method_fns_from_flat(flat) + t.collect_struct_field_generic_decl_types_from_flat(flat) + timing := os.getenv('V2_TTIME') != '' + mut sw := time.new_stopwatch() + mut prepared_dirty := true + for iter in 0 .. 64 { + spec_count := t.monomorphized_specs.len + generic_count := t.generic_types_spec_count() + struct_count := t.generic_struct_specs.len + ts_start := sw.elapsed().milliseconds() + if prepared_dirty { + t.collect_generic_call_specs_from_flat(flat, extra_stmts) + } + ts_collect1 := sw.elapsed().milliseconds() + before_mono := t.monomorphized_specs.len + t.monomorphize_pass_from_flat(flat, mut extra_stmts) + ts_mono := sw.elapsed().milliseconds() + if t.monomorphized_specs.len != before_mono { + t.collect_generic_call_specs_in_new_clones_from_flat(flat, extra_stmts) + } + ts_collect2 := sw.elapsed().milliseconds() + t.inject_generic_struct_specializations_from_flat(flat, mut extra_stmts) + ts_inject := sw.elapsed().milliseconds() + if t.inject_changed_files { + t.collect_generic_call_specs_in_new_structs_from_flat(flat, extra_stmts) + } + ts_collect3 := sw.elapsed().milliseconds() + prepared_dirty = false + if timing { + eprintln(' [ttime] iter ${iter}: collect1=${ts_collect1 - ts_start}ms mono_pass=${ts_mono - ts_collect1}ms collect2=${ts_collect2 - ts_mono}ms inject=${ts_inject - ts_collect2}ms collect3=${ts_collect3 - ts_inject}ms files=${flat.files.len}') + } + t_print_mem('monomorphize iter ${iter}') + if t.monomorphized_specs.len == spec_count && t.generic_types_spec_count() == generic_count + && t.generic_struct_specs.len == struct_count { + break + } + } + if dump_path := os.getenv_opt('V2_TDUMP') { + t.dump_flat_monomorphize_specs(dump_path, flat, extra_stmts) + } + t.collect_struct_default_decl_infos_from_flat(flat, extra_stmts) + t.collect_concrete_embedded_owner_names_from_flat(flat, extra_stmts) + return extra_stmts +} + // dump_monomorphize_specs writes a deterministic snapshot of the fixpoint's // result (all monomorphized fn spec keys, generic struct spec keys, generic // binding signatures, and per-file appended-stmt counts) to `path`. Used only @@ -164,6 +217,50 @@ fn (t &Transformer) dump_monomorphize_specs(path string, prepared []ast.File) { os.write_file(path, lines.join('\n')) or {} } +fn (t &Transformer) dump_flat_monomorphize_specs(path string, flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + mut lines := []string{} + mut mspecs := t.monomorphized_specs.keys() + mspecs.sort() + lines << '# monomorphized_specs (${mspecs.len})' + for k in mspecs { + lines << 'M ${k}' + } + mut sspecs := t.generic_struct_specs.keys() + sspecs.sort() + lines << '# generic_struct_specs (${sspecs.len})' + for k in sspecs { + lines << 'S ${k}' + } + mut gkeys := t.env.generic_types.keys() + gkeys.sort() + lines << '# generic_types' + for k in gkeys { + blist := t.env.generic_types[k] or { continue } + mut sigs := []string{} + for b in blist { + sigs << generic_bindings_signature(b) + } + sigs.sort() + for sig in sigs { + lines << 'G ${k} :: ${sig}' + } + } + lines << '# files (${flat.files.len})' + for i, ff in flat.files { + extra := extra_stmts[i] or { []ast.Stmt{} } + stmt_count := flat_file_stmt_count(flat, i) + extra.len + lines << 'F ${flat.file_mod(ff)}/${flat.file_name(ff)} stmts=${stmt_count}' + } + os.write_file(path, lines.join('\n')) or {} +} + +fn flat_file_stmt_count(flat &ast.FlatAst, fi int) int { + if fi < 0 || fi >= flat.files.len { + return 0 + } + return flat.file_cursor(fi).stmts().len() +} + fn (t &Transformer) generic_types_spec_count() int { mut count := 0 for _, bindings_list in t.env.generic_types { @@ -183,6 +280,24 @@ fn (mut t Transformer) collect_declared_method_fns(files []ast.File) { } } +fn (mut t Transformer) collect_declared_method_fns_from_flat(flat &ast.FlatAst) { + t.declared_method_fns = map[string]bool{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + stmts := flat.file_cursor(fi).stmts() + for si in 0 .. stmts.len() { + c := stmts.at(si) + if c.kind() != .stmt_fn_decl || !c.flag(ast.flag_is_method) { + continue + } + stmt := flat.decode_stmt(c.id) + if stmt is ast.FnDecl { + t.register_declared_method_fn(stmt, module_name) + } + } + } +} + fn (mut t Transformer) register_declared_method_fn(decl ast.FnDecl, module_name string) { recv_name := t.get_receiver_type_name(decl.receiver.typ) if recv_name == '' || decl.name == '' { @@ -221,6 +336,35 @@ fn (mut t Transformer) collect_struct_field_generic_decl_types(files []ast.File) t.scope = old_scope } +fn (mut t Transformer) collect_struct_field_generic_decl_types_from_flat(flat &ast.FlatAst) { + old_module := t.cur_module + old_scope := t.scope + t.struct_field_generic_decl_types = map[string]types.Type{} + t.struct_field_generic_decl_bindings = map[string]map[string]types.Type{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + t.cur_module = module_name + if scope := t.get_module_scope(module_name) { + t.scope = scope + } else { + t.scope = unsafe { nil } + } + stmts := flat.file_cursor(fi).stmts() + for si in 0 .. stmts.len() { + c := stmts.at(si) + if c.kind() != .stmt_struct_decl { + continue + } + stmt := flat.decode_stmt(c.id) + if stmt is ast.StructDecl { + t.collect_struct_decl_generic_field_types(stmt, module_name) + } + } + } + t.cur_module = old_module + t.scope = old_scope +} + fn (mut t Transformer) collect_struct_decl_generic_field_types(decl ast.StructDecl, module_name string) { parent_names := struct_decl_field_lookup_names(decl.name, module_name) if parent_names.len == 0 { @@ -295,6 +439,38 @@ fn (mut t Transformer) collect_concrete_embedded_owner_names(files []ast.File) { t.cur_import_aliases = old_import_aliases.clone() } +fn (mut t Transformer) collect_concrete_embedded_owner_names_from_flat(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + old_module := t.cur_module + old_file := t.cur_file_name + old_scope := t.scope + old_import_aliases := t.cur_import_aliases.clone() + t.concrete_embedded_owner_names = map[string]map[string]string{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + t.cur_file_name = flat.file_name(flat.files[fi]) + t.cur_module = module_name + t.cur_import_aliases = flat_import_aliases_for_generic_collect(flat, fi) + if scope := t.get_module_scope(module_name) { + t.scope = scope + } else { + t.scope = unsafe { nil } + } + for stmt in flat_file_stmts_with_extra(flat, extra_stmts, fi) { + if stmt is ast.StructDecl { + mut embedded := []ast.Expr{cap: stmt.embedded.len} + for item in stmt.embedded { + embedded << t.rewrite_concrete_generic_struct_type_expr(item) + } + t.register_concrete_embedded_owner_names(stmt, embedded) + } + } + } + t.cur_module = old_module + t.cur_file_name = old_file + t.scope = old_scope + t.cur_import_aliases = old_import_aliases.clone() +} + fn (mut t Transformer) collect_struct_default_decl_infos(files []ast.File) { t.struct_default_decl_infos = map[string]StructDefaultDeclInfo{} for file in files { @@ -315,6 +491,27 @@ fn (mut t Transformer) collect_struct_default_decl_infos(files []ast.File) { } } +fn (mut t Transformer) collect_struct_default_decl_infos_from_flat(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + t.struct_default_decl_infos = map[string]StructDefaultDeclInfo{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + for stmt in flat_file_stmts_with_extra(flat, extra_stmts, fi) { + if stmt is ast.StructDecl { + info := StructDefaultDeclInfo{ + decl: stmt + module_name: module_name + } + c_name := generic_struct_decl_c_name(stmt, module_name) + t.struct_default_decl_infos[c_name] = info + if c_name.contains('__') { + t.struct_default_decl_infos[c_name.all_after_last('__')] = info + } + t.struct_default_decl_infos[stmt.name] = info + } + } + } +} + fn generic_struct_runtime_args(args []ast.Expr) []ast.Expr { mut out := []ast.Expr{cap: args.len} for arg in args { @@ -1045,6 +1242,149 @@ pub fn (mut t Transformer) monomorphize_pass(files []ast.File) []ast.File { return new_files } +fn (mut t Transformer) monomorphize_pass_from_flat(flat &ast.FlatAst, mut extra_stmts map[int][]ast.Stmt) { + mut decl_owner := map[string]int{} + mut decl_node := map[string]ast.FnDecl{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + for stmt in flat_file_stmts_with_extra(flat, extra_stmts, fi) { + if stmt is ast.FnDecl { + if decl_generic_param_names(stmt).len == 0 { + continue + } + t.index_generic_fn_decl_for_monomorphize(mut decl_owner, mut decl_node, stmt, fi, + module_name) + } + } + } + mut per_file_clones := map[int][]ast.Stmt{} + old_deferred_specs := t.deferred_generic_call_specs.clone() + t.deferred_generic_call_specs = []DeferredGenericCallSpec{} + for fn_key, bindings_list in t.env.generic_types { + lookup_key := t.resolve_monomorphize_decl_key(fn_key, decl_node) or { continue } + decl := decl_node[lookup_key] or { continue } + fi := decl_owner[lookup_key] or { continue } + decl_mod := flat.file_mod(flat.files[fi]) + for bindings in bindings_list { + spec_name := t.specialized_fn_name(decl, bindings).clone() + if spec_name == decl.name { + continue + } + clone_name := monomorphized_clone_name(lookup_key, decl, spec_name).clone() + spec_key := '${lookup_key}:${clone_name}'.clone() + if spec_key in t.monomorphized_specs { + continue + } + t.monomorphized_specs[spec_key] = true + owner_key := generic_spec_owner_key(fn_key, bindings) + owner_file := t.generic_spec_owner_file[owner_key] or { fi } + nested_struct_file := if t.generic_bindings_visible_in_module(decl_mod, bindings) { + fi + } else { + owner_file + } + if nested_struct_file < 0 || nested_struct_file >= flat.files.len { + continue + } + clone_file := nested_struct_file + clone_mod := flat.file_mod(flat.files[clone_file]) + t.register_monomorphized_fn_bindings(decl_mod, clone_name, bindings) + old_owner_file := t.cur_generic_call_file_idx + old_import_aliases := t.cur_import_aliases.clone() + t.cur_generic_call_file_idx = clone_file + t.cur_import_aliases = flat_import_aliases_for_generic_collect(flat, clone_file) + mut cloned := t.clone_fn_decl_with_substitutions(decl, bindings, clone_name, decl_mod, + clone_mod) + if clone_mod != decl_mod { + cloned = t.qualify_moved_clone_source_module_types(cloned, decl_mod) + } + t.cur_generic_call_file_idx = old_owner_file + t.cur_import_aliases = old_import_aliases.clone() + mut bucket := per_file_clones[clone_file] or { []ast.Stmt{} } + bucket << ast.Stmt(cloned) + per_file_clones[clone_file] = bucket + } + } + deferred_specs := t.deferred_generic_call_specs.clone() + t.deferred_generic_call_specs = old_deferred_specs.clone() + t.flush_deferred_generic_call_specs(deferred_specs) + t.last_mono_clones = per_file_clones.clone() + for fi, clones in per_file_clones { + if clones.len == 0 { + continue + } + append_flat_extra_stmts(mut extra_stmts, fi, clones) + } +} + +fn (mut t Transformer) inject_generic_struct_specializations_from_flat(flat &ast.FlatAst, mut extra_stmts map[int][]ast.Stmt) { + t.inject_changed_files = false + t.last_struct_clones = map[int][]ast.Stmt{} + if t.generic_struct_specs.len == 0 { + return + } + mut existing := map[string]bool{} + mut base_decls := map[string]ast.StructDecl{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + for stmt in flat_file_stmts_with_extra(flat, extra_stmts, fi) { + if stmt is ast.StructDecl { + c_name := generic_struct_decl_c_name(stmt, module_name) + existing[c_name] = true + if stmt.generic_params.len > 0 { + base_decls[c_name] = stmt + } + } + } + } + spec_keys := t.generic_struct_specs.keys() + mut sorted_keys := spec_keys.clone() + sorted_keys.sort() + mut any_pending := false + for key in sorted_keys { + spec := t.generic_struct_specs[key] or { continue } + if spec.concrete_c_name !in existing { + any_pending = true + break + } + } + if !any_pending { + return + } + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + mut injected := []ast.Stmt{} + for key in sorted_keys { + spec := t.generic_struct_specs[key] or { continue } + if spec.file_idx < 0 || spec.file_idx >= flat.files.len || spec.file_idx != fi + || spec.concrete_c_name in existing { + continue + } + struct_decl := base_decls[spec.base_c_name] or { continue } + cloned := ast.Stmt(t.clone_generic_struct_decl(struct_decl, spec, module_name)) + injected << cloned + existing[spec.concrete_c_name] = true + } + for key in sorted_keys { + spec := t.generic_struct_specs[key] or { continue } + if (spec.file_idx >= 0 && spec.file_idx < flat.files.len) + || spec.module_name != module_name + || spec.concrete_c_name in existing { + continue + } + struct_decl := base_decls[spec.base_c_name] or { continue } + cloned := ast.Stmt(t.clone_generic_struct_decl(struct_decl, spec, module_name)) + injected << cloned + existing[spec.concrete_c_name] = true + } + if injected.len > 0 { + t.last_struct_clones[fi] = injected.clone() + append_flat_extra_stmts(mut extra_stmts, fi, injected) + } + } + t.inject_changed_files = true +} + fn (mut t Transformer) flush_deferred_generic_call_specs(specs []DeferredGenericCallSpec) { old_file_idx := t.cur_generic_call_file_idx for spec in specs { @@ -1963,6 +2303,35 @@ fn (mut t Transformer) collect_generic_call_specs(files []ast.File) { t.cur_import_aliases = old_import_aliases.clone() } +fn (mut t Transformer) collect_generic_call_specs_from_flat(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + old_module := t.cur_module + old_file := t.cur_file_name + old_scope := t.scope + old_file_idx := t.cur_generic_call_file_idx + old_import_aliases := t.cur_import_aliases.clone() + t.build_generic_fn_decl_index_from_flat(flat, extra_stmts) + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + t.cur_file_name = flat.file_name(flat.files[fi]) + t.cur_module = module_name + t.cur_generic_call_file_idx = fi + t.cur_import_aliases = flat_import_aliases_for_generic_collect(flat, fi) + if scope := t.get_module_scope(module_name) { + t.scope = scope + } else { + t.scope = unsafe { nil } + } + for stmt in flat_file_stmts_with_extra(flat, extra_stmts, fi) { + t.collect_generic_call_specs_in_stmt(stmt) + } + } + t.cur_module = old_module + t.cur_file_name = old_file + t.scope = old_scope + t.cur_generic_call_file_idx = old_file_idx + t.cur_import_aliases = old_import_aliases.clone() +} + // build_generic_fn_decl_index (re)builds t.generic_fn_decl_index from every // generic FnDecl across all files. Generic declarations never change during the // fixpoint (monomorphize_pass only appends concrete clones), but the index maps @@ -1990,6 +2359,30 @@ fn (mut t Transformer) build_generic_fn_decl_index(files []ast.File) { } } +fn (mut t Transformer) build_generic_fn_decl_index_from_flat(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + t.generic_fn_decl_index = map[string]ast.FnDecl{} + t.generic_call_candidate_names = map[string]bool{} + mut dummy_owner := map[string]int{} + for fi in 0 .. flat.files.len { + module_name := flat.file_mod(flat.files[fi]) + for stmt in flat_file_stmts_with_extra(flat, extra_stmts, fi) { + if stmt is ast.FnDecl { + if decl_generic_param_names(stmt).len == 0 { + continue + } + t.index_generic_fn_decl_for_monomorphize(mut dummy_owner, mut + t.generic_fn_decl_index, stmt, fi, module_name) + } + } + } + for key, _ in t.generic_fn_decl_index { + t.register_generic_call_candidate_name(key) + } + for name, _ in t.generic_fn_value_names { + t.register_generic_call_candidate_name(name) + } +} + fn (mut t Transformer) register_generic_call_candidate_name(name string) { base := generic_base_name_without_specialization(name) if base == '' { @@ -2064,6 +2457,42 @@ fn (mut t Transformer) collect_generic_call_specs_in_new_clones(files []ast.File t.cur_import_aliases = old_import_aliases.clone() } +fn (mut t Transformer) collect_generic_call_specs_in_new_clones_from_flat(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + if t.last_mono_clones.len == 0 { + return + } + old_module := t.cur_module + old_file := t.cur_file_name + old_scope := t.scope + old_file_idx := t.cur_generic_call_file_idx + old_import_aliases := t.cur_import_aliases.clone() + t.build_generic_fn_decl_index_from_flat(flat, extra_stmts) + for fi in 0 .. flat.files.len { + clones := t.last_mono_clones[fi] or { continue } + if clones.len == 0 { + continue + } + module_name := flat.file_mod(flat.files[fi]) + t.cur_file_name = flat.file_name(flat.files[fi]) + t.cur_module = module_name + t.cur_generic_call_file_idx = fi + t.cur_import_aliases = flat_import_aliases_for_generic_collect(flat, fi) + if scope := t.get_module_scope(module_name) { + t.scope = scope + } else { + t.scope = unsafe { nil } + } + for stmt in clones { + t.collect_generic_call_specs_in_stmt(stmt) + } + } + t.cur_module = old_module + t.cur_file_name = old_file + t.scope = old_scope + t.cur_generic_call_file_idx = old_file_idx + t.cur_import_aliases = old_import_aliases.clone() +} + fn (mut t Transformer) collect_generic_call_specs_in_new_structs(files []ast.File) { if t.last_struct_clones.len == 0 { return @@ -2099,6 +2528,42 @@ fn (mut t Transformer) collect_generic_call_specs_in_new_structs(files []ast.Fil t.cur_import_aliases = old_import_aliases.clone() } +fn (mut t Transformer) collect_generic_call_specs_in_new_structs_from_flat(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt) { + if t.last_struct_clones.len == 0 { + return + } + old_module := t.cur_module + old_file := t.cur_file_name + old_scope := t.scope + old_file_idx := t.cur_generic_call_file_idx + old_import_aliases := t.cur_import_aliases.clone() + t.build_generic_fn_decl_index_from_flat(flat, extra_stmts) + for fi in 0 .. flat.files.len { + clones := t.last_struct_clones[fi] or { continue } + if clones.len == 0 { + continue + } + module_name := flat.file_mod(flat.files[fi]) + t.cur_file_name = flat.file_name(flat.files[fi]) + t.cur_module = module_name + t.cur_generic_call_file_idx = fi + t.cur_import_aliases = flat_import_aliases_for_generic_collect(flat, fi) + if scope := t.get_module_scope(module_name) { + t.scope = scope + } else { + t.scope = unsafe { nil } + } + for stmt in clones { + t.collect_generic_call_specs_in_stmt(stmt) + } + } + t.cur_module = old_module + t.cur_file_name = old_file + t.scope = old_scope + t.cur_generic_call_file_idx = old_file_idx + t.cur_import_aliases = old_import_aliases.clone() +} + fn import_aliases_for_generic_collect(imports []ast.ImportStmt) map[string]string { mut aliases := map[string]string{} for imp in imports { @@ -2111,6 +2576,37 @@ fn import_aliases_for_generic_collect(imports []ast.ImportStmt) map[string]strin return aliases } +fn flat_import_aliases_for_generic_collect(flat &ast.FlatAst, fi int) map[string]string { + if fi < 0 || fi >= flat.files.len { + return map[string]string{} + } + return import_aliases_for_generic_collect(flat.read_file_imports(flat.files[fi])) +} + +fn flat_file_stmts_with_extra(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt, fi int) []ast.Stmt { + if fi < 0 || fi >= flat.files.len { + return []ast.Stmt{} + } + stmts := flat.read_file_stmts(flat.files[fi]) + extra := extra_stmts[fi] or { []ast.Stmt{} } + if extra.len == 0 { + return stmts + } + mut out := []ast.Stmt{cap: stmts.len + extra.len} + out << stmts + out << extra + return out +} + +fn append_flat_extra_stmts(mut extra_stmts map[int][]ast.Stmt, fi int, stmts []ast.Stmt) { + if stmts.len == 0 { + return + } + mut bucket := extra_stmts[fi] or { []ast.Stmt{} } + bucket << stmts + extra_stmts[fi] = bucket +} + fn (mut t Transformer) collect_generic_call_specs_in_stmts(stmts []ast.Stmt) { for stmt in stmts { t.collect_generic_call_specs_in_stmt(stmt) diff --git a/vlib/v2/transformer/propagate_types_from_flat_test.v b/vlib/v2/transformer/propagate_types_from_flat_test.v index 69e001cca..2620d12ff 100644 --- a/vlib/v2/transformer/propagate_types_from_flat_test.v +++ b/vlib/v2/transformer/propagate_types_from_flat_test.v @@ -3,15 +3,13 @@ // that can be found in the LICENSE file. // vtest build: macos // -// Bit-equality pin for s167: `propagate_types_from_flat` (a thin -// rehydrate-then-call wrapper) must produce the same `t.env` mutations as -// `propagate_types`. Also pins `apply_post_pass_tail_from_flat` against -// `apply_post_pass_tail`. +// 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 wrapper rehydrates `flat` into `[]ast.File` via -// `flat.to_files_range(0, flat.files.len)` and delegates to the legacy -// walker. Behavior is identical by construction provided flatten+rehydrate -// preserves the bits propagate_types reads (pos.id, stmt/expr shape). +// The flat path decodes one top-level stmt at a time and reuses the legacy +// stmt/expression propagation walkers. Behavior should match legacy provided +// flattening preserves the bits propagate_types reads (pos.id, stmt/expr shape). module transformer import v2.ast diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index bc118c009..8ad79eb84 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -1185,9 +1185,10 @@ fn (mut t Transformer) collect_generic_fn_value_refs(files []ast.File) { fn (mut t Transformer) collect_generic_fn_value_refs_from_flat(flat &ast.FlatAst) { t.collect_generic_fn_decl_names_from_flat(flat) - for ff in flat.files { - for stmt in flat.read_file_stmts(ff) { - t.collect_generic_fn_value_refs_in_stmt(stmt, false) + for i in 0 .. flat.files.len { + stmts := flat.file_cursor(i).stmts() + for j in 0 .. stmts.len() { + t.collect_generic_fn_value_refs_in_stmt(flat.decode_stmt(stmts.at(j).id), false) } } t.collect_generic_fn_value_dependencies() @@ -1202,9 +1203,10 @@ fn (mut t Transformer) collect_generic_fn_decl_names(files []ast.File) { } fn (mut t Transformer) collect_generic_fn_decl_names_from_flat(flat &ast.FlatAst) { - for ff in flat.files { - for stmt in flat.read_file_stmts(ff) { - t.collect_generic_fn_decl_names_in_stmt(stmt) + for i in 0 .. flat.files.len { + stmts := flat.file_cursor(i).stmts() + for j in 0 .. stmts.len() { + t.collect_generic_fn_decl_names_in_stmt(flat.decode_stmt(stmts.at(j).id)) } } } @@ -1549,15 +1551,18 @@ pub fn (mut t Transformer) pre_pass(files []ast.File) { t.cache_env_maps() } -// pre_pass_from_flat is the FlatAst-driven equivalent of pre_pass. Reads -// file-level stmts via flat.read_file_stmts(ff) so the transformer no -// longer depends on a pre-rehydrated []ast.File for its accumulators. -// The per-stmt walks reuse the existing recursive visitors, which still -// operate on legacy ast.Stmt/Expr — flat-native walks are a later step. +// pre_pass_from_flat is the FlatAst-driven equivalent of pre_pass. It walks +// file-level stmt cursors and decodes one stmt at a time so the transformer no +// longer depends on pre-rehydrated []ast.File or per-file []ast.Stmt arrays for +// its accumulators. The per-stmt walks reuse the existing recursive visitors, +// which still operate on legacy ast.Stmt/Expr — flat-native walks are a later +// step. pub fn (mut t Transformer) pre_pass_from_flat(flat &ast.FlatAst) { t.collect_generic_fn_value_refs_from_flat(flat) - for ff in flat.files { - for stmt in flat.read_file_stmts(ff) { + for i in 0 .. flat.files.len { + stmts := flat.file_cursor(i).stmts() + for j in 0 .. stmts.len() { + stmt := flat.decode_stmt(stmts.at(j).id) if stmt is ast.FnDecl { for attr in stmt.attributes { if attr.comptime_cond !is ast.EmptyExpr { @@ -2606,6 +2611,50 @@ pub fn (mut t Transformer) transform_files_to_flat_direct(files []ast.File) ast. return builder.flat } +// transform_flat_to_flat_direct transforms flat input into flat output without +// collecting a whole legacy []ast.File input or the transformed legacy output. +// Generic monomorphization is append-only, so flat preparation keeps cloned +// declarations in a per-file side table and the per-file loop decodes one +// source file at a time. +pub fn (mut t Transformer) transform_flat_to_flat_direct(flat &ast.FlatAst, files []ast.File) ast.FlatAst { + if files.len > 0 { + return t.transform_files_to_flat_direct(files) + } + timing := os.getenv('V2_TTIME') != '' + mut sw := time.new_stopwatch() + t_print_mem('enter') + t.pre_pass_from_flat(flat) + t_print_mem('after pre_pass') + if timing { + eprintln(' [ttime] flat input direct pre_pass: ${sw.elapsed().milliseconds()}ms') + sw = time.new_stopwatch() + } + extra_stmts := t.prepare_flat_for_transform(flat) + t_print_mem('after prepare/monomorphize') + if timing { + eprintln(' [ttime] flat input direct prepare: ${sw.elapsed().milliseconds()}ms') + sw = time.new_stopwatch() + } + mut builder := new_transform_output_flat_builder_from_flat(flat) + for i in 0 .. flat.files.len { + src_file := prepared_flat_file_for_transform(flat, extra_stmts, i) or { continue } + t.transform_file_to_flat(src_file, mut builder) + } + t_print_mem('after per-file flat loop') + if timing { + eprintln(' [ttime] flat input direct per-file: ${sw.elapsed().milliseconds()}ms') + sw = time.new_stopwatch() + } + generated_parts := t.generated_fns_parts_from_flat(&builder.flat) + t.post_pass_to_flat(mut builder, generated_parts) + t.apply_post_pass_tail_from_flat(&builder.flat) + t_print_mem('after post_pass') + if timing { + eprintln(' [ttime] flat input direct post_pass: ${sw.elapsed().milliseconds()}ms') + } + return builder.flat +} + fn new_transform_output_flat_builder(files []ast.File) ast.FlatBuilder { mut total_bytes := i64(0) for file in files { @@ -2618,6 +2667,42 @@ fn new_transform_output_flat_builder(files []ast.File) ast.FlatBuilder { return ast.new_flat_builder_with_capacity(nodes_cap, edges_cap, strings_cap) } +fn new_transform_output_flat_builder_from_flat(flat &ast.FlatAst) ast.FlatBuilder { + mut total_bytes := i64(0) + for ff in flat.files { + name := flat.file_name(ff) + if name == '' || !os.exists(name) { + continue + } + total_bytes += os.file_size(name) + } + nodes_cap, edges_cap, strings_cap := ast.arena_caps_for_bytes(total_bytes * 2) + return ast.new_flat_builder_with_capacity(nodes_cap, edges_cap, strings_cap) +} + +fn prepared_flat_file_for_transform(flat &ast.FlatAst, extra_stmts map[int][]ast.Stmt, fi int) ?ast.File { + src_arr := flat.to_files_range(fi, fi + 1) + if src_arr.len == 0 { + return none + } + mut file := src_arr[0] + extra := extra_stmts[fi] or { []ast.Stmt{} } + if extra.len > 0 { + mut stmts := []ast.Stmt{cap: file.stmts.len + extra.len} + stmts << file.stmts + stmts << extra + file = ast.File{ + name: file.name + mod: file.mod + selector_names: file.selector_names + attributes: file.attributes + imports: file.imports + stmts: stmts + } + } + return file +} + fn runtime_const_init_base_name(mod string) string { mut suffix := if mod == '' { 'main' } else { mod } suffix = suffix.replace('.', '_').replace('-', '_') @@ -2755,6 +2840,109 @@ fn (t &Transformer) imported_runtime_init_modules(files []ast.File) map[string]b return modules } +fn runtime_init_available_modules_from_flat(flat &ast.FlatAst) map[string]bool { + mut available := map[string]bool{} + available[''] = true + available['main'] = true + for ff in flat.files { + mod_name := flat.file_mod(ff) + if mod_name != '' { + available[mod_name] = true + } + } + return available +} + +fn (t &Transformer) collect_runtime_init_imports_from_if_expr_cursor(node ast.Cursor, mut imports []ast.ImportStmt) { + if !node.is_valid() || node.kind() != .expr_if { + return + } + cond := node.edge(0) + if t.eval_comptime_cond(node.flat.decode_expr(cond.id)) { + t.collect_runtime_init_imports_from_if_cursor_stmts(node, mut imports) + return + } + else_expr := node.edge(1) + if !else_expr.is_valid() || else_expr.kind() != .expr_if { + return + } + else_cond := else_expr.edge(0) + if !else_cond.is_valid() || else_cond.kind() == .expr_empty { + t.collect_runtime_init_imports_from_if_cursor_stmts(else_expr, mut imports) + return + } + t.collect_runtime_init_imports_from_if_expr_cursor(else_expr, mut imports) +} + +fn (t &Transformer) collect_runtime_init_imports_from_if_cursor_stmts(node ast.Cursor, mut imports []ast.ImportStmt) { + for i in 2 .. node.edge_count() { + t.collect_runtime_init_imports_from_stmt_cursor(node.edge(i), mut imports) + } +} + +fn (t &Transformer) collect_runtime_init_imports_from_stmt_cursor(stmt ast.Cursor, mut imports []ast.ImportStmt) { + if !stmt.is_valid() { + return + } + match stmt.kind() { + .stmt_import { + imports << stmt.import_stmt() + } + .stmt_expr { + expr := stmt.edge(0) + if !expr.is_valid() || expr.kind() != .expr_comptime { + return + } + inner := expr.edge(0) + if inner.is_valid() && inner.kind() == .expr_if { + t.collect_runtime_init_imports_from_if_expr_cursor(inner, mut imports) + } + } + else {} + } +} + +fn (t &Transformer) collect_runtime_init_imports_from_stmt_cursors(stmts ast.CursorList, mut imports []ast.ImportStmt) { + for i in 0 .. stmts.len() { + t.collect_runtime_init_imports_from_stmt_cursor(stmts.at(i), mut imports) + } +} + +fn (t &Transformer) active_runtime_init_imports_from_flat_file(fc ast.FileCursor) []ast.ImportStmt { + file_imports := fc.imports() + mut imports := []ast.ImportStmt{cap: file_imports.len()} + for i in 0 .. file_imports.len() { + imp := file_imports.at(i) + if imp.kind() == .stmt_import { + imports << imp.import_stmt() + } + } + t.collect_runtime_init_imports_from_stmt_cursors(fc.stmts(), mut imports) + return imports +} + +fn (t &Transformer) imported_runtime_init_modules_from_flat(flat &ast.FlatAst) map[string]bool { + mut modules := map[string]bool{} + modules[''] = true + available_modules := runtime_init_available_modules_from_flat(flat) + add_runtime_init_module_key(mut modules, 'main', available_modules) + mut changed := true + for changed { + changed = false + for i in 0 .. flat.files.len { + fc := flat.file_cursor(i) + if fc.mod() !in modules { + continue + } + for imp in t.active_runtime_init_imports_from_flat_file(fc) { + changed = add_runtime_init_module_key(mut modules, imp.name, available_modules) + || changed + } + } + } + return modules +} + fn is_builtin_main_arg_global(mod string, name string) bool { return mod == 'builtin' && (name == 'g_main_argc' || name == 'g_main_argv') } @@ -3977,10 +4165,7 @@ pub fn (mut t Transformer) runtime_const_init_main_calls_parts_from_flat(flat &a mut seen_init_mods := map[string]bool{} minimal_x64_runtime := t.uses_minimal_x64_runtime() init_modules := if minimal_x64_runtime { - // Minimal x64 paths are uncommon and reuse the legacy import collector - // for comptime-active imports until that logic is ported to cursors. - rehydrated := flat.to_files_range(0, flat.files.len) - t.imported_runtime_init_modules(rehydrated) + t.imported_runtime_init_modules_from_flat(flat) } else { map[string]bool{} } @@ -5201,6 +5386,13 @@ fn (mut t Transformer) transform_assign_stmt_parts(stmt ast.AssignStmt) (token.T if rhs_type := t.get_array_init_expr_type(rhs_src[0] as ast.ArrayInitExpr) { t.register_local_var_type(lhs_name, rhs_type) } + } else if rhs_src[0] is ast.MapInitExpr { + map_init := rhs_src[0] as ast.MapInitExpr + if map_init.typ !is ast.EmptyExpr { + if rhs_type := t.type_from_param_type_expr(map_init.typ, []) { + t.register_local_var_type(lhs_name, rhs_type) + } + } } else if rhs_src[0] is ast.CallExpr || rhs_src[0] is ast.CallOrCastExpr || rhs_src[0] is ast.InitExpr || rhs_src[0] is ast.Ident || rhs_src[0] is ast.SelectorExpr { @@ -5583,6 +5775,48 @@ fn (t &Transformer) map_index_lhs_type(lhs ast.Expr) ?types.Type { return none } +fn (t &Transformer) index_expr_from_or_target(expr ast.Expr) ?ast.IndexExpr { + match expr { + ast.IndexExpr { + return expr + } + ast.GenericArgOrIndexExpr { + if lhs_type := t.get_expr_type(expr.lhs) { + if t.is_callable_type(lhs_type) { + return none + } + } + return ast.IndexExpr{ + lhs: expr.lhs + expr: expr.expr + is_gated: false + pos: expr.pos + } + } + ast.GenericArgs { + if expr.args.len != 1 { + return none + } + if lhs_type := t.get_expr_type(expr.lhs) { + if t.is_callable_type(lhs_type) { + return none + } + } + return ast.IndexExpr{ + lhs: expr.lhs + expr: expr.args[0] + is_gated: false + pos: expr.pos + } + } + else { + return none + } + } + + return none +} + fn (mut t Transformer) map_index_lhs_to_ptr(lhs ast.Expr, lhs_type types.Type) ast.Expr { // Check if lhs is a map index expression (nested map case) if lhs is ast.IndexExpr { @@ -6038,25 +6272,21 @@ fn (mut t Transformer) expand_direct_or_expr_assign(stmt ast.AssignStmt, or_expr // Check for map index with or block: map[key] or { fallback } // This is handled specially since it doesn't use Result/Option types - if call_expr is ast.IndexExpr { - if map_result := t.try_expand_map_index_or_assign(stmt, or_expr) { - return map_result - } + if map_result := t.try_expand_map_index_or_assign(stmt, or_expr) { + return map_result } // Check for array index with or block: arr[idx] or { fallback } - if call_expr is ast.IndexExpr { - if array_result := t.try_expand_array_index_or_assign(stmt, or_expr) { - return array_result - } + if array_result := t.try_expand_array_index_or_assign(stmt, or_expr) { + return array_result } // Check for string range with or block: s[0..20] or { 'fallback' } // The checker types this as `string`, but it needs `string__substr_with_check` // which returns `!string` (Result). mut is_string_range_or := false - if call_expr is ast.IndexExpr { - if call_expr.expr is ast.RangeExpr && t.is_string_expr(call_expr.lhs) { + if index_expr := t.index_expr_from_or_target(call_expr) { + if index_expr.expr is ast.RangeExpr && t.is_string_expr(index_expr.lhs) { is_string_range_or = true } } @@ -6114,7 +6344,7 @@ fn (mut t Transformer) expand_direct_or_expr_assign(stmt ast.AssignStmt, or_expr if t.is_native_be && is_string_range_or { // String ranges still need the native inline bounds-check path because the // checker records them as `string` instead of `!string`. - idx_expr := call_expr as ast.IndexExpr + idx_expr := t.index_expr_from_or_target(call_expr) or { return none } mut stmts := []ast.Stmt{} result_expr := t.expand_string_range_or_native_expr(idx_expr, or_expr.stmts, mut stmts) stmts << ast.AssignStmt{ @@ -8649,8 +8879,8 @@ fn (mut t Transformer) expand_single_or_expr(or_expr ast.OrExpr, mut prefix_stmt // Check for string range with or block: s[0..20] or { 'fallback' } mut is_string_range_or := false - if call_expr is ast.IndexExpr { - if call_expr.expr is ast.RangeExpr && t.is_string_expr(call_expr.lhs) { + if index_expr := t.index_expr_from_or_target(call_expr) { + if index_expr.expr is ast.RangeExpr && t.is_string_expr(index_expr.lhs) { is_string_range_or = true } } @@ -8674,7 +8904,7 @@ fn (mut t Transformer) expand_single_or_expr(or_expr ast.OrExpr, mut prefix_stmt } if t.is_native_be && is_string_range_or { - idx_expr := call_expr as ast.IndexExpr + idx_expr := t.index_expr_from_or_target(call_expr) or { return ast.empty_expr } return t.expand_string_range_or_native_expr(idx_expr, or_expr.stmts, mut prefix_stmts) } @@ -8963,10 +9193,7 @@ fn (t &Transformer) index_or_lhs_is_array_or_string(lhs ast.Expr) bool { } fn (mut t Transformer) try_expand_array_index_or(or_expr ast.OrExpr, mut _prefix_stmts []ast.Stmt) ?ast.Expr { - if or_expr.expr !is ast.IndexExpr { - return none - } - index_expr := or_expr.expr as ast.IndexExpr + index_expr := t.index_expr_from_or_target(or_expr.expr) or { return none } // Skip map indices and range expressions if _ := t.get_map_type_for_expr(index_expr.lhs) { return none @@ -9016,10 +9243,7 @@ fn (mut t Transformer) try_expand_array_index_or(or_expr ast.OrExpr, mut _prefix // try_expand_array_index_or_assign handles: x := arr[idx] or { fallback } and // x := str[idx] or { fallback }. fn (mut t Transformer) try_expand_array_index_or_assign(stmt ast.AssignStmt, or_expr ast.OrExpr) ?[]ast.Stmt { - if or_expr.expr !is ast.IndexExpr { - return none - } - index_expr := or_expr.expr as ast.IndexExpr + index_expr := t.index_expr_from_or_target(or_expr.expr) or { return none } if _ := t.get_map_type_for_expr(index_expr.lhs) { return none } @@ -9103,11 +9327,7 @@ fn (mut t Transformer) try_expand_array_index_or_assign(stmt ast.AssignStmt, or_ // Transforms it to use map_get_check for safe lookup with fallback. // Returns none if not a map index expression. fn (mut t Transformer) try_expand_map_index_or(or_expr ast.OrExpr, mut prefix_stmts []ast.Stmt) ?ast.Expr { - // Check if the inner expression is an IndexExpr - if or_expr.expr !is ast.IndexExpr { - return none - } - index_expr := or_expr.expr as ast.IndexExpr + index_expr := t.index_expr_from_or_target(or_expr.expr) or { return none } // Get the map type from environment and extract key/value types map_expr_typ := t.map_index_lhs_type(index_expr.lhs) or { return none } @@ -9272,10 +9492,10 @@ fn (mut t Transformer) try_expand_map_index_or(or_expr ast.OrExpr, mut prefix_st } fn (mut t Transformer) try_expand_map_index_addr_or_nil(or_expr ast.OrExpr, mut prefix_stmts []ast.Stmt) ?ast.Expr { - if t.is_eval_backend() || !or_stmts_are_nil(or_expr.stmts) || or_expr.expr !is ast.IndexExpr { + if t.is_eval_backend() || !or_stmts_are_nil(or_expr.stmts) { return none } - index_expr := or_expr.expr as ast.IndexExpr + index_expr := t.index_expr_from_or_target(or_expr.expr) or { return none } map_expr_typ := t.map_index_lhs_type(index_expr.lhs) or { return none } map_type := t.unwrap_map_type(map_expr_typ) or { return none } @@ -9312,11 +9532,7 @@ fn (mut t Transformer) try_expand_map_index_addr_or_nil(or_expr ast.OrExpr, mut // try_expand_map_index_or_assign handles: var := map[key] or { fallback } // Returns a list of statements that expand the assignment with proper map lookup. fn (mut t Transformer) try_expand_map_index_or_assign(stmt ast.AssignStmt, or_expr ast.OrExpr) ?[]ast.Stmt { - // Check if the inner expression is an IndexExpr - if or_expr.expr !is ast.IndexExpr { - return none - } - index_expr := or_expr.expr as ast.IndexExpr + index_expr := t.index_expr_from_or_target(or_expr.expr) or { return none } // Get the map type from environment and extract key/value types map_expr_typ := t.map_index_lhs_type(index_expr.lhs) or { return none } @@ -10084,6 +10300,11 @@ fn (mut t Transformer) local_decl_rhs_type(expr ast.Expr) ?types.Type { return typ } match expr { + ast.MapInitExpr { + if expr.typ !is ast.EmptyExpr { + return t.type_from_param_type_expr(expr.typ, []) + } + } ast.StringLiteral, ast.StringInterLiteral { return types.Type(types.string_) } diff --git a/vlib/v2/transformer/transformer_flat_diff_test.v b/vlib/v2/transformer/transformer_flat_diff_test.v index 456ea4325..8c109d51b 100644 --- a/vlib/v2/transformer/transformer_flat_diff_test.v +++ b/vlib/v2/transformer/transformer_flat_diff_test.v @@ -772,6 +772,16 @@ fn use_if_guard_assign_map() int { } ' +const fixture_generic_fn = ' +fn id[T](x T) T { + return x +} + +fn use_id() int { + return id[int](3) +} +' + fn all_transformer_fixtures() []string { return [ fixture_plain_fn, @@ -1829,6 +1839,47 @@ fn test_to_flat_direct_parity_if_guard_assign() { run_to_flat_direct_parity('to_flat_direct_if_guard_assign', fixture_if_guard_assign) } +fn run_flat_input_to_flat_direct_matches_file_input_direct(label string, src string) { + p_files := parse_transformer_fixture(src) + p_flat := parse_transformer_fixture(src) + + mut t_files := Transformer.new_with_pref(p_files.env, p_files.prefs) + files_direct := t_files.transform_files_to_flat_direct(p_files.files) + + mut t_flat := Transformer.new_with_pref(p_flat.env, p_flat.prefs) + flat_direct := t_flat.transform_flat_to_flat_direct(&p_flat.flat, []) + + if files_direct.files.len != flat_direct.files.len { + assert false, '${label}: file count mismatch: files=${files_direct.files.len} flat=${flat_direct.files.len}' + return + } + for i in 0 .. files_direct.files.len { + files_stmts := files_direct.child_at(files_direct.files[i].file_id, 2) + flat_stmts := flat_direct.child_at(flat_direct.files[i].file_id, 2) + files_sig := files_direct.subtree_signature(files_stmts) + flat_sig := flat_direct.subtree_signature(flat_stmts) + if files_sig == flat_sig { + continue + } + pa, pb := dump_signature_pair('${label}_file${i}', files_sig, flat_sig) + eprintln('[${label}] file ${i} subtree diverged.') + eprintln(' files input: ${pa}') + eprintln(' flat input: ${pb}') + eprintln(' diff with: diff -u ${pa} ${pb}') + assert false, '${label}: output diverged at file ${i} (see /tmp dumps above)' + } +} + +fn test_flat_input_to_flat_direct_matches_file_input_direct() { + run_flat_input_to_flat_direct_matches_file_input_direct('flat_input_direct_if_guard_assign', + fixture_if_guard_assign) +} + +fn test_flat_input_to_flat_direct_monomorphizes_generics_like_file_input() { + run_flat_input_to_flat_direct_matches_file_input_direct('flat_input_direct_generic_fn', + fixture_generic_fn) +} + fn test_to_flat_direct_parity_for_in_map() { run_to_flat_direct_parity('to_flat_direct_for_in_map', fixture_for_in_map) } diff --git a/vlib/v2/transformer/type_propagation.v b/vlib/v2/transformer/type_propagation.v index d9083af17..b61705408 100644 --- a/vlib/v2/transformer/type_propagation.v +++ b/vlib/v2/transformer/type_propagation.v @@ -23,28 +23,35 @@ fn (mut t Transformer) propagate_types(files []ast.File) { } // propagate_types_from_flat is the FlatAst-input counterpart to -// `propagate_types`. Today it's a thin wrapper that rehydrates `flat` -// back into `[]ast.File` via `flat.to_files_range(0, flat.files.len)` and -// delegates to the legacy walker — identical behaviour by construction. +// `propagate_types`. It walks `flat.files` directly and decodes one top-level +// statement at a time, avoiding whole-program legacy []ast.File and per-file +// []ast.Stmt allocations in flat-output transformer paths. // // Lets `apply_post_pass_tail_from_flat` (s167) take `&FlatAst` instead of // `[]ast.File`, which closes the last `[]ast.File` consumer in the -// post_pass tail. The wrapper does NOT save memory under -gc none (the -// rehydrated array is the same size as legacy `result`), but it -// straightens the call graph ahead of the SSA migration and fixes a -// latent staleness bug in the `_via_driver` wedges: legacy -// `post_pass(result)` mutates `result` BEFORE `propagate_types` runs, -// whereas s162/s163 wedges passed un-post_pass'd `result` to -// `apply_post_pass_tail` so `propagate_types` saw stale stmts. With this -// helper plus the s167 `apply_post_pass_tail_from_flat`, the wedge passes -// `&builder.flat` (already post_pass'd by `post_pass_to_flat`) so the -// non-arm64 propagation sees the same post_pass'd stmts as legacy. +// post_pass tail. The flat path also fixes a latent staleness bug in the +// `_via_driver` wedges: legacy `post_pass(result)` mutates `result` BEFORE +// `propagate_types` runs, whereas s162/s163 wedges passed un-post_pass'd +// `result` to `apply_post_pass_tail` so `propagate_types` saw stale stmts. +// With this helper plus `apply_post_pass_tail_from_flat`, the wedge passes +// `&builder.flat` (already post_pass'd by `post_pass_to_flat`) so non-arm64 +// propagation sees the same post_pass'd stmts as legacy. fn (mut t Transformer) propagate_types_from_flat(flat &ast.FlatAst) { if flat.files.len == 0 { return } - files := flat.to_files_range(0, flat.files.len) - t.propagate_types(files) + for i in 0 .. flat.files.len { + fc := flat.file_cursor(i) + mod_name := fc.mod() + if mod_scope := t.cached_scopes[mod_name] { + t.scope = mod_scope + } + t.cur_module = mod_name + stmts := fc.stmts() + for j in 0 .. stmts.len() { + t.prop_stmt(flat.decode_stmt(stmts.at(j).id)) + } + } } fn (mut t Transformer) prop_exprs(exprs []ast.Expr) { diff --git a/vlib/v2/transformer/types.v b/vlib/v2/transformer/types.v index 0394be7b3..65ac8e8f8 100644 --- a/vlib/v2/transformer/types.v +++ b/vlib/v2/transformer/types.v @@ -405,6 +405,12 @@ fn (t &Transformer) type_from_init_expr(expr ast.InitExpr) ?types.Type { name: generic_type_name }) } + if typ := t.type_from_param_type_expr(expr.typ, []) { + normalized := t.normalize_type(typ) + if t.type_to_c_name(normalized) != '' { + return normalized + } + } if typ := t.get_expr_type(expr.typ) { normalized := t.normalize_type(typ) if t.type_to_c_name(normalized) != '' { diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index 7956d901b..b9082f83b 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -1763,11 +1763,7 @@ fn (c &Checker) collect_active_imports_from_stmts_cursor(stmts ast.CursorList, m fn (c &Checker) collect_active_imports_from_stmt_cursor(s ast.Cursor, mut imports []ast.ImportStmt) { match s.kind() { .stmt_import { - // Import nodes are tiny (name/alias/symbols); decode just this one. - imp := s.flat.decode_stmt(s.id) - if imp is ast.ImportStmt { - imports << imp - } + imports << s.import_stmt() } .stmt_expr { inner := s.edge(0) @@ -1785,8 +1781,7 @@ fn (c &Checker) collect_active_imports_from_stmt_cursor(s ast.Cursor, mut import // collect_active_imports_from_if_cursor mirrors collect_active_imports_from_if_expr. // expr_if layout: edge0 = cond, edge1 = else_expr, edge2.. = then-branch stmts. fn (c &Checker) collect_active_imports_from_if_cursor(if_c ast.Cursor, mut imports []ast.ImportStmt) { - cond := if_c.flat.decode_expr(if_c.edge(0).id) - if c.eval_comptime_cond(cond) { + if c.eval_comptime_cond_cursor(if_c.edge(0)) { for i in 2 .. if_c.edge_count() { c.collect_active_imports_from_stmt_cursor(if_c.edge(i), mut imports) } @@ -1804,6 +1799,47 @@ fn (c &Checker) collect_active_imports_from_if_cursor(if_c ast.Cursor, mut impor } } +fn (c &Checker) eval_comptime_cond_cursor(cond ast.Cursor) bool { + if !cond.is_valid() { + return false + } + match cond.kind() { + .expr_ident { + return c.eval_comptime_flag(cond.name()) + } + .expr_prefix { + op := unsafe { token.Token(int(cond.aux())) } + if op == .not { + return !c.eval_comptime_cond_cursor(cond.edge(0)) + } + } + .expr_infix { + op := unsafe { token.Token(int(cond.aux())) } + if op == .and { + return c.eval_comptime_cond_cursor(cond.edge(0)) + && c.eval_comptime_cond_cursor(cond.edge(1)) + } + if op == .logical_or { + return c.eval_comptime_cond_cursor(cond.edge(0)) + || c.eval_comptime_cond_cursor(cond.edge(1)) + } + } + .expr_postfix { + op := unsafe { token.Token(int(cond.aux())) } + inner := cond.edge(0) + if op == .question && inner.kind() == .expr_ident { + return pref.comptime_optional_flag_value(c.pref, inner.name()) + } + } + .expr_paren { + return c.eval_comptime_cond_cursor(cond.edge(0)) + } + else {} + } + + return false +} + // preregister_all_scopes_from_flat mirrors preregister_all_scopes but pulls // each file's mod + imports through the FlatAst readers, no []ast.File hop. fn (mut c Checker) preregister_all_scopes_from_flat(flat &ast.FlatAst) { diff --git a/vlib/v2/types/checker_flat_test.v b/vlib/v2/types/checker_flat_test.v index 6cd529f7c..d765b48f7 100644 --- a/vlib/v2/types/checker_flat_test.v +++ b/vlib/v2/types/checker_flat_test.v @@ -161,3 +161,29 @@ fn pass(id Id) Id { ret := fn_type.get_return_type() or { panic('check_flat missing pass return type') } assert ret.name() == ret_files.name() } + +fn test_check_flat_matches_check_files_for_unsafe_pointer_selector_lhs() { + src := 'module main + +struct Node { +mut: + children &voidptr + len int +} + +fn node_at(raw voidptr) int { + return unsafe { &Node(raw) }.len +} + +fn child_len(n Node, idx int) int { + return unsafe { &Node(n.children[idx]) }.len +} +' + env_files := check_via_files(src) + env_flat := check_via_flat(src) + fn_files := env_files.lookup_fn('main', 'node_at') or { panic('check_files missing node_at') } + fn_flat := env_flat.lookup_fn('main', 'node_at') or { panic('check_flat missing node_at') } + ret_files := fn_files.get_return_type() or { panic('check_files missing node_at return type') } + ret_flat := fn_flat.get_return_type() or { panic('check_flat missing node_at return type') } + assert ret_flat.name() == ret_files.name() +} -- 2.39.5