Summary
On colliding glob imports (use A::* and use B::* where both export the same public name), the typechecker binds the LAST import and the flattening path that feeds codegen binds the FIRST. The program type-checks against one definition and runs another.
lib/module_loader.ml documents the typechecker's rule on the function that implements the opposite one, which is why this has survived: the docstring and the code disagree, and the docstring is the half a reader trusts.
The two rules
Typechecker — LAST wins. lib/resolve.ml:781, in the ImportGlob arm:
Hashtbl.replace type_ctx.Typecheck.var_types sym.Symbol.sym_id scheme;
Hashtbl.replace type_ctx.Typecheck.name_types sym.Symbol.sym_name scheme
name_types is keyed on the bare symbol name, and imports are folded in declaration order. Hashtbl.replace overwrites, so the later use B::* clobbers the scheme installed by the earlier use A::*.
Flattening for codegen — FIRST wins. lib/module_loader.ml:338:
List.filter_map (fun (name, decl_kind) ->
if Hashtbl.mem already_in name then None
else begin
Hashtbl.add already_in name ();
...
already_in is seeded with local decl names and then accumulated across imports in declaration order, so the first import to contribute a name claims it and every later one is dropped.
The docstring is inverted
lib/module_loader.ml:265-267, on flatten_imports itself:
Imports are processed in declaration order; later imports override earlier ones with the same fn name. Local decls in prog.prog_decls always win over imported ones.
Sentence two is implemented correctly (locals are pre-seeded into already_in, so they do win). Sentence one is the exact opposite of what line 338 does. It happens to describe resolve.ml's behaviour — the other side of the mismatch.
Expected failure shape
A.affine: pub fn render(x: Int) -> String
B.affine: pub fn render(x: String) -> String
main.affine:
use A::*
use B::*
render("hi") // typechecks against B (last), runs A::render (first)
Typecheck succeeds against B.render : String -> String; the flattened program contains A.render : Int -> String. On a dynamically-typed backend this is a silent wrong answer or a coercion crash at the call site rather than a type error at the import — the worst place for it to surface.
Whichever rule is chosen, this should be a hard error at the import site rather than a silent pick: a glob-glob collision has no correct silent resolution.
Scope and provenance — please read
- Verified by reading the code on
main, at the line numbers cited (resolve.ml:781, module_loader.ml:265-267 and :338). Both hunks are present upstream today.
- NOT verified by a runtime repro. The example above is the predicted shape, not an executed test. It was first noticed by an automated port probe that terminated before preserving its reproduction, so the observation is being filed on the strength of the code reading rather than that probe's report.
- Affects the non-WASM path.
flatten_imports is the inlining route for backends without a module system; the WASM backend goes through codegen.ml's gen_imports/register_imported_types instead, which dedups separately (seen) and may have a third resolution order. Not audited here.
- No fix proposed, per the standing hold on type-system work until REAL-LIFT lands. This is filed so the finding is not lost.
Related
Refs #519 (resolver determinism / import-cycle rejection / qualified-path canonicality, ADR-014) — adjacent but not a duplicate: #519 is about resolver determinism and cycles, this is a divergence between two resolvers that are each internally deterministic. A determinism proof over resolve.ml alone would pass while this bug remains.
Refs #228 (no module-qualified path in the type/effect grammar) — a qualified-path spelling would give users a way to disambiguate a collision that today has none.
Read against main 2026-09-08.
Summary
On colliding glob imports (
use A::*anduse B::*where both export the same public name), the typechecker binds the LAST import and the flattening path that feeds codegen binds the FIRST. The program type-checks against one definition and runs another.lib/module_loader.mldocuments the typechecker's rule on the function that implements the opposite one, which is why this has survived: the docstring and the code disagree, and the docstring is the half a reader trusts.The two rules
Typechecker — LAST wins.
lib/resolve.ml:781, in theImportGlobarm:name_typesis keyed on the bare symbol name, and imports are folded in declaration order.Hashtbl.replaceoverwrites, so the lateruse B::*clobbers the scheme installed by the earlieruse A::*.Flattening for codegen — FIRST wins.
lib/module_loader.ml:338:already_inis seeded with local decl names and then accumulated across imports in declaration order, so the first import to contribute a name claims it and every later one is dropped.The docstring is inverted
lib/module_loader.ml:265-267, onflatten_importsitself:Sentence two is implemented correctly (locals are pre-seeded into
already_in, so they do win). Sentence one is the exact opposite of what line 338 does. It happens to describeresolve.ml's behaviour — the other side of the mismatch.Expected failure shape
Typecheck succeeds against
B.render : String -> String; the flattened program containsA.render : Int -> String. On a dynamically-typed backend this is a silent wrong answer or a coercion crash at the call site rather than a type error at the import — the worst place for it to surface.Whichever rule is chosen, this should be a hard error at the import site rather than a silent pick: a glob-glob collision has no correct silent resolution.
Scope and provenance — please read
main, at the line numbers cited (resolve.ml:781,module_loader.ml:265-267and:338). Both hunks are present upstream today.flatten_importsis the inlining route for backends without a module system; the WASM backend goes throughcodegen.ml'sgen_imports/register_imported_typesinstead, which dedups separately (seen) and may have a third resolution order. Not audited here.Related
Refs #519 (resolver determinism / import-cycle rejection / qualified-path canonicality, ADR-014) — adjacent but not a duplicate: #519 is about resolver determinism and cycles, this is a divergence between two resolvers that are each internally deterministic. A determinism proof over
resolve.mlalone would pass while this bug remains.Refs #228 (no module-qualified path in the type/effect grammar) — a qualified-path spelling would give users a way to disambiguate a collision that today has none.
Read against
main2026-09-08.