This guide is for contributors working on how a Turmeric program made of several files becomes C. It covers two layers:
defmodule, import/export, load, the stdlib
autoload, the search path, and qualified-name resolution. This happens in the
elaborator and is the same for every build..h/.c pair per
module, and the rules that keep those translation units (TUs) agreeing at
link time.The user-level view of modules is in the
module system guide. Symbol spelling is in the
name-mangling guide. The build driver (build
directories, cc lines, caches) is in the
build-driver internals guide, and the
shared runtime TU is in the
runtime preamble and linkage guide.
Trust the code over the header comments. Several comments in this area describe an earlier state (for example the "SX1 experiment gate" in
stdlib_autoload.c, or "the cache" for a file nothing reads). Where a comment and this guide disagree, read the code. If the code disagrees with this guide, fix the guide.
is_from_stdlib.(load "path") is a textual splice, expanded before elaboration,
at most once per path per compile.(import m) elaborates m's file into the same global scope, once
per compile, and records its exports.tur build <dir> with a build.tur and exactly
one main is whole-program too.tur build --shared, a bare directory
without build.tur, a project with zero or several mains or a non-entry
#[used] module, and tur emit-c --output-dir. There each module is
elaborated on its own, and its imports become #include "<mod>.h".static copy. Stdlib code is always a per-TU
static copy. A generic specialization a borrower needs is pushed back to
its owner in a second pass ("forced specs").| File | Role |
|---|---|
src/compiler/elab_module.c |
import loading (elab_load_module_impl), import specs, defmodule, export validation, elab_lookup_sym (visibility and qualified names), elab_force_load_module |
src/compiler/elab_toplevel.c |
elaborate_program; the load preprocessor (load_expand_forms, load_path_key, elab_expand_module_loads); the stdlib promotion sweep |
src/compiler/stdlib_autoload.c, .h |
The autoload list (autoload_files_), tur_stdlib_autoload_files, tur_stdlib_prepend_forms |
src/compiler/spice_search.c, .h |
Walk-up to the enclosing build.tur (spice_find_root), its src/, :spices deps and workspace siblings (spice_search_append) |
src/driver/ |
Drivers: compile_to_c (whole-program), compile_to_h, compile_to_implementation and load_project_prelude (pipeline.c); cmd_build_project, cmd_build_multi and cmd_build_multi_files (project.c); cmd_emit_c_to_dir (cmd_emit.c); auto_append_spice_includes (spice.c) |
src/compiler/emit_module.c |
emit_header, emit_implementation, emit_abi_clone_name, emit_inst_method_wants_external, emit_imported_adt_ctors, the r7rs split helpers (emit_split_lib_owns) |
src/compiler/emit.h |
ForcedAbiSpec, BorrowSpecInfo, the public emit entry points |
src/compiler/emit_fns.c |
emit_fn_def: the needs_static linkage decision |
src/compiler/emit_cps_ir.c |
The CPS entry wrapper's matching linkage decision |
src/compiler/emit_split.h |
The r7rs LIB/IMP/CLIENT split -- a different mechanism, see below |
| Mechanism | When | What happens | Dedup |
|---|---|---|---|
| Stdlib autoload | Every compile | Forms of 34 stdlib files are prepended ahead of the entry file | n/a (once per compile) |
(load "path") |
Top level, or directly in a defmodule body |
The file's forms are spliced in place, before elaboration | Load-visited set, by canonical path |
(import m ...) |
In a defmodule header (top level only in REPL sessions) |
m's file is read, its loads expanded, its forms elaborated into the global scope |
loaded_modules registry, by module name |
#[used] force-load |
Whole-program builds | elab_force_load_module loads a module no import reaches |
Same registry; a no-op under separate compilation |
load is a preprocessorload_expand_forms (elab_toplevel.c) walks the entry's forms depth-first,
in source order, before either elaboration pass:
(load "p") form is replaced by the file's own (recursively expanded)
forms. A file's transitive loads land before the rest of that file.(defmodule ...) form is descended into, so a load in a module body
splices into the module's scope.(import (scheme time)) splices that library's file the same way.The visited set (Elab.load_expanded_paths) is compilation-global. The
key is load_path_key: realpath of the path, except that a stdlib/<rest>
path is first resolved under the stdlib directory. Before expansion,
elaborate_program seeds the set with the canonical path of every autoloaded
file. So an explicit (load "stdlib/json.tur") of an autoloaded file is a
no-op, not a redefinition.
Two consequences worth knowing:
load. elab_read_file(path_buf) reads it as given;
the only fallback retries a stdlib/<rest> path under the stdlib directory.
Verified: tur run sub/app.tur fails with
load: cannot open 'helpers.tur', and the same command from sub/ works.load of the same path splices nothing. For a stdlib/ path this
is harmless: load_expand_forms (elab_toplevel.c) hoists a stdlib load
written inside a defmodule body to the level the module sits at, so its
definitions are global wherever the load is written
(load-in-defmodule-claims-file-for-one-module,
resolved 2026-10-16). A non-stdlib file loaded in a module body still
splices into that module, so a second module that loads it gets nothing.A load that survives to elaboration (inside a defn or let body) reaches
elab_load (elab_module.c), which is a hard error.
import elaborates the module in placeelab_load_module_impl (elab_module.c), in order:
e->loaded_modules. A hit returns the cached entry;
a hit with is_loading set is circular import: module 'x' is already being loaded.is_loading = true) before reading anything.SourceFile with a
fresh file_id and honour its #lang line.elab_expand_module_loads, same visited set), and lower it if it is
#lang r7rs.in_imported_module = true, after a
forward-declaration pass over its top-level defns.!e->separate_compilation): register the
module's EX_DEFMODULE, and any bare top-level definitions a load
spliced in, for file-scope emission (elab_register_file_def). This is
what flattens an imported module into the importer's C file.defining_module_name is this module and is_exported, plus
export-from re-exports (the defining module's own Binding, so no
wrapper), exported macros, and exported effects.Step 7 is the whole difference between the two build shapes. Under separate compilation the imported module is still fully elaborated -- the importer needs its types and signatures -- but none of its definitions are emitted.
The registry is per Elab, so per compile. Under separate compilation every TU re-elaborates each module it imports.
defmoduleelab_defmodule (elab_module.c) accepts
(defmodule name [doc] (export ...) (export-from m ...) (import ...)... body...):
has_defmodule; reset at each file boundary of a load
splice, so two loaded module files do not trip it).[A-Za-z0-9_-/], not starting or ending with /.elab_apply_import), then a forward-declaration pass
over the body, then the body. Body errors are all reported, not just the
first.(export x) must name a binding or macro defined in
this module; (mut g) additionally requires a ^mut data global.
export-from must name a module this module imports and a name that module
exports.Imported bindings live in the one global scope. Visibility is a check on
the binding, not a separate scope. elab_lookup_sym (elab_module.c):
tur/-prefixed module is always visible (the implicit
tur/ import);symbol 'x' is private to module 'm', with a note at the definition;--Werror=deprecated)./ that missed is unbound.<current-module>/x looks up x.alias/x for each :as alias import, then
full/module/name/x for each import. Only exports resolve; a private
match gets a note saying so.:as aliases, then the symbol
is split at each / from the right and matched against exported global
bindings of that module. So a/b/f tries module a/b first.Because of step 1, (import util) with no :refer already makes util's
exported names visible unqualified. Verified with (import util) followed by
a bare (twice 4), which compiles and prints 8. :refer adds the binding
to the scope a second time and is what injects macros and effects; for plain
bindings it is not what grants access.
After the stdlib region, the promotion sweep in elaborate_program
rewrites every tur/-module binding's defining_module_name to NULL and sets
is_from_stdlib, so stdlib names behave as pre-module globals.
elab_load_module_impl tries, in order, and stops at the first file found:
| # | Directory | Source |
|---|---|---|
| 1 | e->module_base_dir |
The entry file's directory (drv_dir_of_path(path) in the driver) |
| 2 | e->module_stdlib_dir |
TUR_STDLIB_DIR, else the literal stdlib |
| 3 | e->module_include_dirs[0..n) |
-I flags first, then auto-discovered spice dirs |
Under each directory it tries <name>.tur, then <name>.scm
(module_read_named). elab_module_resolve_path (used by :for-macros and
Scheme library lookup) walks the same order and must stay in lockstep.
Row 1 is the entry file's directory, not the importing file's. It is set
once per compile and not updated for nested imports. Verified: with
app.tur importing a/x, and a/x.tur importing y, the compiler looks for
./y.tur next to app.tur, not a/y.tur. That is the design -- a module
name is a path under the project root -- and the "not found" diagnostic
labels the row (project root). It used to say (importing file's directory)
(nested-import-resolves-from-entry-dir).
A "not found" lists every path tried and adds a hint: with no -I dirs, that
this looks like an intra-spice import; for a Scheme (import (two a)), that a
library is found by its name.
auto_append_spice_includes (driver/spice.c) appends to the include list, after the
user's -I flags, whatever spice_search_append (spice_search.c) finds.
It is skipped under --no-auto-spice.
spice_find_root walks up from the input file's directory, at most 16
steps (TUR_SPICE_WALK_MAX), to the first directory holding build.tur
or build.tur.sweet.<root>/src, if it exists.:spices dep of its manifest:
:global -- the tur install registry;:path -- <root>/<path>;:ref -- <root>/spices/<name>-<ref>, else <root>/spices/<name>;:subdir, then src/ if present (else the dep dir itself). Only
existing directories are added.:members lists this spice (matched by
realpath), every other member's src/ is added. An import satisfied
there warns once, unless the consumer declares that sibling in :spices.The per-file commands that call it: check, expand, audit-spans,
emit-c (both forms), emit-h, run <file>, compile, the interpreter
entry points -- and build <file>. Verified: in a spice with
src/demo/main.tur importing demo/lib, tur build src/demo/main.tur
links and prints 42, and fails with module 'demo/lib' not found only under
--no-auto-spice.
A project build (tur build <dir> with a manifest) resolves its include path
from the manifest in cmd_build_project instead.
autoload_files_ (stdlib_autoload.c) is the list, in load order. Order is
load-bearing: macros first, typeclasses before the containers that instance
them.
| Group | Files |
|---|---|
| Core | macros.tur, safe.tur, contract.tur, hamt.tur |
| Typeclasses | typeclass-eq, -ord, -functor, -clone, -drop, -hash, -applicative, -alternative, -monad, -monaderror, -bifunctor (each .tur) |
| Data | char.tur, map.tur, vec.tur, slice.tur, option.tur, result.tur, pair.tur, tuple.tur, list.tur, grid.tur, zipper.tur, set.tur, mutmap.tur |
| Other | json.tur, schema.tur, sym.tur, unique.tur, trail.tur, region.tur |
tur_stdlib_autoload_files returns that list, plus the entry dialect's
prelude (g_lang_prelude: saffron/prelude.tur or r7rs/prelude.tur)
appended last. Always read the list through the accessor.
Two loaders consume it:
| Loader | Used by | Notes |
|---|---|---|
tur_stdlib_prepend_forms via prepend_stdlib_forms |
compile_to_c (whole-program) |
Honours --no-auto-stdlib (check/expand/audit-spans only): when the entry file is itself an autoload file, that file and every later one are skipped. Records where the file-id band ends (diag_note_autoload_file_ids). |
load_project_prelude |
compile_to_h, compile_to_implementation |
Same list, same order. No --no-auto-stdlib. Does not record the file-id band. |
Both read each file from the stdlib root, skip a missing file silently, and
return the number of prepended forms. elaborate_program uses that count as
stdlib_prefix: forms below it elaborate with in_stdlib_load = true, which
sets Binding.is_from_stdlib. That flag is what separate compilation keys
its per-TU static copies on.
To add a file: add it to autoload_files_ at the right point in the
order. Both loaders and the WASM analyzer (src/web/wasm_lsp.c) pick it up.
The file should be a (defmodule tur/<name> ...) so the promotion sweep makes
its exports global. Check that nothing in it collides with a name a user
(load "stdlib/<it>.tur") already relied on; the visited-set seed makes such
a load a no-op.
| Command | Shape | Driver |
|---|---|---|
tur build <file>, tur run <file>, tur emit-c <file> |
Whole-program, one TU | compile_to_c -> emit_program |
tur build <dir> with build.tur, exactly one main, no #[used] outside the entry |
Whole-program, one TU | cmd_build_project -> cmd_build |
tur build <dir> with build.tur: --shared, zero or several mains, or a non-entry #[used] file |
Separate | cmd_build_project -> cmd_build_multi_files |
tur build <dir> without build.tur |
Separate | cmd_build_multi -> cmd_build_multi_files |
tur emit-c --output-dir <dir> a.tur b.tur ... |
Separate, emit only | cmd_emit_c_to_dir |
tur emit-h <file> |
One header, first pass only | compile_to_h |
The single-main route exists because whole-program emission carries the full
runtime and sees every function at once, which separate compilation cannot.
Anything that only works whole-program -- see Known gaps --
therefore works in most project builds and breaks under --shared.
#[used] disqualifies the shortcut because a module reached only through a
raw C symbol is not in the entry's import closure (file_has_used_attr). On
the whole-program file path, cmd_build instead force-loads #[used] modules
found on the -I dirs.
cmd_build_multi_files (and its twin cmd_emit_c_to_dir):
derive_module_name), and its files by the mangled form:
src/demo/lib.tur -> demo/lib -> obj/demo__lib.{h,c}. The importer's
#include comes from the import's module name, so a file whose path
does not match its defmodule name gets an include that no file answers.compile_to_h then
compile_to_implementation, each a full read + prelude + core passes
with separate_compilation = true. Collect each module's borrow specs.owning_module and add a ForcedAbiSpec to the owner's list (deduped by
clone name)..tur-abi-cache/index (unless disabled).obj/_main.c (executables only; it only #includes the module
headers -- main must come from a module), obj/tur_runtime.h, and
obj/tur_runtime.c (#define TUR_RT_OWNER 1, the one owner of the
runtime's globals).cc call over all of it. TUR_SHOW_CC=1 prints it.So a module is elaborated two to four times as a build target, plus once per
TU that imports it. The intermediates stay in <build-dir>/obj/ for
inspection.
tur emit-c --output-dir writes the module pairs and the cache but no
tur_runtime.h, so its output does not compile as is.
emit_header_inner)In order, with separate_compilation true:
TUR_<sanitized module>_H, then #include "tur_runtime.h".#includes (so every importer sees their typedefs).tur_poly_fn_t carrier and struct __tur_sym;.#include "<mangled import>.h" for each import of this file's
defmodule.TUR_TD_/TUR_TY_ guards, so
two headers carrying the same type do not collide
(result-typedef-duplicated-across-modules).defining_module_name != NULL), including forced ones.extern prototypes of every extern-c, and extern declarations
of exported defs. Stdlib functions are skipped. Each prototype mirrors
the .c definition's spelling rules (fn params as int64_t, ^fat as
int64_t, pass-by-pointer aggregates as const T *, inline-C results as
int64_t).emit_implementation_inner)#include "<module>.h", the cons helper, and the per-TU closure/fat
runtime (emit_closure_fat_runtime, all static or macros).emit_abi_scan_program) finds every specialization this TU
calls. A spec whose generic FnDef is in this TU's items is owned
(fn_expr != NULL); one whose generic lives in an imported module is a
borrow spec (fn_expr == NULL): its call sites are rewritten to the
clone name and no body is emitted.static. Forced specs are added as owned and external.BorrowSpecInfo for the driver.static
copies of the constructors of every imported ADT this module builds
(emit_imported_adt_ctors).static unless exported), function bodies, spec clone bodies, the
__ffi export shims.main is
synthesized under separate compilation.| Item | Owning TU | Every other TU |
|---|---|---|
Exported defn |
external definition | prototype via the header |
Private defn |
static |
nothing |
#[used] defn (retain_c_linkage) |
external | nothing (reached by raw C name) |
| User module's instance method | external (emit_inst_method_wants_external) |
prototype via the header |
Stdlib defn, stdlib or stdlib-loaded instance |
static copy |
its own static copy |
| Spec clone of a user generic | external, prototype in the owner's header | call sites renamed (borrow) |
| Spec clone of a stdlib generic | static copy |
its own static copy |
Exported def |
external definition | extern in the header |
| Constructors of an ADT | static |
static copy if it constructs one |
__cps twin of a colored defn |
static |
nothing |
| CPS entry wrapper (plain name) | linkage of the defn | as the defn |
The function decision is needs_static in emit_fn_def (emit_fns.c):
external iff separate_compilation and (is_exported or retain_c_linkage
or a user instance method) and not is_from_stdlib. The CPS entry wrapper
(emit_cps_ir.c) repeats the same test; the two must stay in lockstep or cc
reports static declaration follows non-static declaration.
emit_abi_clone_name (emit_module.c) builds a clone's name from:
ma__ for module ma, geom__vector__ for
geom/vector; binding_module_prefix, shared with
raw_name_for_binding) -- nothing for a top-level defn;__spec__;_ plus the C type name of each argument, in order.Every non-alphanumeric character of the name and the C types becomes _
(the prefix is already a C identifier). If the name is already taken by
another spec in the same TU, __h1, __h2, ... is appended in intern order.
That suffix only separates names within one TU, which is why the prefix is
needed: two modules exporting same-named generics instantiated at the same
types used to emit one symbol twice
(spec-clone-names-not-module-qualified).
Real names from tur emit-c:
| Clone | Reading |
|---|---|
pick__spec__const_char___int64_t_const_char__ |
pick -> cstr, args int, cstr |
some__spec__tur_adt_Option__int_int64_t |
some -> (Option int), arg int |
some___spec__bool_tur_adt_Option__int |
some? (? -> _) -> bool, arg (Option int) |
Ownership: the module that defines the generic owns every clone of it. A
borrower's clone reaches the owner as a ForcedAbiSpec
{clone_name, fn_symbol, result_kind, arg_kinds[], n_args, result_adt, arg_adts[]}, and the owner
emits the body and the header prototype in pass 2. In the tiny project below,
app calls util's ident at cstr, and the result is:
$ tur build . # util.tur + app.tur, no build.tur
$ grep -n ident__spec build/obj/*.h build/obj/*.c (trimmed)
build/obj/util.h:551:const char * util__ident__spec__const_char___const_char__(const char *);
build/obj/util.c:5435:const char * util__ident__spec__const_char___const_char__(const char * x) {
build/obj/app.c:5414: ... (util__ident__spec__const_char___const_char__("hi"));
A forced spec carries TypeKinds, plus the ADT name at each TY_ADT
position (forced_spec_adt_name). The owner rebuilds each position with
forced_spec_pos_type: a named ADT the owner itself defines becomes its full
type, so the clone takes the by-value struct the borrower passes; everything
else goes through emit_type_from_kind, which is exact for scalars and cstr.
An ADT the owner does not define -- the borrower's own, or a third module's --
still falls back to the carrier (see Known gaps).
An instance method is never is_exported (a definstance has no export
list), so emit_inst_method_wants_external decides: external when the
function is an instance method (owner_instance set), its binding is not
is_from_stdlib, and the instance's origin_file_id does not resolve to a
path containing stdlib/. The header loop declares
such methods even though they are not exported. History:
definstance-not-dispatchable-across-modules.
A non-stdlib file (load ...)-ed into several modules is spliced into each,
so each TU defines its instance methods. Those copies are defined
__attribute__((weak)) (emit_inst_method_linkage_attr, at the definition,
the forward declaration and the CPS entry wrapper): a separately compiled
module's own source is file 0 of its unit, so an instance whose
origin_file_id is anything else is a loaded copy. The linker keeps one, and
an importer still links against the method through the loading module's
header, which static would break
(loaded-user-definstance-duplicated-across-tus).
An instance in the module's own file keeps a strong definition.
in_imported_module keeps an imported module's self-registering forms
(classes, instances) from registering for emission in the importer's TU.
Coloring and classification (ensure_S, see the
CPS internals guide) run over the program being
emitted, which under separate compilation is one module. The __cps twin is
always static. A call into another module's colored function is therefore an
ordinary direct call to its entry wrapper, which seeds a fresh root
continuation with __dk_enter0. Handlers installed by the caller are not on
that continuation.
emit_split_lib_owns is not involved: it belongs to the r7rs split below and
returns false whenever that split is off.
emit_split.h describes EMIT_SPLIT_LIB / IMP / CLIENT: a single
#lang r7rs program emitted as two or three units (the prelude library, the
imported libraries, the program) so the library unit's object can be cached.
It runs inside compile_to_c when prelude_split_applies() holds: an
r7rs entry on a 64-bit host, Linux or macOS unless TUR_PRELUDE_SPLIT=1
forces it elsewhere, and TUR_PRELUDE_SPLIT=0 turns it off. Ownership there
is by emit_split_binding_home: is_from_stdlib -> LIB, is_from_implib ->
IMP, else CLIENT. It has nothing to do with module TUs.
.tur-abi-cache/)cmd_build_multi_files and cmd_emit_c_to_dir write
<build-dir>/.tur-abi-cache/index after pass 2 (via index.tmp and a
rename). One line per borrow spec, tab-separated:
clone_name owning_module fn_symbol result_kind n_args arg_kind...
The kinds are TypeKind integers. For the example above:
util__ident__spec__const_char___const_char__ util ident 5 1 5
Nothing reads it. The forced specs of a build are always recomputed by
pass 1 of that build; no source file opens the index (searched: only the two
write sites and the pkg.c directory skip mention it). --no-abi-cache or
TUR_NO_ABI_CACHE=1 (any value but 0) suppresses the write and changes
nothing else. Treat the index as a debugging record of which module was asked
to own which clone. Build-directory placement is in the
build-driver internals guide.
First, confirm the build really split: a project with one main and no
--shared is whole-program, so a split-only bug will not reproduce there.
Reproduce with a bare directory (tur build <dir> without build.tur) or
--shared.
Then look at the evidence:
<build-dir>/obj/<mod>.{h,c} -- the emitted pair. Grep for the symbol in
every .h and .c: is it defined twice, defined static, or declared
but never defined?<build-dir>/.tur-abi-cache/index -- which owner each borrowed clone was
forced onto.TUR_SHOW_CC=1 -- the exact cc line.tur build <entry-file> -- the whole-program build of the same code. If
it works, the bug is in the split.| Symptom | Usual cause |
|---|---|
multiple definition of '<fn>__spec__...' in two module .c files |
Before 2026-10-21: two modules define generics with the same bare name, instantiated at the same types (clone names were not module-qualified). Now the clone carries its module's prefix, so look for two specs of one module's generic minted under one name |
multiple definition of '__inst_<Class>_<m>_<T>' |
Before 2026-10-21: a definstance in a non-stdlib file that several modules (load ...). Such copies are weak now; a strong duplicate means the instance is in two modules' own files |
incompatible type for argument N of '<fn>__spec__tur_adt_...' |
A forced spec at a struct the owner does not define: the owner's clone takes int64_t. Before 2026-10-21 this happened for the owner's own structs too (forced-spec-loses-aggregate-type) |
undefined reference to '<mod>__<fn>' |
The module is not in the build set, or inline C spells a mangled name by hand (cross-module-private-helper-dropped-at-link) |
undefined reference to 'main' |
No module of a separate executable build defines main; _main.c does not synthesize one |
static declaration follows non-static declaration, conflicting types |
The header prototype and the .c definition disagree (linkage or parameter spelling) -- e.g. sepcomp-cps-entry-wrapper-byptr-agg-param-mismatch |
unknown type name 'tur_adt_X', field has incomplete type |
A typedef the header did not carry or ordered wrongly -- e.g. sepcomp-recursive-byval-fixpoint-adt-incomplete-header-type |
redefinition of 'Result__...' |
A monomorph typedef without its include guard (result-typedef-duplicated-across-modules) |
error: `f` (module m) can perform the effect `E`, and this call crosses into it from another module |
A call into another module whose callee can perform a user effect. Refused at build time since 2026-10-18; before, it built and aborted at run time with tur: unhandled effect (tag N) |
Two modules each export (defn ident [A] [x : A] : A x). user1 calls
mb's at cstr; app calls ma's at cstr and imports user1. Each
importing TU sees only one ident, so no __h1 suffix was added, and both
owners were forced to emit the same name:
$ tur build . (trimmed)
ma.c:(.text+0x10): multiple definition of
`ident__spec__const_char___const_char__'; mb.c:(.text+0x10): first defined here
collect2: error: ld returned 1 exit status
$ cat build/.tur-abi-cache/index
ident__spec__const_char___const_char__ mb ident 5 1 5
ident__spec__const_char___const_char__ ma ident 5 1 5
The index showed the cause directly: one clone name, two owners. The names
are ma__ident__spec__... and mb__ident__spec__... now, and the build
links (build-project-spec-same-name-two-modules in tests/run-build-project.sh).
Measured on 2026-10-16 with tiny bare-directory projects. Whole-program builds of the same sources work in every case. None of these was filed when this guide was written; link the reports here once they are.
| Gap | Where |
|---|---|
| A forced spec at a by-value struct only the CALLER can see (its own) cannot be emitted by the generic's module, and is refused there with a diagnostic naming the struct; one at a third module's struct both import builds (forced-spec-at-foreign-aggregate, resolved 2026-10-22) | forced_spec_pos_type resolves the type from the owner's items, then the defs its elaboration saw (ctor_census_find_adt) |
An effect performed in one module's TU cannot reach a handle in another's. It is refused at the call site, not threaded |
per-TU coloring; static __cps twins; the entry wrapper's fresh root continuation; the refusal is effect_check.c (foreign_check) |
| Nested imports resolve against the entry file's directory, and the diagnostic calls it the importing file's | elab_module.c:elab_load_module_impl (module_base_dir) |
| What | Where |
|---|---|
| Multi-module whole-program fixtures | tests/fixtures/module-*, load-in-imported-module, cross-module-* (run by tests/run.sh) |
Project builds, manifest descent, --shared, the runtime owner TU |
tests/run-build-project.sh |
| Two headers carrying one monomorph | tests/run-result-typedef-multi-module.sh (ctest tur_result_typedef_multi_module) |
| Forced specs at the owner's by-value structs | tests/run-forced-spec-aggregate.sh (ctest tur_forced_spec_aggregate) |
Most module-* fixtures run whole-program, so they do not exercise the split.
A change to header or implementation emission wants a case in
run-build-project.sh (or a bare-directory build) as well.
Each was reproduced against the compiler this guide describes and filed in
docs/reported/. When one is fixed and archived,
update the passage above that describes it and drop it from this list.
Update this guide in the same PR when you:
load path
resolution;tur build shapes take the whole-program route;needs_static, emit_inst_method_wants_external,
spec external_linkage) or the header contents;emit_abi_clone_name, ForcedAbiSpec or BorrowSpecInfo;.tur-abi-cache/, or delete it;Cite functions, not line numbers. Regenerate the excerpts with tur build
on a scratch directory when the shapes they show change.