Skip to content

markused,gen: fix direct str()call on a smart-casted interface variable#27715

Open
rilaaax wants to merge 2 commits into
vlang:masterfrom
rilaaax:markused/fix-interface-smartcast-str-intp
Open

markused,gen: fix direct str()call on a smart-casted interface variable#27715
rilaaax wants to merge 2 commits into
vlang:masterfrom
rilaaax:markused/fix-interface-smartcast-str-intp

Conversation

@rilaaax

@rilaaax rilaaax commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #27713

Problem

Calling .str() directly on an interface variable that's been smart-cast to a primitive type inside a match branch (without rebinding it to a new variable first) had two separate bugs:

  1. It crashed the C compiler instead of producing a working binary:
cc: error: call to undeclared function 'builtin__str_intp'
cc: error: use of undeclared identifier 'StrIntpData'
  1. Once that crash was fixed, the printed result was wrong: &42 instead of 42

Minimal repro:

interface Value {}

fn get_str(v Value) string {
	return match v {
		int { v.str() }
		else { '' }
	}
}

fn main() {
	println(get_str(42))
}

Note: rebinding first (x := v; x.str()) avoids both issues, which was the
first clue pointing at cgen/dead-code-elimination rather than the checker.

Cause

Interfaces box non-pointer-kind variants (like int) behind an allocated pointer in C (_int is stored as int*). For a direct .str() call on a smart-cast identifier, cgen resolves the receiver using the raw internal smartcast type, which still carries that pointer flag — unlike the "exposed" V-level type (plain int) that the checker and node.left_type use.

This raw, pointer-flagged type causes two independent problems:

  1. markused/walker.v decides whether to keep str_intp (the string interpolation runtime) based on node.left_type (plain int, no pointer flag), so it never learns that cgen's pointer-flagged code path will actually need str_intp, and strips it from the final C output.
  2. gen_expr_to_string() in str.v treats any pointer-flagged type as "this is a real reference the user wants printed with &" — correct for genuine pointers/structs stored by reference, but not for a primitive value that only looks like a pointer because of interface boxing.

Fix

  • markused/walker.v: when registering .str() usage, also check whether the receiver is a smart-cast identifier whose raw smartcast type is a pointer, and register that type too, so str_intp/StrIntpData aren't stripped when cgen will actually need them.
  • str.v: detect the same case (an interface variable smart-cast to a primitive/non struct-like type) and use an empty ref prefix instead of &, leaving all other pointer/struct/reference printing behavior unchanged.

Test

Regression test added.

@GGRei GGRei closed this Jul 11, 2026
@GGRei GGRei reopened this Jul 11, 2026
@GGRei

GGRei commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58be88136c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/gen/c/str.v
Comment on lines +190 to +191
is_interface_smartcast_boxed_primitive := is_ptr && expr is ast.Ident && expr.obj is ast.Var
&& (expr.obj as ast.Var).orig_type != 0 && g.table.final_sym(g.unwrap_generic((expr.obj as ast.Var).orig_type)).kind == .interface && g.table.final_sym(g.unwrap_generic(typ.deref())).kind !in [.struct, .interface, .sum_type, .array, .array_fixed, .map, .multi_return]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve real pointer prefixes in interface .str()

When an interface is smart-cast to an actual pointer primitive, for example an empty-interface value containing &int and a match v { &int { v.str() } } arm, this predicate is also true because typ.deref() resolves to int. The code then suppresses ref_str, so direct .str() loses the pointer-style prefix (42 instead of the real pointer receiver formatting such as &42) and no longer matches rebinding before .str(). Please distinguish boxed value smartcasts from genuine pointer variants before dropping the & prefix.

Useful? React with 👍 / 👎.

Comment thread vlib/v/gen/c/str.v
Comment on lines +190 to +191
is_interface_smartcast_boxed_primitive := is_ptr && expr is ast.Ident && expr.obj is ast.Var
&& (expr.obj as ast.Var).orig_type != 0 && g.table.final_sym(g.unwrap_generic((expr.obj as ast.Var).orig_type)).kind == .interface && g.table.final_sym(g.unwrap_generic(typ.deref())).kind !in [.struct, .interface, .sum_type, .array, .array_fixed, .map, .multi_return]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep aggregate smartcasts out of primitive handling

For grouped interface match arms such as A, B { v.str() }, the smartcast target is represented as an .aggregate, but .aggregate is not excluded here even though the surrounding smartcast code treats it like structs/arrays rather than scalars. That makes is_interface_smartcast_boxed_primitive true for aggregate arms containing struct variants and drops the leading &, so direct .str() in those arms prints A{}/B{} instead of the existing interface-smartcast struct formatting (&A{}/&B{}) and differs from single-variant arms. Please include .aggregate in the non-primitive set.

Useful? React with 👍 / 👎.

Comment thread vlib/v/markused/walker.v
if v.smartcasts.len > 0 {
raw_smartcast_typ := v.smartcasts.last()
if raw_smartcast_typ.is_ptr() {
w.uses_str[raw_smartcast_typ] = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Avoid retaining str_intp for enum smartcasts

For an interface match arm whose variant is an enum, e.g. match v { MyEnum { v.str() } }, cgen handles the pointer-flagged receiver in the enum branch and emits the enum str call directly rather than builtin__str_intp. This unconditional raw-pointer mark therefore keeps str_intp/StrIntpData in the generated C even though the call does not need them, regressing skip-unused output size for enum smartcasts. Please restrict the extra mark to the raw smartcast kinds whose .str() code path actually emits string interpolation support.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cgen: builtin__str_intp is not imported when calling.str() directly on a smart-casted interface variable

2 participants