// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module transformer import v2.ast import v2.types import v2.token fn (t &Transformer) active_local_decl_type_for_expr(expr ast.Expr) ?types.Type { match expr { ast.Ident { return t.lookup_local_decl_type(expr.name) } ast.ParenExpr { return t.active_local_decl_type_for_expr(expr.expr) } ast.ModifierExpr { return t.active_local_decl_type_for_expr(expr.expr) } ast.ComptimeExpr { return t.active_local_decl_type_for_expr(expr.expr) } else {} } return none } fn (mut t Transformer) transform_expr(expr ast.Expr) ast.Expr { // Guard against corrupt Expr with NULL data pointer (ARM64 backend issue). // Default-initialized sum types have data_ptr=0, which crashes on field access. if !expr_has_valid_data(expr) { return expr } return match expr { ast.CallExpr { t.transform_call_expr(expr) } ast.CallOrCastExpr { t.transform_call_or_cast_expr(expr) } ast.IfExpr { t.transform_if_expr(expr) } ast.InfixExpr { t.transform_infix_expr(expr) } ast.ParenExpr { ast.Expr(ast.ParenExpr{ expr: t.transform_expr(expr.expr) pos: expr.pos }) } ast.PrefixExpr { if expr.op == .amp { if pointer_cast := t.amp_type_cast_expr(expr.expr) { return t.transform_expr(pointer_cast) } } // Lower `&{base | field: val}` (AssocExpr) in the transformer so backends do not // need to deal with address-of rvalue compound expressions. if expr.op == .amp { if assoc := t.unwrap_assoc_expr(expr.expr) { return t.lower_assoc_expr(assoc, true) } } if expr.op == .arrow && expr.expr is ast.OrExpr { or_expr := expr.expr as ast.OrExpr return t.transform_expr(ast.Expr(ast.OrExpr{ expr: ast.Expr(ast.PrefixExpr{ op: .arrow expr: or_expr.expr pos: expr.pos }) stmts: or_expr.stmts pos: or_expr.pos })) } ast.Expr(ast.PrefixExpr{ op: expr.op expr: t.transform_expr(expr.expr) pos: expr.pos }) } ast.PostfixExpr { if expr.op in [.not, .question] { mut inner := t.transform_expr(expr.expr) // For string range with `!` propagation, use checked version if expr.expr is ast.IndexExpr && expr.expr.expr is ast.RangeExpr && t.is_string_expr(expr.expr.lhs) { inner = t.rename_substr_to_checked(inner) } is_native_backend := t.pref != unsafe { nil } && t.is_native_be if is_native_backend { return ast.Expr(ast.PostfixExpr{ op: expr.op expr: inner pos: expr.pos }) } // Non-SSA backends still lower `expr!`/`expr?` in the transformer. // Keep codegen valid by converting them to a cast of the underlying // result/option expression to the checker-inferred value type. mut type_name := '' if inner_type := t.get_expr_type(expr.expr) { match inner_type { types.ResultType { type_name = t.type_to_c_name(inner_type.base_type) } types.OptionType { type_name = t.type_to_c_name(inner_type.base_type) } else {} } } if type_name == '' { if typ := t.get_expr_type(expr) { type_name = t.type_to_c_name(typ) } } if type_name != '' { return ast.CastExpr{ typ: ast.Expr(ast.Ident{ name: type_name }) expr: inner } } inner } else { ast.Expr(ast.PostfixExpr{ op: expr.op expr: t.transform_expr(expr.expr) pos: expr.pos }) } } ast.CastExpr { // Casts to sum types must be lowered to explicit sum type initialization, // since C does not allow casting a variant struct to the sum type struct. sumtype_name := t.type_expr_name_full(expr.typ) if sumtype_name != '' && t.is_sum_type(sumtype_name) { if wrapped := t.wrap_sumtype_value(expr.expr, sumtype_name) { return wrapped } } ast.Expr(ast.CastExpr{ typ: expr.typ expr: t.transform_expr(expr.expr) pos: expr.pos }) } ast.IndexExpr { t.transform_index_expr(expr) } ast.ArrayInitExpr { t.transform_array_init_expr(expr) } ast.MapInitExpr { t.transform_map_init_expr(expr) } ast.SqlExpr { t.transform_sql_expr(expr) } ast.MatchExpr { t.transform_match_expr(expr) } ast.ComptimeExpr { t.transform_comptime_expr(expr) } ast.InitExpr { t.transform_init_expr(expr) } ast.UnsafeExpr { // Normalize `unsafe { nil }` to plain `nil`. // This keeps pointer-null semantics and avoids backend-specific null lowering bugs. if t.is_unsafe_nil_expr(expr) { ast.Expr(ast.Ident{ name: 'nil' pos: expr.pos }) } else { ast.Expr(ast.UnsafeExpr{ stmts: t.transform_stmts(expr.stmts) }) } } ast.LockExpr { // Lower to mutex lock/unlock calls wrapped in UnsafeExpr (compound expression) mut stmts := t.expand_lock_expr(expr) // When used as a value expression (GCC compound expr), the last statement's // value is returned. But the unlock calls come after the body, making the // compound expr return void. Fix: duplicate the value-producing statement // (last body stmt) after the unlock calls so it becomes the final expression. n_unlocks := expr.lock_exprs.len + expr.rlock_exprs.len if n_unlocks > 0 && stmts.len > n_unlocks { body_end := stmts.len - n_unlocks stmts << stmts[body_end - 1] } ast.Expr(ast.UnsafeExpr{ stmts: stmts }) } ast.AssocExpr { t.lower_assoc_expr(expr, false) } ast.FieldInit { // Transform the value inside field initializations (e.g., fn(name: expr)) ast.Expr(ast.FieldInit{ name: expr.name value: t.transform_expr(expr.value) }) } ast.SelectorExpr { t.transform_selector_expr(expr) } ast.Ident { if expr.name == '@VMODROOT' { return ast.Expr(t.vmodroot_string_literal(expr.pos)) } // Check for smart cast on simple identifiers (e.g., if x is Type { x }) if ctx := t.find_smartcast_for_expr(expr.name) { return t.apply_smartcast_direct_ctx(expr, ctx) } ast.Expr(expr) } ast.StringInterLiteral { // Transform interpolations, applying smart cast if needed t.transform_string_inter_literal(expr) } ast.AsCastExpr { // Keep explicit `as` casts as casts. A same-expression smartcast can // otherwise rewrite the operand into a direct sum payload access and hide // the original storage type from the backend, which is wrong for nested // sum types like Expr(Type(ArrayFixedType)). expr_key := t.expr_to_string(expr.expr) saved_smartcast_stack := t.smartcast_stack.clone() saved_smartcast_expr_counts := t.smartcast_expr_counts.clone() for _ in 0 .. smartcast_search_limit { if _ := t.remove_smartcast_for_expr(expr_key) { continue } break } transformed_inner := t.transform_expr(expr.expr) t.smartcast_stack = saved_smartcast_stack.clone() t.smartcast_expr_counts = saved_smartcast_expr_counts.clone() ast.Expr(ast.AsCastExpr{ expr: transformed_inner typ: expr.typ pos: expr.pos }) } ast.OrExpr { // OrExpr in expression context (e.g., nested, in return, in for-loop condition) mut prefix_stmts := []ast.Stmt{} result_expr := t.expand_single_or_expr(expr, mut prefix_stmts) if prefix_stmts.len > 0 { // Wrap in UnsafeExpr — cleanc emits as GCC compound expression ({ ... }) prefix_stmts << ast.ExprStmt{ expr: result_expr } ast.Expr(ast.UnsafeExpr{ stmts: prefix_stmts }) } else { result_expr } } ast.IfGuardExpr { // IfGuardExpr should only appear as IfExpr condition, handled by transform_if_expr. // If it somehow reaches here standalone, just evaluate the RHS. if expr.stmt.rhs.len > 0 { return t.transform_expr(expr.stmt.rhs[0]) } expr } ast.GenericArgs { // typeof[T] — keep as-is so typeof[T]().name can be resolved later if t.is_typeof_generic_args(expr) { return expr } // Disambiguate `x[y]` parsed as GenericArgs: if lhs is not callable and there // is a single argument, this is an index expression. if expr.args.len == 1 { if lhs_type := t.get_expr_type(expr.lhs) { if !t.is_callable_type(lhs_type) { return t.transform_index_expr(ast.IndexExpr{ lhs: expr.lhs expr: expr.args[0] is_gated: false pos: expr.pos }) } } } // Keep selectors as selectors so module and method calls still lower // through the normal call paths after specialization. t.specialize_generic_callable_expr(expr.lhs, expr.args, expr.pos) } ast.GenericArgOrIndexExpr { // Disambiguate parser ambiguity `x[y]`: // - callable lhs => generic arg specialization token (`x_T`) // - otherwise => normal index expression if lhs_type := t.get_expr_type(expr.lhs) { if t.is_callable_type(lhs_type) { return t.specialize_generic_callable_expr(expr.lhs, [expr.expr], expr.pos) } } return t.transform_index_expr(ast.IndexExpr{ lhs: expr.lhs expr: expr.expr is_gated: false pos: expr.pos }) } ast.ModifierExpr { ast.Expr(ast.ModifierExpr{ kind: expr.kind expr: t.transform_expr(expr.expr) pos: expr.pos }) } ast.FnLiteral { ast.Expr(ast.FnLiteral{ typ: expr.typ captured_vars: expr.captured_vars stmts: t.transform_stmts(expr.stmts) pos: expr.pos }) } ast.LambdaExpr { ast.Expr(ast.LambdaExpr{ args: expr.args expr: t.transform_expr(expr.expr) pos: expr.pos }) } ast.KeywordOperator { if expr.op == .key_typeof && expr.exprs.len > 0 { type_name := t.resolve_typeof_expr(expr.exprs[0]) if type_name != '' { return ast.StringLiteral{ kind: .v value: quote_v_string_literal(type_name) pos: expr.pos } } } if expr.op == .key_go && expr.exprs.len > 0 { return t.lower_go_call(expr) } ast.Expr(expr) } else { expr } } } fn (t &Transformer) amp_type_cast_expr(expr ast.Expr) ?ast.Expr { match expr { ast.IndexExpr { if expr.lhs !is ast.CallOrCastExpr { return none } cast_lhs := expr.lhs as ast.CallOrCastExpr if !t.call_or_cast_lhs_is_type(cast_lhs.lhs) { return none } return ast.IndexExpr{ lhs: ast.CastExpr{ typ: ast.PrefixExpr{ op: .amp expr: cast_lhs.lhs pos: cast_lhs.pos } expr: cast_lhs.expr pos: cast_lhs.pos } expr: expr.expr is_gated: expr.is_gated pos: expr.pos } } ast.SelectorExpr { if expr.lhs !is ast.CallOrCastExpr { return none } cast_lhs := expr.lhs as ast.CallOrCastExpr if !t.call_or_cast_lhs_is_type(cast_lhs.lhs) { return none } return ast.SelectorExpr{ lhs: ast.CastExpr{ typ: ast.PrefixExpr{ op: .amp expr: cast_lhs.lhs pos: cast_lhs.pos } expr: cast_lhs.expr pos: cast_lhs.pos } rhs: expr.rhs pos: expr.pos } } else { return none } } } // is_typeof_generic_args checks if a GenericArgs node is typeof[T] // without using an `is` check that would trigger smartcast narrowing. fn (t &Transformer) is_typeof_generic_args(ga ast.GenericArgs) bool { return ga.lhs.name() == 'typeof' } fn (t &Transformer) resolve_typeof_generic_arg_type_name(arg ast.Expr) string { type_name_from_expr := t.resolve_typeof_expr(arg) if type_name_from_expr != '' { return type_name_from_expr } type_name := t.expr_to_type_name(arg) if type_name == '' { return '' } return c_name_to_v_name(type_name) } fn (t &Transformer) resolve_specialized_typeof_call_type_name(call_name string) string { marker := 'typeof_T_' idx := call_name.index(marker) or { return '' } type_name := call_name[idx + marker.len..] if type_name == '' { return '' } return c_name_to_v_name(type_name) } fn (t &Transformer) resolve_typeof_call_lhs_type_name(lhs ast.Expr) string { if lhs is ast.GenericArgs { if t.is_typeof_generic_args(lhs) && lhs.args.len > 0 { return t.resolve_typeof_generic_arg_type_name(lhs.args[0]) } } if lhs is ast.GenericArgOrIndexExpr { if lhs.lhs.name() == 'typeof' { return t.resolve_typeof_generic_arg_type_name(lhs.expr) } } if lhs is ast.Ident { return t.resolve_specialized_typeof_call_type_name(lhs.name) } return '' } fn typeof_selector_result(type_name string, selector string, pos token.Pos) ?ast.Expr { if type_name == '' { return none } if selector == 'idx' { return ast.Expr(typeof_idx_literal(type_name, pos)) } if selector == 'name' { return ast.Expr(ast.StringLiteral{ kind: .v value: quote_v_string_literal(type_name) pos: pos }) } return none } fn (mut t Transformer) transform_index_expr(expr ast.IndexExpr) ast.Expr { // Lower slices in transformer so backends do not need slice-specific type logic. if expr.expr is ast.RangeExpr { lhs := t.transform_expr(expr.lhs) return t.transform_slice_index_expr(lhs, expr.lhs, expr.expr, expr.is_gated) } // Keep gated indexing as-is (`arr#[i]`). if expr.is_gated { return ast.IndexExpr{ lhs: t.transform_expr(expr.lhs) expr: t.transform_expr(expr.expr) is_gated: expr.is_gated pos: expr.pos } } // Lower map reads `m[key]` to `map__get(&m, &key, &zero)` in transformer so backends // do not need map-specific IndexExpr logic. mut map_expr_type_opt := t.map_index_lhs_type(expr.lhs) if map_expr_type_opt == none && expr.lhs is ast.Ident { map_expr_type_opt = t.lookup_var_type((expr.lhs as ast.Ident).name) } if map_expr_typ := map_expr_type_opt { if map_type := t.unwrap_map_type(map_expr_typ) { if t.is_eval_backend() { return ast.IndexExpr{ lhs: t.transform_expr(expr.lhs) expr: t.transform_expr(expr.expr) is_gated: expr.is_gated pos: expr.pos } } mut stmts := []ast.Stmt{} // Map arg: map__get expects a pointer to the map. map_arg := t.map_expr_to_runtime_ptr(expr.lhs, map_expr_typ, map_type) // Key temp (so we can take its address safely for the duration of the whole expression). key_tmp := t.gen_temp_name() key_ident := t.typed_temp_ident(key_tmp, map_type.key_type) mut key_lhs := []ast.Expr{cap: 1} key_lhs << ast.Expr(key_ident) mut key_rhs := []ast.Expr{cap: 1} key_rhs << t.transform_expr(expr.expr) stmts << ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: key_lhs rhs: key_rhs pos: key_ident.pos }) // Zero/default temp. zero_tmp := t.gen_temp_name() zero_ident := t.typed_temp_ident(zero_tmp, map_type.value_type) mut zero_lhs := []ast.Expr{cap: 1} zero_lhs << ast.Expr(zero_ident) mut zero_rhs := []ast.Expr{cap: 1} zero_rhs << t.zero_value_expr_for_type(map_type.value_type) stmts << ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: zero_lhs rhs: zero_rhs pos: zero_ident.pos }) get_call := ast.CallExpr{ lhs: ast.Ident{ name: 'map__get' } args: [ map_arg, t.voidptr_cast(ast.Expr(ast.PrefixExpr{ op: .amp expr: key_ident })), t.voidptr_cast(ast.Expr(ast.PrefixExpr{ op: .amp expr: zero_ident })), ] } cast_ptr_type := ast.Expr(ast.PrefixExpr{ op: .amp expr: t.type_to_ast_type_expr(map_type.value_type) }) deref_pos := t.next_synth_pos() t.register_synth_type(deref_pos, map_type.value_type) typed_ptr := ast.Expr(ast.CastExpr{ typ: cast_ptr_type expr: get_call }) deref_expr := ast.Expr(ast.PrefixExpr{ op: .mul expr: typed_ptr pos: deref_pos }) stmts << ast.Stmt(ast.ExprStmt{ expr: ast.Expr(ast.ParenExpr{ expr: deref_expr pos: deref_pos }) }) return ast.Expr(ast.UnsafeExpr{ stmts: stmts pos: deref_pos }) } } return ast.IndexExpr{ lhs: t.transform_expr(expr.lhs) expr: t.transform_expr(expr.expr) is_gated: expr.is_gated pos: expr.pos } } // is_simple_slice_bound reports whether a slice bound expression is free of // side effects and cheap enough to reference more than once (so a fixed-array // slice can inline it instead of binding it to a temp). fn is_simple_slice_bound(e ast.Expr) bool { return match e { ast.BasicLiteral { true } ast.Ident { true } ast.SelectorExpr { is_simple_slice_bound(e.lhs) } ast.ParenExpr { is_simple_slice_bound(e.expr) } else { false } } } fn (mut t Transformer) transform_slice_index_expr(lhs ast.Expr, orig_lhs ast.Expr, range ast.RangeExpr, _is_gated bool) ast.Expr { start_expr := if range.start is ast.EmptyExpr { ast.Expr(ast.BasicLiteral{ kind: .number value: '0' }) } else { t.transform_expr(range.start) } // Build end expression for lowering target calls: // `a..b` -> b, `a...b` -> b + 1, `a..` -> lhs.len. mut end_expr := ast.Expr(ast.empty_expr) if range.end is ast.EmptyExpr { end_expr = t.synth_selector(lhs, 'len', types.Type(types.int_)) } else { end_expr = t.transform_expr(range.end) if range.op == .ellipsis { end_expr = ast.Expr(ast.InfixExpr{ op: .plus lhs: end_expr rhs: ast.BasicLiteral{ kind: .number value: '1' } }) } } string_end_expr := if range.end is ast.EmptyExpr { ast.Expr(ast.BasicLiteral{ kind: .number value: '2147483647' }) } else { end_expr } array_slice_fn_name := if range.end is ast.EmptyExpr { 'array__slice_ni' } else { 'array__slice' } // Prefer semantic string detection over position-based type tags. // Expression positions are often shared with parent index/slice nodes, // which can incorrectly stamp the source as array-like. if t.is_string_expr(orig_lhs) || t.is_string_expr(lhs) { return ast.CallExpr{ lhs: ast.Ident{ name: 'string__substr' } args: [lhs, start_expr, string_end_expr] } } if lhs_type := t.get_expr_type(orig_lhs) { match lhs_type { types.String { return ast.CallExpr{ lhs: ast.Ident{ name: 'string__substr' } args: [lhs, start_expr, string_end_expr] } } types.Alias { if lhs_type.name == 'string' || lhs_type.base_type is types.String { return ast.CallExpr{ lhs: ast.Ident{ name: 'string__substr' } args: [lhs, start_expr, string_end_expr] } } } types.Array { return ast.CallExpr{ lhs: ast.Ident{ name: array_slice_fn_name } args: [lhs, start_expr, end_expr] } } types.ArrayFixed { elem_c_name := lhs_type.elem_type.name() // For a fixed array the default high bound (`a[..]`) is the // compile-time length, not a runtime `.len` selector. Emitting // the selector (`u.b.len`) leaves cleanc unable to type the // bound temp and it falls back to the fixed-array type, which // then breaks the `new_array_from_c_array` pointer arithmetic. fixed_end_expr := if range.end is ast.EmptyExpr { ast.Expr(ast.BasicLiteral{ kind: .number value: lhs_type.len.str() }) } else { end_expr } sizeof_expr := ast.Expr(ast.KeywordOperator{ op: .key_sizeof exprs: [ ast.Expr(ast.Ident{ name: elem_c_name }), ] }) // When both bounds are side-effect free we can reference them // directly: `new_array_from_c_array(end - start, end - start, // sizeof(T), &a[start])`. This avoids emitting bound temps, // which the cleanc backend can mis-type as the fixed-array type // (turning `end - start` into pointer arithmetic on a struct). if is_simple_slice_bound(start_expr) && is_simple_slice_bound(fixed_end_expr) { len_expr := ast.Expr(ast.InfixExpr{ op: .minus lhs: fixed_end_expr rhs: start_expr }) return ast.CallExpr{ lhs: ast.Ident{ name: 'new_array_from_c_array' } args: [ len_expr, len_expr, sizeof_expr, ast.Expr(ast.PrefixExpr{ op: .amp expr: ast.IndexExpr{ lhs: lhs expr: start_expr } }), ] } } // Complex bounds (calls, arithmetic, ...) must be evaluated once, // so bind them to typed temps before the slice call. start_ident := t.gen_typed_temp_ident(types.Type(types.int_)) end_ident := t.gen_typed_temp_ident(types.Type(types.int_)) len_expr := ast.Expr(ast.InfixExpr{ op: .minus lhs: ast.Expr(end_ident) rhs: ast.Expr(start_ident) }) slice_call := ast.Expr(ast.CallExpr{ lhs: ast.Ident{ name: 'new_array_from_c_array' } args: [ len_expr, len_expr, sizeof_expr, ast.Expr(ast.PrefixExpr{ op: .amp expr: ast.IndexExpr{ lhs: lhs expr: ast.Expr(start_ident) } }), ] }) return ast.UnsafeExpr{ stmts: [ ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(start_ident)] rhs: [start_expr] pos: start_ident.pos }), ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(end_ident)] rhs: [fixed_end_expr] pos: end_ident.pos }), ast.Stmt(ast.ExprStmt{ expr: slice_call }), ] } } types.Pointer { if lhs_type.base_type is types.Array { return ast.CallExpr{ lhs: ast.Ident{ name: array_slice_fn_name } args: [ ast.Expr(ast.PrefixExpr{ op: .mul expr: lhs }), start_expr, end_expr, ] } } } else {} } } // Fallback when env type lookup misses selector/if-guard positions. if t.infer_expr_type(orig_lhs) == 'string' { return ast.CallExpr{ lhs: ast.Ident{ name: 'string__substr' } args: [lhs, start_expr, string_end_expr] } } // Type lookup failed; default to array__slice (most common case). return ast.CallExpr{ lhs: ast.Ident{ name: array_slice_fn_name } args: [lhs, start_expr, end_expr] } } // transform_selector_expr transforms a selector expression, applying smart cast if applicable fn (mut t Transformer) transform_selector_expr(expr ast.SelectorExpr) ast.Expr { // typeof(x).name -> string literal with V type name if expr.lhs is ast.KeywordOperator && expr.lhs.op == .key_typeof && expr.rhs.name in ['name', 'idx'] { if expr.lhs.exprs.len > 0 { type_name := t.resolve_typeof_expr(expr.lhs.exprs[0]) if result := typeof_selector_result(type_name, expr.rhs.name, expr.pos) { return result } } } // typeof[T]().name -> string literal with V type name // Parser creates: SelectorExpr{lhs: CallExpr{lhs: GenericArgs{lhs: Ident{"typeof"}, args: [T]}}, rhs: "name"} if expr.rhs.name in ['name', 'idx'] && expr.lhs is ast.CallExpr { call := expr.lhs as ast.CallExpr type_name := t.resolve_typeof_call_lhs_type_name(call.lhs) if result := typeof_selector_result(type_name, expr.rhs.name, expr.pos) { return result } } if expr.rhs.name in ['name', 'idx'] && expr.lhs is ast.CallOrCastExpr { call := expr.lhs as ast.CallOrCastExpr if call.expr is ast.EmptyExpr { type_name := t.resolve_typeof_call_lhs_type_name(call.lhs) if result := typeof_selector_result(type_name, expr.rhs.name, expr.pos) { return result } } } // Generated sumtype representation fields already have their base expression // lowered. Do not apply smartcasts to them again on a later transform pass. if expr.rhs.name in ['_tag', '_data'] || (expr.rhs.name.starts_with('_') && expr.lhs is ast.SelectorExpr && (expr.lhs as ast.SelectorExpr).rhs.name == '_data') { return ast.Expr(expr) } // Check for smart cast field access: check ALL contexts in the stack if t.has_active_smartcast() { full_str := t.expr_to_string(expr) // First check if the ENTIRE selector matches a direct smartcast context // This handles cases like `sel := rhs_expr.lhs` inside `if rhs_expr.lhs is SelectorExpr` if direct_ctx := t.find_smartcast_for_expr(full_str) { // Direct access to smartcast variable - apply direct smartcast return t.apply_smartcast_direct_ctx(expr, direct_ctx) } // Check if LHS matches any smartcast context for field access lhs_str := t.expr_to_string(expr.lhs) if ctx := t.find_smartcast_for_expr(lhs_str) { // This is a field access on the smartcast variable // e.g., w.valera.len when w.valera is smartcast to string return t.apply_smartcast_field_access_ctx(expr.lhs, expr.rhs.name, ctx) } } if expr.lhs is ast.Ident && expr.lhs.name == 'os' && expr.rhs.name == 'args' { return ast.CallExpr{ lhs: ast.Ident{ name: 'arguments' pos: expr.pos } pos: expr.pos } } // Handle module-qualified enum value access: module.EnumType.value -> module__EnumType__value // Also handle nested module references: rand.seed.time_seed_array -> seed__time_seed_array if expr.lhs is ast.SelectorExpr { lhs_sel := expr.lhs as ast.SelectorExpr if lhs_sel.lhs is ast.Ident { module_name := lhs_sel.lhs.name type_name := lhs_sel.rhs.name qualified := '${module_name}__${type_name}' if typ := t.lookup_type(qualified) { if typ is types.Enum { t.register_synth_type(expr.pos, typ) return ast.Ident{ name: t.enum_member_ident_for_lookup(qualified, typ, expr.rhs.name) pos: expr.pos } } } // Nested module reference: rand.seed.time_seed_array -> seed__time_seed_array // Only resolve when both the outer ident is a module AND the inner name // is also a sub-module (not a variable like os.args.len). if t.is_module_ident(module_name) { sub_mod := lhs_sel.rhs.name fn_name := expr.rhs.name // Check if sub_mod is actually a module scope if t.get_module_scope(sub_mod) != none { return ast.Ident{ name: '${sub_mod}__${fn_name}' pos: expr.pos } } // Try full qualified name (module__sub_mod as scope key) full_mod := '${module_name}__${sub_mod}' if t.get_module_scope(full_mod) != none { return ast.Ident{ name: '${full_mod}__${fn_name}' pos: expr.pos } } } } } // Handle same-module enum access: Op.identify -> discord__Op__identify if expr.lhs is ast.Ident { lhs_name := expr.lhs.name // Check if LHS is an enum type in the current module qualified := if t.cur_module != '' && t.cur_module != 'main' && t.cur_module != 'builtin' && !lhs_name.contains('__') { '${t.cur_module}__${lhs_name}' } else { lhs_name } if typ := t.lookup_type(qualified) { if typ is types.Enum { t.register_synth_type(expr.pos, typ) return ast.Ident{ name: t.enum_member_ident_for_lookup(qualified, typ, expr.rhs.name) pos: expr.pos } } } } // Default transformation return ast.SelectorExpr{ lhs: t.transform_expr(expr.lhs) rhs: expr.rhs pos: expr.pos } } // transform_string_inter_literal transforms string interpolations, applying smart cast where needed fn (mut t Transformer) transform_match_expr(expr ast.MatchExpr) ast.Expr { match_expr, branches := t.transform_match_expr_parts(expr) return t.lower_match_expr_to_if(match_expr, branches) } fn sumtype_match_variant_base_name(name string) string { c_name := name.replace('.', '__') if bracket_idx := c_name.index('[') { return c_name[..bracket_idx] } return c_name } fn sumtype_match_generic_base_is_unique(sumtype_name string, variants []string, base_variant string) bool { mut matches := 0 for variant in variants { if sum_type_variant_matches_for_sumtype(sumtype_name, sumtype_match_variant_base_name(variant), base_variant) { matches++ if matches > 1 { return false } } } return matches == 1 } fn (t &Transformer) generic_match_branch_variant_info(lhs ast.Expr, args []ast.Expr) (string, string, string, bool) { base_name := t.type_expr_name(lhs) if base_name == '' { return '', '', '', false } base_full := t.type_expr_name_full(lhs) suffix := t.generic_specialization_suffix(args) variant_full := if base_full != '' && suffix != '' { base_full + suffix } else { base_full } variant_module := if base_full.contains('__') { base_full.all_before_last('__') } else { '' } return base_name, variant_full, variant_module, true } fn (t &Transformer) match_branch_variant_info(c ast.Expr) (string, string, string, bool, bool) { if c is ast.Ident { c_variant_name := c.name c_variant_name_full := if t.cur_module != '' && t.cur_module != 'main' && t.cur_module != 'builtin' && c.name !in ['int', 'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64', 'byte', 'rune', 'f32', 'f64', 'usize', 'isize', 'bool', 'string', 'voidptr', 'charptr', 'byteptr'] { '${t.cur_module}__${c.name}' } else { c.name } return c_variant_name, c_variant_name_full, '', false, c_variant_name != '' } if c is ast.SelectorExpr { c_variant_name := c.rhs.name c_variant_name_full := t.type_expr_name_full(c) c_variant_module := if c_variant_name_full.contains('__') { c_variant_name_full.all_before_last('__') } else { '' } return c_variant_name, c_variant_name_full, c_variant_module, false, c_variant_name != '' } if c is ast.Type { if c is ast.GenericType { c_variant_name, c_variant_name_full, c_variant_module, _ := t.generic_match_branch_variant_info(c.name, c.params) return c_variant_name, c_variant_name_full, c_variant_module, true, c_variant_name != '' } c_variant_name := t.type_variant_name(c) return c_variant_name, t.type_variant_name_full(c), '', false, c_variant_name != '' } if c is ast.GenericArgs { c_variant_name, c_variant_name_full, c_variant_module, _ := t.generic_match_branch_variant_info(c.lhs, c.args) return c_variant_name, c_variant_name_full, c_variant_module, true, c_variant_name != '' } if c is ast.GenericArgOrIndexExpr { c_variant_name, c_variant_name_full, c_variant_module, _ := t.generic_match_branch_variant_info(c.lhs, [ c.expr, ]) return c_variant_name, c_variant_name_full, c_variant_module, true, c_variant_name != '' } return '', '', '', false, false } // transform_match_expr_parts returns the two inputs to `lower_match_expr_to_if` // — the transformed match expression (either `_tag` access for sumtype matches // or the plain transformed `expr.expr` for non-sumtype matches) and the // transformed branch list. The flat-write arm calls this directly + then // invokes `lower_match_expr_to_if_flat` to skip every nested IfExpr wrapper- // struct allocation in the lowered chain. The legacy `transform_match_expr` // above wraps it for non-flat callers via `lower_match_expr_to_if`. Same // `_parts` extraction template as `transform_return_stmt_parts` (session 59) // and `transform_assign_stmt_parts` (session 60). fn (mut t Transformer) transform_match_expr_parts(expr ast.MatchExpr) (ast.Expr, []ast.MatchBranch) { // Check if matching on a sum type mut sumtype_name := t.get_sumtype_name_for_expr(expr.expr) smartcast_expr := t.expr_to_string(expr.expr) // Verify that it's actually a sum type match by checking branch conditions. // If conditions are string/number literals, it's NOT a sum type match even if // the expression type looks like a sum type. if sumtype_name != '' && expr.branches.len > 0 { first_branch := expr.branches[0] if first_branch.cond.len > 0 { first_cond := first_branch.cond[0] if first_cond is ast.BasicLiteral || first_cond is ast.StringLiteral || first_cond is ast.StringInterLiteral { sumtype_name = '' } } } if sumtype_name != '' { // Sum type match - set up smartcast context for each branch variants := t.get_sum_type_variants(sumtype_name) mut branches := []ast.MatchBranch{cap: expr.branches.len} for branch in expr.branches { if branch.cond.len > 0 { mut cond_tags := []int{cap: branch.cond.len} mut cond_variants := []string{cap: branch.cond.len} mut cond_variants_full := []string{cap: branch.cond.len} mut can_split_branch := true for c in branch.cond { c_variant_name, c_variant_name_full, c_variant_module, c_variant_is_generic, ok := t.match_branch_variant_info(c) if !ok { can_split_branch = false break } qualified_variant := if c_variant_module != '' && !c_variant_name.starts_with('Array_') && !c_variant_name.starts_with('Map_') { '${c_variant_module}__${c_variant_name}' } else { c_variant_name } qualified_variant_full := if c_variant_name_full != '' && c_variant_name_full != c_variant_name { c_variant_name_full } else if c_variant_module != '' { '${c_variant_module}__${c_variant_name}' } else { c_variant_name } mut c_tag := -1 for i, v in variants { if c_variant_is_generic && sum_type_variant_matches_for_sumtype(sumtype_name, v, qualified_variant_full) { c_tag = i break } if sum_type_variant_matches_for_sumtype(sumtype_name, v, qualified_variant) { c_tag = i break } if c_variant_is_generic && sumtype_match_generic_base_is_unique(sumtype_name, variants, qualified_variant) && sum_type_variant_matches_for_sumtype(sumtype_name, sumtype_match_variant_base_name(v), qualified_variant) { c_tag = i break } // Handle array variant matching: // c_variant_name is 'Array_Attribute' or 'Array_ast__Attribute' (C format) // v is '[]Attribute' or '[]ast__Attribute' (V format from types.Array.name()) if c_variant_name.starts_with('Array_') && v.starts_with('[]') { c_elem := c_variant_name[6..] // Strip 'Array_' v_elem := v[2..] // Strip '[]' c_elem_short := if c_elem.contains('__') { c_elem.all_after_last('__') } else { c_elem } v_elem_short := if v_elem.contains('__') { v_elem.all_after_last('__') } else { v_elem } if c_elem == v_elem || c_elem_short == v_elem_short { c_tag = i break } } // Handle fixed array variant matching if c_variant_name.starts_with('Array_fixed_') && v.starts_with('[') { // TODO: implement fixed array matching if needed } // Handle map variant matching if c_variant_name.starts_with('Map_') && v.starts_with('map[') { // TODO: implement map matching if needed } } if c_tag < 0 { can_split_branch = false break } cond_tags << c_tag cond_variants << if c_variant_is_generic && qualified_variant_full != '' { qualified_variant_full } else { qualified_variant } cond_variants_full << qualified_variant_full } if can_split_branch && cond_tags.len > 0 { // When a branch has multiple sum variants, each condition needs its own // smartcast context. Splitting preserves correct dispatch in branch bodies. for i, c_tag in cond_tags { smartcast_stack_before := t.smartcast_stack.clone() smartcast_counts_before := t.smartcast_expr_counts.clone() t.push_smartcast_full(smartcast_expr, cond_variants[i], cond_variants_full[i], sumtype_name) mut transformed_stmts := t.transform_match_branch_stmts(branch.stmts) if t.sumtype_return_wrap != '' { transformed_stmts = t.wrap_sumtype_return_branch_tail_stmts(transformed_stmts, t.sumtype_return_wrap) } t.smartcast_stack = smartcast_stack_before.clone() t.smartcast_expr_counts = smartcast_counts_before.clone() branches << ast.MatchBranch{ cond: [ ast.Expr(ast.BasicLiteral{ kind: token.Token.number value: '${c_tag}' }), ] stmts: transformed_stmts pos: branch.pos } } } else { // No variant name found, just transform normally mut fallback_stmts := t.transform_match_branch_stmts(branch.stmts) if t.sumtype_return_wrap != '' { fallback_stmts = t.wrap_sumtype_return_branch_tail_stmts(fallback_stmts, t.sumtype_return_wrap) } branches << ast.MatchBranch{ cond: branch.cond stmts: fallback_stmts pos: branch.pos } } } else { // else branch - no smartcast context mut else_stmts := t.transform_match_branch_stmts(branch.stmts) if t.sumtype_return_wrap != '' { else_stmts = t.wrap_sumtype_return_branch_tail_stmts(else_stmts, t.sumtype_return_wrap) } branches << ast.MatchBranch{ cond: branch.cond stmts: else_stmts pos: branch.pos } } } // Transform match expression to use _tag field. // For nested sum type matches (e.g., match o { Inner { match o { Small { ... } } } }), // the expression is already smartcasted to the inner sum type. We must keep the // smartcast so we access the INNER type's _tag, not the outer one. // Only remove smartcast contexts when this is NOT a nested sum type match. mut is_nested_sumtype_match := false if ctx := t.find_smartcast_for_expr(smartcast_expr) { if t.is_sum_type(ctx.variant) || t.is_sum_type(if ctx.variant.contains('__') { ctx.variant.all_after_last('__') } else { ctx.variant }) { is_nested_sumtype_match = true } } transformed_match_expr := if !is_nested_sumtype_match && expr.expr is ast.Ident { ast.Expr(expr.expr) } else { t.transform_expr(expr.expr) } tag_access := t.synth_selector(transformed_match_expr, '_tag', types.Type(types.int_)) return tag_access, branches } // Non-sum type match - simple transformation // Determine if match expression is an enum type so we can resolve shorthands (.red → Color__red) mut enum_type_name := '' if typ := t.get_expr_type(expr.expr) { c_name := t.type_to_c_name(typ) if c_name != '' { if resolved := t.lookup_type(c_name) { if resolved is types.Enum { enum_type_name = c_name } } } } mut branches := []ast.MatchBranch{cap: expr.branches.len} for branch in expr.branches { mut conds := branch.cond.clone() if enum_type_name != '' { conds = []ast.Expr{cap: branch.cond.len} for c in branch.cond { conds << t.resolve_enum_shorthand(c, enum_type_name) } } branches << ast.MatchBranch{ cond: conds stmts: t.transform_match_branch_stmts(branch.stmts) pos: branch.pos } } return t.transform_expr(expr.expr), branches } fn (mut t Transformer) transform_match_branch_stmts(stmts []ast.Stmt) []ast.Stmt { if stmts.len == 0 { return []ast.Stmt{} } if !t.preserve_match_branch_value { return t.transform_stmts(stmts) } last := stmts[stmts.len - 1] if last is ast.ExprStmt { mut out := []ast.Stmt{} if stmts.len > 1 { out << t.transform_stmts(stmts[..stmts.len - 1]) } saved_pending := t.pending_stmts t.pending_stmts = []ast.Stmt{} saved_skip_if_value_lowering := t.skip_if_value_lowering if last.expr is ast.IfExpr { t.skip_if_value_lowering = true } mut branch_expr := last.expr if t.cur_fn_ret_type_name != '' { if ret_type := t.lookup_type(t.cur_fn_ret_type_name) { if ret_type is types.Enum { branch_expr = t.resolve_expr_with_expected_type(branch_expr, ret_type) } } } transformed_expr := t.transform_expr(branch_expr) t.skip_if_value_lowering = saved_skip_if_value_lowering final_pending := t.pending_stmts t.pending_stmts = saved_pending for stmt in final_pending { out << stmt } out << ast.Stmt(ast.ExprStmt{ expr: transformed_expr }) return out } return t.transform_stmts(stmts) } fn (mut t Transformer) wrap_sumtype_return_branch_tail_stmts(stmts []ast.Stmt, sumtype_name string) []ast.Stmt { if stmts.len == 0 { return stmts } last_idx := stmts.len - 1 last_stmt := stmts[last_idx] if last_stmt !is ast.ExprStmt { return stmts } mut out := stmts.clone() out[last_idx] = ast.Stmt(ast.ExprStmt{ expr: t.wrap_sumtype_return_branch_tail_expr((last_stmt as ast.ExprStmt).expr, sumtype_name, stmts[..last_idx]) }) return out } fn (mut t Transformer) wrap_sumtype_return_branch_tail_expr(expr ast.Expr, sumtype_name string, prefix []ast.Stmt) ast.Expr { if expr is ast.IfExpr { return ast.Expr(ast.IfExpr{ cond: expr.cond stmts: t.wrap_sumtype_return_branch_tail_stmts(expr.stmts, sumtype_name) else_expr: t.wrap_sumtype_return_branch_tail_expr(expr.else_expr, sumtype_name, []ast.Stmt{}) pos: expr.pos }) } info := t.sumtype_wrap_info_for_name(sumtype_name) or { return expr } return t.wrap_sumtype_return_tail_leaf_expr(expr, info, prefix) } fn (mut t Transformer) wrap_sumtype_return_tail_leaf_expr(expr ast.Expr, info ConcreteSumtypeWrapInfo, prefix []ast.Stmt) ast.Expr { if wrapped := t.wrap_sumtype_value_transformed_with_variants(expr, info.name, info.variants) { return wrapped } if expr is ast.Ident { if typ := t.lookup_var_type(expr.name) { type_name := t.type_to_c_name(typ) if type_name != '' && type_name != 'void' && !t.is_same_sumtype_name(type_name, info.name) { if variant_name := t.match_variant(type_name, info.variants) { return t.build_sumtype_init_with_variants(expr, variant_name, info.name, info.variants) or { expr } } } } if variant_name := t.sumtype_return_variant_for_tail_ident(expr.name, info, prefix) { return t.build_sumtype_init_with_variants(expr, variant_name, info.name, info.variants) or { expr } } } return expr } fn (mut t Transformer) sumtype_return_variant_for_tail_ident(name string, info ConcreteSumtypeWrapInfo, prefix []ast.Stmt) ?string { if name == '' || prefix.len == 0 { return none } for i := prefix.len - 1; i >= 0; i-- { stmt := prefix[i] if stmt !is ast.AssignStmt { continue } assign := stmt as ast.AssignStmt if assign.lhs.len != 1 || assign.rhs.len != 1 { continue } lhs := assign.lhs[0] if lhs !is ast.Ident || (lhs as ast.Ident).name != name { continue } if typ := t.get_expr_type(lhs) { if variant_name := t.sumtype_return_variant_from_type(typ, info) { return variant_name } } if variant_name := t.sumtype_return_variant_from_expr(assign.rhs[0], info) { return variant_name } } return none } fn (mut t Transformer) sumtype_return_variant_from_expr(expr ast.Expr, info ConcreteSumtypeWrapInfo) ?string { if typ := t.get_expr_type(expr) { if variant_name := t.sumtype_return_variant_from_type(typ, info) { return variant_name } } if expr is ast.InitExpr { init_variant_name := t.init_expr_sumtype_variant_name(expr, info.variants, info.name) if init_variant_name != '' { return init_variant_name } } if expr is ast.CastExpr { return t.sumtype_return_variant_from_type_expr(expr.typ, info) } if expr is ast.ParenExpr { return t.sumtype_return_variant_from_expr(expr.expr, info) } if expr is ast.ModifierExpr { return t.sumtype_return_variant_from_expr(expr.expr, info) } return none } fn (mut t Transformer) sumtype_return_variant_from_type_expr(typ_expr ast.Expr, info ConcreteSumtypeWrapInfo) ?string { concrete_typ_expr := if t.cur_monomorphized_fn_bindings.len > 0 { t.substitute_type_in_expr(typ_expr, t.cur_monomorphized_fn_bindings) } else { typ_expr } if typ := t.type_from_init_expr(ast.InitExpr{ typ: concrete_typ_expr }) { if variant_name := t.sumtype_return_variant_from_type(typ, info) { return variant_name } } type_name := t.get_init_expr_type_name(concrete_typ_expr) if type_name == '' || t.is_same_sumtype_name(type_name, info.name) { return none } return t.match_variant(type_name, info.variants) } fn (t &Transformer) sumtype_return_variant_from_type(typ types.Type, info ConcreteSumtypeWrapInfo) ?string { mut type_name := t.type_to_c_name(typ) if type_name == '' { type_name = typ.name() } if type_name == '' || type_name == 'void' || t.is_same_sumtype_name(type_name, info.name) { return none } return t.match_variant(type_name, info.variants) } // lower_match_expr_to_if converts a transformed match expression into a nested IfExpr chain. // Backends only need to support IfExpr after this lowering. fn (mut t Transformer) lower_match_expr_to_if(match_expr ast.Expr, branches []ast.MatchBranch) ast.Expr { is_match_true := match_expr is ast.BasicLiteral && match_expr.kind == .key_true is_match_false := match_expr is ast.BasicLiteral && match_expr.kind == .key_false mut current := ast.Expr(ast.empty_expr) for i := branches.len - 1; i >= 0; i-- { branch := branches[i] if branch.cond.len == 0 { current = ast.Expr(ast.IfExpr{ cond: ast.empty_expr stmts: branch.stmts else_expr: current }) continue } branch_cond := t.build_match_branch_cond(match_expr, branch.cond, is_match_true, is_match_false) current = ast.Expr(ast.IfExpr{ cond: branch_cond stmts: branch.stmts else_expr: current }) } return current } // lower_match_expr_to_if_flat is the direct-emit mirror of // `lower_match_expr_to_if`. Emits the nested IfExpr chain straight into the // flat builder via `emit_if_expr_by_ids`, skipping the `ast.IfExpr` wrapper- // struct allocation per branch (the legacy helper allocates one IfExpr struct // per branch — N-branch matches allocate N structs that the leaf `emit_expr` // walk would still have to traverse). Children are already-transformed by // `transform_match_expr_parts`, so leaf-encode via `out.emit_expr` / // `out.emit_stmt` (mirrors `add_expr(IfExpr)`'s `push_expr`/`push_stmt` // walk). The flat MatchExpr arm calls this after `_parts`. fn (mut t Transformer) lower_match_expr_to_if_flat(match_expr ast.Expr, branches []ast.MatchBranch, mut out ast.FlatBuilder) ast.FlatNodeId { is_match_true := match_expr is ast.BasicLiteral && match_expr.kind == .key_true is_match_false := match_expr is ast.BasicLiteral && match_expr.kind == .key_false mut current := out.emit_expr(ast.empty_expr) for i := branches.len - 1; i >= 0; i-- { branch := branches[i] mut stmt_ids := []ast.FlatNodeId{cap: branch.stmts.len} for s in branch.stmts { stmt_ids << out.emit_stmt(s) } if branch.cond.len == 0 { cond_id := out.emit_expr(ast.empty_expr) current = out.emit_if_expr_by_ids(cond_id, current, stmt_ids, token.Pos{}) continue } branch_cond := t.build_match_branch_cond(match_expr, branch.cond, is_match_true, is_match_false) cond_id := out.emit_expr(branch_cond) current = out.emit_if_expr_by_ids(cond_id, current, stmt_ids, token.Pos{}) } return current } fn transformer_stmts_contain_label_stmt(stmts []ast.Stmt) bool { for stmt in stmts { if transformer_stmt_contains_label_stmt(stmt) { return true } } return false } fn transformer_stmt_contains_label_stmt(stmt ast.Stmt) bool { if !stmt_has_valid_data(stmt) { return false } match stmt { ast.BlockStmt { return transformer_stmts_contain_label_stmt(stmt.stmts) } ast.ComptimeStmt { return transformer_stmt_contains_label_stmt(stmt.stmt) } ast.DeferStmt { return transformer_stmts_contain_label_stmt(stmt.stmts) } ast.ExprStmt { return transformer_expr_contains_label_stmt(stmt.expr) } ast.ForStmt { return transformer_stmt_contains_label_stmt(stmt.init) || transformer_stmt_contains_label_stmt(stmt.post) || transformer_expr_contains_label_stmt(stmt.cond) || transformer_stmts_contain_label_stmt(stmt.stmts) } ast.LabelStmt { return true } ast.ReturnStmt { for return_expr in stmt.exprs { if transformer_expr_contains_label_stmt(return_expr) { return true } } return false } else { return false } } } fn transformer_expr_contains_label_stmt(expr ast.Expr) bool { if !expr_has_valid_data(expr) { return false } match expr { ast.IfExpr { return transformer_expr_contains_label_stmt(expr.cond) || transformer_stmts_contain_label_stmt(expr.stmts) || transformer_expr_contains_label_stmt(expr.else_expr) } ast.MatchExpr { for branch in expr.branches { if transformer_stmts_contain_label_stmt(branch.stmts) { return true } } return false } ast.SelectExpr { return transformer_stmts_contain_label_stmt(expr.stmts) || transformer_expr_contains_label_stmt(expr.next) } ast.UnsafeExpr { return transformer_stmts_contain_label_stmt(expr.stmts) } else { return false } } } fn (mut t Transformer) transform_if_expr(expr ast.IfExpr) ast.Expr { has_labeled_else_branch := transformer_expr_contains_label_stmt(expr.else_expr) // Normalize long && chains containing `is` checks so smartcast lowering can // apply to patterns like `a && x is T && b`. if !has_labeled_else_branch && expr.cond is ast.InfixExpr && expr.cond.op == token.Token.and { terms := t.flatten_and_terms(expr.cond) if terms.len > 2 { mut is_idx := -1 for i, term in terms { if term is ast.InfixExpr && term.op in [.key_is, .eq] && t.smartcast_context_from_is_check(term) != none { is_idx = i break } } if is_idx >= 0 { pre_terms := if is_idx > 0 { terms[..is_idx] } else { []ast.Expr{} } post_terms := if is_idx + 1 < terms.len { terms[is_idx + 1..] } else { []ast.Expr{} } mut inner_cond := terms[is_idx] if post_terms.len > 0 { post_cond := if post_terms.len == 1 { post_terms[0] } else { ast.Expr(ast.ParenExpr{ expr: t.join_and_terms(post_terms) pos: post_terms[0].pos() }) } inner_cond = ast.Expr(ast.InfixExpr{ op: token.Token.and lhs: inner_cond rhs: post_cond }) } inner_if := ast.IfExpr{ cond: inner_cond stmts: expr.stmts else_expr: expr.else_expr pos: expr.pos } if pre_terms.len > 0 { outer_if := ast.IfExpr{ cond: t.join_and_terms(pre_terms) stmts: [ast.Stmt(ast.ExprStmt{ expr: inner_if })] else_expr: expr.else_expr pos: expr.pos } return t.transform_if_expr(outer_if) } return t.transform_if_expr(inner_if) } } } // Check for compound && condition with is check: if x is Type && cond { ... } // Transform to nested ifs: if x is Type { if cond { ... } } // This allows the smart cast context to be active for the inner condition if !has_labeled_else_branch && expr.cond is ast.InfixExpr { cond := expr.cond as ast.InfixExpr if cond.op == .and { // Check if LHS is an is-check if cond.lhs is ast.InfixExpr { lhs_infix := cond.lhs as ast.InfixExpr if lhs_infix.op in [.key_is, .eq] && t.smartcast_context_from_is_check(lhs_infix) != none { // Transform: if x is Type && rest { body } else { else_body } // Handle directly to ensure else_body is transformed WITHOUT smartcast // Get variant info from lhs_infix (the is-check) mut variant_name := '' mut variant_module := '' if lhs_infix.rhs is ast.Ident { variant_name = (lhs_infix.rhs as ast.Ident).name } else if lhs_infix.rhs is ast.SelectorExpr { sel := lhs_infix.rhs as ast.SelectorExpr variant_name = sel.rhs.name if sel.lhs is ast.Ident { variant_module = (sel.lhs as ast.Ident).name } } if variant_name != '' { variant_lookup_name := sum_type_variant_name_with_module(variant_module, variant_name) mut sumtype_name := t.get_sumtype_name_for_expr(lhs_infix.lhs) if sumtype_name == '' { sumtype_name = t.find_sumtype_for_variant(variant_lookup_name) } if sumtype_name != '' { variants := t.get_sum_type_variants(sumtype_name) mut tag_value := -1 for i, v in variants { if sum_type_variant_matches_for_sumtype(sumtype_name, v, variant_lookup_name) { tag_value = i break } } if tag_value >= 0 { smartcast_expr := t.expr_to_string(lhs_infix.lhs) qualified_variant := if variant_module != '' { '${variant_module}__${variant_name}' } else { variant_name } // For full variant name (type casts), always include module prefix qualified_variant_full := if variant_module != '' { '${variant_module}__${variant_name}' } else if t.cur_module != '' && t.cur_module != 'main' && t.cur_module != 'builtin' { '${t.cur_module}__${variant_name}' } else { variant_name } transformed_lhs := t.transform_tag_check_lhs_for_sumtype(lhs_infix.lhs, sumtype_name) // Push smartcast for body and inner condition smartcast_stack_before := t.smartcast_stack.clone() smartcast_counts_before := t.smartcast_expr_counts.clone() t.push_smartcast_full(smartcast_expr, qualified_variant, qualified_variant_full, sumtype_name) condition_smartcast_stack := t.smartcast_stack.clone() condition_smartcast_counts := t.smartcast_expr_counts.clone() // Check if inner condition (rest) is also an is-check mut inner_cond_lowered := false mut inner_tag_check := ast.Expr(ast.BasicLiteral{ kind: token.Token.number value: '0' }) inner_rhs_expr := smartcast_lhs_expr(cond.rhs) if inner_rhs_expr is ast.InfixExpr && (inner_rhs_expr as ast.InfixExpr).op in [.key_is, .eq] { orig_inner := inner_rhs_expr as ast.InfixExpr mut inner_smartcast_pushed := false if inner_ctx := t.smartcast_context_from_is_check(orig_inner) { t.push_smartcast_ctx(inner_ctx) inner_smartcast_pushed = true } mut inner_vname := '' mut inner_vmodule := '' if orig_inner.rhs is ast.Ident { inner_vname = (orig_inner.rhs as ast.Ident).name } else if orig_inner.rhs is ast.SelectorExpr { inner_sel := orig_inner.rhs as ast.SelectorExpr inner_vname = inner_sel.rhs.name if inner_sel.lhs is ast.Ident { inner_vmodule = (inner_sel.lhs as ast.Ident).name } } if inner_vname != '' { inner_lookup_name := sum_type_variant_name_with_module(inner_vmodule, inner_vname) // Get sum type from ORIGINAL expression (before smartcast) mut inner_stype := t.get_sumtype_name_for_expr(orig_inner.lhs) if inner_stype == '' { inner_stype = t.find_sumtype_for_variant(inner_lookup_name) } if inner_stype != '' { inner_variants := t.get_sum_type_variants(inner_stype) mut inner_tag := -1 for iv, iv_name in inner_variants { if sum_type_variant_matches_for_sumtype(inner_stype, iv_name, inner_lookup_name) { inner_tag = iv break } } if inner_tag >= 0 && !inner_smartcast_pushed { // Push inner smartcast for body inner_qv := if inner_vmodule != '' { '${inner_vmodule}__${inner_vname}' } else { inner_vname } inner_qvf := if inner_vmodule != '' { '${inner_vmodule}__${inner_vname}' } else if t.cur_module != '' && t.cur_module != 'main' && t.cur_module != 'builtin' { '${t.cur_module}__${inner_vname}' } else { inner_vname } inner_se := t.expr_to_string(orig_inner.lhs) t.push_smartcast_full(inner_se, inner_qv, inner_qvf, inner_stype) inner_smartcast_pushed = true } } } } active_smartcast_stack := t.smartcast_stack.clone() active_smartcast_counts := t.smartcast_expr_counts.clone() // Transform inner condition with smartcast (if not already lowered). // Expression expansions from the condition must run before the // nested if, while the outer smartcast is active. saved_rest_pending := t.pending_stmts t.pending_stmts = []ast.Stmt{} t.smartcast_stack = condition_smartcast_stack.clone() t.smartcast_expr_counts = condition_smartcast_counts.clone() transformed_rest := if inner_cond_lowered { inner_tag_check } else { t.transform_expr(cond.rhs) } rest_pending := t.pending_stmts t.pending_stmts = saved_rest_pending t.smartcast_stack = active_smartcast_stack.clone() t.smartcast_expr_counts = active_smartcast_counts.clone() mut seen_body_smartcasts := map[string]bool{} for sc in t.smartcast_stack { seen_body_smartcasts['${sc.expr}|${sc.variant}|${sc.variant_full}|${sc.sumtype}'] = true } for term in t.flatten_and_terms_unwrapped(cond.rhs) { if term is ast.InfixExpr { if body_ctx := t.smartcast_context_from_condition_term(term) { key := '${body_ctx.expr}|${body_ctx.variant}|${body_ctx.variant_full}|${body_ctx.sumtype}' if key !in seen_body_smartcasts { t.push_smartcast_ctx(body_ctx) seen_body_smartcasts[key] = true } } } } // Transform body with smartcast(s) transformed_body := t.transform_stmts(expr.stmts) // Restore the surrounding context before transforming else. t.smartcast_stack = smartcast_stack_before.clone() t.smartcast_expr_counts = smartcast_counts_before.clone() // Transform else WITHOUT smartcast transformed_else := t.transform_expr(expr.else_expr) // Build tag check tag_check := ast.InfixExpr{ op: token.Token.eq lhs: t.synth_selector(transformed_lhs, '_tag', types.Type(types.int_)) rhs: ast.BasicLiteral{ kind: token.Token.number value: '${tag_value}' } pos: lhs_infix.pos } // Build inner if (with already-transformed components) inner_if := ast.IfExpr{ cond: transformed_rest stmts: transformed_body else_expr: transformed_else pos: expr.pos } mut outer_stmts := []ast.Stmt{cap: rest_pending.len + 1} for pending_stmt in rest_pending { outer_stmts << pending_stmt } outer_stmts << ast.Stmt(ast.ExprStmt{ expr: inner_if }) // Build outer if return ast.IfExpr{ cond: tag_check stmts: outer_stmts else_expr: transformed_else pos: expr.pos } } } } // Fallback: use smartcast_context_from_is_check to get the proper // context (interface-aware with __iface__ prefix, or plain). if iface_ctx := t.smartcast_context_from_is_check(lhs_infix) { // Transform else_expr FIRST without smartcast transformed_else_fallback := t.transform_expr(expr.else_expr) // Push interface-aware smartcast context t.push_smartcast_ctx(iface_ctx) // Transform inner condition and body with smartcast saved_rest_pending := t.pending_stmts t.pending_stmts = []ast.Stmt{} transformed_rest_fallback := t.transform_expr(cond.rhs) rest_pending := t.pending_stmts t.pending_stmts = saved_rest_pending transformed_body_fallback := t.transform_stmts(expr.stmts) // Pop smartcast t.pop_smartcast() inner_if := ast.IfExpr{ cond: transformed_rest_fallback stmts: transformed_body_fallback else_expr: transformed_else_fallback pos: expr.pos } mut outer_stmts := []ast.Stmt{cap: rest_pending.len + 1} for pending_stmt in rest_pending { outer_stmts << pending_stmt } outer_stmts << ast.Stmt(ast.ExprStmt{ expr: inner_if }) // Keep original is-check condition (let cleanc handle it) return ast.IfExpr{ cond: t.transform_expr(ast.Expr(lhs_infix)) stmts: outer_stmts else_expr: transformed_else_fallback pos: expr.pos } } } } // Check if RHS is an is-check: if cond && x is Type { ... } // Transform to: if cond { if x is Type { ... } else { else_body } } else { else_body } if cond.rhs is ast.InfixExpr { rhs_infix := cond.rhs as ast.InfixExpr if rhs_infix.op in [.key_is, .eq] && t.smartcast_context_from_is_check(rhs_infix) != none { // Transform: if cond && x is Type { body } else { else_body } // To: if cond { if x is Type { body } else { else_body } } else { else_body } inner_if := ast.IfExpr{ cond: ast.Expr(cond.rhs) stmts: expr.stmts else_expr: expr.else_expr pos: expr.pos } outer_if := ast.IfExpr{ cond: cond.lhs stmts: [ast.Stmt(ast.ExprStmt{ expr: inner_if })] else_expr: expr.else_expr pos: expr.pos } // Recursively transform - inner will handle smartcast return t.transform_if_expr(outer_if) } } } } // Check for sum type smart cast: if x is Type { ... } if expr.cond is ast.InfixExpr { cond := expr.cond as ast.InfixExpr if cond.op in [.key_is, .eq] && t.smartcast_context_from_is_check(cond) != none { if ctx := t.smartcast_context_from_is_check(cond) { if tag_value := t.smartcast_context_tag_value(ctx) { transformed_lhs := t.transform_tag_check_lhs_for_sumtype(cond.lhs, ctx.sumtype) t.push_smartcast_ctx(ctx) transformed_stmts := t.transform_stmts(expr.stmts) t.pop_smartcast() tag_check := ast.InfixExpr{ op: token.Token.eq lhs: t.synth_selector(transformed_lhs, '_tag', types.Type(types.int_)) rhs: ast.BasicLiteral{ kind: token.Token.number value: '${tag_value}' } pos: cond.pos } return ast.IfExpr{ cond: tag_check stmts: transformed_stmts else_expr: t.transform_expr(expr.else_expr) pos: expr.pos } } } // Get the variant type name from RHS // Also extract module for qualified types like types.Type mut variant_name := '' mut variant_module := '' if cond.rhs is ast.Ident { variant_name = (cond.rhs as ast.Ident).name } else if cond.rhs is ast.SelectorExpr { // Handle module-qualified types like types.Type sel := cond.rhs as ast.SelectorExpr variant_name = sel.rhs.name // Extract module name (e.g., from types.Type, extract "types") if sel.lhs is ast.Ident { variant_module = (sel.lhs as ast.Ident).name } } if variant_name != '' { variant_lookup_name := sum_type_variant_name_with_module(variant_module, variant_name) // Get the sum type name from LHS type // First try normal lookup, then fall back to finding sumtype by variant mut sumtype_name := t.get_sumtype_name_for_expr(cond.lhs) // If that failed or returned wrong sumtype, try finding by variant if sumtype_name == '' { sumtype_name = t.find_sumtype_for_variant(variant_lookup_name) } // Cross-check: if the variant has an explicit module prefix (e.g. types.FnType) // but the resolved sumtype is from a different module (e.g. ast__Type), // the resolution picked the wrong homonymous sum type. Re-resolve using the // variant's module to find the correct sum type. if variant_module != '' && sumtype_name.contains('__') { st_mod := sumtype_name.all_before_last('__') if st_mod != variant_module { st_short := sumtype_name.all_after_last('__') candidate := '${variant_module}__${st_short}' if t.is_sum_type(candidate) { sumtype_name = candidate } } } if sumtype_name != '' { // Find the tag value for this variant variants := t.get_sum_type_variants(sumtype_name) mut tag_value := -1 for i, v in variants { if sum_type_variant_matches_for_sumtype(sumtype_name, v, variant_lookup_name) { tag_value = i break } } if tag_value >= 0 { // Get the string representation of the LHS expression smartcast_expr := t.expr_to_string(cond.lhs) // Create fully qualified variant name if module is specified // e.g., types.Type -> types__Type qualified_variant := if variant_module != '' { '${variant_module}__${variant_name}' } else { variant_name } // For full variant name (type casts), always include module prefix qualified_variant_full := if variant_module != '' { '${variant_module}__${variant_name}' } else if t.cur_module != '' && t.cur_module != 'main' && t.cur_module != 'builtin' { '${t.cur_module}__${variant_name}' } else { variant_name } transformed_lhs := t.transform_tag_check_lhs_for_sumtype(cond.lhs, sumtype_name) // Push smart cast context for transforming body (supports nested smartcasts) t.push_smartcast_full(smartcast_expr, qualified_variant, qualified_variant_full, sumtype_name) // Transform body with smart cast context transformed_stmts := t.transform_stmts(expr.stmts) // Pop context t.pop_smartcast() // Transform condition: v is Type -> v._tag == TAG_VALUE // This prevents cleanc from also applying smart cast // Use transformed_lhs to apply outer smartcasts to the tag check tag_check := ast.InfixExpr{ op: token.Token.eq lhs: t.synth_selector(transformed_lhs, '_tag', types.Type(types.int_)) rhs: ast.BasicLiteral{ kind: token.Token.number value: '${tag_value}' } pos: cond.pos } // Transform else_expr (may have its own smart cast context) return ast.IfExpr{ cond: tag_check stmts: transformed_stmts else_expr: t.transform_expr(expr.else_expr) pos: expr.pos } } } // Interface smartcast: if iface_var is ConcreteType { ... } // The condition is left as-is (cleanc handles _type_id comparison). // We only need to push the smartcast context so field accesses in // the body are lowered to ((ConcreteType*)(iface._object))->field. if iface_ctx := t.smartcast_context_from_is_check(cond) { t.push_smartcast_ctx(iface_ctx) transformed_stmts := t.transform_stmts(expr.stmts) t.pop_smartcast() return ast.IfExpr{ cond: t.transform_expr(expr.cond) stmts: transformed_stmts else_expr: t.transform_expr(expr.else_expr) pos: expr.pos } } } } } // Handle if-guard expression in condition (for nested/expression-level if-guards) // For Option-returning expressions in if-guards, we need to expand them with temp variables. // Transform: if r := opt_func() { body } else { else_body } // To: { _tmp := opt_func(); if _tmp.state == 0 { r := _tmp.data; body } else { else_body } } if expr.cond is ast.IfGuardExpr { guard := expr.cond as ast.IfGuardExpr if guard.stmt.rhs.len > 0 { synth_pos := t.next_synth_pos() rhs := guard.stmt.rhs[0] // Check if RHS returns Result or Option type mut is_result := t.expr_returns_result(rhs) mut is_option := t.expr_returns_option(rhs) if !is_result && !is_option { fn_name := t.get_call_fn_name(rhs) is_result = fn_name != '' && t.fn_returns_result(fn_name) is_option = fn_name != '' && t.fn_returns_option(fn_name) } if !is_result && !is_option { if ret_type := t.fn_pointer_call_return_type(rhs) { is_result = ret_type is types.ResultType || ret_type.name().starts_with('!') is_option = ret_type is types.OptionType || ret_type.name().starts_with('?') } } if is_result { // Handle Result if-guard using temp variable pattern // Transform: if var := result_call() { body } else { else_body } // To: { _tmp := result_call(); if !_tmp.is_error { var := _tmp.data; body } else { else_body } } temp_name := t.gen_temp_name() temp_pos := synth_pos if_pos := t.next_synth_pos() temp_ident := ast.Ident{ name: temp_name pos: temp_pos } mut is_blank := false if guard.stmt.lhs.len == 1 { lhs0 := guard.stmt.lhs[0] if lhs0 is ast.Ident { if lhs0.name == '_' { is_blank = true } } } mut data_type := types.Type(types.voidptr_) if wrapper_type := t.expr_wrapper_type_for_or(rhs) { t.register_temp_var(temp_name, wrapper_type) t.register_synth_type(temp_pos, wrapper_type) if wrapper_type is types.ResultType { data_type = wrapper_type.base_type } } else if wrapper_type := t.get_expr_type(rhs) { t.register_temp_var(temp_name, wrapper_type) t.register_synth_type(temp_pos, wrapper_type) if wrapper_type is types.ResultType { data_type = wrapper_type.base_type } } else if wrapper_type := t.fn_pointer_call_return_type(rhs) { t.register_temp_var(temp_name, wrapper_type) t.register_synth_type(temp_pos, wrapper_type) if wrapper_type is types.ResultType { data_type = wrapper_type.base_type } } // 1. _tmp := result_call() temp_assign := ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(temp_ident)] rhs: [t.transform_expr(rhs)] pos: temp_pos } // 2. Condition: !_tmp.is_error success_cond := ast.PrefixExpr{ op: .not expr: t.synth_selector(temp_ident, 'is_error', types.Type(types.bool_)) } // 3. Body: var := _tmp.data; original_body mut body_stmts := []ast.Stmt{} if !is_blank { data_access := t.synth_selector(temp_ident, 'data', data_type) t.register_if_guard_lhs_payload_type(guard.stmt.lhs, data_type) body_stmts << ast.AssignStmt{ op: .decl_assign lhs: guard.stmt.lhs rhs: [data_access] pos: guard.stmt.pos } } for s in expr.stmts { body_stmts << s } modified_if := ast.IfExpr{ cond: success_cond stmts: t.transform_stmts(body_stmts) else_expr: t.transform_expr(expr.else_expr) pos: if_pos } // Propagate the original IfExpr type to the synthesized node if orig_type := t.get_expr_type(ast.Expr(expr)) { t.register_synth_type(if_pos, orig_type) } // Wrap temp assignment + if in UnsafeExpr (compound expression) return ast.UnsafeExpr{ stmts: [ast.Stmt(temp_assign), ast.ExprStmt{ expr: modified_if }] } } if is_option { // Handle Option if-guard using a temp variable to avoid taking // addresses of rvalue wrappers when extracting `.data`. // Transform: if var := opt_call() { body } else { else_body } // To: { _tmp := opt_call(); if _tmp.state == 0 { var := _tmp.data; body } else { else_body } } temp_name := t.gen_temp_name() temp_pos := synth_pos if_pos := t.next_synth_pos() temp_ident := ast.Ident{ name: temp_name pos: temp_pos } mut data_type := types.Type(types.voidptr_) if wrapper_type := t.expr_wrapper_type_for_or(rhs) { t.register_temp_var(temp_name, wrapper_type) t.register_synth_type(temp_pos, wrapper_type) if wrapper_type is types.OptionType { data_type = wrapper_type.base_type } } else if wrapper_type := t.get_expr_type(rhs) { t.register_temp_var(temp_name, wrapper_type) t.register_synth_type(temp_pos, wrapper_type) if wrapper_type is types.OptionType { data_type = wrapper_type.base_type } } else if wrapper_type := t.fn_pointer_call_return_type(rhs) { t.register_temp_var(temp_name, wrapper_type) t.register_synth_type(temp_pos, wrapper_type) if wrapper_type is types.OptionType { data_type = wrapper_type.base_type } } mut is_blank := false if guard.stmt.lhs.len == 1 { lhs0 := guard.stmt.lhs[0] if lhs0 is ast.Ident { if lhs0.name == '_' { is_blank = true } } } temp_assign := ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(temp_ident)] rhs: [t.transform_expr(rhs)] pos: temp_pos } opt_success_cond := ast.InfixExpr{ op: .eq lhs: t.synth_selector(temp_ident, 'state', types.Type(types.int_)) rhs: ast.BasicLiteral{ kind: .number value: '0' } } mut body_stmts := []ast.Stmt{} if !is_blank { data_access := t.synth_selector(temp_ident, 'data', data_type) t.register_if_guard_lhs_payload_type(guard.stmt.lhs, data_type) body_stmts << ast.AssignStmt{ op: .decl_assign lhs: guard.stmt.lhs rhs: [data_access] pos: guard.stmt.pos } } for s in expr.stmts { body_stmts << s } modified_if := ast.IfExpr{ cond: opt_success_cond stmts: t.transform_stmts(body_stmts) else_expr: t.transform_expr(expr.else_expr) pos: if_pos } // Propagate the original IfExpr type to the synthesized node if orig_type := t.get_expr_type(ast.Expr(expr)) { t.register_synth_type(if_pos, orig_type) } return ast.UnsafeExpr{ stmts: [ast.Stmt(temp_assign), ast.ExprStmt{ expr: modified_if }] } } // Non-option case: use simple transformation // For map if-guards: if r := map[key] { body } else { else_body } // Transform to: if (key in map) { r := map[key]; body } else { else_body } // For other cases: if (rhs) { r := rhs; body } else { else_body } mut is_blank := false if guard.stmt.lhs.len == 1 { lhs0 := guard.stmt.lhs[0] if lhs0 is ast.Ident { if lhs0.name == '_' { is_blank = true } } } // Check if RHS is a map or array index expression mut cond_expr := ast.Expr(t.transform_expr(rhs)) if rhs is ast.IndexExpr { // Try to see if this is a map index if _ := t.get_map_type_for_expr(rhs.lhs) { // This is a map access - generate "key in map" check cond_expr = ast.Expr(ast.InfixExpr{ op: .key_in lhs: rhs.expr // the key expression rhs: rhs.lhs // the map expression pos: rhs.pos }) } else { // This is an array access - generate bounds check: index < array.len cond_expr = ast.Expr(ast.InfixExpr{ op: .lt lhs: t.transform_expr(rhs.expr) // the index expression rhs: t.synth_selector(t.transform_expr(rhs.lhs), 'len', types.Type(types.int_)) pos: rhs.pos }) } } mut new_stmts := []ast.Stmt{cap: expr.stmts.len + 1} if !is_blank { guard_assign := ast.AssignStmt{ op: .decl_assign lhs: guard.stmt.lhs rhs: guard.stmt.rhs pos: guard.stmt.pos } new_stmts << guard_assign } for s in expr.stmts { new_stmts << s } saved_p := t.pending_stmts t.pending_stmts = []ast.Stmt{} t_stmts := t.transform_stmts(new_stmts) inner_p := t.pending_stmts t.pending_stmts = saved_p for ip in inner_p { t.pending_stmts << ip } saved_p2 := t.pending_stmts t.pending_stmts = []ast.Stmt{} t_else := t.transform_expr(expr.else_expr) inner_p2 := t.pending_stmts t.pending_stmts = saved_p2 for ip in inner_p2 { t.pending_stmts << ip } result_if := ast.IfExpr{ cond: t.transform_expr(cond_expr) stmts: t_stmts else_expr: t_else pos: synth_pos } // Propagate the original IfExpr type to the synthesized node if orig_type := t.get_expr_type(ast.Expr(expr)) { t.register_synth_type(synth_pos, orig_type) } return result_if } } // Default transformation mut body_smartcasts := []SmartcastContext{} mut seen_smartcasts := map[string]bool{} for term in t.flatten_and_terms_unwrapped(expr.cond) { if term is ast.InfixExpr { if ctx := t.smartcast_context_from_condition_term(term) { key := '${ctx.expr}|${ctx.variant}|${ctx.variant_full}' if key !in seen_smartcasts { seen_smartcasts[key] = true body_smartcasts << ctx } } } } outer_smartcast_stack := t.smartcast_stack.clone() outer_smartcast_counts := t.smartcast_expr_counts.clone() for ctx in body_smartcasts { t.push_smartcast_full(ctx.expr, ctx.variant, ctx.variant_full, ctx.sumtype) } // Save and restore pending_stmts around branch transformation to prevent // outer pending_stmts (e.g., from earlier RHS if-expr lowering) from being // flushed into the inner branch's transform_stmts. saved_pending := t.pending_stmts t.pending_stmts = []ast.Stmt{} transformed_stmts := t.transform_stmts(expr.stmts) // Merge any pending_stmts generated by the then-branch transform back into the outer context. inner_pending_then := t.pending_stmts t.pending_stmts = saved_pending for ip in inner_pending_then { t.pending_stmts << ip } t.smartcast_stack = outer_smartcast_stack.clone() t.smartcast_expr_counts = outer_smartcast_counts.clone() saved_pending2 := t.pending_stmts t.pending_stmts = []ast.Stmt{} transformed_else := t.transform_expr(expr.else_expr) // Merge any pending_stmts generated by the else-expr transform back into the outer context. inner_pending := t.pending_stmts t.pending_stmts = saved_pending2 for ip in inner_pending { t.pending_stmts << ip } t.smartcast_stack = outer_smartcast_stack.clone() t.smartcast_expr_counts = outer_smartcast_counts.clone() transformed_if := ast.IfExpr{ cond: t.transform_expr(expr.cond) stmts: transformed_stmts else_expr: transformed_else pos: expr.pos } t.smartcast_stack = outer_smartcast_stack.clone() t.smartcast_expr_counts = outer_smartcast_counts.clone() // Lower value-position IfExpr: hoist to a temp variable assignment. // This eliminates expression-valued IfExpr so backends don't need statement-expressions. // A value-position if has an else branch and its body ends with an ExprStmt (producing a value). // Skip lowering when: // - The IfExpr is directly inside an ExprStmt (statement position, not value) // - The IfExpr is already the RHS of a decl_assign (cleanc handles this efficiently) if !t.skip_if_value_lowering && transformed_if.else_expr !is ast.EmptyExpr && t.if_expr_is_value(transformed_if) { return t.lower_if_expr_value(transformed_if) } return transformed_if } // get_sumtype_name_for_expr returns the sum type name for an expression, or empty string if not a sum type // This function is smartcast-aware: if the expression is already smartcasted to a variant that is // itself a sum type, it returns that sum type name. fn (mut t Transformer) transform_infix_expr(expr ast.InfixExpr) ast.Expr { // Preserve short-circuit semantics while making smartcasts available to all // following terms in `&&` chains (including multiple `is` checks). if expr.op == .and { terms := t.flatten_and_terms_unwrapped(expr) if terms.len > 1 { mut transformed_terms := []ast.Expr{cap: terms.len} mut pushed := 0 mut changed := false for term in terms { if term is ast.InfixExpr { if ctx := t.smartcast_context_from_condition_term(term) { transformed_terms << t.transform_expr(term) t.push_smartcast_full(ctx.expr, ctx.variant, ctx.variant_full, ctx.sumtype) pushed++ changed = true continue } } transformed_terms << t.transform_expr(term) } for _ in 0 .. pushed { t.pop_smartcast() } if changed { return t.join_and_terms(transformed_terms) } } } // Lower sum type checks/comparisons to tag comparisons. // This also handles parser/checker-lowered `!is` that appears as `!=` with type RHS. if expr.op in [.key_is, .not_is, .eq, .ne] { mut variant_name := '' mut variant_module := '' if expr.rhs is ast.Ident { variant_name = (expr.rhs as ast.Ident).name } else if expr.rhs is ast.SelectorExpr { sel := expr.rhs as ast.SelectorExpr variant_name = sel.rhs.name if sel.lhs is ast.Ident { variant_module = (sel.lhs as ast.Ident).name } } else if expr.rhs is ast.Type { // Handle complex type expressions like []Any, map[string]Any variant_name = t.type_expr_to_variant_name(expr.rhs) } if variant_name != '' { variant_lookup_name := sum_type_variant_name_with_module(variant_module, variant_name) mut sumtype_name := t.get_sumtype_name_for_expr(expr.lhs) mut lhs_is_enum := t.get_enum_type_name(expr.lhs) != '' if lhs_type := t.get_expr_type(expr.lhs) { lhs_base := t.unwrap_alias_and_pointer_type(lhs_type) lhs_is_enum = lhs_is_enum || lhs_base is types.Enum } rhs_is_enum_shorthand := expr.rhs is ast.SelectorExpr && (expr.rhs as ast.SelectorExpr).lhs is ast.EmptyExpr rhs_is_type_like := variant_name.len > 0 && variant_name[0] >= `A` && variant_name[0] <= `Z` can_find_sumtype_fallback := expr.op in [.key_is, .not_is] || rhs_is_type_like if sumtype_name == '' && !lhs_is_enum && !rhs_is_enum_shorthand && can_find_sumtype_fallback { sumtype_name = t.find_sumtype_for_variant(variant_lookup_name) } if sumtype_name != '' { variants := t.get_sum_type_variants(sumtype_name) mut tag_value := -1 for i, v in variants { if sum_type_variant_matches_for_sumtype(sumtype_name, v, variant_lookup_name) { tag_value = i break } } if tag_value >= 0 { transformed_lhs := t.transform_tag_check_lhs_for_sumtype(expr.lhs, sumtype_name) cmp_op := if expr.op in [.key_is, .eq] { token.Token.eq } else { token.Token.ne } return t.make_infix_expr_at(cmp_op, t.synth_selector(transformed_lhs, '_tag', types.Type(types.int_)), ast.Expr(ast.BasicLiteral{ kind: token.Token.number value: '${tag_value}' pos: expr.pos }), expr.pos) } } // Interface self-type check: `iface_var is InterfaceType` where the variable // is already of that interface type. This checks non-nil (type_id != 0). if sumtype_name == '' { if lhs_type := t.get_expr_type(expr.lhs) { lhs_base := t.unwrap_alias_and_pointer_type(lhs_type) if lhs_base is types.Interface { is_self_check := lhs_base.name.ends_with(variant_name) || lhs_base.name == variant_name if is_self_check { transformed_lhs := t.transform_expr(expr.lhs) cmp_op := if expr.op in [.key_is, .eq] { token.Token.ne } else { token.Token.eq } return t.make_infix_expr_at(cmp_op, t.synth_selector(transformed_lhs, '_type_id', types.Type(types.int_)), ast.Expr(ast.BasicLiteral{ kind: token.Token.number value: '0' pos: expr.pos }), expr.pos) } } } } } } // Check for string concatenation: string + string if expr.op == .plus { if literal := folded_string_literal_concat(expr) { return ast.Expr(ast.StringLiteral{ kind: .v value: literal pos: expr.pos }) } lhs_is_str := t.is_string_expr(expr.lhs) rhs_is_str := t.is_string_expr(expr.rhs) // Check if either side is a string literal lhs_is_str_literal := if expr.lhs is ast.StringLiteral { true } else if expr.lhs is ast.BasicLiteral { expr.lhs.kind == .string } else { false } rhs_is_str_literal := if expr.rhs is ast.StringLiteral { true } else if expr.rhs is ast.BasicLiteral { expr.rhs.kind == .string } else { false } // Also check if either side is a string__* call (already transformed) lhs_is_str_call := if expr.lhs is ast.CallExpr { if expr.lhs.lhs is ast.Ident { (expr.lhs.lhs as ast.Ident).name.starts_with('string__') } else { false } } else { false } rhs_is_str_call := if expr.rhs is ast.CallExpr { if expr.rhs.lhs is ast.Ident { (expr.rhs.lhs as ast.Ident).name.starts_with('string__') } else { false } } else { false } // Also check for string InfixExpr (chained concatenation like s1 + s2 + s3) lhs_is_str_infix := expr.lhs is ast.InfixExpr && (expr.lhs as ast.InfixExpr).op == .plus && lhs_is_str rhs_is_str_infix := expr.rhs is ast.InfixExpr && (expr.rhs as ast.InfixExpr).op == .plus && rhs_is_str // Determine if this is a string concatenation using multiple signals should_transform := (lhs_is_str && rhs_is_str) || (lhs_is_str_literal && (rhs_is_str || expr.rhs is ast.Ident || rhs_is_str_call)) || (rhs_is_str_literal && (lhs_is_str || expr.lhs is ast.Ident || lhs_is_str_call)) || (lhs_is_str_call && (rhs_is_str || expr.rhs is ast.Ident)) || (rhs_is_str_call && (lhs_is_str || expr.lhs is ast.Ident)) || (lhs_is_str_infix && expr.rhs is ast.Ident) // Chained: (s1 + s2) + ident || (rhs_is_str_infix && expr.lhs is ast.Ident) // Chained: ident + (s1 + s2) // Check for chained concatenation: (s1 + s2) + s3 -> string__plus_two(s1, s2, s3) if expr.lhs is ast.InfixExpr && should_transform { lhs_infix := expr.lhs as ast.InfixExpr if lhs_infix.op == .plus && t.is_string_expr(lhs_infix.lhs) && t.is_string_expr(lhs_infix.rhs) { // Transform to string__plus_two(s1, s2, s3) return ast.CallExpr{ lhs: ast.Ident{ name: 'string__plus_two' } args: [ t.transform_expr(lhs_infix.lhs), t.transform_expr(lhs_infix.rhs), t.transform_expr(expr.rhs), ] pos: expr.pos } } } // Check for simple concatenation: s1 + s2 -> string__plus(s1, s2) if should_transform { return ast.CallExpr{ lhs: ast.Ident{ name: 'string__plus' } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } } } // Check for 'in' operator with arrays: elem in arr => Array_T_contains(arr, elem) if expr.op in [.key_in, .not_in] { // Range membership: x in a..b -> x >= a && x < b if expr.rhs is ast.RangeExpr { range := expr.rhs as ast.RangeExpr lhs_trans := t.transform_expr(expr.lhs) lower_bound := ast.Expr(ast.InfixExpr{ op: .ge lhs: lhs_trans rhs: t.transform_expr(range.start) pos: expr.pos }) mut range_check := lower_bound if range.end !is ast.EmptyExpr { upper_op := if range.op == .dotdot { token.Token.lt } else { token.Token.le } upper_bound := ast.Expr(ast.InfixExpr{ op: upper_op lhs: lhs_trans rhs: t.transform_expr(range.end) pos: expr.pos }) range_check = ast.Expr(ast.InfixExpr{ op: .and lhs: lower_bound rhs: upper_bound pos: expr.pos }) } if expr.op == .not_in { return ast.PrefixExpr{ op: .not expr: range_check pos: expr.pos } } return range_check } // Map membership: key in map -> map__exists(&map, &key) if rhs_type := t.map_index_lhs_type(expr.rhs) { if map_typ := t.unwrap_map_type(rhs_type) { if t.is_eval_backend() { return ast.InfixExpr{ op: expr.op lhs: t.transform_expr(expr.lhs) rhs: t.transform_expr(expr.rhs) pos: expr.pos } } map_ptr := t.map_expr_to_runtime_ptr(expr.rhs, rhs_type, map_typ) // For the key, declare a temp variable in the same scope as the // map__exists call so the pointer stays valid during the call. // Using addr_of_expr_with_temp would nest the temp inside a // statement expression whose scope ends before map__exists reads it. lhs_trans := t.transform_expr(expr.lhs) mut key_ptr := ast.Expr(ast.empty_expr) mut key_stmts := []ast.Stmt{} if t.can_take_address_expr(lhs_trans) && !t.is_enum_rvalue(lhs_trans, map_typ.key_type) { key_ptr = ast.PrefixExpr{ op: .amp expr: lhs_trans } } else { key_tmp := t.gen_temp_name() key_ident := t.typed_temp_ident(key_tmp, map_typ.key_type) key_stmts << ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(key_ident)] rhs: [lhs_trans] pos: key_ident.pos }) key_ptr = ast.PrefixExpr{ op: .amp expr: key_ident } } exists_call := ast.Expr(ast.CallExpr{ lhs: ast.Ident{ name: 'map__exists' } args: [map_ptr, key_ptr] pos: expr.pos }) mut result_expr := exists_call if expr.op == .not_in { result_expr = ast.PrefixExpr{ op: .not expr: exists_call pos: expr.pos } } if key_stmts.len > 0 { // Wrap in UnsafeExpr so the temp key variable is in scope // for the entire map__exists call. key_stmts << ast.Stmt(ast.ExprStmt{ expr: result_expr }) return ast.UnsafeExpr{ stmts: key_stmts } } return result_expr } } // For inline array literals, expand to a chain of equality checks // instead of a contains() call — simpler and works for all backends. if expr.rhs is ast.ArrayInitExpr { arr := expr.rhs as ast.ArrayInitExpr if arr.exprs.len > 0 { lhs_trans := t.transform_expr(expr.lhs) // Get enum type from LHS for resolving shorthand in array elements enum_type := t.get_enum_type_name(expr.lhs) // Build: lhs == arr[0] || lhs == arr[1] || ... first_elem := if enum_type != '' { t.resolve_enum_shorthand(t.transform_expr(arr.exprs[0]), enum_type) } else { t.transform_expr(arr.exprs[0]) } mut chain := t.make_infix_expr_at(.eq, lhs_trans, first_elem, expr.pos) for i := 1; i < arr.exprs.len; i++ { elem := if enum_type != '' { t.resolve_enum_shorthand(t.transform_expr(arr.exprs[i]), enum_type) } else { t.transform_expr(arr.exprs[i]) } elem_cmp := t.make_infix_expr_at(.eq, lhs_trans, elem, expr.pos) chain = t.make_infix_expr_at(.logical_or, chain, elem_cmp, expr.pos) } if expr.op == .not_in { return ast.PrefixExpr{ op: .not expr: chain pos: expr.pos } } return chain } } // Array membership: elem in arr -> Array_T_contains(arr, elem) if arr_info := t.get_array_method_info(expr.rhs) { // Get enum type from LHS for resolving shorthand in array enum_type := t.get_enum_type_name(expr.lhs) // If array contains enum shorthand but we can't resolve the type, // skip transformation and let cleanc handle it mut has_unresolved_shorthand := false if enum_type == '' && expr.rhs is ast.ArrayInitExpr { arr := expr.rhs as ast.ArrayInitExpr if arr.exprs.len > 0 { first := arr.exprs[0] // Check for enum shorthand (.value with empty LHS) if first is ast.SelectorExpr { sel := first as ast.SelectorExpr // Check for enum shorthand: EmptyExpr or empty Ident as LHS is_shorthand := if sel.lhs is ast.EmptyExpr { true } else if sel.lhs is ast.Ident { // Also check for empty ident name (sel.lhs as ast.Ident).name == '' } else { false } if is_shorthand { has_unresolved_shorthand = true } } } } // Check if this is a sum type variant check: sumtype_var in [TypeA, TypeB] // If LHS is a sum type and RHS contains variant type names, let cleanc handle it mut is_sumtype_variant_check := false if expr.rhs is ast.ArrayInitExpr { // Get LHS sum type name (if it is a sum type) lhs_sumtype := t.get_sumtype_name_for_expr(expr.lhs) if lhs_sumtype != '' { // Check if the array elements are variant type names (Idents) arr := expr.rhs as ast.ArrayInitExpr if arr.exprs.len > 0 && arr.exprs[0] is ast.Ident { // This looks like a sum type variant check // Verify that the ident names are actually variants variants := t.get_sum_type_variants(lhs_sumtype) first_ident := arr.exprs[0] as ast.Ident // Extract module prefix from sum type name (e.g., "ast__Expr" -> "ast__") mod_prefix := if lhs_sumtype.contains('__') { lhs_sumtype.all_before_last('__') + '__' } else { '' } // Check both with and without module prefix variant_name := first_ident.name variant_mangled := mod_prefix + variant_name if variant_name in variants || variant_mangled in variants { is_sumtype_variant_check = true } } } } // If this is a sum type variant check, skip transformation entirely // and return the original expression (cleanc will handle tag checks) if is_sumtype_variant_check { // Return unchanged - cleanc will generate the tag check return t.make_infix_expr_at(expr.op, t.transform_expr(expr.lhs), expr.rhs, expr.pos) } if !has_unresolved_shorthand { mut method_info := arr_info if enum_type != '' && !arr_info.is_fixed { enum_c_name := t.v_type_name_to_c_name(enum_type) if enum_c_name != '' { method_info = ArrayMethodInfo{ array_type: 'Array_${enum_c_name}' elem_type: enum_c_name is_fixed: false } } } contains_fn_name := t.register_needed_array_method(method_info, 'contains') // Transform array with enum context if needed transformed_rhs := if enum_type != '' && expr.rhs is ast.ArrayInitExpr { t.transform_array_with_enum_context(expr.rhs as ast.ArrayInitExpr, enum_type) } else { t.transform_expr(expr.rhs) } contains_call := ast.CallExpr{ lhs: ast.Ident{ name: contains_fn_name } args: [transformed_rhs, t.transform_expr(expr.lhs)] pos: expr.pos } if expr.op == .not_in { // !in => !Array_T_contains(arr, elem) return ast.PrefixExpr{ op: .not expr: contains_call pos: expr.pos } } return contains_call } else { // Unresolved shorthand - return as-is for cleanc to handle expansion return t.make_infix_expr_at(expr.op, t.transform_expr(expr.lhs), expr.rhs, expr.pos) } } } // Check for array append: arr << elem => builtin__array_push_noscan((array*)&arr, _MOV((T[]){ elem })) // If RHS is also an array, use push_many instead // Note: map[key] << value is handled at the statement level // by try_transform_map_index_push in transform_stmts. if expr.op in [.amp, .pipe, .xor] && expr.lhs is ast.InfixExpr { left := expr.lhs as ast.InfixExpr if left.op == .left_shift { if _ := t.get_array_elem_type_str(left.lhs) { combined_rhs := ast.InfixExpr{ lhs: left.rhs op: expr.op rhs: expr.rhs pos: expr.pos } return t.transform_expr(ast.InfixExpr{ lhs: left.lhs op: .left_shift rhs: combined_rhs pos: left.pos }) } } } if expr.op == .left_shift { if elem_type_name := t.get_array_elem_type_str(expr.lhs) { if t.is_eval_backend() { return ast.InfixExpr{ op: expr.op lhs: t.transform_expr(expr.lhs) rhs: t.transform_expr(expr.rhs) pos: expr.pos } } // Use push_many only when RHS element type matches the LHS element type. // This avoids mis-lowering [][]T << []T, which must stay a single push. mut rhs_is_array := false // Enum shorthand (.member) is never an array value, even if the checker // annotated it with the LHS array type instead of the element enum type. rhs_is_enum_shorthand := expr.rhs is ast.SelectorExpr && (expr.rhs as ast.SelectorExpr).lhs is ast.EmptyExpr rhs_is_bitwise_expr := expr.rhs is ast.InfixExpr && (expr.rhs as ast.InfixExpr).op in [.amp, .pipe, .xor, .left_shift, .right_shift] if !rhs_is_enum_shorthand && !rhs_is_bitwise_expr { rhs_is_array = t.append_rhs_is_array_value_compatible(elem_type_name, expr.rhs) } // Or-data-extract: _or_tN.data where the temp var holds a Result/Option of array. // extract_or_expr replaces `call()!` with `_or_tN.data`, losing the PostfixExpr // that array_value_elem_type would have recognized. Check the temp var's type. if !rhs_is_array && expr.rhs is ast.SelectorExpr { rhs_sel := expr.rhs as ast.SelectorExpr if rhs_sel.rhs.name == 'data' && rhs_sel.lhs is ast.Ident { or_ident := rhs_sel.lhs as ast.Ident if or_ident.name.starts_with('_or_t') { if or_type := t.lookup_var_type(or_ident.name) { unwrapped := if or_type is types.ResultType { or_type.base_type } else if or_type is types.OptionType { or_type.base_type } else { or_type } if unwrapped is types.Array { rhs_is_array = true } } } } } lhs_is_ptr := t.is_pointer_type_expr(expr.lhs) && !t.array_append_lhs_uses_local_array_storage(expr.lhs) // Create (array*)&arr or (array*)arr expression depending on whether LHS is already a pointer arr_ptr_expr := if lhs_is_ptr { // Already a pointer, just cast ast.Expr(ast.CastExpr{ typ: ast.Ident{ name: 'array*' } expr: t.transform_expr(expr.lhs) }) } else { // Take address then cast ast.Expr(ast.CastExpr{ typ: ast.Ident{ name: 'array*' } expr: ast.PrefixExpr{ op: .amp expr: t.transform_expr(expr.lhs) pos: expr.pos } }) } if rhs_is_array { // RHS is an array - use array__push_many(array*, val.data, val.len) rhs_transformed := t.transform_expr(expr.rhs) // Materialize RHS values that should not be selected twice as rvalues. // The VarDecl is hoisted via pending_stmts before the current statement. if t.contains_call_expr(expr.rhs) || expr.rhs is ast.ArrayInitExpr { t.temp_counter++ tmp_name := '_pm_t${t.temp_counter}' tmp_ident := ast.Ident{ name: tmp_name } if rhs_type := t.get_expr_type(expr.rhs) { t.register_temp_var(tmp_name, rhs_type) } t.pending_stmts << ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(tmp_ident)] rhs: [rhs_transformed] }) return ast.CallExpr{ lhs: ast.Ident{ name: 'array__push_many' } args: [ arr_ptr_expr, t.synth_selector(ast.Expr(tmp_ident), 'data', types.Type(types.voidptr_)), t.synth_selector(ast.Expr(tmp_ident), 'len', types.Type(types.int_)), ] pos: expr.pos } } // Wrap PrefixExpr in parens to fix operator precedence (*other.data -> (*other).data) rhs_for_selector := if expr.rhs is ast.PrefixExpr { ast.Expr(ast.ParenExpr{ expr: rhs_transformed }) } else { rhs_transformed } return ast.CallExpr{ lhs: ast.Ident{ name: 'array__push_many' } args: [ arr_ptr_expr, t.synth_selector(rhs_for_selector, 'data', types.Type(types.voidptr_)), t.synth_selector(rhs_for_selector, 'len', types.Type(types.int_)), ] pos: expr.pos } } // Create (T[]){ elem } expression for single element push // Note: cleanc will add _MOV wrapper when generating ArrayInitExpr push_rhs := t.single_nested_array_append_value(expr.rhs, elem_type_name) or { expr.rhs } mut transformed_rhs := t.transform_expr(push_rhs) rhs_is_nested_array_literal := push_rhs is ast.ArrayInitExpr && (elem_type_name.starts_with('Array_') || elem_type_name.starts_with('Array_fixed_')) if t.contains_call_expr(push_rhs) || rhs_is_nested_array_literal { t.temp_counter++ tmp_name := '_ap_t${t.temp_counter}' tmp_ident := ast.Ident{ name: tmp_name } if rhs_type := t.get_expr_type(push_rhs) { t.register_temp_var(tmp_name, rhs_type) } t.pending_stmts << ast.Stmt(ast.AssignStmt{ op: .decl_assign lhs: [ast.Expr(tmp_ident)] rhs: [transformed_rhs] }) transformed_rhs = ast.Expr(tmp_ident) } // Clone strings when pushing to arrays to prevent use-after-free // (V1 does: array_push(&arr, _MOV((string[]){ string_clone(s) }))) cloned_rhs := if elem_type_name == 'string' { ast.Expr(ast.CallExpr{ lhs: ast.Ident{ name: 'string__clone' } args: [transformed_rhs] }) } else { transformed_rhs } push_elem := t.wrap_array_push_elem_value(cloned_rhs, elem_type_name) arr_literal := ast.ArrayInitExpr{ typ: ast.Expr(ast.Type(ast.ArrayType{ elem_type: ast.Ident{ name: elem_type_name } })) exprs: [push_elem] } return ast.CallExpr{ lhs: ast.Ident{ name: 'builtin__array_push_noscan' } args: [ arr_ptr_expr, ast.Expr(arr_literal), ] pos: expr.pos } } } // Check for enum shorthand in comparisons: x.op == .amp -> x.op == Token.amp if expr.op in [.eq, .ne] { // Check if RHS is enum shorthand (.member with empty LHS) if expr.rhs is ast.SelectorExpr { rhs_sel := expr.rhs as ast.SelectorExpr if rhs_sel.lhs is ast.EmptyExpr { // RHS is enum shorthand - resolve using LHS type enum_type := t.get_enum_type_name(expr.lhs) if enum_type != '' { resolved_rhs := t.resolve_enum_shorthand(expr.rhs, enum_type) return t.make_infix_expr_at(expr.op, t.transform_expr(expr.lhs), t.transform_expr(resolved_rhs), expr.pos) } } } // Check if LHS is enum shorthand (.member with empty LHS) if expr.lhs is ast.SelectorExpr { lhs_sel := expr.lhs as ast.SelectorExpr if lhs_sel.lhs is ast.EmptyExpr { // LHS is enum shorthand - resolve using RHS type enum_type := t.get_enum_type_name(expr.rhs) if enum_type != '' { resolved_lhs := t.resolve_enum_shorthand(expr.lhs, enum_type) return t.make_infix_expr_at(expr.op, t.transform_expr(resolved_lhs), t.transform_expr(expr.rhs), expr.pos) } } } } // Check for string comparisons: s1 == s2, s1 < s2, etc. // Skip if either side is nil — that's a pointer comparison, not a string comparison. if expr.op in [.eq, .ne, .lt, .gt, .le, .ge] && !t.is_nil_expr(expr.lhs) && !t.is_nil_expr(expr.rhs) { mut lhs_has_active_type := false mut lhs_is_str := false if lhs_active_type := t.active_local_decl_type_for_expr(expr.lhs) { lhs_has_active_type = true lhs_is_str = t.type_is_string(lhs_active_type) } else { lhs_is_str = t.is_string_expr(expr.lhs) } mut rhs_has_active_type := false mut rhs_is_str := false if rhs_active_type := t.active_local_decl_type_for_expr(expr.rhs) { rhs_has_active_type = true rhs_is_str = t.type_is_string(rhs_active_type) } else { rhs_is_str = t.is_string_expr(expr.rhs) } // Also check type environment for expression types if is_string_expr didn't find it, // unless an active local declaration already resolved the operand type. if !lhs_is_str && !lhs_has_active_type { if expr_type := t.get_expr_type(expr.lhs) { lhs_is_str = t.type_is_string(expr_type) } } if !rhs_is_str && !rhs_has_active_type { if expr_type := t.get_expr_type(expr.rhs) { rhs_is_str = t.type_is_string(expr_type) } } // If one side is a string literal and the other is unknown (but likely a string), // treat as string comparison. Only do this for string literals, not other string expressions. lhs_is_str_literal := if expr.lhs is ast.StringLiteral { true } else if expr.lhs is ast.BasicLiteral { expr.lhs.kind == .string } else { false } rhs_is_str_literal := if expr.rhs is ast.StringLiteral { true } else if expr.rhs is ast.BasicLiteral { expr.rhs.kind == .string } else { false } // Only infer string comparison if at least one side is a string literal AND // the other is identified as string OR is an ident (could be loop variable) // Also transform if both are Ident and at least one is known to be string // (the other is likely also string in a comparison context) // Also transform if one side is a SelectorExpr and the other is a string literal // (field access compared with string literal is almost always string comparison) // If either side is known to be a string, the other must also be string // (V is type-checked). This handles cases where is_string_expr fails on // complex expressions like Result data access selectors. uses_generic_type := t.expr_uses_current_generic_type(expr.lhs) || t.expr_uses_current_generic_type(expr.rhs) active_non_string_type := (lhs_has_active_type && !lhs_is_str) || (rhs_has_active_type && !rhs_is_str) should_transform := !uses_generic_type && !active_non_string_type && (lhs_is_str || rhs_is_str || (lhs_is_str_literal && (expr.rhs is ast.Ident || expr.rhs is ast.SelectorExpr)) || (rhs_is_str_literal && (expr.lhs is ast.Ident || expr.lhs is ast.SelectorExpr))) if should_transform { // Transform string comparisons to function calls match expr.op { .eq { // s1 == s2 -> string__eq(s1, s2) return ast.CallExpr{ lhs: ast.Ident{ name: 'string__eq' } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } } .ne { // s1 != s2 -> !string__eq(s1, s2) return ast.PrefixExpr{ op: .not expr: ast.CallExpr{ lhs: ast.Ident{ name: 'string__eq' } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } pos: expr.pos } } .lt { // s1 < s2 -> string__lt(s1, s2) return ast.CallExpr{ lhs: ast.Ident{ name: 'string__lt' } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } } .gt { // s1 > s2 -> string__lt(s2, s1) return ast.CallExpr{ lhs: ast.Ident{ name: 'string__lt' } args: [t.transform_expr(expr.rhs), t.transform_expr(expr.lhs)] pos: expr.pos } } .le { // s1 <= s2 -> !string__lt(s2, s1) return ast.PrefixExpr{ op: .not expr: ast.CallExpr{ lhs: ast.Ident{ name: 'string__lt' } args: [t.transform_expr(expr.rhs), t.transform_expr(expr.lhs)] pos: expr.pos } pos: expr.pos } } .ge { // s1 >= s2 -> !string__lt(s1, s2) return ast.PrefixExpr{ op: .not expr: ast.CallExpr{ lhs: ast.Ident{ name: 'string__lt' } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } pos: expr.pos } } else {} } } // Check for array comparisons: arr1 == arr2 or arr1 != arr2 lhs_arr_type := t.get_array_type_str(expr.lhs) rhs_arr_type := t.get_array_type_str(expr.rhs) // For native backends, fixed arrays use memcmp, not array__eq. // get_array_type_str returns 'Array_fixed_...' for fixed arrays. mut is_fixed_array := false if lhs_str := lhs_arr_type { if lhs_str.starts_with('Array_fixed_') { is_fixed_array = true } } if lhs_arr_type != none && rhs_arr_type != none && !is_fixed_array { // Use type-specific equality for arrays of structs/maps (memcmp won't work) mut eq_fn_name := 'array__eq' if t.pref.backend != .arm64 && t.pref.backend != .x64 { if t.array_elem_needs_deep_eq(expr.lhs) || t.array_elem_needs_deep_eq(expr.rhs) { eq_fn_name = '${lhs_arr_type}__eq' } } // Transform array comparisons to function calls eq_call := ast.CallExpr{ lhs: ast.Ident{ name: eq_fn_name } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } if expr.op == .ne { // arr1 != arr2 -> !array__eq(arr1, arr2) return ast.PrefixExpr{ op: .not expr: eq_call pos: expr.pos } } // arr1 == arr2 -> array__eq(arr1, arr2) return eq_call } // Check for fixed array comparisons: [N]T == [N]T // For native backends, lower to C.memcmp(&a, &b, N * sizeof(T)) == 0 // Only for memcmp-safe element types (primitives, fixed arrays of primitives). // Dynamic arrays, strings, maps, and structs contain heap pointers. if t.is_native_be && expr.op in [.eq, .ne] { if lhs_type := t.get_expr_type(expr.lhs) { lhs_base := t.unwrap_alias_and_pointer_type(lhs_type) if lhs_base is types.ArrayFixed { if t.is_memcmp_safe_type(lhs_base.elem_type) { // Primitives/fixed arrays of primitives: use memcmp elem_size := t.type_sizeof(lhs_base.elem_type) total_size := lhs_base.len * elem_size memcmp_call := ast.CallExpr{ lhs: ast.Ident{ name: 'C__memcmp' } args: [ ast.Expr(ast.PrefixExpr{ op: .amp expr: t.transform_expr(expr.lhs) pos: expr.pos }), ast.Expr(ast.PrefixExpr{ op: .amp expr: t.transform_expr(expr.rhs) pos: expr.pos }), ast.Expr(ast.BasicLiteral{ kind: .number value: total_size.str() }), ] pos: expr.pos } zero_lit := ast.BasicLiteral{ kind: .number value: '0' } return t.make_infix_expr_at(expr.op, memcmp_call, ast.Expr(zero_lit), expr.pos) } // Non-memcmp-safe elements (dynamic arrays, strings, maps): // Generate element-by-element comparison using the appropriate // equality function, since synthesized AST nodes lack type info. elem_base := t.unwrap_alias_and_pointer_type(lhs_base.elem_type) eq_fn := if elem_base is types.Array { 'array__eq' } else if t.type_to_c_name(elem_base) == 'string' { 'string__==' } else if elem_base is types.Map { 'map_map_eq' } else { '' // unsupported element type } if eq_fn != '' { lhs_trans := t.transform_expr(expr.lhs) rhs_trans := t.transform_expr(expr.rhs) mut chain := ast.Expr(ast.CallExpr{ lhs: ast.Ident{ name: eq_fn } args: [ ast.Expr(ast.IndexExpr{ lhs: lhs_trans expr: ast.BasicLiteral{ kind: .number value: '0' } }), ast.Expr(ast.IndexExpr{ lhs: rhs_trans expr: ast.BasicLiteral{ kind: .number value: '0' } }), ] pos: expr.pos }) for i := 1; i < lhs_base.len; i++ { elem_cmp := ast.CallExpr{ lhs: ast.Ident{ name: eq_fn } args: [ ast.Expr(ast.IndexExpr{ lhs: lhs_trans expr: ast.BasicLiteral{ kind: .number value: i.str() } }), ast.Expr(ast.IndexExpr{ lhs: rhs_trans expr: ast.BasicLiteral{ kind: .number value: i.str() } }), ] pos: expr.pos } chain = t.make_infix_expr_at(.and, chain, elem_cmp, expr.pos) } if expr.op == .ne { return ast.PrefixExpr{ op: .not expr: chain pos: expr.pos } } return chain } } } } // Check for map comparisons: map1 == map2 or map1 != map2 // Exclude pointer-to-map types (those should be pointer comparisons) if expr.op in [.eq, .ne] { mut is_lhs_ptr_map := false if lhs_type := t.get_expr_type(expr.lhs) { if lhs_type is types.Pointer { is_lhs_ptr_map = true } } mut is_rhs_ptr_map := false if rhs_type := t.get_expr_type(expr.rhs) { if rhs_type is types.Pointer { is_rhs_ptr_map = true } } if !is_lhs_ptr_map && !is_rhs_ptr_map { lhs_map_type := t.get_map_type_for_expr(expr.lhs) rhs_map_type := t.get_map_type_for_expr(expr.rhs) if lhs_map_type != none || rhs_map_type != none { if t.is_eval_backend() { return ast.InfixExpr{ op: expr.op lhs: t.transform_expr(expr.lhs) rhs: t.transform_expr(expr.rhs) pos: expr.pos } } map_type_name := lhs_map_type or { rhs_map_type or { 'map' } } eq_fn := if map_type_name.starts_with('Map_') { '${map_type_name}_map_eq' } else { 'map_map_eq' } map_eq_call := ast.CallExpr{ lhs: ast.Ident{ name: eq_fn } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } if expr.op == .ne { return ast.PrefixExpr{ op: .not expr: map_eq_call pos: expr.pos } } return map_eq_call } } } } // Note: struct == / != is handled by cleanc's memcmp/field-by-field comparison. // Check for struct operator overloading (e.g., time.Time - time.Time) // This transforms t1 - t2 into time__Time__op_minus(t1, t2) for structs with operator overloading // Only applies to specific known struct types that define operator methods if expr.op in [.plus, .minus, .mul, .div, .mod] { if lhs_type := t.get_expr_type(expr.lhs) { match lhs_type { types.Struct { type_name := t.type_to_c_name(lhs_type) // Only transform known struct types with operator overloading known_struct_ops := ['time__Time'] if type_name in known_struct_ops { // Determine operator method name op_name := match expr.op { .plus { '__op_plus' } .minus { '__op_minus' } .mul { '__op_mul' } .div { '__op_div' } .mod { '__op_mod' } else { '' } } if op_name != '' { // Generate function call: Type__op(lhs, rhs) fn_name := '${type_name}${op_name}' return ast.CallExpr{ lhs: ast.Ident{ name: fn_name } args: [t.transform_expr(expr.lhs), t.transform_expr(expr.rhs)] pos: expr.pos } } } } else {} } } } // Default: just transform children lhs_trans := t.transform_expr(expr.lhs) rhs_trans := t.transform_expr(expr.rhs) return t.make_infix_expr_at(expr.op, lhs_trans, rhs_trans, expr.pos) } fn folded_string_literal_concat(expr ast.Expr) ?string { match expr { ast.StringLiteral { if expr.kind != .v { return none } return strip_string_literal_quotes(expr.value) } ast.InfixExpr { if expr.op != .plus { return none } left := folded_string_literal_concat(expr.lhs) or { return none } right := folded_string_literal_concat(expr.rhs) or { return none } return left + right } ast.ParenExpr { return folded_string_literal_concat(expr.expr) } else { return none } } } fn strip_string_literal_quotes(raw string) string { if raw.len < 2 { return raw } first := raw[0] last := raw[raw.len - 1] if first == last && first in [`'`, `"`] { return raw[1..raw.len - 1] } return raw } // resolve_field_type looks up the type of a field on a variable // e.g., for a.flags where a is Array, returns 'ArrayFlags' fn (mut t Transformer) transform_comptime_expr(expr ast.ComptimeExpr) ast.Expr { // The inner expression should be an IfExpr for $if inner := expr.expr if inner is ast.IfExpr { return t.eval_comptime_if(inner) } if inner is ast.CallExpr { if inner.lhs is ast.Ident && inner.lhs.name == 'res' { return ast.Expr(ast.BasicLiteral{ kind: .key_false value: 'false' pos: expr.pos }) } } if inner is ast.CallOrCastExpr { if inner.lhs is ast.Ident && inner.lhs.name == 'res' { return ast.Expr(ast.BasicLiteral{ kind: .key_false value: 'false' pos: expr.pos }) } } if inner is ast.CallExpr { if inner.lhs is ast.Ident && inner.lhs.name == 'embed_file' { return t.transform_embed_file_comptime_expr(expr, inner.args) } } if inner is ast.CallOrCastExpr { if inner.lhs is ast.Ident && inner.lhs.name == 'embed_file' { return t.transform_embed_file_comptime_expr(expr, [inner.expr]) } } if inner is ast.Ident { if inner.name in ['VMODROOT', '@VMODROOT'] { return ast.Expr(t.vmodroot_string_literal(expr.pos)) } } if transformed := t.transform_embed_file_comptime_chain(inner, expr.pos) { return transformed } // For other comptime expressions, just return them transformed return ast.ComptimeExpr{ expr: t.transform_expr(inner) pos: expr.pos } } fn (mut t Transformer) transform_embed_file_comptime_chain(expr ast.Expr, comptime_pos token.Pos) ?ast.Expr { if transformed := t.transform_embed_file_chain_lhs(expr, comptime_pos) { return transformed } match expr { ast.SelectorExpr { if transformed_lhs := t.transform_embed_file_chain_lhs(expr.lhs, comptime_pos) { return ast.Expr(ast.SelectorExpr{ lhs: transformed_lhs rhs: expr.rhs pos: expr.pos }) } } ast.CallExpr { mut transformed_lhs := ast.empty_expr if expr.lhs is ast.SelectorExpr { if transformed_base := t.transform_embed_file_chain_lhs(expr.lhs.lhs, comptime_pos) { transformed_lhs = ast.Expr(ast.SelectorExpr{ lhs: transformed_base rhs: expr.lhs.rhs pos: expr.lhs.pos }) } } if transformed_lhs !is ast.EmptyExpr { mut args := []ast.Expr{cap: expr.args.len} for arg in expr.args { args << t.transform_expr(arg) } return ast.Expr(ast.CallExpr{ lhs: transformed_lhs args: args pos: expr.pos }) } } else {} } return none } fn (mut t Transformer) transform_embed_file_chain_lhs(expr ast.Expr, comptime_pos token.Pos) ?ast.Expr { match expr { ast.CallExpr { if expr.lhs is ast.Ident && expr.lhs.name == 'embed_file' { return t.transform_embed_file_comptime_expr(ast.ComptimeExpr{ expr: ast.Expr(expr) pos: comptime_pos }, expr.args) } if expr.lhs is ast.SelectorExpr { sel := expr.lhs as ast.SelectorExpr if transformed_base := t.transform_embed_file_chain_lhs(sel.lhs, comptime_pos) { mut args := []ast.Expr{cap: expr.args.len} for arg in expr.args { args << t.transform_expr(arg) } return ast.Expr(ast.CallExpr{ lhs: ast.Expr(ast.SelectorExpr{ lhs: transformed_base rhs: sel.rhs pos: sel.pos }) args: args pos: expr.pos }) } } } ast.CallOrCastExpr { if expr.lhs is ast.Ident && expr.lhs.name == 'embed_file' { return t.transform_embed_file_comptime_expr(ast.ComptimeExpr{ expr: ast.Expr(expr) pos: comptime_pos }, [expr.expr]) } } ast.SelectorExpr { if transformed_lhs := t.transform_embed_file_chain_lhs(expr.lhs, comptime_pos) { return ast.Expr(ast.SelectorExpr{ lhs: transformed_lhs rhs: expr.rhs pos: expr.pos }) } } ast.ComptimeExpr { if transformed_inner := t.transform_embed_file_comptime_chain(expr.expr, expr.pos) { return transformed_inner } } else {} } return none } // lower_go_call transforms `go foo(a, b)` into a regular call to // goroutines__goroutine_create, avoiding backend-specific go handling. // For zero-arg calls: goroutines__goroutine_create(voidptr(foo), voidptr(0), 0) // For calls with args: generates a wrapper struct + trampoline function, // then calls goroutines__goroutine_create(trampoline, packed_args, sizeof). fn (mut t Transformer) lower_go_call(expr ast.KeywordOperator) ast.Expr { call := expr.exprs[0] mut call_lhs := ast.empty_expr mut call_args := []ast.Expr{} if call is ast.CallExpr { call_lhs = call.lhs call_args = call.args.clone() } else if call is ast.CallOrCastExpr { call_lhs = call.lhs call_args = [call.expr] } else { // Not a call expression, return as-is return expr } // Resolve the V-level function name for the wrapper fn_name := t.resolve_go_fn_name(call_lhs) if fn_name == '' { // Cannot resolve — fall back to keeping the original expression return expr } // Transform all arguments mut transformed_args := []ast.Expr{cap: call_args.len} for arg in call_args { transformed_args << t.transform_expr(arg) } // Resolve argument type names for struct generation mut arg_type_names := []string{cap: call_args.len} for arg in call_args { if typ := t.get_expr_type(arg) { arg_type_names << t.type_to_c_name(typ) } else { arg_type_names << 'int' } } // Check for method call (receiver needs to be first arg in wrapper) mut is_method := false mut receiver_expr := ast.empty_expr mut receiver_type_name := '' if call_lhs is ast.SelectorExpr { sel := call_lhs as ast.SelectorExpr sel_lhs := sel.lhs if !(sel_lhs is ast.Ident && t.is_module_name((sel_lhs as ast.Ident).name)) { is_method = true receiver_expr = t.transform_expr(sel_lhs) if recv_type := t.get_expr_type(sel_lhs) { receiver_type_name = t.type_to_c_name(recv_type) } else { receiver_type_name = 'voidptr' } } } wrapper_name := '__go_wrap_${fn_name}' if wrapper_name !in t.needed_go_wrappers { mut param_names := []string{} mut param_types := []string{} if is_method { param_names << '_recv' param_types << receiver_type_name } for i, type_name in arg_type_names { param_names << '_a${i}' param_types << type_name } t.needed_go_wrappers[wrapper_name] = GoWrapperInfo{ fn_name: fn_name wrapper_name: wrapper_name param_names: param_names param_types: param_types } } // Build call args: [receiver, args...] for methods, [args...] for functions mut wrapper_args := []ast.Expr{} if is_method { wrapper_args << receiver_expr } for arg in transformed_args { wrapper_args << arg } return ast.CallExpr{ lhs: ast.Ident{ name: wrapper_name } args: wrapper_args pos: expr.pos } } // resolve_go_fn_name extracts the C-mangled function name from a call LHS expression. fn (t &Transformer) resolve_go_fn_name(lhs ast.Expr) string { if lhs is ast.Ident { if t.cur_module != '' { return '${t.cur_module}__${lhs.name}' } return lhs.name } if lhs is ast.SelectorExpr { if lhs.lhs is ast.Ident { mod_name := lhs.lhs.name return '${mod_name}__${lhs.rhs.name}' } } return '' } // is_module_name checks if a name refers to a known module. fn (t &Transformer) is_module_name(name string) bool { // Check if the name is a known module by looking up its scope return name in t.cached_scopes } // eval_comptime_if evaluates a compile-time $if and returns the selected branch expression