From 5ac1641b93ea1bf3bd615384d85469068f241fcb Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 25 May 2026 13:39:36 +0300 Subject: [PATCH] v2: conditional struct fields via `$if` blocks and `@[if cond ?]` (#27249) --- cmd/tools/modules/testing/common.v | 69 +++++- cmd/tools/vtest.v | 7 + cmd/v2/v2.v | 9 +- vlib/v2/parser/parser.v | 222 ++++++++++++++++-- vlib/v2/pref/comptime.v | 119 ++++++++++ .../tests/conditional_struct_field_test.vv2 | 34 +++ vlib/v2/transformer/if.v | 113 +-------- vlib/v2/transformer/transformer_test.v | 215 +++++++++++++++++ 8 files changed, 660 insertions(+), 128 deletions(-) create mode 100644 vlib/v2/pref/comptime.v create mode 100644 vlib/v2/tests/conditional_struct_field_test.vv2 diff --git a/cmd/tools/modules/testing/common.v b/cmd/tools/modules/testing/common.v index 7fbcba426..f014cf9b9 100644 --- a/cmd/tools/modules/testing/common.v +++ b/cmd/tools/modules/testing/common.v @@ -579,9 +579,40 @@ fn worker_trunner(mut p pool.PoolProcessor, idx int, thread_id int) voidptr { if ts.show_stats { skip_running = '' } - reproduce_cmd := '${os.quoted_path(ts.vexe)} ${reproduce_options.join(' ')} ${os.quoted_path(file)}' compile_options := cmd_options.filter(it != '-silent') - cmd := '${os.quoted_path(ts.vexe)} ${skip_running} ${compile_options.join(' ')} ${os.quoted_path(file)}' + mut compile_vexe := ts.vexe + mut compile_args := '${skip_running} ${compile_options.join(' ')}' + mut reproduce_vexe := ts.vexe + mut reproduce_args := reproduce_options.join(' ') + // `_test.vv2` files are v2-only integration tests: full V programs that + // exercise v2-specific syntax. Compile them with the v2 binary instead of + // v1, forwarding only flags that v2 recognizes — v2 errors on unknown + // flags, so v1-specific options must be stripped. Preserving `-d `, + // `-b`/`-backend`, `-cc`, `-stats`, etc. keeps `v test -d feature ...` + // and per-file `// vtest vflags` working for conditional code paths. + is_vv2 := relative_file.ends_with('_test.vv2') + if is_vv2 { + mut v2_bin := os.join_path(ts.vroot, 'cmd', 'v2', 'v2') + $if windows { + v2_bin += '.exe' + } + if !os.is_executable(v2_bin) { + ts.append_message(.info, 'SKIP ${relative_file}: v2 binary not built. Run: ${os.quoted_path(ts.vexe)} -o ${os.quoted_path(v2_bin)} ${os.quoted_path(os.join_path(ts.vroot, + 'cmd', 'v2', 'v2.v'))}', + mtc) + ts.benchmark.skip() + tls_bench.skip() + return pool.no_result + } + compile_vexe = v2_bin + compile_args = filter_args_for_v2(compile_options) + // Reproduction command must invoke v2 too, otherwise the suggested + // rerun fails immediately on v2-only syntax. + reproduce_vexe = v2_bin + reproduce_args = filter_args_for_v2(reproduce_options) + } + reproduce_cmd := '${os.quoted_path(reproduce_vexe)} ${reproduce_args} ${os.quoted_path(file)}' + cmd := '${os.quoted_path(compile_vexe)} ${compile_args} ${os.quoted_path(file)}' run_cmd := if run_js { 'node ${os.quoted_path(generated_binary_fpath)}' } else { @@ -913,6 +944,40 @@ pub fn h_divider() { eprintln(term.h_divider('-')#[..max_header_len]) } +// filter_args_for_v2 returns a command-line string containing only the flags +// the v2 compiler accepts (`vlib/v2/pref/pref.v`). v2 errors on any unknown +// flag, so this is used when forwarding `v test` options to v2 for +// `_test.vv2` files. Keep these lists in sync with v2's pref validator. +fn filter_args_for_v2(compile_options []string) string { + v2_value_flags := ['-backend', '-b', '-o', '-output', '-arch', '-printfn', '-gc', + '-d', '-hot-fn', '-cc'] + v2_bool_flags := ['--debug', '--verbose', '-v', '--skip-genv', '--skip-builtin', + '--skip-imports', '--skip-type-check', '--no-parallel', '-nocache', '--nocache', + '-nomarkused', '--nomarkused', '-showcc', '--showcc', '-stats', '--stats', + '-print-parsed-files', '--print-parsed-files', '-keepc', '--profile-alloc', + '-profile-alloc', '-enable-globals', '--enable-globals', '-shared', '--shared', + '-O0', '--single-backend', '-single-backend', '-prod', '-prealloc', + '-ownership'] + tokens := vflags.tokenize_to_args(compile_options.join(' ')) + mut out := []string{} + mut i := 0 + for i < tokens.len { + t := tokens[i] + if t in v2_value_flags { + if i + 1 < tokens.len { + out << t + out << os.quoted_path(tokens[i + 1]) + i += 2 + continue + } + } else if t in v2_bool_flags { + out << t + } + i++ + } + return out.join(' ') +} + // setup_new_vtmp_folder creates a new nested folder inside VTMP, then resets VTMP to it, // so that V programs/tests will write their temporary files to new location. // The new nested folder, and its contents, will get removed after all tests/programs succeed. diff --git a/cmd/tools/vtest.v b/cmd/tools/vtest.v index ebd800972..36601ab5f 100644 --- a/cmd/tools/vtest.v +++ b/cmd/tools/vtest.v @@ -141,6 +141,13 @@ fn (mut ctx Context) should_test(path string, backend string) ShouldTestStatus { } return .skip } + // `_test.vv2` files are v2-only integration tests. They are full V programs + // (with `main()`) that exercise v2-specific syntax; the test runner routes + // them through the v2 binary instead of v1. Honor `-run-only` so targeted + // runs do not pull in unrelated vv2 tests. + if path.ends_with('_test.vv2') { + return ctx.should_test_when_it_contains_matching_fns(path, backend) + } if path.ends_with('.v') && path.count('.') == 2 { if !path.all_before_last('.v').all_before_last('.').ends_with('_test') { return .ignore diff --git a/cmd/v2/v2.v b/cmd/v2/v2.v index bb9f03649..291466f35 100644 --- a/cmd/v2/v2.v +++ b/cmd/v2/v2.v @@ -37,7 +37,12 @@ fn main() { // Auto-run test binaries after compilation (matching v1 behavior) if prefs.output_file == '' && is_test_file(files) { - output_name := os.file_name(files.last()).all_before_last('.v') + last := os.file_name(files.last()) + output_name := if last.ends_with('.vv2') { + last.all_before_last('.vv2') + } else { + last.all_before_last('.v') + } if os.exists(output_name) { ret := os.system('./' + output_name) os.rm(output_name) or {} @@ -93,7 +98,7 @@ fn run_ast_command(args []string) { fn is_test_file(files []string) bool { for file in files { - if file.ends_with('_test.v') { + if file.ends_with('_test.v') || file.ends_with('_test.vv2') { return true } } diff --git a/vlib/v2/parser/parser.v b/vlib/v2/parser/parser.v index 2e8d30864..a9070e26c 100644 --- a/vlib/v2/parser/parser.v +++ b/vlib/v2/parser/parser.v @@ -2976,10 +2976,26 @@ fn (mut p Parser) struct_decl(is_public bool, attributes []ast.Attribute) ast.St // returns (embedded_types, fields) fn (mut p Parser) struct_decl_fields(language ast.Language, expect_semi bool) ([]ast.Expr, []ast.FieldDecl) { p.expect(.lcbr) - // fields mut embedded := []ast.Expr{} mut fields := []ast.FieldDecl{} + p.parse_struct_field_list(language, mut embedded, mut fields) + p.next() // rcbr + if expect_semi { + p.expect(.semicolon) + } + return embedded, fields +} + +// parse_struct_field_list parses fields until it hits `}`. Handles `$if` blocks +// and `@[if cond ?]` field attributes by evaluating the condition at parse time +// and omitting non-selected fields from the AST. +fn (mut p Parser) parse_struct_field_list(language ast.Language, mut embedded []ast.Expr, mut fields []ast.FieldDecl) { for p.tok != .rcbr { + // `$if cond { ... } $else { ... }` block grouping a set of fields. + if p.tok == .dollar && p.peek() == .key_if { + p.parse_comptime_struct_field_branch(language, false, mut embedded, mut fields) + continue + } is_pub := p.tok == .key_pub if is_pub { p.next() @@ -3009,11 +3025,13 @@ fn (mut p Parser) struct_decl_fields(language ast.Language, expect_semi bool) ([ if p.tok == .semicolon { p.next() } - fields << ast.FieldDecl{ - name: field_name - typ: field_type - value: field_value - attributes: field_attributes + if !attributes_elide_field(field_attributes, mut p) { + fields << ast.FieldDecl{ + name: field_name + typ: field_type + value: field_value + attributes: field_attributes + } } continue } @@ -3051,18 +3069,110 @@ fn (mut p Parser) struct_decl_fields(language ast.Language, expect_semi bool) ([ if p.tok == .semicolon { p.next() } - fields << ast.FieldDecl{ - name: field_name - typ: field_type - value: field_value - attributes: field_attributes + if !attributes_elide_field(field_attributes, mut p) { + fields << ast.FieldDecl{ + name: field_name + typ: field_type + value: field_value + attributes: field_attributes + } } } - p.next() - if expect_semi { - p.expect(.semicolon) +} + +// attributes_elide_field returns true if any `@[if cond ?]` attribute evaluates +// to false, meaning the field should be omitted from the struct. Conditions +// that aren't shaped like flag expressions (e.g. type checks) are rejected +// rather than silently dropped — see `can_eval_comptime_cond`. +fn attributes_elide_field(attributes []ast.Attribute, mut p Parser) bool { + for attr in attributes { + if attr.comptime_cond !is ast.EmptyExpr { + if !p.can_eval_comptime_cond(attr.comptime_cond) { + p.error_with_pos('@[if ...] struct-field condition must be a compile-time flag expression (no type checks)', + attr.comptime_cond.pos()) + } + if !p.eval_comptime_cond(attr.comptime_cond) { + return true + } + } + } + return false +} + +// parse_comptime_struct_field_branch parses a `$if cond { ... } $else ...` +// block in a struct field position. Only the matched branch's fields are added +// to `embedded`/`fields`; other branches are parsed and discarded so token +// positions stay correct. `force_skip` propagates "already matched" through +// `$else $if` chains so at most one branch contributes fields. +fn (mut p Parser) parse_comptime_struct_field_branch(language ast.Language, force_skip bool, mut embedded []ast.Expr, mut fields []ast.FieldDecl) { + p.next() // $ + p.next() // if + // `p.expr` would otherwise greedily parse `linux { ... }` as a struct init. + exp_lcbr := p.exp_lcbr + allow_init_in_exp_lcbr := p.allow_init_in_exp_lcbr + p.exp_lcbr = true + p.allow_init_in_exp_lcbr = true + cond := p.expr(.lowest) + p.exp_lcbr = exp_lcbr + p.allow_init_in_exp_lcbr = allow_init_in_exp_lcbr + if !p.can_eval_comptime_cond(cond) { + p.error_with_pos('\$if struct-field condition must be a compile-time flag expression (no type checks)', + cond.pos()) + } + cond_matches := !force_skip && p.eval_comptime_cond(cond) + // Allow `{` on next line after `cond ?` — scanner inserts `;` after `?`. + if p.tok == .semicolon && p.peek() == .lcbr { + p.next() + } + p.expect(.lcbr) + if cond_matches { + p.parse_struct_field_list(language, mut embedded, mut fields) + } else { + mut tmp_emb := []ast.Expr{} + mut tmp_flds := []ast.FieldDecl{} + p.parse_struct_field_list(language, mut tmp_emb, mut tmp_flds) + } + p.next() // rcbr + // `};\n$else` — auto-inserted `;` sits between `}` and `$`. Consume it so + // `peek_dollar_keyword` (which reads the source bytes at the scanner offset + // for the *current* token) sees the `else` letters past `$`. + if p.tok == .semicolon && p.peek() == .dollar { + p.next() + } + if !(p.tok == .dollar && p.peek_dollar_keyword() == 'else') { + if p.tok == .semicolon { + p.next() + } + return + } + p.next() // $ + p.next() // else + // `else` may be followed by auto-inserted `;` if `$if` / `{` is on the next line. + if p.tok == .semicolon && p.peek() == .dollar { + p.next() + } + if p.tok == .dollar && p.peek() == .key_if { + // `$else $if` — propagate match status so at most one branch fires. + new_force_skip := force_skip || cond_matches + p.parse_comptime_struct_field_branch(language, new_force_skip, mut embedded, mut fields) + return + } + if p.tok == .semicolon && p.peek() == .lcbr { + p.next() + } + p.expect(.lcbr) + else_matches := !force_skip && !cond_matches + if else_matches { + p.parse_struct_field_list(language, mut embedded, mut fields) + } else { + mut tmp_emb := []ast.Expr{} + mut tmp_flds := []ast.FieldDecl{} + p.parse_struct_field_list(language, mut tmp_emb, mut tmp_flds) + } + p.next() // rcbr + if p.tok == .semicolon { + p.next() } - return embedded, fields } fn (mut p Parser) select_expr() ast.SelectExpr { @@ -3480,3 +3590,85 @@ fn (mut p Parser) error_with_position(msg string, pos token.Position) { p.error_message(msg, .error, pos) exit(1) } + +// can_eval_comptime_cond reports whether `cond` is shaped like a flag +// expression decidable at parse time (idents, `!`, `&&`, `||`, postfix `?`, +// parens). Anything else — notably type checks like `T is string` — is left +// for a later stage. Struct field parsing rejects non-evaluable conditions +// with a parse error rather than silently choosing a branch, because the +// field set affects layout / type checking. +fn (p &Parser) can_eval_comptime_cond(cond ast.Expr) bool { + match cond { + ast.Ident { + return true + } + ast.PrefixExpr { + if cond.op != .not { + return false + } + return p.can_eval_comptime_cond(cond.expr) + } + ast.InfixExpr { + if cond.op != .and && cond.op != .logical_or { + return false + } + return p.can_eval_comptime_cond(cond.lhs) && p.can_eval_comptime_cond(cond.rhs) + } + ast.PostfixExpr { + if cond.op != .question { + return false + } + // `feature?` is a flag wrapped in `?`; the inner expression follows + // the same shape rules (e.g. `(!feature)?` is allowed). + return p.can_eval_comptime_cond(cond.expr) + } + ast.ParenExpr { + return p.can_eval_comptime_cond(cond.expr) + } + else { + return false + } + } +} + +// eval_comptime_cond evaluates a compile-time condition expression at parse +// time. Callers must have checked `can_eval_comptime_cond` first; this +// function assumes the condition is in the supported shape. +fn (p &Parser) eval_comptime_cond(cond ast.Expr) bool { + match cond { + ast.Ident { + return p.eval_comptime_flag(cond.name) + } + ast.PrefixExpr { + if cond.op == .not { + return !p.eval_comptime_cond(cond.expr) + } + } + ast.InfixExpr { + if cond.op == .and { + return p.eval_comptime_cond(cond.lhs) && p.eval_comptime_cond(cond.rhs) + } + if cond.op == .logical_or { + return p.eval_comptime_cond(cond.lhs) || p.eval_comptime_cond(cond.rhs) + } + } + ast.PostfixExpr { + // `feature?` form — the `?` is syntactic sugar for the inner flag + // expression, so just delegate to it. + if cond.op == .question { + return p.eval_comptime_cond(cond.expr) + } + } + ast.ParenExpr { + return p.eval_comptime_cond(cond.expr) + } + else {} + } + return false +} + +// eval_comptime_flag delegates to the shared `pref.comptime_flag_value` so +// the parser and the transformer recognize exactly the same flag names. +fn (p &Parser) eval_comptime_flag(name string) bool { + return pref.comptime_flag_value(p.pref, name) +} diff --git a/vlib/v2/pref/comptime.v b/vlib/v2/pref/comptime.v new file mode 100644 index 000000000..5c9cf3d4e --- /dev/null +++ b/vlib/v2/pref/comptime.v @@ -0,0 +1,119 @@ +// 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 pref + +// comptime_flag_value evaluates a single comptime flag identifier (as it would +// appear in `$if name {` or `@[if name ?]`). Shared between the parser (struct +// field conditionals) and the transformer (statement / expression $if). +// +// `pref` may be `nil` for early uses (some tests construct partial state); +// flags that depend on backend / user_defines then evaluate to `false`. +pub fn comptime_flag_value(pref &Preferences, name string) bool { + match name { + 'macos', 'darwin' { + $if macos { + return true + } + return false + } + 'linux' { + $if linux { + return true + } + return false + } + 'windows' { + $if windows { + return true + } + return false + } + 'bsd' { + $if macos || freebsd || openbsd || netbsd || dragonfly { + return true + } + return false + } + 'freebsd' { + $if freebsd { + return true + } + return false + } + 'openbsd' { + $if openbsd { + return true + } + return false + } + 'netbsd' { + $if netbsd { + return true + } + return false + } + 'dragonfly' { + $if dragonfly { + return true + } + return false + } + 'x64', 'amd64' { + $if amd64 { + return true + } + return false + } + 'arm64', 'aarch64' { + $if arm64 { + return true + } + return false + } + 'little_endian' { + $if little_endian { + return true + } + return false + } + 'big_endian' { + $if big_endian { + return true + } + return false + } + 'debug' { + $if debug { + return true + } + return false + } + 'native' { + return pref != unsafe { nil } && (pref.backend == .arm64 || pref.backend == .x64) + } + // Native backend cannot resolve C.stdout/C.stderr data symbols through GOT, + // so use C.write() instead of fwrite() for I/O operations. + 'builtin_write_buf_to_fd_should_use_c_write' { + return pref != unsafe { nil } && (pref.backend == .arm64 || pref.backend == .x64) + } + 'tinyc' { + // For native backends, inline assembly from V source is not supported + // by the SSA builder. Pretend we're TinyCC so that `$if arm64 && !tinyc` + // guards select the software fallback path instead of inline asm. + return pref != unsafe { nil } && (pref.backend == .arm64 || pref.backend == .x64) + } + 'prealloc' { + return pref != unsafe { nil } && pref.prealloc + } + 'new_int', 'gcboehm', 'autofree', 'ppc64' { + return false + } + else { + if pref != unsafe { nil } && name in pref.user_defines { + return true + } + return false + } + } +} diff --git a/vlib/v2/tests/conditional_struct_field_test.vv2 b/vlib/v2/tests/conditional_struct_field_test.vv2 new file mode 100644 index 000000000..82d4796e4 --- /dev/null +++ b/vlib/v2/tests/conditional_struct_field_test.vv2 @@ -0,0 +1,34 @@ +// End-to-end test for v2's conditional struct field parsing. +// Exercises `$if cond { ... }` / `$else $if` / `$else` blocks and the +// per-field `@[if cond ?]` attribute. Compiled and run via the v2 binary. + +module main + +struct Container { + name string +$if !no_log ? { + logger string = 'default' +} + always int = 42 +$if my_feature ? { + feature_val string = 'on' +} $else { + feature_val string = 'off' +} + dev_menu string = 'dev' @[if !no_dev_menu ?] + never_built string = 'nope' @[if absent_flag ?] +} + +fn main() { + c := Container{ + name: 'app' + } + assert c.name == 'app' + assert c.logger == 'default' + assert c.always == 42 + // `my_feature` is not defined → `$else` branch contributes the field. + assert c.feature_val == 'off' + // `no_dev_menu` is not defined → `!no_dev_menu` is true → field kept. + assert c.dev_menu == 'dev' + println('conditional_struct_field_test PASS') +} diff --git a/vlib/v2/transformer/if.v b/vlib/v2/transformer/if.v index d5ba8242b..6a53d5e0d 100644 --- a/vlib/v2/transformer/if.v +++ b/vlib/v2/transformer/if.v @@ -5,6 +5,7 @@ module transformer import v2.ast +import v2.pref import v2.token import v2.types @@ -1320,114 +1321,8 @@ fn (t &Transformer) eval_comptime_cond(cond ast.Expr) bool { return false } -// eval_comptime_flag evaluates a single comptime flag/identifier +// eval_comptime_flag delegates to the shared `pref.comptime_flag_value` so +// the parser and the transformer recognize exactly the same flag names. fn (t &Transformer) eval_comptime_flag(name string) bool { - match name { - 'macos', 'darwin' { - $if macos { - return true - } - return false - } - 'linux' { - $if linux { - return true - } - return false - } - 'windows' { - $if windows { - return true - } - return false - } - 'bsd' { - $if macos || freebsd || openbsd || netbsd || dragonfly { - return true - } - return false - } - 'freebsd' { - $if freebsd { - return true - } - return false - } - 'openbsd' { - $if openbsd { - return true - } - return false - } - 'netbsd' { - $if netbsd { - return true - } - return false - } - 'dragonfly' { - $if dragonfly { - return true - } - return false - } - 'x64', 'amd64' { - $if amd64 { - return true - } - return false - } - 'arm64', 'aarch64' { - $if arm64 { - return true - } - return false - } - 'little_endian' { - $if little_endian { - return true - } - return false - } - 'big_endian' { - $if big_endian { - return true - } - return false - } - 'debug' { - $if debug { - return true - } - return false - } - 'native' { - return t.pref != unsafe { nil } && (t.pref.backend == .arm64 || t.pref.backend == .x64) - } - // Native backend cannot resolve C.stdout/C.stderr data symbols through GOT, - // so use C.write() instead of fwrite() for I/O operations. - 'builtin_write_buf_to_fd_should_use_c_write' { - return t.pref != unsafe { nil } && (t.pref.backend == .arm64 || t.pref.backend == .x64) - } - 'tinyc' { - // For native backends, inline assembly from V source is not supported - // by the SSA builder. Pretend we're TinyCC so that $if arm64 && !tinyc - // guards select the software fallback path instead of inline asm. - return t.pref != unsafe { nil } && (t.pref.backend == .arm64 || t.pref.backend == .x64) - } - 'prealloc' { - return t.pref != unsafe { nil } && t.pref.prealloc - } - // Feature flags that are typically false - 'new_int', 'gcboehm', 'autofree', 'ppc64' { - return false - } - else { - // Check user-defined comptime flags from -d - if t.pref != unsafe { nil } && name in t.pref.user_defines { - return true - } - return false - } - } + return pref.comptime_flag_value(t.pref, name) } diff --git a/vlib/v2/transformer/transformer_test.v b/vlib/v2/transformer/transformer_test.v index 9cf05955e..7db75624e 100644 --- a/vlib/v2/transformer/transformer_test.v +++ b/vlib/v2/transformer/transformer_test.v @@ -6493,3 +6493,218 @@ fn uses_smartcasted_field_method(init Expr) bool { } assert 'Ident__is_mut' in call_names, 'expected smartcasted method call, got ${call_names}' } + +fn 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 parse_code_with_defines_for_test(code string, defines []string) []ast.File { + return parse_code_with_prefs_for_test(code, .cleanc, defines) +} + +fn parse_code_with_prefs_for_test(code string, backend vpref.Backend, defines []string) []ast.File { + tmp_file := '/tmp/v2_parser_cond_field_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: backend + no_parallel: true + user_defines: defines + } + mut file_set := token.FileSet.new() + mut par := parser.Parser.new(prefs) + return par.parse_files([tmp_file], mut file_set) +} + +fn test_struct_comptime_if_field_block_default_branch() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if my_feature ? { + feature_val string = "on" +} $else { + feature_val string = "off" +} + always_present int +} +', []) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['feature_val', 'always_present'] +} + +fn test_struct_comptime_if_field_block_selected_branch() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if my_feature ? { + feature_val string = "on" +} $else { + feature_val string = "off" +} + always_present int +} +', ['my_feature']) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['feature_val', 'always_present'] +} + +fn test_struct_comptime_if_field_block_omits_when_unset() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if optional ? { + opt_field int +} + always int +} +', []) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['always'] +} + +fn test_struct_comptime_else_if_chain_picks_first_match() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if feat_a ? { + tag string = "A" +} $else $if feat_b ? { + tag string = "B" +} $else { + tag string = "default" +} +} +', ['feat_b']) + assert files.len == 1 + names := struct_field_names(files[0], 'Container') + assert names == ['tag'] +} + +fn test_struct_field_if_attribute_elides_when_false() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { + name string + debug_only int @[if absent_flag ?] +} +', []) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['name'] +} + +fn test_struct_field_if_attribute_keeps_when_true() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { + name string + debug_only int @[if present_flag ?] +} +', ['present_flag']) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['name', 'debug_only'] +} + +// Regression: `$else` starts on the line after `}`. The auto-inserted `;` +// between `}` and `$` must not hide the `$else` branch. +fn test_struct_comptime_else_on_next_line() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if my_feature ? { + val string = "on" +} +$else { + val string = "off" +} + always int +} +', []) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['val', 'always'] +} + +// Regression: `{` starts on the line after `$if cond ?`. The scanner inserts +// a `;` after `?`, which must not block the opening brace. +fn test_struct_comptime_if_lcbr_on_next_line() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if my_feature ? +{ + val string = "on" +} +$else +{ + val string = "off" +} + always int +} +', ['my_feature']) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['val', 'always'] +} + +// Regression: `$else $if` chain where each `$else` starts on a new line. +fn test_struct_comptime_else_if_on_next_line() { + files := parse_code_with_defines_for_test(' +module main + +struct Container { +$if feat_a ? { + tag string = "A" +} +$else $if feat_b ? { + tag string = "B" +} +$else { + tag string = "default" +} +} +', ['feat_b']) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['tag'] +} + +// `tinyc` and `builtin_write_buf_to_fd_should_use_c_write` are recognized by +// the shared `pref.comptime_flag_value` and evaluate true on the native +// backends. Confirms the parser sees the same flag set the transformer does. +fn test_struct_comptime_native_backend_flags() { + files := parse_code_with_prefs_for_test(' +module main + +struct Container { +$if tinyc ? { + tinyc_field string +} +$if builtin_write_buf_to_fd_should_use_c_write ? { + write_field string +} +$if new_int ? { + never_field string +} + always int +} +', .x64, []) + assert files.len == 1 + assert struct_field_names(files[0], 'Container') == ['tinyc_field', 'write_field', + 'always'] +} -- 2.39.5