Skip to content

t27c gen: two imported modules that each declare a helper of the same name make the splicer drop both, so a pulled function loses its own helper #10556

Closed

Description

@gHashTag

t27c gen refuses to splice a helper that a pulled function calls internally whenever the same helper name is declared in a second module the spec also imports, even though the call can only mean the helper next to it. Each generated file then calls a function it never declares.

Found while writing specs/t27f/frontend.t27 (PR #10551). claw_serves could not call service::admit and uses home_of as a workaround; the PR's "Not claimed" section records it. Part of the self-host work under #5980.

What happens

use_resolve.rs loads every declaration of every module the spec imports into one map keyed by the bare name (available). It then walks a frontier: pull the declarations the importer names, then the identifiers inside each pulled declaration. When the map holds two different declarations under one name, the name is written as // UNRESOLVED <name>: declared in A and B -- ambiguous, not spliced and nothing is pulled.

That is right for a name the importer itself wrote. It is wrong for a name found inside a pulled declaration: admit in service.t27 calls name_ok, and the only name_ok its author could mean is the one in service.t27. The splicer forgets which module admit came from, looks name_ok up globally, finds tool_schema.t27's name_ok as well, and drops both. t27c gen still exits 0, so the failure surfaces later, in the Zig compile.

Real case (specs on master at 6bc4171): t27mcp/service.t27 has pub fn name_ok(name: string) -> bool and t27mcp/tool_schema.t27 has pub fn name_ok(parent_kind: u32, named: bool) -> bool. A spec that imports service::admit and use t27mcp::tool_schema; cannot be built.

Repro, run on the Railway lab (/root/t27/target/release/t27c, built 2026-10-18)

Tree /root/repro-splicer/specs/repro/ (sha256 of each file: alpha 573e360a, beta 80bc46f4, importer 4ebdbcac, importer_control 3a075ac6):

; specs/repro/alpha.t27 -- first of two modules that each define a helper named name_ok.
module ReproAlpha;

/// Alpha's helper: one argument.
pub fn name_ok(n: u32) -> bool {
    return n > 0 and n < 64;
}

/// Calls the helper inside its own module. This is the function the importer wants.
pub fn admit(n: u32) -> u32 {
    if (name_ok(n) == false) { return 3; }
    return 0;
}

test "alpha: admit refuses 0 and accepts 5" {
    assert admit(0) == 3;
    assert admit(5) == 0;
}

; specs/repro/beta.t27 -- second module: a different helper that happens to share the name name_ok.
module ReproBeta;

/// Beta's helper: two arguments, a different meaning.
pub fn name_ok(kind: u32, named: bool) -> bool {
    return kind > 0 or named;
}

pub fn beta_fine(kind: u32) -> bool {
    return name_ok(kind, false);
}

test "beta: beta_fine" {
    assert beta_fine(1) == true;
    assert beta_fine(0) == false;
}

; specs/repro/importer.t27 -- imports alpha::admit and, whole, beta. Both define name_ok.
module ReproImporter;

use repro::alpha::admit;
use repro::beta;

pub fn serves(n: u32) -> bool {
    return admit(n) == 0 and beta::beta_fine(n);
}

test "importer: serves" {
    assert serves(5) == true;
    assert serves(0) == false;
}

(The three files are shown verbatim, concatenated. importer_control.t27 is importer.t27 with use repro::beta; and the beta::beta_fine term removed.)

$ t27c gen specs/repro/importer.t27 > /tmp/imp.zig
UNRESOLVED name_ok: declared in repro/alpha.t27 and repro/beta.t27 -- ambiguous, not spliced
(exit 0)
$ zig test /tmp/imp.zig
imp.zig:27:9: error: use of undeclared identifier 'name_ok'
    if (name_ok(n) == false) {
imp.zig:33:12: error: use of undeclared identifier 'name_ok'
    return name_ok(kind, false);

Both callers lose their helper: admit (alpha's) and beta_fine (beta's).

Control, same tree, beta not imported:

$ t27c gen specs/repro/importer_control.t27 > /tmp/ctl.zig      # stderr empty, exit 0
$ zig test /tmp/ctl.zig
1/1 ctl.test.control: serves...OK
All 1 tests passed.

The real modules fail the same way. A 4-line importer (use t27mcp::service::admit; use t27mcp::tool_schema;, one function that calls admit):

UNRESOLVED name_ok: declared in t27mcp/service.t27 and t27mcp/tool_schema.t27 -- ambiguous, not spliced
/tmp/real.zig:33:9: error: use of undeclared identifier 'name_ok'
    if (name_ok(name) == false) {

Why a fix is not one line

Resolving a pulled declaration's identifiers against its own module first fixes the lookup. But the output is one flat namespace, so splicing both name_ok bodies would emit two declarations of one name. The splice also has to give each a distinct flat name (for example <module>__name_ok) and rewrite the call sites inside the pulled text. That is the design question here, and flatten_qualified (#5574) and the all_agree rule (identical declarations may merge) both have to keep working. Related, not the same defect: #7939 (the unspliced name exits 0 on gen-c), #9876 (an unresolvable use exits 0).

Boundary

  • bootstrap/src/use_resolve.rs
  • the #[cfg(test)] module of bootstrap/src/use_resolve.rs (a regression test with the two-module repro)
  • specs/t27mcp/service.t27, specs/t27mcp/tool_schema.t27 (read-only here; no rename as a workaround)

User Scenarios

  • Given two modules that each declare name_ok with different signatures, and a spec that imports a function of the first that calls name_ok plus the second module whole, when t27c gen runs, then it splices both helpers, each called only by the functions of its own module, and prints no UNRESOLVED line.
  • Given a spec that itself writes name_ok(...) while two imported modules declare it, when t27c gen runs, then it still refuses with the UNRESOLVED line, because that choice belongs to the author.
  • Given specs/t27f/frontend.t27, when claw_serves calls service::admit, then the file builds and its tests pass.

Requirements

  • FR-001: A name found inside a pulled declaration MUST resolve to the declaration in that declaration's own module when that module declares it, regardless of other modules that declare the same name.
  • FR-002: When two such helpers are both spliced, the output MUST give them distinct names and rewrite the calls inside the spliced text, so the flat output has no duplicate declaration.
  • FR-003: A name written in the importing spec that two imported modules declare differently MUST still be reported as UNRESOLVED (current behaviour, two_real_definitions_stay_ambiguous).
  • FR-004: The fix MUST add a regression test built from the alpha/beta repro above, and MUST NOT change the generated output of any currently resolving spec (compare t27c suite generated artefacts before and after on the Railway lab).

Success Criteria

Epic: #5980. Found in: #10551.

Activity

  1. gHashTag commented on Oct 18, 2026

    @gHashTag
    OwnerAuthor

    Another instance, measured on t27c 0.5.2 at master d1d4f16.

    A new module that imports t27f::lesson_reel::phrase_start gets // UNRESOLVED phrase_start: declared in t27f/lesson_reel.t27 and t27f/token_floor_mux.t27 -- ambiguous, not spliced. The same happens for cta_from (also declared in token_floor_mux.t27) and for voice_fits (also declared in automation/owner_voice_reel.t27).

    The use names the module explicitly. The splicer still treats the bare name as global and drops both candidates. Note that token_floor_mux.t27's copies are private fns, not pub fns, so a private declaration elsewhere is enough to block a public import.

    The cost: #10596 had to put its beat clock inside lesson_reel.t27 instead of in a module of its own (PR #10598).

  2. added 2 commits that reference this issue on Oct 19, 2026
    1df7383
    621a67c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions