Skip to content

Analyse the .tmc binding mechanism: what is missing? #95

Description

@mellonis

.tmc reuses a world three ways — call, graft, bind — and the mechanism works, but it has accumulated a set of constraints that were each reasonable locally and may not be reasonable together. This issue is the analysis, not a fix: establish which constraints are inherent to the design and which are implementation limits we have been living with, then decide what (if anything) to change.

The starting evidence is that the standard library ships as source, and its own header says why:

Against the compiled, LINKED object — the normal way to consume the library — the only path is a transparent call with an empty argument list… Binding a tape into a library routine (call …(num = num)) needs that routine's tape signature, and grafting one of the graphs needs the graph's source; neither crosses a link boundary, so both are rejected there (external-binding-unsupported, undefined-graph). Both work only when the library's source is compiled into the consumer's own unit — which is why both forms are exported: the library ships as source.

That is a design compromise the language reference states as a rule but never justifies as a goal. It deserves to be examined on purpose.

Lines of inquiry

1. Cross-unit binding, and whether RoutineSig is one field short.

The reference says a call into another compilation unit "must not bind tapes at all… Projecting them would need the callee's signature, which this unit does not have". But MO v3 objects already carry RoutineSig { arity: u8, cardinalities: Vec<u32> }, and .tma has a .routine signature directive. So the signature is not absent — it is incomplete: arity and per-tape cardinality are there, parameter names and alphabet glyphs are not, and binding is by parameter name.

Determine: what would a signature have to carry for call lib::op(num = myTape) to typecheck across a link boundary? Is it a new MO record, an extension of RoutineSig, or is the whole idea wrong because the map's glyph-level checking cannot survive to link time anyway? If it is feasible, what does it cost in container-format churn, and does it let the stdlib stop shipping as source?

2. graft cannot cross a link boundary at all, and needs source by construction.

Grafting splices states at compile time, so it needs the graph's body, not a signature. That looks inherent rather than fixable — but confirm it, and say so explicitly in the outcome, because "we checked and it cannot work" is a useful answer that stops the question recurring.

3. A graph cannot contain a call.

The reference records this as "0.1 rejects a graft whose graph body contains a call, reporting it at the graft site", and the stdlib pays for it directly: minusOne and invertNumber are plain routines with no graph form "because a graph cannot contain a call", so consumers lose the multi-exit form for exactly the operations that are compositions. Is this a fundamental consequence of splicing a return-stack-using body into a host world, or a 0.1 implementation limit with a known shape? If it is liftable, what breaks — the frame register, the compose directory, --call-mech=mono stamping?

4. Index identity between separately declared alphabets is a convention, not a check.

Because an exported alphabet contributes no linkable symbol, a consumer must "declare a local alphabet listing the SAME glyphs in the SAME order" — a transparent call binds by index. Nothing verifies that. Reorder the local alphabet and the program keeps compiling and starts doing something else. The only thing pointing at it is index-identity-map, deliberately warn-tier because omitted-map index identity is intended semantics.

This is the one item on the list that is a correctness hazard rather than an expressiveness limit, and it should be weighted accordingly. Determine whether anything could make the mismatch detectable — a cardinality check at link time, a glyph digest in the object, an opt-in strict mode — or whether the answer is that it is the user's contract to keep.

5. Smaller shapes, each a yes/no.

  • callee_arity ≤ caller_arity is a consequence of injective binding, so a callee can never declare more tapes than its caller. Is a routine that wants a scratch tape the caller does not have a real use case we are refusing? (Cross-check the flagship brainfuck UTM and the stdlib for places where a scratch tape was worked around.)
  • bind names a pre-bound call target only. Is a pre-bound graft — one argument list, several sites — a missing symmetry or a thing nobody wants?
  • A bind fixes the whole argument list; there is no partial binding. Same question.
  • The LSP is narrower here by language rule: only a transparent argless call/bind or a use path may name something outside the compilation unit, never a graft target, binding name/value, with map pair, or vector-cell glyph. Is that narrowness a consequence of items 1-2, such that fixing one widens the editor surface for free?

What a good outcome looks like

Not a patch. A written verdict per item — inherent / liftable / not worth lifting — with the reasoning, and for anything judged liftable, enough of a sketch to become its own issue. Items 1, 3 and 4 are the ones that could change the shape of the language; 2 and 5 mostly need to be settled so they stop being asked.

The analysis should also state plainly whether "the stdlib ships as source" is a consequence we accept permanently or a symptom we intend to remove, because several of the items above only matter if the answer is the latter.

Docs that bound the current behavior: docs/tmt/language.md (reuse, symbol maps), docs/tmt/isa.md (call mechanisms), docs/formats.md (routine signatures), and the header comment of crates/turing-machine/src/stdlib/std.tmc, which is the most candid description of the limits we have.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions