@@ -44,13 +44,13 @@ to custom impls that do not exist. (An earlier note swapped only the runtime
4444core.js and reported +538; that captured the collection wave alone, undercounting
4545the total by ~ 3x.)
4646
47- The fix is to invert the dispatch, like CLJS: base fns handle only native types
48- (Object/Array/primitive/Map/Set) and name no protocol slot; ` extend-type ` and
49- ` deftype ` / ` defrecord ` install the dispatch. An app with no protocol impls pulls
50- zero protocol machinery. The DCE-clean mechanism is for ` extend-type ` to wrap the
51- affected core fns, so the base fn holds no protocol reference to retain. The
52- binding constraint is that ESM imports are read-only, so the fns need a mutable
53- holder or a registry indirection .
47+ The fix inverts the dispatch, like CLJS: base fns handle native types
48+ (Object/Array/primitive/Map/Set) inline and name no protocol slot; built-in
49+ branded types (records, sorted collections, atoms) dispatch by brand; a
50+ constructor registry, populated by ` extend-type ` / ` deftype ` / ` defrecord ` , is
51+ consulted only for the remaining non-native types. An app with no protocol
52+ impls pulls zero protocol machinery. A proof of concept (doc/ai/adr/0007-poc/)
53+ validated this, ruled out one alternative on perf, and surfaced one blocker .
5454
5555### Variadic call-site cost
5656
@@ -67,15 +67,70 @@ analysis both succeed, and in throttled Chrome they did not (a fast-arity runtim
6767#887 reverted it (double-evaluation); per-arity functions avoid that. This is the
6868doc/ai/ideas.md "Emit direct fixed-arity calls" idea.
6969
70+ ## Proof of concept
71+
72+ Three standalone experiments in doc/ai/adr/0007-poc/ (run with ` node ` ), modelling
73+ ` get ` :
74+
75+ - perf.cjs - the plain-data hot path is flat (~ 1x baseline) for the protocol-free
76+ base. But the naive "wrap the fn" mechanism regresses 2.4x the moment any
77+ ` extend-type ` exists, because every call then threads the wrapper (and it
78+ compounds per extension). A registry the base consults only for non-native
79+ types stays flat in both cases.
80+ - dce.cjs - with a base that names no slot, a plain-data app bundle drops every
81+ protocol symbol (12 -> 0 in the model) and shrinks. Conditional: built-in
82+ types must not be eager-registered (` registry.set(T, ...) ` at top level is a
83+ side effect that pins T into every bundle), so they stay brand-dispatched.
84+ - extend.cjs - a user can extend their own type to a built-in protocol (works via
85+ the registry). Overriding a native type is ignored (the inline path wins) -
86+ which is already true in today's squint, so no regression.
87+
88+ Decision matrix (++ good, -- deal-breaker, ~ caveat; squint down each column):
89+
90+ | criterion | baseline (slots) | protocol-free | wrapping | registry (hybrid) |
91+ | ------------------------------| :----------------:| :-------------:| :--------:| :-----------------:|
92+ | hot path, plain data | ++ | ++ | ++ | ++ |
93+ | hot path, extends present | ++ | ++ | ** --** | ++ |
94+ | DCE: slot symbols shake out | ** --** | ++ | ++ | ++ |
95+ | runtime-extensible | ++ | ** --** | ++ | ++ |
96+ | backward-compatible ABI | ++ | ** --** | ** --** | ** --** |
97+ | built-in types DCE cleanly | ++ | ~ | ~ | ~ |
98+
99+ Reading the columns: baseline fails only DCE (the whole reason for this ADR);
100+ protocol-free and wrapping each carry two deal-breakers; the registry hybrid
101+ carries exactly one - the ABI break - and that break is shared by * every* option
102+ that recovers DCE. So the matrix collapses to a single trade: ** DCE win vs ABI
103+ stability** . Every row-pair has one and only one ` ++ ` ; you cannot have both.
104+ Among the mechanisms that do fix DCE, registry is strictly best.
105+
106+ ## The blocker: it is a breaking ABI change
107+
108+ The mechanism moves protocol dispatch from prototype slots to a registry, so it
109+ changes the contract between compiled code and core.js. ` extend-type ` /` deftype ` /
110+ ` defrecord ` would emit registry calls instead of ` prototype[SLOT] = fn ` , and core
111+ fns would consult the registry instead of ` coll[SLOT] ` . Consequences:
112+
113+ - Code compiled by an older squint (slot-style) run against a newer registry
114+ core.js has its protocol impls silently ignored, and vice versa. A pre-compiled
115+ published library that uses protocols breaks against a mismatched core.js.
116+ - It cannot be made backward-compatible. Keeping a slot check in the base fns for
117+ old code re-references the slot symbol, which pins it, which forfeits the entire
118+ DCE win. DCE and slot-compat are mutually exclusive.
119+
120+ So this is a breaking change requiring recompilation across the ecosystem, and it
121+ should be batched into a major version, not shipped in a point release. reagami
122+ itself is unaffected (it defines no protocols), but the change is squint-wide.
123+
70124## Decision
71125
72126Defer both. Ship neither fix now.
73127
74128- The protocol bundle cost is ~ 1.5 KB gzip (~ 17% of the shipping app), in two
75- waves. At that size the win clearly justifies the work; what defers it is
76- risk, not payoff. Recovering it means reworking the protocol system that
77- landed weeks ago, with regression risk to records, transients and
78- ` extend-type ` . Do it once that code has settled, not against fresh code.
129+ waves. The size clearly justifies the work and the POC found a viable,
130+ perf-neutral mechanism (the registry hybrid; wrapping rejected on perf). What
131+ defers it is not payoff: it is a breaking ABI change (above) that belongs in a
132+ major version, plus the regression risk of reworking protocol code that landed
133+ weeks ago. Do it in a deliberate breaking window, once that code has settled.
79134- The variadic runtime patch was tried in #961 and dropped: a 2-arg ` min ` /` max `
80135 arity that node cannot show a win for and that Chrome does not benefit from. The
81136 real fix is the compiler-level per-arity emission, which is larger.
@@ -91,10 +146,11 @@ Defer both. Ship neither fix now.
91146
92147## When to revisit
93148
94- - Protocol bundle cost: the ~ 17% is already a size that matters, so the gate is
95- not the payoff but the protocol code settling. Revisit once records,
96- transients and ` extend-type ` have proven stable in the wild, or sooner if a
97- size-sensitive consumer is blocked. ` test:size ` keeps the number honest
149+ - Protocol bundle cost: the ~ 17% is already a size that matters, and the POC
150+ de-risked the mechanism, so the gate is the ABI break. Revisit at the next
151+ major/breaking-change window, once records, transients and ` extend-type ` have
152+ proven stable in the wild - or sooner if a size-sensitive consumer is blocked
153+ and a coordinated recompile is acceptable. ` test:size ` keeps the number honest
98154 meanwhile.
99155- Variadic call cost: alongside the fixed-arity codegen work in doc/ai/ideas.md,
100156 which supersedes the reverted runtime patch.
0 commit comments