Turmeric identifiers can contain characters that are illegal in C (-, /, ?,
!, >, <, ...). The compiler maps every source name to a valid C identifier
before emitting code. Two requirements govern that mapping:
(defn foo-bar [] : int 1) and (defn foo_bar [] : int 2)
would both emit foo_bar, a hard C redefinition error.nm output, crash backtraces).All global, linker-visible symbols use the injective scheme defined in
src/compiler/mangle.c / src/compiler/mangle.h.
| Source byte | Output | Notes |
|---|---|---|
[A-Za-z0-9] |
itself | unchanged |
_ (literal underscore) |
_un |
was passthrough in old scheme |
- (hyphen / kebab) |
_hy |
was _ in old scheme |
/ (slash, if raw) |
_sl |
was _ in old scheme |
> < = ? ! ... |
_ + 2-letter mnemonic |
e.g. > -> _gt, ? -> _qu |
| any other byte | _xHH (hex) |
escape hatch |
__ (double underscore) |
reserved structural separator | module-path boundaries only |
_ always introduces an escapeBecause a literal _ in a source name encodes as _un, a lone _ in the
output can never be data. Every _ in a mangled identifier is the start of a
two-letter mnemonic (_hy, _gt, _un, ...) or the four-byte hex escape
_xHH. This makes the encoding self-delimiting and the demangler
unambiguous.
__ is exclusively structuralBecause data can never produce two adjacent underscores (a literal _ in the
source encodes as _un, and two adjacent _un encodings produce _un_un),
__ is free to mean "module/namespace boundary". The module prefix convention
<module>__<name> (e.g. geom/vector -> geom__vector__, binding add2 ->
geom__vector__add2) works exactly because of this invariant.
| Source name | Injective C spelling | Note |
|---|---|---|
foo-bar |
foo_hybar |
kebab hyphen |
foo_bar |
foo_unbar |
literal underscore |
eq? |
eq_qu |
question-mark sigil |
>>> |
_gt_gt_gt |
three > sigils |
list->vec |
list_hy_gtvec |
distinct from list/vec |
list/vec |
list_slvec |
raw slash (within a name) |
geom/vector (module prefix) |
geom__vector__ |
/ -> __ structural sep |
add2 in geom/vector |
geom__vector__add2 |
full qualified name |
tur_demangle(mangled, out, cap) (in src/compiler/mangle.c) inverts the
encoding: copy alnum; on __ emit /; on a lone _ read the next two bytes
as a mnemonic or x+two hex digits. The decoded form is never longer than
the input, so cap >= strlen(mangled) + 1 always suffices. Returns 0 on
malformed input.
Some contexts intentionally use the older lossy fold:
| Context | Scheme | Reason |
|---|---|---|
| Function parameters and locals | legacy fold | inline-C bodies reference them by source spelling (my-param -> my_param in C) |
| Struct fields | legacy fold | same: inline-C accesses obj->my_field |
extern-c bindings |
legacy fold | the C function already has a fixed name |
File base names (foo-bar.c) |
legacy fold | filesystem names; injectivity is irrelevant, long mnemonics hurt readability |
The legacy fold is implemented as tur_mangle_legacy_append in mangle.c:
[A-Za-z0-9_] passes through, -// become _, other sigils get their
two-letter mnemonic, remaining bytes get _xHH. Crucially, a literal _ is
not re-encoded, so foo_bar and foo-bar both produce foo_bar.
Locals cannot collide across the program (they are block-scoped), so the lossy fold is safe there. Struct fields are scoped to their struct, so no linker collision is possible.
mangle_mod_basename in src/main.c maps / -> __, - -> _ for the
on-disk .c/.h filenames emitted by tur build. This is Option A from the
plan: filenames stay legacy-lossy, only symbol names go injective.
The linker-visible binding prefix (e.g. my-mod__fn) still uses the injective
scheme via raw_name_for_binding, so my-mod/fn and my_mod/fn emit
distinct symbols even though their generated filenames might collide.
| Function | Location | Use for |
|---|---|---|
tur_mangle_append(dst, pk, name, len) |
mangle.c |
Append mangled bytes into a pre-allocated buffer |
tur_mangle_legacy_append(dst, pk, name, len) |
mangle.c |
Append legacy-folded bytes |
tur_mangle_ident(name, out, cap) |
mangle.c |
NUL-terminated convenience wrapper |
tur_demangle(mangled, out, cap) |
mangle.c |
Inverse: C identifier -> source name |
tur_mangle_bound(src_len) |
mangle.h |
Worst-case output length (4x input) |
tur_name_is_c_identifier(name, len) |
mangle.h |
True if name needs no mangling |
The exports.manifest file generated by tur build --manifest <path> stores
<mod>/<name> -> <mangled> :: ... lines, where <mangled> is the C symbol
as emitted by the injective scheme. The spice loader (src/turi/spice_loader.c)
reads these at runtime via dlsym. As long as the manifest is regenerated
whenever the .so is rebuilt (tur repl handles this automatically), no
manual update is needed when the mangling scheme changes.
src/compiler/mangle.c, src/compiler/mangle.htests/mangle_test.c /
tur_mangle_unit ctest targettests/fixtures/mangle-kebab-snake-coexist/ and
tests/fixtures/mangle-arrow-name-vs-module/