38. Definitions and hashing
38.1 What a definition is
Definitions are top-level functions (overloads included), consts, classes, types and interfaces.
- Destructuring.
const { a, b: [c] } = init()binds one definition per name. The bindings share the statement, so it still runs once. - A type and a value can share a name, like TypeScript's own type and value spaces (
const User = z.object(…)withtype User = z.infer<typeof User>). Each half is its own definition, updated, diffed and merged independently. The#codebaseshim exports both under the one name. - Declarations that exist only in types merge with a type of their name, as TypeScript merges them: an
interface Effect<A>with adeclare namespace Effect { type Success<T> = … }of types beside it, adeclare const s: unique symbolwithtype s = typeof s, a class with a namespace of types. They're one definition, written together (Effect<number>andEffect.Success<…>are both uses of it). - A statement setting up a constant goes with it. A top-level statement that writes into one constant declared
above it, and nothing else, finishes setting it up: filling a table (
for (…) codes[c] = i), a constructor'sprototype(Impl.prototype = Proto),Object.freeze(types),Object.defineProperty(Proto, …). It's part of that constant's definition, run and hashed with it. Writing means assigning into it (x[i] =,x.p =,x.n++,delete x.p) or handing it to one ofObject's orReflect's functions that change what they're given; what a function in the statement does later doesn't count, nor does a method call (server.listen()). Any other statement is refused. - An augmentation is a definition of its own. A file's
declare global { … }(several are one) is the definition<file's namespace>.global, and itsdeclare module "./schema" { namespace Annotations … }(adding to another module's definition, one per block) is<file's namespace>.augments, which names the definition it adds to by hash, so it follows renames; in a scratch file it's writtendeclare module "#codebase/lib/schema". It has no name to import: every check in the codebase includes it (scratch files, re-checks, imports), as TypeScript includes it in its whole program. Code relying on what it adds (e.hinton anError) doesn't name it, so its hash doesn't include it, as with@typespackages. - Classes hash as one unit. Methods aren't split out:
this,#private,superand initialization order make per-method hashing meaningless. - Rejected in scratch files, with a reason: top-level
var/using, enums, namespaces, decorators,declare, and top-level statements other than watch expressions and statements setting up a constant (an expression writing into one is that, not a watch expression).tcm importaccepts most of these, because existing code uses them. - Dynamic
import("pkg")of a package or Node builtin is pinned like a static import, so it's still content-addressed. Lazy imports of local files or codebase modules are rejected. A computed specifier (import(url)) is allowed with a warning, because it loads code the codebase doesn't track.
38.2 How a definition is hashed
tcmts.core.canonicalize lowers every definition to an S-expression IR built from syntax-kind
names, not numbers, with every child labelled by its slot (name, body, …). Unknown syntax is an error,
never skipped, so two different programs can't collide.
- Comments, formatting and
exportare dropped. - Literals are normalized:
1_000=0x3e8, and'a'="a". Shorthand properties hash like their explicit form. - Locals become de Bruijn levels, so renaming a local doesn't change the hash.
- Every identifier is resolved through the type checker. A reference to another definition becomes
Ref <hash>, an npm import becomesExt npm:<pkg>@<compatible range>(npm:lodash@4; packages), and a global becomesG name. - Property names stay structural, and so do JSX tags and attributes (
<div className>); a component tag (<Counter />) is a reference like any other. - JSX text is normalized as React reads it: indentation and line breaks between tags don't count, spaces within a
line do. A definition with JSX also hashes the JSX runtime it compiles to (
npm:react@<version>). - Type annotations are part of the hash.
- Mutually recursive definitions are hashed as a cycle (Tarjan SCC + Unison-style ordering), including cycles that span files.
38.3 Identity
- Lineage salts. In JavaScript every declaration has its own identity:
new Map()twice is two maps, and two identical classes fail each other'sinstanceof. So every function, class and value is hashed with a lineage salt, the codebase path where it was first added. Updates and renames reuse the existing salt, so renames stay free and re-adding the same file reproduces the same hashes. Identical code added at two paths is two definitions. - Types are unsalted, since they have no runtime identity, so identical types deduplicate. The exception is
an ambient
declare const x: unique symbol, which is salted: each one is a distinct symbol. - Names are hashed exactly where JavaScript can observe them (
.name): function and class declarations,const f = () => …, and local functions or bindings that name an anonymous function (function helper() {},(cb = () => {}) =>). Generated code keeps these names, sotcm mvchanges the codebase path but not.name. All other locals and parameters stay alpha-renamable.
38.4 The hash format
- The format is versioned (
tcm --versionsays which: v2).tcmts.core.test.goldenpins the hashes of a corpus that covers most syntax, so a change to them is deliberate. A codebase written with another format is refused rather than mixed with this one's hashes, and says to migrate it. tcm migratemoves a codebase to this tcm's format, history and all. Each format comes with a step that turns the last one's IR into its own (tcmts.core.migrate.IR_STEPS; v1 to v2 drops parentheses around an optional chain that nothing goes on from, which a formatter adds). Every definition, dependencies first, is taken through the steps, its references moved to their new hashes, and hashed again; then its history, from the bottom up (namespaces and history entries, with their times and commands); and everything keyed by a hash follows: names, branches, releases, patches, test results. It's a pure function of the codebase, so a clone and its server, each migrating, get the same hashes and go on syncing (tcm serve's server migrates when it starts). It's safe: the database as it was is kept beside it (.tcm/codebase.v1.sqlite), and the migrated one is used only if this tcm reads every live definition back to its hash (tcm verify); otherwise the codebase is left as it was.- A codebase whose tables an older tcm laid out differently is refused too, and says to migrate it:
tcm migratelays them out as this tcm does first, taking each layout's step in turn (tcmts.store.LAYOUT_STEPS; layout 8f648138 to 85655ca2 moves patches kept whole for each branch into sets branches share), and keeps the database as it was beside it (.tcm/codebase.layout-<layout>.sqlite). Hashes don't move. A codebase copied from a package an older tcm packed (the web UI's, the registry's, its libraries) is migrated as it's copied. A layout with no step from it (a newer tcm's, or one from before there were steps) is made again (imported from its files, or pulled). - Hashes don't depend on the compiler or its version. The IR visits each node's children in an order that's
tcm's own (
tcmts.core.slots, generated from TypeScript 7.1's), so a TypeScript release that adds or reorders children changes no hash until that table does.pnpm compare-compilershashes this repo and the corpus with TypeScript 7 and with tsrs: the hashes are identical. tcm verifyre-renders every live definition from the store and re-hashes it. Rendering is supposed to be the inverse of hashing, so any mismatch means stored source, splicing or canonicalization lost information.TCM_KEEP_VERIFY=1leaves the rendered files in.tcm/cachefor debugging a failing re-check. A component it verified already isn't checked again while the names it renders with stay the same and the tcm verifying is the same build: what it renders to and how that hashes follow from those, and the verifying code's own hash covers everything it uses (a change to hashing or rendering re-checks everything).tcm verify --freshchecks every definition again.