@geastack/compiler 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -240,8 +240,30 @@ export const narrowableIntegersOf = (body, storage = noStorageFacts) => {
240
240
  return true;
241
241
  if (compute.form === 'update')
242
242
  return integral.has(left.value);
243
- if (compute.form === 'unary')
244
- return (compute.operator === '-' || compute.operator === '+') && integral.has(left.value);
243
+ if (compute.form === 'unary') {
244
+ if (compute.operator !== '-' && compute.operator !== '+')
245
+ return false;
246
+ // Negating a zero this census can SEE is the one negation whose result a
247
+ // 64-bit integer cannot carry. `-0` is an integer by every arithmetic
248
+ // test -- `Number.isSafeInteger(-0)` is true and `-0 === 0` -- so nothing
249
+ // else here strikes it; but two's complement has a single zero, and the
250
+ // sign is gone at the store. ECMA-262 can read that sign back
251
+ // (`Object.is(x, -0)` is true where `Object.is(x, 0)` is false, and
252
+ // `1 / -0` is `-Infinity`), so a slot narrowed on the strength of a `-0`
253
+ // answers both the other way with no diagnostic. `emit.ts` already
254
+ // spells this operand `-0.0` for exactly that reason; the value it
255
+ // carefully produces is then rounded away by the storage this census
256
+ // hands out.
257
+ //
258
+ // Only the statically visible one is refused. A `-0` that arises at
259
+ // runtime -- `x * -1`, `-1 % 1`, `x - x` never, but `x * y` with either
260
+ // zero -- is the same class of hazard as an overflowing sum, which this
261
+ // design already tolerates: refusing every product and remainder that
262
+ // COULD be a negative zero would narrow nothing at all.
263
+ if (compute.operator === '-' && constants.get(left.value) === 0)
264
+ return false;
265
+ return integral.has(left.value);
266
+ }
245
267
  if (compute.form !== 'binary' || !right)
246
268
  return false;
247
269
  if (compute.operator === '%') {
@@ -137,7 +137,6 @@ export const ownedDyingValuesOf = (body) => {
137
137
  }
138
138
  }
139
139
  const graph = controlFlowGraphOf(body);
140
- const cyclic = cyclicBlocksOf(body);
141
140
  const dying = new Set();
142
141
  for (const [value, definition] of definitions) {
143
142
  const readers = uses.get(value);
@@ -146,17 +145,27 @@ export const ownedDyingValuesOf = (body) => {
146
145
  const use = readers[0];
147
146
  if (use === undefined)
148
147
  continue;
149
- // A value defined outside a loop can still have its sole SSA use inside
150
- // the loop. Moving at that use empties the storage on the first iteration
151
- // and every later iteration observes a null Ref. The CFG re-entry check
152
- // below only handles distinct definition/use blocks, so exclude cyclic
153
- // uses before the same-block fast path as well.
154
- if (cyclic.has(use))
155
- continue;
148
+ // Defined and consumed in ONE block: the definition dominates the use, so
149
+ // every execution of the block writes the storage before the use reads it.
150
+ // A loop re-entering the block re-runs the definition first, so the move
151
+ // can never be observed by a later iteration -- the same argument
152
+ // `buildDyingArgumentIndex` makes for a value a block defines and passes.
153
+ // This is what a `for`/`for-in` body is made of: `b = read(k)` inside a
154
+ // loop copied a whole string or union per iteration because its block was
155
+ // cyclic, although nothing could ever read the temporary again.
156
156
  if (use === definition) {
157
157
  dying.add(value);
158
158
  continue;
159
159
  }
160
+ // A value defined outside a loop can still have its sole SSA use inside
161
+ // the loop. Moving at that use empties the storage on the first iteration
162
+ // and every later iteration observes a null Ref. That is exactly what the
163
+ // re-entry search below decides: it walks forward from the use with the
164
+ // DEFINITION block removed from the graph, so it finds the use again only
165
+ // when some path re-enters it without redefining the value first. A use in
166
+ // a loop whose every trip runs the definition (`k = cursor.next()` in one
167
+ // block, `key = k` in the next) is not re-entered by that measure, and a
168
+ // use in a loop the definition sits outside of is.
160
169
  // Exceptional region edges are not represented by this CFG.
161
170
  if (body.tryRegions.length > 0 || (body.iteratorCloseRegions?.length ?? 0) > 0)
162
171
  continue;
@@ -44,7 +44,7 @@ import { censusRefusalCounts } from './normalize/census-refusal.js';
44
44
  import { createCellPolicyRegistry, defaultCellEvidencePolicies, buildCellFactsTable, publishCellFacts } from './normalize/cells/index.js';
45
45
  import { settleBindingCensus } from './normalize/binding-fixpoint.js';
46
46
  import { withJsDocTypeNames } from './normalize/jsdoc-type-names.js';
47
- import { censusReachability, moduleEvaluationOrder, nodeIsReachable } from './normalize/reachability.js';
47
+ import { censusReachability, fileEvaluates, moduleEvaluationOrder, nodeIsReachable } from './normalize/reachability.js';
48
48
  import { createProgram, defaultCompilerOptions } from './program.js';
49
49
  import { createFrontendTiming } from './frontend-timing.js';
50
50
  import { classHeritageOf } from './class-heritage.js';
@@ -1105,7 +1105,10 @@ export const runFrontend = (input) => {
1105
1105
  // reachability walk pruned whole, or one whose top level censused nothing,
1106
1106
  // has no region -- and a name in this list that no region answers would be
1107
1107
  // a call to a function nothing defines.
1108
+ // `fileEvaluates` is the reached-statement half of that: a file kept only
1109
+ // for a layout-only class has a region and, by design, no body.
1108
1110
  moduleOrder: moduleEvaluationOrder({ checker: compiled.checker, files: compiled.sourceFiles, entries: compiled.entryFiles })
1111
+ .filter((file) => fileEvaluates(reachable, file))
1109
1112
  .map((file) => regionId(identities.nodeIdOf(file), 'module-body'))
1110
1113
  .filter((region) => normalized.graph.regions.has(region)),
1111
1114
  sourceFileNames: identities.sourceFileNames,
@@ -5146,9 +5146,7 @@ const closedMemberCallableUses = (checker, flow, member, countedCalls, givenRece
5146
5146
  const origins = key === null ? null : allocationOriginsOf(receiver);
5147
5147
  traceClosedCallee(call, 'invocationTargetsOf/origins', `key=${key ?? 'null'} origins=${origins === null ? 'null' : origins.classes.size}`);
5148
5148
  if (origins !== null) {
5149
- const privateDeclarations = ts.isPropertyAccessExpression(callee) && ts.isPrivateIdentifier(callee.name)
5150
- ? privateMemberDeclarationsOf(callee.name)
5151
- : null;
5149
+ const privateDeclarations = ts.isPropertyAccessExpression(callee) && ts.isPrivateIdentifier(callee.name) ? privateMemberDeclarationsOf(callee.name) : null;
5152
5150
  const selected = new Set();
5153
5151
  for (const owner of origins.classes) {
5154
5152
  const slotDeclarations = privateDeclarations ??
@@ -4416,7 +4416,7 @@ computedKeysOf = () => null, trustSeed = initialCensusSeed) => {
4416
4416
  // value.
4417
4417
  if ((ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent)) && parent.propertyName === node)
4418
4418
  return true;
4419
- return (((ts.isVariableDeclaration(parent) ||
4419
+ return ((ts.isVariableDeclaration(parent) ||
4420
4420
  ts.isImportSpecifier(parent) ||
4421
4421
  ts.isExportSpecifier(parent) ||
4422
4422
  ts.isImportClause(parent) ||
@@ -4441,7 +4441,7 @@ computedKeysOf = () => null, trustSeed = initialCensusSeed) => {
4441
4441
  ts.isPropertyAssignment(parent) ||
4442
4442
  ts.isShorthandPropertyAssignment(parent) ||
4443
4443
  ts.isTypeParameterDeclaration(parent)) &&
4444
- parent.name === node));
4444
+ parent.name === node);
4445
4445
  };
4446
4446
  let globalEscapeAnswer = null;
4447
4447
  const globalObjectEscapesIntoData = () => {
@@ -37,17 +37,16 @@ import ts from 'typescript';
37
37
  * - A **function declaration**. Its evaluation is `InstantiateFunctionObject`:
38
38
  * a closure is created and bound. Nothing observable happens, and the body
39
39
  * does not run.
40
- * - A **class declaration** with no decorators, no `extends` clause, no static
40
+ * - A **class declaration** with no decorators, no static
41
41
  * block, no static field initializer and no computed member name.
42
42
  * `ClassDefinitionEvaluation` for such a class evaluates nothing at all, and
43
43
  * this compiler lowers class evaluation to *no runtime step at all*
44
44
  * (`ir/lower.ts`, `case 'class-lifecycle'`): the struct and the construct
45
45
  * function it emits already are the layout the definition events record. A
46
46
  * static block or a static initializer is the opposite -- each is a real
47
- * region with a body -- so a class carrying one is kept; so is a class with
48
- * a heritage clause, whose one definition-time step (the heritage read) is
49
- * the event the projection takes the struct's base from (see
50
- * `classDefinitionIsInert`).
47
+ * region with a body -- so a class carrying one is kept. An `extends`
48
+ * clause is tolerated only in the two spellings `heritageIsInert` proves
49
+ * evaluate nothing; any other base keeps the class.
51
50
  * - A **variable statement** whose every declarator binds a plain name and has
52
51
  * either no initializer or an initializer drawn from `initializerIsInert`'s
53
52
  * whitelist: literals, function/arrow/class expressions, and object/array
@@ -185,5 +184,21 @@ export interface ReachabilityInput {
185
184
  */
186
185
  readonly hostReachedMemberKeys?: ReadonlySet<string>;
187
186
  }
187
+ /**
188
+ * Whether the statements this program REACHES in `file` evaluate anything.
189
+ *
190
+ * `evaluatesNothing` above is the syntactic answer for a whole file;
191
+ * this is the same question after pruning, and it is the one the entry needs:
192
+ * a module body is called because something in it runs. A class retained for
193
+ * its LAYOUT alone is the case that separates them. It is a live statement --
194
+ * it publishes a class-lifecycle operation, so the census gives its file a
195
+ * module-body region -- and by `layoutClasses`' own definition it runs no
196
+ * constructor, no initializer and no module effect, so lowering gives that
197
+ * region no body. A rooted script whose every class is pruned except one some
198
+ * annotation names (node-compat's `whatwg-streams.ts`, in a program that
199
+ * touches no stream) was scheduled on the strength of the region and refused
200
+ * by the printer for want of the body.
201
+ */
202
+ export declare const fileEvaluates: (reachable: ProgramReachability, file: ts.SourceFile) => boolean;
188
203
  export declare const moduleEvaluationOrder: (input: ReachabilityInput) => readonly ts.SourceFile[];
189
204
  export declare const censusReachability: (input: ReachabilityInput) => ProgramReachability;
@@ -92,21 +92,63 @@ const computedKeyIsInert = (checker, name) => {
92
92
  const declarations = checker.getSymbolAtLocation(root)?.declarations ?? [];
93
93
  return declarations.length > 0 && declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile);
94
94
  };
95
+ /**
96
+ * Whether evaluating a class's `extends` clause is provably not an action.
97
+ *
98
+ * `ClassDefinitionEvaluation` (15.7.14) evaluates the heritage expression and
99
+ * requires a constructor. Two spellings cannot do anything else. A bare
100
+ * identifier naming a class DECLARED EARLIER at the top level of the same file
101
+ * is a read of an initialized binding -- past its temporal dead zone, never a
102
+ * getter, always a constructor. And a bare identifier whose every declaration
103
+ * is ambient names the language's own intrinsic (`Error`), for the reason
104
+ * `computedKeyIsInert` gives. Everything else is kept: a call (`mixin(Base)`),
105
+ * a property read (`ns.Base`, which can be a getter), a base declared later
106
+ * (a TDZ throw), and an imported base, whose module a cycle may not have
107
+ * evaluated yet.
108
+ *
109
+ * This used to answer `false` for every `extends`, for a reason that was never
110
+ * about evaluation: `projection/classes.ts` takes a struct's base from the
111
+ * heritage event a LIVE class publishes, so pruning a subclass some type still
112
+ * named left `struct One final {}` with no base under an upcast. That is a
113
+ * question about who names the class, and `markTypeNamedClasses` now answers
114
+ * it where it belongs -- a class any live annotation, alias or interface names
115
+ * is opened for its layout and publishes that event. What is pruned here is a
116
+ * subclass NOTHING names, whose struct nothing emits.
117
+ *
118
+ * The blanket rule's cost was every unused hierarchy in a rooted script:
119
+ * node-compat's `whatwg-streams.ts` and `abort-events.ts` are roots of every
120
+ * program, so every program carried `ReadableByteStreamController`,
121
+ * `TextEncoderStream`, `DOMException` and, through them, the whole stream
122
+ * implementation -- ~2.4k emitted lines in a program that prints one number.
123
+ */
124
+ const heritageIsInert = (checker, node) => {
125
+ for (const clause of node.heritageClauses ?? []) {
126
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword)
127
+ continue;
128
+ for (const type of clause.types) {
129
+ if (!ts.isIdentifier(type.expression))
130
+ return false;
131
+ const declarations = checker.getSymbolAtLocation(type.expression)?.declarations ?? [];
132
+ if (declarations.length === 0)
133
+ return false;
134
+ if (declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile))
135
+ continue;
136
+ const file = node.getSourceFile();
137
+ const settled = declarations.every((declaration) => ts.isClassDeclaration(declaration) &&
138
+ declaration.getSourceFile() === file &&
139
+ declaration.parent === file &&
140
+ declaration.end <= node.pos);
141
+ if (!settled)
142
+ return false;
143
+ }
144
+ }
145
+ return true;
146
+ };
95
147
  /** Whether a class's *definition* evaluates anything beyond binding its own name. */
96
148
  const classDefinitionIsInert = (checker, node) => {
97
149
  if (ts.canHaveDecorators(node) && (ts.getDecorators(node)?.length ?? 0) > 0)
98
150
  return false;
99
- // The one step `ClassDefinitionEvaluation` performs for a decorator-free
100
- // class is its heritage read, and that step is not inert in THIS compiler
101
- // even though it calls nothing: `projection/classes.ts` reads a class's
102
- // `base` link off the class-lifecycle heritage event the census publishes
103
- // for the definition, and `records.ts` spells `struct D : B` from that
104
- // link alone -- while the struct itself is emitted whenever the class's
105
- // shape is reachable, which a type position keeps it. Pruning an
106
- // un-instantiated `class One extends Base` whose type still names a union
107
- // arm emitted `struct One final {}` with no base, and the upcast
108
- // `Ref<Base>(Ref<One>)` the union's recast renders stopped compiling.
109
- if (node.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword))
151
+ if (!heritageIsInert(checker, node))
110
152
  return false;
111
153
  return node.members.every((member) => {
112
154
  if (ts.canHaveDecorators(member) && (ts.getDecorators(member)?.length ?? 0) > 0)
@@ -238,6 +280,31 @@ const objectMemberIsInert = (checker, member) => {
238
280
  * module (TS2448, which this compiler refuses the program for), it does not
239
281
  * across an import cycle.
240
282
  */
283
+ /**
284
+ * `Symbol()` / `Symbol('description')` on the language's own `Symbol`.
285
+ *
286
+ * ECMA-262 20.4.1.1 does one thing a program could observe: `ToString` of the
287
+ * description, and `ToString` of a string literal (or of nothing) runs no
288
+ * code. What is left is minting a symbol, and a symbol bound to a name nothing
289
+ * reads is not observable -- which is the only case this is ever asked about,
290
+ * because a binding something names is opened regardless. The callee must be
291
+ * the ambient intrinsic for the reason `computedKeyIsInert` gives: a user
292
+ * `Symbol` is an ordinary call.
293
+ *
294
+ * A module-private brand token is what reaches it: node-compat's
295
+ * `abort-events.ts` is a root of every program and declares
296
+ * `const abortSignalConstructionToken = Symbol('AbortSignal construction')`,
297
+ * so every program kept a module body whose whole content was that call.
298
+ */
299
+ const intrinsicSymbolCallIsInert = (checker, node) => {
300
+ if (!ts.isIdentifier(node.expression) || node.expression.text !== 'Symbol' || node.arguments.length > 1)
301
+ return false;
302
+ const description = node.arguments[0];
303
+ if (description !== undefined && !ts.isStringLiteral(description) && !ts.isNoSubstitutionTemplateLiteral(description))
304
+ return false;
305
+ const declarations = checker.getSymbolAtLocation(node.expression)?.declarations ?? [];
306
+ return declarations.length > 0 && declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile);
307
+ };
241
308
  const initializerIsInert = (checker, node) => {
242
309
  if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))
243
310
  return initializerIsInert(checker, node.expression);
@@ -286,7 +353,7 @@ const initializerIsInert = (checker, node) => {
286
353
  declarations.every((declaration) => ts.isClassDeclaration(declaration) && declaration.getSourceFile() === node.getSourceFile()));
287
354
  }
288
355
  if (ts.isCallExpression(node))
289
- return closureFactoryCallIsInert(checker, node);
356
+ return intrinsicSymbolCallIsInert(checker, node) || closureFactoryCallIsInert(checker, node);
290
357
  // `-1` and `+1` are how a negative numeric constant is spelled; nothing else
291
358
  // unary is admitted, because every other operand form can reach user code.
292
359
  if (ts.isPrefixUnaryExpression(node)) {
@@ -547,9 +614,33 @@ const evaluatesNothing = (file) => file.statements.every((statement) => {
547
614
  return true;
548
615
  if (statement.moduleSpecifier !== undefined)
549
616
  return false;
550
- return statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause) && statement.exportClause.elements.length === 0;
617
+ return (statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause) && statement.exportClause.elements.length === 0);
551
618
  }
552
- return ts.canHaveModifiers(statement) && (ts.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword);
619
+ return (ts.canHaveModifiers(statement) &&
620
+ (ts.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword));
621
+ });
622
+ /**
623
+ * Whether the statements this program REACHES in `file` evaluate anything.
624
+ *
625
+ * `evaluatesNothing` above is the syntactic answer for a whole file;
626
+ * this is the same question after pruning, and it is the one the entry needs:
627
+ * a module body is called because something in it runs. A class retained for
628
+ * its LAYOUT alone is the case that separates them. It is a live statement --
629
+ * it publishes a class-lifecycle operation, so the census gives its file a
630
+ * module-body region -- and by `layoutClasses`' own definition it runs no
631
+ * constructor, no initializer and no module effect, so lowering gives that
632
+ * region no body. A rooted script whose every class is pruned except one some
633
+ * annotation names (node-compat's `whatwg-streams.ts`, in a program that
634
+ * touches no stream) was scheduled on the strength of the region and refused
635
+ * by the printer for want of the body.
636
+ */
637
+ export const fileEvaluates = (reachable, file) => reachable.statementsOf(file).some((statement) => {
638
+ if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement))
639
+ return false;
640
+ if (ts.isClassDeclaration(statement) && reachable.classIsLayoutOnly(statement))
641
+ return false;
642
+ return !(ts.canHaveModifiers(statement) &&
643
+ (ts.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword));
553
644
  });
554
645
  export const moduleEvaluationOrder = (input) => {
555
646
  const compiled = new Set(input.files);
@@ -787,11 +878,25 @@ export const censusReachability = (input) => {
787
878
  * header already gives, and layout-only is the smallest promotion there is:
788
879
  * no module evaluation, no constructor, no member body.
789
880
  */
881
+ // A type alias or interface is a NAME for the types inside it, so a class it
882
+ // names is named by whatever names the alias: `type Arm = One | Two` then
883
+ // `let v: Arm` types a live cell as `One`. The walk never enters a type
884
+ // declaration on its own (`markReferences` returns on both), so it is entered
885
+ // here, once, from the reference that makes it matter. This is what lets
886
+ // `heritageIsInert` prune an unnamed subclass without losing a named one.
887
+ const followedTypeDeclarations = new Set();
790
888
  const markTypeNamedClasses = (node) => {
791
889
  const named = ts.isTypeReferenceNode(node) ? node.typeName : ts.isExpressionWithTypeArguments(node) ? node.expression : null;
792
890
  if (named !== null) {
793
891
  const symbol = input.checker.getSymbolAtLocation(named);
794
892
  for (const declaration of symbol ? declarationsOf(input.checker, symbol) : []) {
893
+ if (ts.isTypeAliasDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
894
+ if (followedTypeDeclarations.has(declaration))
895
+ continue;
896
+ followedTypeDeclarations.add(declaration);
897
+ ts.forEachChild(declaration, markTypeNamedClasses);
898
+ continue;
899
+ }
795
900
  if (!ts.isClassDeclaration(declaration))
796
901
  continue;
797
902
  const file = declaration.getSourceFile();
@@ -1781,10 +1781,7 @@ export const censusSpecializations = (checker, sourceFiles, reachable, namespace
1781
1781
  // refusal it replaces. Those copies are two layouts and split.
1782
1782
  const stored = parametersInInstanceStorage(checker, declaration);
1783
1783
  const foldable = (candidate) => ![...layoutRelevant].some((index) => stored.has(index) &&
1784
- fillings.some((other, at) => minted[at] === true &&
1785
- other[index] !== candidate[index] &&
1786
- !openFilling(other[index]) &&
1787
- !openFilling(candidate[index])));
1784
+ fillings.some((other, at) => minted[at] === true && other[index] !== candidate[index] && !openFilling(other[index]) && !openFilling(candidate[index])));
1788
1785
  // The chosen layout must be one the program actually builds.
1789
1786
  return (fillings.find((candidate, at) => minted[at] === true && differs(candidate) && foldable(candidate) && fillings.every((other) => absorbs(candidate, other))) ?? null);
1790
1787
  })();
@@ -6,6 +6,7 @@ import { classFamilyOverridesOf } from '../../projection/dispatch.js';
6
6
  export { classFamilyOverridesOf };
7
7
  import { classMemberOf, classStaticMemberOf, type ClassMemberSite } from '../../projection/fields.js';
8
8
  export { classMemberOf, classStaticMemberOf, type ClassMemberSite };
9
+ import type { ConstantLiteral } from '../../semantics/model/operands.js';
9
10
  import { type RecordField, type Representation } from '../../representation/model.js';
10
11
  import { type ConversionSite } from './emit-narrowing.js';
11
12
  /**
@@ -167,6 +168,45 @@ export declare const lazyArrowFieldGetPlanOf: (ctx: Pick<EmitContext, "classes"
167
168
  * the snapshot in hand.
168
169
  */
169
170
  export declare const lazyCalleeReadsOf: (ctx: EmitContext, body: IrBody) => ReadonlySet<IrValueId>;
171
+ /**
172
+ * A class field initializer that is one constant and nothing else.
173
+ *
174
+ * Carried as the constant's own `text`/`literal` rather than as a decided
175
+ * boolean because the question "is this redundant" cannot be answered here:
176
+ * it depends on the PHYSICAL storage the field was given, which
177
+ * `cppFieldInitializerStatements` learns from its `storedFieldOf` and this
178
+ * census never sees. This says what the initializer is; that site says what
179
+ * the storage is; the two together decide.
180
+ */
181
+ export interface ConstantFieldInitializer {
182
+ readonly text: string;
183
+ readonly literal: ConstantLiteral;
184
+ }
185
+ /** Publishes one compile's constant-field-initializer census. Called once, before any body renders -- mirrors `publishLazyArrowFieldPlans`. */
186
+ export declare const publishConstantFieldInitializers: (classes: ReadonlyMap<DeclarationId, ClassLayout>, constants: ReadonlyMap<DeclarationId, ReadonlyMap<string, ConstantFieldInitializer>>) => void;
187
+ /** The single constant one class field's initializer evaluates to, or `null` when it is anything else (including in a compile that published no census). */
188
+ export declare const constantFieldInitializerOf: (classes: ReadonlyMap<DeclarationId, ClassLayout>, declaration: DeclarationId, key: string) => ConstantFieldInitializer | null;
189
+ /**
190
+ * Every class field whose initializer is a single constant.
191
+ *
192
+ * The shape admitted is the narrowest one that proves "evaluating this thunk
193
+ * does nothing but produce this value": one block, one operation in it, that
194
+ * operation a `constant`, and the block returning that constant's own result.
195
+ * A thunk with two operations may have computed something; one that returns a
196
+ * different value than it built is not what it looks like. Neither gets in.
197
+ *
198
+ * A REACTIVE field is refused. Its storage is `Cell<T>`, not `T`
199
+ * (`records.ts`'s `fieldStorageType`), so the assignment runs the cell's own
200
+ * `operator=` and what a value-initialized cell holds is the cell's business,
201
+ * not this function's. The refusal reads the plugin's own per-class statement
202
+ * (`ReactiveCellPlan.fields`) rather than `cell`: gea names a cell spelling in
203
+ * every compile, reactive or not, so testing `cell !== null` would switch the
204
+ * census off for every program. It also cannot wait for the settled
205
+ * `celledFields`, which `cppRecordDeclarations` produces only after this
206
+ * census has to be published -- but that set is derived from these same
207
+ * `fields`, so refusing all of them refuses a superset.
208
+ */
209
+ export declare const censusConstantFieldInitializers: (bodies: readonly IrBody[], classes: ReadonlyMap<DeclarationId, ClassLayout>, reactiveFields: ReadonlyMap<DeclarationId, ReadonlySet<string>>) => ReadonlyMap<DeclarationId, ReadonlyMap<string, ConstantFieldInitializer>>;
170
210
  /**
171
211
  * Which class fields qualify for lazy materialization, computed once for the
172
212
  * whole translation unit.
@@ -62,6 +62,31 @@ classDeclaration, fields, receiver, storedFieldOf) => {
62
62
  return `field "${field.key}" initializer has no complete physical storage contract`;
63
63
  }
64
64
  const stored = storage.value;
65
+ // A field whose initializer writes the value its storage ALREADY holds.
66
+ //
67
+ // `makeRef<T>()` value-initializes -- these structs have no user-provided
68
+ // constructor, so `T()` zero-initializes and then default-constructs every
69
+ // member: a `std::string` field is `""` and a `bool` field is `false`
70
+ // before any statement here runs. `statusMessage = ''` then calls the
71
+ // field's initializer thunk and assigns its result, and for a
72
+ // `std::string` that assignment is an out-of-line libstdc++ `_M_replace`
73
+ // -- per field, per construction. `node:http`'s `ServerResponse` alone
74
+ // declares five, and it is built once per request.
75
+ //
76
+ // Only the store is dropped, and only where dropping it is invisible:
77
+ // the initializer must BE one constant (`censusConstantFieldInitializers`
78
+ // proved the thunk is a single `constant` and a `return` of it, so it has
79
+ // no side effect to lose), its representation must already be the
80
+ // storage's (no conversion to skip), the field must be required (an
81
+ // optional's presence bit is `false` by declaration and would still need
82
+ // setting), and the constant must be the carrier's own value-initialized
83
+ // value.
84
+ const constant = constantFieldInitializerOf(site.classes, classDeclaration, field.key);
85
+ if (constant !== null &&
86
+ storage.required &&
87
+ representationKey(field.representation) === representationKey(stored) &&
88
+ storesTheValueInitializedDefault(stored, constant))
89
+ continue;
65
90
  const markPresent = !storage.required ? ` ${receiver}->${cppRecordFieldPresenceName(field.key)} = true;` : '';
66
91
  // A widening may inspect a tagged union's live arm and therefore name its
67
92
  // input more than once. A field initializer runs exactly once, so bind its
@@ -236,6 +261,93 @@ export const lazyCalleeReadsOf = (ctx, body) => {
236
261
  }
237
262
  return result;
238
263
  };
264
+ const constantFieldInitializerSidecar = new WeakMap();
265
+ /** Publishes one compile's constant-field-initializer census. Called once, before any body renders -- mirrors `publishLazyArrowFieldPlans`. */
266
+ export const publishConstantFieldInitializers = (classes, constants) => {
267
+ constantFieldInitializerSidecar.set(classes, constants);
268
+ };
269
+ /** The single constant one class field's initializer evaluates to, or `null` when it is anything else (including in a compile that published no census). */
270
+ export const constantFieldInitializerOf = (classes, declaration, key) => constantFieldInitializerSidecar.get(classes)?.get(declaration)?.get(key) ?? null;
271
+ /**
272
+ * Whether `storage` value-initializes to exactly what `constant` spells.
273
+ *
274
+ * Deliberately only the two carriers whose C++ value-initialized state is a
275
+ * single known value: `std::string()` is empty and a scalar is zero. Anything
276
+ * else -- an optional, a union, a `Ref`, a record -- either has a presence or
277
+ * tag byte the caller must still write or a default this cannot state, and
278
+ * gets no answer rather than a guessed one.
279
+ *
280
+ * `-0` is refused although `Number('-0') === 0`: a value-initialized `double`
281
+ * is `+0.0`, and the two differ under `Object.is` and `1 / x`. Any negative
282
+ * spelling that reaches here is one of that family, so the sign test is the
283
+ * whole check.
284
+ */
285
+ const storesTheValueInitializedDefault = (storage, constant) => {
286
+ if (storage.kind === 'string')
287
+ return constant.literal === 'string' && constant.text === '';
288
+ if (storage.kind !== 'scalar')
289
+ return false;
290
+ if (storage.domain === 'boolean')
291
+ return constant.literal === 'boolean' && constant.text === 'false';
292
+ if (storage.domain === 'bigint')
293
+ return false;
294
+ return constant.literal === 'number' && !constant.text.startsWith('-') && Number(constant.text) === 0;
295
+ };
296
+ /**
297
+ * Every class field whose initializer is a single constant.
298
+ *
299
+ * The shape admitted is the narrowest one that proves "evaluating this thunk
300
+ * does nothing but produce this value": one block, one operation in it, that
301
+ * operation a `constant`, and the block returning that constant's own result.
302
+ * A thunk with two operations may have computed something; one that returns a
303
+ * different value than it built is not what it looks like. Neither gets in.
304
+ *
305
+ * A REACTIVE field is refused. Its storage is `Cell<T>`, not `T`
306
+ * (`records.ts`'s `fieldStorageType`), so the assignment runs the cell's own
307
+ * `operator=` and what a value-initialized cell holds is the cell's business,
308
+ * not this function's. The refusal reads the plugin's own per-class statement
309
+ * (`ReactiveCellPlan.fields`) rather than `cell`: gea names a cell spelling in
310
+ * every compile, reactive or not, so testing `cell !== null` would switch the
311
+ * census off for every program. It also cannot wait for the settled
312
+ * `celledFields`, which `cppRecordDeclarations` produces only after this
313
+ * census has to be published -- but that set is derived from these same
314
+ * `fields`, so refusing all of them refuses a superset.
315
+ */
316
+ export const censusConstantFieldInitializers = (bodies, classes, reactiveFields) => {
317
+ const result = new Map();
318
+ const bodyBySourceOwner = new Map();
319
+ for (const body of bodies)
320
+ bodyBySourceOwner.set(body.sourceOwner, body);
321
+ for (const [declaration, layout] of classes) {
322
+ for (const field of layout.fields) {
323
+ if (field.initializer === null)
324
+ continue;
325
+ if (reactiveFields.get(declaration)?.has(field.key))
326
+ continue;
327
+ const thunkBody = bodyBySourceOwner.get(field.initializer);
328
+ if (!thunkBody || thunkBody.blocks.size !== 1)
329
+ continue;
330
+ const block = [...thunkBody.blocks.values()][0];
331
+ if (!block || block.operations.length !== 1)
332
+ continue;
333
+ const only = block.operations[0];
334
+ if (!only || only.kind !== 'constant')
335
+ continue;
336
+ const terminator = block.terminator;
337
+ if (terminator.kind !== 'return' || terminator.value === null)
338
+ continue;
339
+ if (terminator.value.value !== only.result.id)
340
+ continue;
341
+ let byKey = result.get(declaration);
342
+ if (!byKey) {
343
+ byKey = new Map();
344
+ result.set(declaration, byKey);
345
+ }
346
+ byKey.set(field.key, { text: only.text, literal: only.literal });
347
+ }
348
+ }
349
+ return result;
350
+ };
239
351
  /**
240
352
  * Which class fields qualify for lazy materialization, computed once for the
241
353
  * whole translation unit.
@@ -523,7 +523,8 @@ const emitLazyArrowFieldCall = (ctx, lines, operation) => {
523
523
  // conversion below then adapts the stored result to the read's, or refuses
524
524
  // and declines the fusion.
525
525
  const storedField = ctx.classes.get(site.owner)?.fields.find((field) => field.key === fieldName)?.representation ?? null;
526
- const storedAbi = storedField !== null && (storedField.kind === 'function' || storedField.kind === 'function-family' || storedField.kind === 'function-value-dispatch')
526
+ const storedAbi = storedField !== null &&
527
+ (storedField.kind === 'function' || storedField.kind === 'function-family' || storedField.kind === 'function-value-dispatch')
527
528
  ? storedField.abi
528
529
  : callee.abi;
529
530
  const bodyAbi = ctx.abiOfCallable(plan.body);
@@ -3828,6 +3828,13 @@ const ownsItsStorage = (carrier) => {
3828
3828
  return carrier.ownership === 'shared-refcount';
3829
3829
  if (carrier.kind === 'optional')
3830
3830
  return ownsItsStorage(carrier.payload);
3831
+ // A sum owns whatever its live arm owns. `gea::TaggedUnion` has a real move
3832
+ // constructor (`TaggedUnionOps::moveConstruct`), so a dying union hands its
3833
+ // string or reference over instead of `copyConstruct`ing it and then
3834
+ // destroying the original -- per header value, per request, in node-compat's
3835
+ // `writeHead`, where this was three heap copies of one content-type string.
3836
+ if (carrier.kind === 'tagged-union')
3837
+ return carrier.arms.some((arm) => ownsItsStorage(arm.value));
3831
3838
  return false;
3832
3839
  };
3833
3840
  /**
@@ -592,7 +592,7 @@ export const emitGet = (ctx, lines, operation) => {
592
592
  // struct-member fallthrough at the end would take `re.test` and emit
593
593
  // `re->test`, which is a clang error naming a member instead of a refusal
594
594
  // naming the method.
595
- const regexpMember = regexpMemberText(ctx, operation.receiver, operation.key, operation.result.id, operation.result.representation, (fieldName, storage) => narrowedFieldReadText(ctx, operation, fieldName, storage)) ?? stringObjectMemberText(ctx, operation.receiver, operation.key, operation.result.representation);
595
+ const regexpMember = regexpMemberText(ctx, operation.receiver, operation.key, operation.result.id, operation.result.representation, (fieldName, storage, declared) => narrowedFieldReadText(ctx, operation, fieldName, storage, null, declared)) ?? stringObjectMemberText(ctx, operation.receiver, operation.key, operation.result.representation);
596
596
  if (regexpMember !== null) {
597
597
  // An empty rendering is a deferred RegExp.prototype method read -- see
598
598
  // the identical comment on the array-access branch above.
@@ -911,8 +911,18 @@ const fixedFieldPresenceText = (ctx, receiver, fieldName) => {
911
911
  * the original defect got in. A disagreement it declines to convert is a
912
912
  * defect upstream, not a spelling this may invent, so that refuses by name.
913
913
  */
914
- const narrowedFieldReadText = (ctx, operation, fieldName, storage, presence = null) => {
915
- const declared = declaredFieldRepresentation(ctx, operation.receiver, fieldName);
914
+ const narrowedFieldReadText = (ctx, operation, fieldName, storage, presence = null,
915
+ /**
916
+ * The member's physical carrier, supplied by a caller that HAS it where the
917
+ * lookup below cannot. A compiler-owned native layout
918
+ * (`ExecResult`/`MatchResult`) never gets a sealed record layout, so
919
+ * `declaredFieldRepresentation` answers null for its members and this
920
+ * function would hand back the bare load -- correct until the read is
921
+ * narrowed, and a type error the moment it is. The caller that knows the
922
+ * struct says so instead of this function guessing.
923
+ */
924
+ declaredStorage = null) => {
925
+ const declared = declaredStorage ?? declaredFieldRepresentation(ctx, operation.receiver, fieldName);
916
926
  if (declared === null)
917
927
  return storage;
918
928
  const published = operation.result.representation;
@@ -72,7 +72,7 @@ export declare const regExpConstructionFromObject: (ctx: EmitContext, argument:
72
72
  * Stated once; `regexpMemberText` and the prototype-read walk both ask it.
73
73
  */
74
74
  export declare const deferredRegexpMethodClaim: (staticKeyTexts: ReadonlyMap<IrValueId, string>, receiver: IrOperand, key: IrOperand) => PrototypeMethodRead | null;
75
- export declare const regexpMemberText: (ctx: EmitContext, receiver: IrOperand, key: IrOperand, result: IrValueId | null, resultRepresentation: Representation | undefined, narrow: (fieldName: string, storage: string) => string) => string | null;
75
+ export declare const regexpMemberText: (ctx: EmitContext, receiver: IrOperand, key: IrOperand, result: IrValueId | null, resultRepresentation: Representation | undefined, narrow: (fieldName: string, storage: string, declared: Representation | null) => string) => string | null;
76
76
  /**
77
77
  * `regexpStoreRefusal`'s whole decision, spelled as three outcomes rather than
78
78
  * collapsed into "throws or doesn't":
@@ -216,6 +216,57 @@ const patternMethods = new Set(['test', 'exec', 'toString']);
216
216
  * report group positions for non-participating groups at all.
217
217
  */
218
218
  const resultDataMembers = new Set(['index', 'input', 'length', 'groups']);
219
+ /**
220
+ * What each of those members PHYSICALLY holds, per role.
221
+ *
222
+ * Stated here because nothing else can state it. These two structs are
223
+ * compiler-owned native layouts: `representation/derive.ts` deliberately does
224
+ * NOT seal a record layout for `RegExpExecArray`/`RegExpMatchArray` -- see its
225
+ * own comment on why sealing "a plausible-looking struct" for them would emit
226
+ * a program that compiles and is not a regular expression -- so
227
+ * `recordFieldsOfShape` answers nothing for their shape id and the generic
228
+ * `narrowedFieldReadText` has no declared carrier to reconcile against. It
229
+ * therefore returned the bare member load for every read, which is right only
230
+ * while the read is NOT narrowed.
231
+ *
232
+ * It is wrong the moment it is. `if (m.groups !== undefined) m.groups['id']`
233
+ * publishes the payload while the field stores the optional, and the bare load
234
+ * emitted `Optional<Ref<Dictionary<std::string>>>` where a
235
+ * `Ref<Dictionary<std::string>>` was wanted -- `->has` then resolved on
236
+ * `gea::Ref`, which has no such member, and every program that read a named
237
+ * capture group by key failed to compile (node-compat's `apps/http-parity`
238
+ * router was the first). The narrowing obligation the `narrow` parameter
239
+ * documents was being honoured at the call and dropped inside it, for want of
240
+ * this table.
241
+ *
242
+ * The three optionals are the two interfaces' own `?`, and they differ by
243
+ * role on purpose: `RegExpExecArray` declares `index`/`input` REQUIRED and
244
+ * `RegExpMatchArray` declares them optional, because a global pattern's match
245
+ * really does answer an array with neither. `length` is `double` on an exec
246
+ * result and the inherited `ArrayObject::length()` on a match result; neither
247
+ * is optional. Keep this in step with `ExecResult`/`MatchResult` in
248
+ * `runtime/gea_runtime.h` -- they are the same two facts, and clang checks
249
+ * only one of them.
250
+ */
251
+ const namedGroupsStorage = {
252
+ kind: 'optional',
253
+ payload: { kind: 'dictionary', key: 'string', value: { kind: 'string' }, ownership: 'shared-refcount' },
254
+ absence: 'undefined'
255
+ };
256
+ const numberStorage = { kind: 'scalar', domain: 'number' };
257
+ const stringStorage = { kind: 'string' };
258
+ const optionalOf = (payload) => ({ kind: 'optional', payload, absence: 'undefined' });
259
+ const resultDataMemberStorage = (role, key) => {
260
+ if (key === 'groups')
261
+ return namedGroupsStorage;
262
+ if (key === 'length')
263
+ return numberStorage;
264
+ if (key === 'index')
265
+ return role === 'match-result' ? optionalOf(numberStorage) : numberStorage;
266
+ if (key === 'input')
267
+ return role === 'match-result' ? optionalOf(stringStorage) : stringStorage;
268
+ return null;
269
+ };
219
270
  /** Whether `text` is a canonical array index -- a run of digits with no sign, point, or leading zero. */
220
271
  const canonicalCaptureSlot = (text) => {
221
272
  if (text.length === 0)
@@ -318,6 +369,11 @@ export const regexpMemberText = (ctx, receiver, key, result, resultRepresentatio
318
369
  // fails to assign. The capture reads below do NOT go through it: they choose
319
370
  // between `capture` and `capturedOrAbsent` on the published carrier already,
320
371
  // and are not fields at all.
372
+ //
373
+ // `declared` is what the member physically holds. The caller cannot look it
374
+ // up for these two receivers -- their layout is never sealed -- so this
375
+ // supplies it from `resultDataMemberStorage`, and passing `null` (a RegExp
376
+ // pattern member) leaves the caller on its own lookup exactly as before.
321
377
  narrow) => {
322
378
  const role = regexpRoleOf(receiver.representation);
323
379
  if (role === null)
@@ -349,7 +405,7 @@ narrow) => {
349
405
  return unboxedReadText(resultRepresentation ?? { kind: 'dynamic', reason: 'declared-any-never-narrowed' }, `gea::runtime::regex::dynamicGet(${receiverText}, ${propertyKeyText(ctx, key, site)})`, site);
350
406
  }
351
407
  if (patternDataMembers.has(staticKey))
352
- return narrow(staticKey, `${receiverText}->${staticKey}`);
408
+ return narrow(staticKey, `${receiverText}->${staticKey}`, null);
353
409
  if (deferredRegexpMethodClaim(ctx.staticKeyTexts, receiver, key) !== null) {
354
410
  if (result === null) {
355
411
  throw createCppEmitBlockedError(`host-invocation:RegExp.prototype.${staticKey}`, `"${staticKey}" is a RegExp.prototype method, and this access publishes no value for its call to consume`);
@@ -369,7 +425,7 @@ narrow) => {
369
425
  // `MatchResult`; one spelling for both compiled the exec result's read as
370
426
  // a call on a `double`.
371
427
  const member = staticKey === 'length' && role === 'match-result' ? 'length()' : staticKey;
372
- return narrow(staticKey, `${receiverText}->${member}`);
428
+ return narrow(staticKey, `${receiverText}->${member}`, resultDataMemberStorage(role, staticKey));
373
429
  }
374
430
  if (canonicalCaptureSlot(staticKey))
375
431
  return captureReadText(role, receiverText, staticKey, resultRepresentation);
@@ -17,7 +17,7 @@ import { createCppDocumentBuilder, emptyCppFacts, render, spliceRendered } from
17
17
  import { beginUnionAliasing, endUnionAliasing } from './types.js';
18
18
  import { cppConstructedThunkName, cppConstructThunkName, cppFormalName, cppReceiverName, cppThunkName, emitBody, isCppEmitBlockedError } from './emit.js';
19
19
  import { cppCaptureFieldName, cppCaptureReceiverFieldName, cppEnvironmentLocalName, cppEnvironmentParamName, cppEnvironmentSlotName, cppEnvironmentStructName, effectiveAbiOf, symbolKeyDefinitions, templateObjectDefinitions } from './emit-context.js';
20
- import { censusClassStaticFieldStorage, censusLazyArrowFields, classBoxable, cppFieldInitializerStatements, publishBoxableClasses, publishClassStaticFieldStorage, publishLazyArrowFieldPlans } from './class-layout.js';
20
+ import { censusClassStaticFieldStorage, censusConstantFieldInitializers, censusLazyArrowFields, classBoxable, cppFieldInitializerStatements, publishBoxableClasses, publishClassStaticFieldStorage, publishConstantFieldInitializers, publishLazyArrowFieldPlans } from './class-layout.js';
21
21
  import { classStaticFieldStorageRows, cppRecordDeclarations, cppStructNameOf, declaredFieldRepresentationOf, declaredRecordFieldOf, recordAccessorBodiesOf, representationNamesMintedStruct, withUnreadParametersUnnamed } from './records.js';
22
22
  import { prototypeReadHooks, virtualMethodEmission, virtualMethodFamiliesOf } from './virtual-methods.js';
23
23
  import { virtualDispatchVerdictOf } from '../../projection/dispatch.js';
@@ -1395,6 +1395,13 @@ export const renderTranslationUnit = (input) => {
1395
1395
  // actual reader in this same function -- `cppRecordDeclarations` itself --
1396
1396
  // so it cannot be published after, the way that one is.
1397
1397
  publishLazyArrowFieldPlans(input.classes, censusLazyArrowFields(input.bodies, input.classes, captures));
1398
+ // Published alongside it, and read by the same constructor emission: which
1399
+ // field initializers are a single constant, so a store that writes what
1400
+ // value-initialization already wrote can be dropped. Passed the plugin's own
1401
+ // `reactive.fields` rather than the settled `celledFields`, which
1402
+ // `cppRecordDeclarations` has not produced yet -- see the census's own
1403
+ // comment for why that is the right superset to refuse.
1404
+ publishConstantFieldInitializers(input.classes, censusConstantFieldInitializers(input.bodies, input.classes, input.hosts.reactive.fields));
1398
1405
  const structs = cppRecordDeclarations(input.plan, deriver, input.classes, input.hosts.reactive, boundRecordFields, structMembers, narrowedStorage.slots, input.wellKnownSymbols, perFile, input.reflection, emissionRepresentations, input.physicalClasses ?? input.classes, (body) => captures.of(body).kind === 'ok', fixedFieldStateConstant, singleEvaluationClasses);
1399
1406
  const recursiveContainers = cppRecursiveContainerDeclarations(input.plan, emissionRepresentations);
1400
1407
  // Real storage for every `ClassName.KEY = value` site the whole program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/compiler",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "The geastack TypeScript-to-C++ compiler: one semantic spine, no source-shaped authority, no boxing of statically typed values.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -67,7 +67,7 @@
67
67
  "check:contracts": "npm run build && node test/contracts.mjs"
68
68
  },
69
69
  "dependencies": {
70
- "@geastack/node-compat": "^1.0.7",
70
+ "@geastack/node-compat": "^1.0.12",
71
71
  "typescript": "^5.9.3"
72
72
  },
73
73
  "devDependencies": {
@@ -77,9 +77,9 @@
77
77
  "prettier": "^3.8.3"
78
78
  },
79
79
  "peerDependencies": {
80
- "@geastack/apple": ">=0.1.0",
81
- "@geastack/geatsc-plugin-apple-native": ">=0.1.0",
82
- "@geastack/geatsc-plugin-gea": ">=0.1.0"
80
+ "@geastack/apple": ">=0.2.8",
81
+ "@geastack/geatsc-plugin-apple-native": ">=0.1.3",
82
+ "@geastack/geatsc-plugin-gea": ">=0.1.6"
83
83
  },
84
84
  "peerDependenciesMeta": {
85
85
  "@geastack/apple": {
@@ -89,8 +89,46 @@ inline Value ordinaryToPrimitive(const Value& value, ToPrimitiveHint hint) {
89
89
  host::throwRuntimeError("TypeError", "Cannot convert object to primitive value");
90
90
  }
91
91
 
92
+ /**
93
+ * ToPrimitive of a boxed typed array, which is always its ToString.
94
+ *
95
+ * A typed array is a native carrier with no dynamic property surface: it has no
96
+ * `@@toPrimitive`, its `valueOf` is Object's and returns the object, and
97
+ * nothing a program can do installs either on it. So 7.1.1 lands on
98
+ * `%TypedArray%.prototype.toString` for every hint, and that is knowable from
99
+ * the payload type alone -- without it the generic path below asks the box for
100
+ * `@@toPrimitive` and the box, having no field table, refuses by name. A view
101
+ * whose host brand states its own ToString (Node's Buffer) answers with that.
102
+ */
103
+ template <typename Element>
104
+ inline bool boxedTypedArrayToStringAs(const Value& value, std::string& out) {
105
+ using Handle = gea::Ref<TypedArray<Element>>;
106
+ if (value.payloadType() != detail::payloadTypeTagFor<Handle>()) return false;
107
+ const Handle& view = value.as<Handle>();
108
+ if constexpr (std::is_same_v<Element, std::uint8_t>) {
109
+ if (view) {
110
+ if (const detail::HostViewToString render = detail::hostViewToStringFor(view->hostBrand())) {
111
+ out = render(*view);
112
+ return true;
113
+ }
114
+ }
115
+ }
116
+ out = runtime::array::join(view);
117
+ return true;
118
+ }
119
+
120
+ inline bool boxedTypedArrayToString(const Value& value, std::string& out) {
121
+ if (value.tag() != Value::Tag::Object || value.isProxy()) return false;
122
+ return boxedTypedArrayToStringAs<std::uint8_t>(value, out) || boxedTypedArrayToStringAs<std::int8_t>(value, out) ||
123
+ boxedTypedArrayToStringAs<ClampedUint8>(value, out) || boxedTypedArrayToStringAs<std::int16_t>(value, out) ||
124
+ boxedTypedArrayToStringAs<std::uint16_t>(value, out) || boxedTypedArrayToStringAs<std::int32_t>(value, out) ||
125
+ boxedTypedArrayToStringAs<std::uint32_t>(value, out) || boxedTypedArrayToStringAs<float>(value, out) ||
126
+ boxedTypedArrayToStringAs<double>(value, out);
127
+ }
128
+
92
129
  inline Value dynamicToPrimitive(const Value& value, ToPrimitiveHint hint = ToPrimitiveHint::Default) {
93
130
  if (!isObjectValue(value)) return value;
131
+ if (std::string text; boxedTypedArrayToString(value, text)) return Value::box(Value::Tag::String, std::move(text));
94
132
  const Value exotic = value.getProperty(PropertyKey::symbol(wellKnownSymbol(detail::WellKnownSymbol::ToPrimitive)));
95
133
  if (exotic.tag() != Value::Tag::Undefined && exotic.tag() != Value::Tag::Null) {
96
134
  if (exotic.tag() != Value::Tag::Function) host::throwRuntimeError("TypeError", "@@toPrimitive is not callable");
@@ -5649,6 +5649,7 @@ class TypedArray {
5649
5649
  */
5650
5650
  void setHostBrand(const void* brand) { hostBrand_ = brand; }
5651
5651
  bool hasHostBrand(const void* brand) const { return brand != nullptr && hostBrand_ == brand; }
5652
+ const void* hostBrand() const { return hostBrand_; }
5652
5653
 
5653
5654
  /**
5654
5655
  * ECMA-262 23.2.3.26 `%TypedArray%.prototype.set`, over a source view of any
@@ -5833,6 +5834,41 @@ class TypedArray {
5833
5834
  const void* hostBrand_ = nullptr;
5834
5835
  };
5835
5836
 
5837
+ /**
5838
+ * What a host brand's views answer to ToString.
5839
+ *
5840
+ * A brand is identity only (see `setHostBrand`), which is enough while the
5841
+ * view's type is known: `buffer.toString()` lowers to the host's own function.
5842
+ * Behind a box it is not. `body + chunk` in a `'data'` listener -- where Node
5843
+ * declares `chunk` as `any` and hands out a Buffer -- is ToPrimitive over an
5844
+ * erased byte view, and the generic answer for a byte view (23.2.3.32, the
5845
+ * comma-joined elements) is not Buffer's, which decodes the bytes as UTF-8.
5846
+ * The host that minted the brand is the only party that knows that, so it
5847
+ * states it here once, keyed by the same opaque address.
5848
+ *
5849
+ * `GEA_HOST_VIEW_TO_STRING` lets a host header register only when this runtime
5850
+ * has the table, so it still builds against a runtime that predates it.
5851
+ */
5852
+ #define GEA_HOST_VIEW_TO_STRING 1
5853
+ namespace detail {
5854
+ using HostViewToString = std::string (*)(const TypedArray<std::uint8_t>&);
5855
+ inline std::vector<std::pair<const void*, HostViewToString>>& hostViewToStrings() {
5856
+ static std::vector<std::pair<const void*, HostViewToString>> table;
5857
+ return table;
5858
+ }
5859
+ inline bool registerHostViewToString(const void* brand, HostViewToString render) {
5860
+ hostViewToStrings().emplace_back(brand, render);
5861
+ return true;
5862
+ }
5863
+ inline HostViewToString hostViewToStringFor(const void* brand) {
5864
+ if (brand == nullptr) return nullptr;
5865
+ for (const auto& [known, render] : hostViewToStrings()) {
5866
+ if (known == brand) return render;
5867
+ }
5868
+ return nullptr;
5869
+ }
5870
+ } // namespace detail
5871
+
5836
5872
  namespace runtime::atomics {
5837
5873
 
5838
5874
  template <typename T>
@@ -6544,6 +6580,32 @@ struct MaxAlignOf<T, Rest...> {
6544
6580
  static constexpr std::size_t value = alignof(T) > MaxAlignOf<Rest...>::value ? alignof(T) : MaxAlignOf<Rest...>::value;
6545
6581
  };
6546
6582
 
6583
+ /**
6584
+ * Whether an arm may be the one a default-constructed union holds.
6585
+ *
6586
+ * A default-constructed `TaggedUnion` has to hold SOMETHING, and which arm it
6587
+ * picks is arbitrary -- the emitter only ever produces one to declare an SSA
6588
+ * slot that is assigned before it is read. Arm 0 is not always a legal choice:
6589
+ * `FunctionValue` deliberately has no default (a Function arm holding a
6590
+ * non-function is the bug its constructors exist to catch), so picking it
6591
+ * aborts a program that has done nothing wrong. Arms opt out here and the
6592
+ * default walks past them.
6593
+ */
6594
+ template <typename Arm>
6595
+ struct UnionDefaultArm : std::true_type {};
6596
+
6597
+ /** The first arm that may be default-constructed; 0 when none opts in. */
6598
+ template <typename... Arms>
6599
+ struct DefaultUnionArmIndex {
6600
+ static constexpr std::size_t value = [] {
6601
+ constexpr bool allowed[] = {UnionDefaultArm<Arms>::value...};
6602
+ for (std::size_t index = 0; index < sizeof...(Arms); ++index) {
6603
+ if (allowed[index]) return index;
6604
+ }
6605
+ return static_cast<std::size_t>(0);
6606
+ }();
6607
+ };
6608
+
6547
6609
  /** Lifetime dispatch over a closed arm set, by index: a raw byte buffer holds whichever arm is live, so construct/copy/move/destroy need a hand-written vtable-by-recursion in place of what the language generates for a real union of non-trivial arms. */
6548
6610
  template <typename... Arms>
6549
6611
  struct TaggedUnionOps;
@@ -6588,7 +6650,9 @@ class TaggedUnion {
6588
6650
  }(std::index_sequence_for<Arms...>{});
6589
6651
  }
6590
6652
 
6591
- TaggedUnion() : index_(0) { detail::TaggedUnionOps<Arms...>::defaultConstruct(0, &storage_); }
6653
+ TaggedUnion() : index_(detail::DefaultUnionArmIndex<Arms...>::value) {
6654
+ detail::TaggedUnionOps<Arms...>::defaultConstruct(index_, &storage_);
6655
+ }
6592
6656
 
6593
6657
  TaggedUnion(const TaggedUnion& other) : index_(other.index_) {
6594
6658
  detail::TaggedUnionOps<Arms...>::copyConstruct(index_, &storage_, &other.storage_);
@@ -10021,6 +10085,19 @@ class FunctionValue : public Value {
10021
10085
  }
10022
10086
  };
10023
10087
 
10088
+ namespace detail {
10089
+ /**
10090
+ * Never the arm a default-constructed union holds.
10091
+ *
10092
+ * `FunctionValue()` aborts on purpose, so a union with a Function arm first
10093
+ * could not be declared at all: `gea_union_N v1;` -- the ordinary way the
10094
+ * emitter opens an SSA slot it assigns further down -- killed the process
10095
+ * before the assignment ran. `node:net`'s `Socket.pipe` is one such union.
10096
+ */
10097
+ template <>
10098
+ struct UnionDefaultArm<FunctionValue> : std::false_type {};
10099
+ }
10100
+
10024
10101
  namespace runtime {
10025
10102
  namespace string {
10026
10103
  inline std::size_t utf16Length(const std::string& value);
@@ -14682,6 +14759,50 @@ inline const Ref<DynamicObject>& Value::functionProperties() const {
14682
14759
 
14683
14760
  Value dynamicFunctionPrototypeGet(const PropertyKey& key);
14684
14761
 
14762
+ namespace detail {
14763
+ /**
14764
+ * The own, non-method surface of a boxed typed array: `length`, `byteLength`,
14765
+ * `byteOffset` and the element at a canonical numeric index (ECMA-262 10.4.5,
14766
+ * 23.2.3).
14767
+ *
14768
+ * A typed array reaches a box wherever a library types it `any` -- Node's
14769
+ * `'data'` chunk is the measured case, and `total += chunk.length` is what a
14770
+ * byte-counting handler does with it. The payload type names the element type
14771
+ * exactly, so these four are answerable from the box with no table. Every other
14772
+ * key keeps the refusal below: a method (`slice`, `toString`) needs a callable
14773
+ * this runtime does not mint for an erased view, and answering `undefined`
14774
+ * would be the plausible wrong answer.
14775
+ */
14776
+ template <typename Element>
14777
+ inline bool boxedTypedArrayOwnAs(const Value& value, const PropertyKey& key, Value& out) {
14778
+ using Handle = gea::Ref<TypedArray<Element>>;
14779
+ if (value.payloadType() != payloadTypeTagFor<Handle>()) return false;
14780
+ const Handle& view = value.as<Handle>();
14781
+ if (!view || key.isSymbol()) return false;
14782
+ std::size_t index = 0;
14783
+ if (arrayIndexOfKey(key, index)) {
14784
+ // 10.4.5.15: an index past the end reads `undefined`, never the prototype.
14785
+ out = index < view->size() ? Value::box(Value::Tag::Number, static_cast<double>((*view)[index])) : Value();
14786
+ return true;
14787
+ }
14788
+ if (key.isNumericSource()) return false;
14789
+ const std::string& name = key.text();
14790
+ if (name == "length") out = Value::box(Value::Tag::Number, static_cast<double>(view->size()));
14791
+ else if (name == "byteLength") out = Value::box(Value::Tag::Number, view->byteLength());
14792
+ else if (name == "byteOffset") out = Value::box(Value::Tag::Number, view->byteOffset());
14793
+ else return false;
14794
+ return true;
14795
+ }
14796
+
14797
+ inline bool boxedTypedArrayOwn(const Value& value, const PropertyKey& key, Value& out) {
14798
+ return boxedTypedArrayOwnAs<std::uint8_t>(value, key, out) || boxedTypedArrayOwnAs<std::int8_t>(value, key, out) ||
14799
+ boxedTypedArrayOwnAs<ClampedUint8>(value, key, out) || boxedTypedArrayOwnAs<std::int16_t>(value, key, out) ||
14800
+ boxedTypedArrayOwnAs<std::uint16_t>(value, key, out) || boxedTypedArrayOwnAs<std::int32_t>(value, key, out) ||
14801
+ boxedTypedArrayOwnAs<std::uint32_t>(value, key, out) || boxedTypedArrayOwnAs<float>(value, key, out) ||
14802
+ boxedTypedArrayOwnAs<double>(value, key, out);
14803
+ }
14804
+ } // namespace detail
14805
+
14685
14806
  inline Value Value::getProperty(const PropertyKey& key) const {
14686
14807
  return getProperty(key, *this);
14687
14808
  }
@@ -14739,6 +14860,8 @@ inline Value Value::getProperty(const PropertyKey& key, const Value& receiver) c
14739
14860
  if (tag_ == Tag::Object) {
14740
14861
  Value method;
14741
14862
  if (detail::boxedPromiseMethod(*this, key, method)) return method;
14863
+ Value own;
14864
+ if (detail::boxedTypedArrayOwn(*this, key, own)) return own;
14742
14865
  }
14743
14866
  if (tag_ == Tag::Object || tag_ == Tag::Function) detail::refuseOpaquePropertyAccess("a property read", key);
14744
14867
  // Every remaining box is a primitive, whose properties all live on a