Module Low_level.Canonical_render

gh-563: the one canonical rendering of lowered code, shared by both digest consumers — analysis_cache_stats' cache (keyed inside optimize) and Schedule_cache.canonicalize (schedule replay across sessions).

The walk is the same for both: index / scalar / statement emission, loop-binder tokens, local-scope alpha renaming, comment skipping, opaque-statement handling. What deliberately differs is the identity policy — the analysis cache keys tensor nodes and static symbols by identity (a hit reuses the stored code verbatim), the schedule cache alpha-renames everything (a hit replays a schedule onto a different-but-isomorphic lowering).

Both digests are correctness-critical, so keep the split honest: a new t / scalar_t construct is rendered in the walk and only there (the matches are exhaustive, so omitting it breaks the build); a new digest-relevant fact enters the walk if it belongs to the code itself, or exactly one policy field / one consumer preamble if it is an identity choice or a consumer-specific companion.

type mma_policy =
  1. | Opaque_mma
    (*

    Mark the rendering incomplete and emit a placeholder — the consumer's guarantees do not extend to the construct.

    *)
  2. | Structural_mma
    (*

    Render operands, extents, lane and fallback body.

    *)

How Tile_mma enters the rendering.

type policy = {
  1. emit_tn : Tnode.t -> Base.unit;
    (*

    Render a tensor node reference.

    *)
  2. emit_free_sym : Indexing.symbol -> Base.unit;
    (*

    Render a symbol that neither an enclosing loop binder nor initial_tokens bound.

    *)
  3. on_bind_loop : Indexing.symbol -> id:Base.int -> shadowed:Base.bool -> Base.unit;
    (*

    Called when a For_loop binder mints the token "b<id>". shadowed iff the symbol already had a token: a duplicated binder makes symbol references ambiguous.

    *)
  4. mark_incomplete : Base.unit -> Base.unit;
    (*

    Called when an opaque construct makes the rendering an unfaithful summary of the code.

    *)
  5. mma : mma_policy;
  6. initial_tokens : (Indexing.symbol * Base.string) Base.list;
    (*

    Symbols pre-bound to a rendering token before the walk — the static indices, for the consumer that renders them positionally.

    *)
}
val emit : buf:Base.Buffer.t -> policy -> t -> Base.unit

Appends the canonical rendering of the code to the buffer. Deterministic: the caller digests the buffer, usually after its own preamble and companion sections.