@geastack/compiler 1.0.15 → 1.0.17

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.
Files changed (61) hide show
  1. package/dist/conversion/nodes.d.ts +31 -0
  2. package/dist/conversion/nodes.js +46 -2
  3. package/dist/conversion/record-view.d.ts +24 -0
  4. package/dist/conversion/record-view.js +54 -0
  5. package/dist/ir/certify.js +23 -1
  6. package/dist/ir/integers.js +24 -2
  7. package/dist/ir/lower-operands.d.ts +10 -1
  8. package/dist/ir/lower-operands.js +58 -2
  9. package/dist/ir/lower.js +12 -3
  10. package/dist/ir/reflection-demand.js +5 -0
  11. package/dist/ir/transfer.js +17 -8
  12. package/dist/plugins/apple/host.d.ts +2 -0
  13. package/dist/plugins/apple/host.js +13 -6
  14. package/dist/plugins/gea/host.d.ts +2 -0
  15. package/dist/plugins/gea/host.js +11 -4
  16. package/dist/plugins/host-package.d.ts +37 -0
  17. package/dist/plugins/host-package.js +28 -0
  18. package/dist/plugins/load.js +15 -0
  19. package/dist/plugins/webgl/host.d.ts +9 -3
  20. package/dist/plugins/webgl/host.js +15 -6
  21. package/dist/plugins/webgl/plugin.js +4 -1
  22. package/dist/representation/embedded-carriers.d.ts +22 -0
  23. package/dist/representation/embedded-carriers.js +105 -0
  24. package/dist/semantics/frontend.js +4 -1
  25. package/dist/semantics/model/operands.d.ts +17 -0
  26. package/dist/semantics/model/operations.d.ts +28 -1
  27. package/dist/semantics/normalize/flow/callable-reach.js +1 -3
  28. package/dist/semantics/normalize/global-host-mutations.js +2 -2
  29. package/dist/semantics/normalize/producers/allocations.js +2 -0
  30. package/dist/semantics/normalize/producers/bindings.js +5 -1
  31. package/dist/semantics/normalize/producers/boundary.d.ts +1 -0
  32. package/dist/semantics/normalize/producers/boundary.js +6 -2
  33. package/dist/semantics/normalize/producers/class-lifecycle.js +3 -0
  34. package/dist/semantics/normalize/producers/control.js +2 -2
  35. package/dist/semantics/normalize/producers/erasure.d.ts +7 -0
  36. package/dist/semantics/normalize/producers/erasure.js +19 -0
  37. package/dist/semantics/normalize/producers/exact-arms.d.ts +12 -0
  38. package/dist/semantics/normalize/producers/exact-arms.js +12 -0
  39. package/dist/semantics/normalize/producers/invocations.js +33 -1
  40. package/dist/semantics/normalize/reachability.d.ts +20 -5
  41. package/dist/semantics/normalize/reachability.js +119 -14
  42. package/dist/semantics/normalize/specialization.js +1 -4
  43. package/dist/targets/cpp/class-layout.d.ts +40 -0
  44. package/dist/targets/cpp/class-layout.js +112 -0
  45. package/dist/targets/cpp/class-properties/emit-class-properties.js +4 -4
  46. package/dist/targets/cpp/conversions.js +63 -1
  47. package/dist/targets/cpp/emit-callable.js +7 -6
  48. package/dist/targets/cpp/emit-context.d.ts +38 -1
  49. package/dist/targets/cpp/emit-context.js +28 -2
  50. package/dist/targets/cpp/emit-narrowing.d.ts +1 -1
  51. package/dist/targets/cpp/emit-narrowing.js +54 -1
  52. package/dist/targets/cpp/emit-properties.js +13 -3
  53. package/dist/targets/cpp/emit-record-view.js +31 -0
  54. package/dist/targets/cpp/emit.d.ts +3 -3
  55. package/dist/targets/cpp/emit.js +11 -5
  56. package/dist/targets/cpp/prototype/emit-prototype-regexp.d.ts +1 -1
  57. package/dist/targets/cpp/prototype/emit-prototype-regexp.js +58 -2
  58. package/dist/targets/cpp/translation-unit.js +32 -16
  59. package/package.json +5 -5
  60. package/src/targets/cpp/runtime/gea_dynamic_proxy.h +38 -0
  61. package/src/targets/cpp/runtime/gea_runtime.h +247 -45
@@ -40,6 +40,28 @@ export interface ConversionCensus {
40
40
  * node's id carries the operation's name and lives in its own table.
41
41
  */
42
42
  readonly coercionFor: (source: Representation, operation: CoercionOperation) => ConversionNode;
43
+ /**
44
+ * The exact-arm projection of a tagged union into the one arm whose carrier
45
+ * IS the target: `get<k>()` guarded by `is<k>()`, a `TypeError` otherwise.
46
+ * `null` where the source is not a tagged union or the target is not
47
+ * exactly one of its arms -- the pair then belongs to `nodeFor`.
48
+ *
49
+ * Its own table for the same reason coercions have one: the pair
50
+ * `tagged-union(...) -> arm` already names `nodeFor`'s dispatch node, which
51
+ * converts EVERY arm into the target (a callable arm through an adapter, a
52
+ * class arm through an upcast), and only the instruction's own node id says
53
+ * which of the two runs. Minted only for an owner whose declaration states
54
+ * `@gea-exact-arms` (`LoweringContext.exactArmNarrowing`): the projection
55
+ * trades a conversion the checker proved total for a runtime check the
56
+ * author vouched for, so nothing mints it on its own initiative.
57
+ *
58
+ * A `static` capability with `nativeFieldProtocol: 'unused'` and every
59
+ * transport preserved: the value handed on is the arm's own payload,
60
+ * untouched, so no field protocol, adapter or fresh alias is involved --
61
+ * which is exactly the proof the reflection census needs to leave the
62
+ * arm's parameters un-promoted.
63
+ */
64
+ readonly exactArmFor: (source: Representation, target: Representation) => ConversionNode | null;
43
65
  /** The node a `convert` instruction names, from whichever table minted it; `null` for an id no census minted. */
44
66
  readonly nodeById: (id: ConversionNodeId) => ConversionNode | null;
45
67
  /** Every node minted through `nodeFor` that the eager graph did not already hold. */
@@ -53,3 +75,12 @@ export declare const conversionNodeIdOf: (source: Representation, target: Repres
53
75
  /** The carrier each abstract operation lands on. */
54
76
  export declare const coercionTargetOf: (operation: CoercionOperation) => Representation;
55
77
  export declare const createConversionNodes: (input: ConversionCensusInput) => ConversionCensus;
78
+ /** The materializer id every exact-arm node carries; the printer dispatches its recipe on it. */
79
+ export declare const EXACT_ARM_MATERIALIZER = "gea::host::exactArm";
80
+ /**
81
+ * Which arm of `source` the target IS, by carrier key, or `null` when the
82
+ * source is not a tagged union or no single arm matches. A union's arms are
83
+ * pairwise disjoint at runtime, so two arms never share a key; the
84
+ * exactly-one test still guards the answer rather than trusting that.
85
+ */
86
+ export declare const exactArmIndexOf: (source: Representation, target: Representation) => number | null;
@@ -86,6 +86,50 @@ export const createConversionNodes = (input) => {
86
86
  coercions.set(id, node);
87
87
  return node;
88
88
  };
89
- const nodeById = (id) => input.nodes.get(id) ?? minted.get(id) ?? coercions.get(id) ?? null;
90
- return { nodeFor, coercionFor, nodeById, minted };
89
+ const exactArms = new Map();
90
+ const exactArmFor = (source, target) => {
91
+ const index = exactArmIndexOf(source, target);
92
+ if (index === null)
93
+ return null;
94
+ const id = `${representationKey(source)}->${representationKey(target)}#exact-arm`;
95
+ const remembered = exactArms.get(id);
96
+ if (remembered !== undefined)
97
+ return remembered;
98
+ const node = {
99
+ id,
100
+ source,
101
+ target,
102
+ capability: {
103
+ kind: 'static',
104
+ materializer: {
105
+ id: EXACT_ARM_MATERIALIZER,
106
+ domain: `static:exact-arm:${index}`,
107
+ allocates: false,
108
+ nativeFieldProtocol: 'unused',
109
+ nativePayloadTransport: 'preserved',
110
+ nativeClassReferenceIdentity: 'preserved',
111
+ callableIdentityTransport: 'preserved'
112
+ }
113
+ }
114
+ };
115
+ exactArms.set(id, node);
116
+ return node;
117
+ };
118
+ const nodeById = (id) => input.nodes.get(id) ?? minted.get(id) ?? coercions.get(id) ?? exactArms.get(id) ?? null;
119
+ return { nodeFor, coercionFor, exactArmFor, nodeById, minted };
120
+ };
121
+ /** The materializer id every exact-arm node carries; the printer dispatches its recipe on it. */
122
+ export const EXACT_ARM_MATERIALIZER = 'gea::host::exactArm';
123
+ /**
124
+ * Which arm of `source` the target IS, by carrier key, or `null` when the
125
+ * source is not a tagged union or no single arm matches. A union's arms are
126
+ * pairwise disjoint at runtime, so two arms never share a key; the
127
+ * exactly-one test still guards the answer rather than trusting that.
128
+ */
129
+ export const exactArmIndexOf = (source, target) => {
130
+ if (source.kind !== 'tagged-union')
131
+ return null;
132
+ const targetKey = representationKey(target);
133
+ const matches = source.arms.flatMap((arm, index) => (representationKey(arm.value) === targetKey ? [index] : []));
134
+ return matches.length === 1 ? (matches[0] ?? null) : null;
91
135
  };
@@ -95,6 +95,13 @@ export interface RecastArmPlan {
95
95
  readonly index: number;
96
96
  readonly via: 'exact' | 'convert' | RecordViewPlan;
97
97
  }
98
+ /** One source arm of a `dispatch` plan, in the source's own arm order. */
99
+ export interface DispatchArmPlan {
100
+ readonly via: 'exact' | 'convert' | 'absent' | RecordViewPlan;
101
+ }
102
+ export declare const isRecordViewTarget: (value: Representation) => value is RecordViewTarget;
103
+ /** Whether this view dispatches on a sum's live arm somewhere along its spine (`dispatch`, possibly under an optional or an assert). */
104
+ export declare const recordViewDispatchesArms: (plan: RecordViewPlan) => boolean;
98
105
  export type RecordViewPlan = {
99
106
  readonly kind: 'owned';
100
107
  readonly plan: OwnedRecordPlan;
@@ -132,6 +139,23 @@ export type RecordViewPlan = {
132
139
  readonly source: TaggedUnion;
133
140
  readonly target: TaggedUnion;
134
141
  readonly arms: readonly RecastArmPlan[];
142
+ }
143
+ /**
144
+ * A sum every arm of which reaches ONE record-shaped target: dispatched on
145
+ * the live arm, never selected. `const ctx: AudioContextLike | null = Ctor ?
146
+ * new Ctor() : createNativeAudioContext()` stores a `record | class` into
147
+ * the interface's slot; the class arm is a structural view of the record
148
+ * and the record arm is itself. Selecting the exact arm (`get<k>()`) read
149
+ * the class instance's bytes as the record whenever the native host was the
150
+ * live one. An `absent` arm is the target optional's own absence.
151
+ */
152
+ | {
153
+ readonly kind: 'dispatch';
154
+ readonly source: TaggedUnion;
155
+ readonly target: RecordViewTarget | Extract<Representation, {
156
+ kind: 'optional';
157
+ }>;
158
+ readonly arms: readonly DispatchArmPlan[];
135
159
  } | {
136
160
  readonly kind: 'fields';
137
161
  readonly source: RecordViewSource;
@@ -47,6 +47,9 @@ export const ownedRecordMaterializationPlan = (source, target, layouts) => {
47
47
  }
48
48
  return { kind: 'record', source, target, fields: planned };
49
49
  };
50
+ export const isRecordViewTarget = (value) => value.kind === 'record' || (value.kind === 'native-record-ref' && value.native === null);
51
+ /** Whether this view dispatches on a sum's live arm somewhere along its spine (`dispatch`, possibly under an optional or an assert). */
52
+ export const recordViewDispatchesArms = (plan) => plan.kind === 'dispatch' || ((plan.kind === 'optional' || plan.kind === 'assert') && recordViewDispatchesArms(plan.payload));
50
53
  /**
51
54
  * Effects of an already-admitted view, not another conversion predicate.
52
55
  * Copying a held carrier (including an already-dynamic cell) reads native
@@ -68,6 +71,8 @@ export const recordViewUsesOnlyDirectFields = (plan) => {
68
71
  }
69
72
  case 'recast-union':
70
73
  return plan.arms.every((arm) => arm.via === 'exact' || (arm.via !== 'convert' && recordViewUsesOnlyDirectFields(arm.via)));
74
+ case 'dispatch':
75
+ return plan.arms.every((arm) => arm.via === 'exact' || arm.via === 'absent' || (arm.via !== 'convert' && recordViewUsesOnlyDirectFields(arm.via)));
71
76
  case 'fields':
72
77
  return (!plan.expando &&
73
78
  plan.indexes.length === 0 &&
@@ -128,6 +133,14 @@ export const structuralRecordViewPlan = (layouts, source, target, convertible) =
128
133
  const home = exact.length === 1 ? exact[0] : undefined;
129
134
  return home === undefined ? null : { kind: 'arm', source, target, index: home.index, payload: home.payload };
130
135
  }
136
+ // A bare sum carrying the target optional's absence as an ARM: the absent
137
+ // arm is the optional's empty state and every other arm dispatches into
138
+ // the payload, so the optional cannot be peeled first here.
139
+ if (target.kind === 'optional' && source.kind === 'tagged-union' && isRecordViewTarget(target.payload)) {
140
+ const dispatched = dispatchUnionPlan(layouts, source, target, convertible);
141
+ if (dispatched !== null)
142
+ return dispatched;
143
+ }
131
144
  // The optional wrapper is peeled on both sides first: the question is about
132
145
  // the two RECORD carriers, and an absence is answered by the optional's own
133
146
  // presence flag either way.
@@ -151,6 +164,8 @@ export const structuralRecordViewPlan = (layouts, source, target, convertible) =
151
164
  }
152
165
  if (source.kind === 'tagged-union' && target.kind === 'tagged-union')
153
166
  return recastUnionPlan(layouts, source, target, convertible);
167
+ if (source.kind === 'tagged-union' && isRecordViewTarget(target))
168
+ return dispatchUnionPlan(layouts, source, target, convertible);
154
169
  const sourceIsRecord = source.kind === 'record' || source.kind === 'record-with-index' || source.kind === 'native-record-ref';
155
170
  if (!sourceIsRecord && (source.kind !== 'class-ref' || source.ownership !== 'shared-refcount'))
156
171
  return null;
@@ -244,6 +259,45 @@ export const structuralRecordViewPlan = (layouts, source, target, convertible) =
244
259
  source.indexes[0]?.value.kind === 'dynamic';
245
260
  return { kind: 'fields', source, target, fields, indexes, expando };
246
261
  };
262
+ /**
263
+ * Every arm of `source` reaching the one record-shaped `target` (or the
264
+ * payload of an optional one), in the source's own arm order.
265
+ *
266
+ * Answered only when some arm reaches the target through a VIEW: a sum whose
267
+ * arms are all exact or chain-convertible is the printer chain's own dispatch
268
+ * (`emit-narrowing.ts`'s `taggedUnionArmText`), and repeating that answer
269
+ * here would move every such program for nothing. An arm with no way to the
270
+ * target refuses the whole plan: a dispatch that skipped an arm would be the
271
+ * unchecked selection this plan exists to replace.
272
+ */
273
+ const dispatchUnionPlan = (layouts, source, target, convertible) => {
274
+ const payload = target.kind === 'optional' ? target.payload : target;
275
+ if (!isRecordViewTarget(payload))
276
+ return null;
277
+ const key = representationKey(payload);
278
+ const arms = [];
279
+ let viewed = false;
280
+ for (const arm of source.arms) {
281
+ if (representationKey(arm.value) === key) {
282
+ arms.push({ via: 'exact' });
283
+ continue;
284
+ }
285
+ if (target.kind === 'optional' && arm.value.kind === target.absence) {
286
+ arms.push({ via: 'absent' });
287
+ continue;
288
+ }
289
+ if (convertible(arm.value, payload)) {
290
+ arms.push({ via: 'convert' });
291
+ continue;
292
+ }
293
+ const view = structuralRecordViewPlan(layouts, arm.value, payload, convertible);
294
+ if (view === null)
295
+ return null;
296
+ viewed = true;
297
+ arms.push({ via: view });
298
+ }
299
+ return viewed ? { kind: 'dispatch', source, target, arms } : null;
300
+ };
247
301
  const recastUnionPlan = (layouts, source, target, convertible) => {
248
302
  const arms = [];
249
303
  for (const arm of source.arms) {
@@ -1,4 +1,5 @@
1
1
  import { operationOfResult } from '../identity/ids.js';
2
+ import { forEachEmbeddedRepresentation } from '../representation/embedded-carriers.js';
2
3
  import { captureCapabilityOf, representationKey } from '../representation/model.js';
3
4
  import { propertyAccessKeysOf } from './certify/property-access.js';
4
5
  import { runtimeHelperKeysOf } from './certify/runtime-helper.js';
@@ -64,6 +65,24 @@ const physicalTypeDemand = (manifest, representation) => {
64
65
  const nativeBoundaryDemand = (representation) => representation.kind === 'native-handle'
65
66
  ? { key: `native-boundary:${representation.native ?? representation.protocol}@${representation.version}` }
66
67
  : null;
68
+ /**
69
+ * The native-boundary demands of every carrier a representation EMBEDS. The
70
+ * root's own demand is `nativeBoundaryDemand`; this is the rest of the tree
71
+ * -- the fields of a record, the arms of a union, a callable's signature --
72
+ * because the emitted C++ spells all of it, and a `native-handle` a struct
73
+ * field names must be as authenticated as one an SSA value holds. Asked of
74
+ * the root alone, lib.dom's `Window` certified with `Navigator`/`History`
75
+ * fields whose tag types no host declared, and clang was the first to say so.
76
+ */
77
+ const embeddedNativeBoundaryDemands = (representation) => {
78
+ const demands = [];
79
+ forEachEmbeddedRepresentation(representation, (carrier) => {
80
+ const boundary = nativeBoundaryDemand(carrier);
81
+ if (boundary)
82
+ demands.push(boundary);
83
+ });
84
+ return demands;
85
+ };
67
86
  const abiDemands = (manifest, abi) => {
68
87
  if (abi === null)
69
88
  return [];
@@ -72,7 +91,8 @@ const abiDemands = (manifest, abi) => {
72
91
  carriers.push(abi.receiver);
73
92
  return carriers.flatMap((carrier) => {
74
93
  const boundary = nativeBoundaryDemand(carrier);
75
- return boundary ? [physicalTypeDemand(manifest, carrier), boundary] : [physicalTypeDemand(manifest, carrier)];
94
+ const embedded = embeddedNativeBoundaryDemands(carrier);
95
+ return boundary ? [physicalTypeDemand(manifest, carrier), boundary, ...embedded] : [physicalTypeDemand(manifest, carrier), ...embedded];
76
96
  });
77
97
  };
78
98
  /**
@@ -462,6 +482,8 @@ export const certifyIr = (input) => {
462
482
  const boundary = nativeBoundaryDemand(representation);
463
483
  if (boundary)
464
484
  decide(owner, boundary, ctx);
485
+ for (const demand of embeddedNativeBoundaryDemands(representation))
486
+ decide(owner, demand, ctx);
465
487
  }
466
488
  for (const blockId of body.blockOrder) {
467
489
  const block = body.blocks.get(blockId);
@@ -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 === '%') {
@@ -8,7 +8,7 @@ import { type CallableAbi, type RecordField, type Representation } from '../repr
8
8
  import type { SealedRepresentationPlan } from '../representation/plan.js';
9
9
  import type { SemanticGraph } from '../semantics/model/graph.js';
10
10
  import { type SemanticOperand } from '../semantics/model/operands.js';
11
- import type { SemanticOperation } from '../semantics/model/operations.js';
11
+ import { type SemanticOperation } from '../semantics/model/operations.js';
12
12
  import type { IrBodyBuilder } from './build.js';
13
13
  import type { PendingShortCircuit } from './lower-short-circuit.js';
14
14
  import type { IrBlockId, IrOperand } from './model.js';
@@ -139,6 +139,15 @@ export interface LoweringContext {
139
139
  readonly values: Map<SemanticResultId, IrValueId>;
140
140
  /** Optional-chain expressions whose merge is recorded but not yet closed (`lower-short-circuit.ts`). */
141
141
  readonly shortCircuits: Map<SemanticResultId, PendingShortCircuit>;
142
+ /**
143
+ * Whether this owner's declaration states `@gea-exact-arms`
144
+ * (`AllocationOperation.exactArms`): a tagged-union operand entering a slot
145
+ * that is exactly one of its arms takes the census's exact-arm projection
146
+ * instead of `nodeFor`'s per-arm dispatch. Per owner rather than per site
147
+ * because the tag is a statement about the body's own guards, which the
148
+ * lowering has no way to evaluate site by site.
149
+ */
150
+ readonly exactArmNarrowing: boolean;
142
151
  }
143
152
  export declare const describeOperand: (operand: SemanticOperand) => string;
144
153
  /** A published result's carrier plus the SSA id already minted for it -- the only legal source for an operand that names a prior result. */
@@ -2,6 +2,7 @@ import { detachedMethodAbiOf, isClosedContiguousTupleRecord } from '../projectio
2
2
  import { classMemberOf } from '../projection/fields.js';
3
3
  import { representationKey } from '../representation/model.js';
4
4
  import { operandOf, resultOf } from '../semantics/model/operands.js';
5
+ import { conversionRoleTargetOf } from '../semantics/model/operations.js';
5
6
  import { anchorResultOf, IrLoweringBlockedError, requireRepresentation } from './lower-graph.js';
6
7
  /**
7
8
  * Whether a property read's receiver+key names a field an installed plugin
@@ -181,7 +182,10 @@ export const requireResultRepresentation = (ctx, operation, role, describe) => {
181
182
  export const convertTo = (ctx, block, lineage, operand, slot, via = 'slot') => {
182
183
  if (representationKey(operand.representation) === representationKey(slot))
183
184
  return operand;
184
- const node = ctx.program.conversions.nodeFor(operand.representation, slot);
185
+ // The owner's own declaration asked for the projection; where the slot is
186
+ // not exactly one arm the census answers `null` and the ordinary pair runs.
187
+ const exact = ctx.exactArmNarrowing ? ctx.program.conversions.exactArmFor(operand.representation, slot) : null;
188
+ const node = exact ?? ctx.program.conversions.nodeFor(operand.representation, slot);
185
189
  if (node.capability.kind === 'never')
186
190
  return null;
187
191
  const value = ctx.builder.convert(block, lineage, node.id, operand, slot);
@@ -233,12 +237,64 @@ export const enter = (ctx, block, lineage, operation, operand, resolved) => {
233
237
  // value it then re-viewed.
234
238
  if (answer.source === 'alias')
235
239
  return resolved;
236
- const entered = convertTo(ctx, block, lineage, resolved, answer.representation);
240
+ const entered = exactArmEntry(ctx, block, lineage, operation, operand, resolved, answer.representation) ??
241
+ convertTo(ctx, block, lineage, resolved, answer.representation) ??
242
+ assertedArmEntry(ctx, block, lineage, operand, resolved, answer.representation);
237
243
  if (entered !== null)
238
244
  return entered;
239
245
  recordDrift(ctx, block, operation.id, operand.role, operand.ordinal, resolved.representation, answer.representation);
240
246
  return resolved;
241
247
  };
248
+ /**
249
+ * An argument entering an `@gea-exact-arms` callee's union slot through the
250
+ * arm the resolved overload named (`ConversionRoleTarget.owner`'s
251
+ * `exact-arm`): the value converted into THAT arm's carrier, then wrapped --
252
+ * an exact wrap, since the arm's own key is one of the union's. `null` where
253
+ * the call published no such role, the slot is not a union, or the named
254
+ * type is not an arm of it, and the ordinary slot conversion runs instead.
255
+ *
256
+ * Two `convert`s rather than one so each names a census node the certificate
257
+ * already knows how to read: source-into-arm is the same pair a direct call
258
+ * of the overload would mint, and arm-into-union is the identity wrap.
259
+ */
260
+ const exactArmEntry = (ctx, block, lineage, operation, operand, resolved, slot) => {
261
+ if (slot.kind !== 'tagged-union' || operand.role !== 'argument')
262
+ return null;
263
+ const target = conversionRoleTargetOf(operation, 'argument', operand.ordinal, 'exact-arm');
264
+ if (target === undefined)
265
+ return null;
266
+ const arm = slot.arms.find((candidate) => candidate.semanticType === target.type) ??
267
+ (() => {
268
+ const key = representationKey(ctx.constantDeriver.layoutOf(target.type));
269
+ return slot.arms.find((candidate) => representationKey(candidate.value) === key);
270
+ })();
271
+ if (arm === undefined)
272
+ return null;
273
+ const held = convertTo(ctx, block, lineage, resolved, arm.value, 'exact-arm');
274
+ return held === null ? null : convertTo(ctx, block, lineage, held, slot, 'exact-arm');
275
+ };
276
+ /**
277
+ * A value the program's own type assertion states the arm of
278
+ * (`SemanticOperand.asserted`), entering a slot that is exactly one arm of
279
+ * its union, where the ordinary pair has no recipe: the census's exact-arm
280
+ * projection, checked at runtime. Asked only AFTER `convertTo` declined, so
281
+ * a pair the census answers soundly for every arm keeps that answer and the
282
+ * assertion changes nothing -- exactly as it changes nothing in JavaScript.
283
+ * The pair the census declines is the one where an arm has no home in the
284
+ * slot at all (`conversions.ts`'s `classArmWithoutHome`): there the only
285
+ * alternatives are refusing the program or reading the wrong arm's bytes,
286
+ * and the author has written which arm it is.
287
+ */
288
+ const assertedArmEntry = (ctx, block, lineage, operand, resolved, slot) => {
289
+ if (operand.asserted !== true)
290
+ return null;
291
+ const node = ctx.program.conversions.exactArmFor(resolved.representation, slot);
292
+ if (node === null)
293
+ return null;
294
+ const value = ctx.builder.convert(block, lineage, node.id, resolved, slot);
295
+ traceSpeculativeLoad(lineage, 'asserted-arm', node, value);
296
+ return { value, representation: slot };
297
+ };
242
298
  /** The row `LoweringProgram.drift` keeps for a pair the census has no recipe for. */
243
299
  const recordDrift = (ctx, block, operation, role, ordinal, source, slot, node = ctx.program.conversions.nodeFor(source, slot)) => {
244
300
  ctx.program.drift.push({
package/dist/ir/lower.js CHANGED
@@ -1050,7 +1050,9 @@ const iteratorCloseRegionsOf = (graph, membership, order, blockStarts, flow, ctx
1050
1050
  };
1051
1051
  const lowerOwner = (graph, plan, constantDeriver, program, abi, construct, abiRefusal, owner, operationIds, plugins, ownerEdges, regionParts, deferredIteratorCloses,
1052
1052
  /** Whether this owner is a `function*` -- see `generatorPrologueBoundary` below. */
1053
- isGenerator) => {
1053
+ isGenerator,
1054
+ /** Whether this owner's declaration states `@gea-exact-arms` -- see `LoweringContext.exactArmNarrowing`. */
1055
+ exactArmNarrowing) => {
1054
1056
  // Membership first: the execution order below keeps each branch's operations
1055
1057
  // contiguous, which it can only do once every operation's scope chain is
1056
1058
  // known. Nothing in the membership reads a position from the list it is
@@ -1067,7 +1069,8 @@ isGenerator) => {
1067
1069
  abiRefusal,
1068
1070
  builder,
1069
1071
  values: new Map(),
1070
- shortCircuits: new Map()
1072
+ shortCircuits: new Map(),
1073
+ exactArmNarrowing
1071
1074
  };
1072
1075
  const blockStarts = new Map();
1073
1076
  const finiteCloseStarts = new Map();
@@ -1265,6 +1268,10 @@ export const lowerToIr = (input) => {
1265
1268
  const bodies = new Map();
1266
1269
  const blocked = [];
1267
1270
  const functionFacts = new Map();
1271
+ // Owners whose declaration states `@gea-exact-arms`. Kept apart from
1272
+ // `functionFacts`, which is spread onto the emitted body: the tag changes
1273
+ // how the body LOWERS and is nothing the body needs to carry afterwards.
1274
+ const exactArmOwners = new Set();
1268
1275
  // The graph is sealed: its loop-owned closes are identical for every body.
1269
1276
  // Collect once instead of scanning the entire program for each function.
1270
1277
  const deferredIteratorCloses = new Set();
@@ -1278,6 +1285,8 @@ export const lowerToIr = (input) => {
1278
1285
  functionLength: operation.functionLength ?? 0,
1279
1286
  generator: operation.generatorFunction === true
1280
1287
  });
1288
+ if (operation.exactArms === true)
1289
+ exactArmOwners.add(operation.callable);
1281
1290
  }
1282
1291
  }
1283
1292
  // A function whose body is empty owns no operations, so grouping the graph by
@@ -1333,7 +1342,7 @@ export const lowerToIr = (input) => {
1333
1342
  const construct = input.constructs.get(owner) ?? null;
1334
1343
  const abiRefusal = input.abiBlockers.get(owner) ?? null;
1335
1344
  const facts = functionFacts.get(owner);
1336
- const body = lowerOwner(input.graph, input.plan, constantDeriver, program, abi, construct, abiRefusal, owner, operationIds, input.plugins, edgesByOwner.get(owner) ?? noEdges, regionParts, deferredIteratorCloses, facts?.generator === true);
1345
+ const body = lowerOwner(input.graph, input.plan, constantDeriver, program, abi, construct, abiRefusal, owner, operationIds, input.plugins, edgesByOwner.get(owner) ?? noEdges, regionParts, deferredIteratorCloses, facts?.generator === true, exactArmOwners.has(owner));
1337
1346
  bodies.set(body.owner, facts === undefined ? body : { ...body, ...facts });
1338
1347
  }
1339
1348
  catch (error) {
@@ -1219,6 +1219,11 @@ export const reflectionExposureOf = (bodies, classes, deriver, options) => {
1219
1219
  // publish those inputs to a dynamic source listener. That publication
1220
1220
  // happens inside generated adapter code, not in a separate IR call.
1221
1221
  const targetAbi = unwrappedCallableAbi(operation.result.representation);
1222
+ // Under `GEA_REFLECTION_DEBUG`, the site too: a row's reasons say WHAT
1223
+ // was promoted, and an adapter's cost is only removable at the
1224
+ // conversion that installs it.
1225
+ if (reflectionWatch !== undefined && targetAbi !== null)
1226
+ console.error(`[REFLECTION-ADAPTER] lineage=${String(operation.lineage)} node=${operation.conversionUse} capability=${node?.capability.kind ?? 'none'}`);
1222
1227
  if (targetAbi !== null) {
1223
1228
  if (targetAbi.receiver !== null)
1224
1229
  promoteFull(targetAbi.receiver, 'callable-adapter-input');
@@ -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;
@@ -113,6 +113,8 @@ export declare const appleNativeTypes: () => ReadonlyMap<string, string>;
113
113
  export declare const appleMemberTables: () => AppleHostShimSlice | null;
114
114
  /** The SDK's own platform description, one step upstream of the shim tables; empty when the packages are not installed. */
115
115
  export declare const appleBridgeMetadata: () => AppleBridgeMetadataSlice;
116
+ /** The package entry the CLI loaded is the one this host's tables come from -- see `plugins/host-package.ts`. */
117
+ export declare const adoptApplePackage: (file: string) => void;
116
118
  /**
117
119
  * Each declaration file of the Apple SDK package, to the native-only marker the
118
120
  * module implementing that same subpath calls.
@@ -1,7 +1,7 @@
1
- import { createRequire } from 'node:module';
1
+ import { createHostPackageLoader } from '../host-package.js';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, resolve as resolvePath } from 'node:path';
4
- const require_ = createRequire(import.meta.url);
4
+ const packageLoader = createHostPackageLoader(import.meta.url);
5
5
  /**
6
6
  * `nativeTypes`, narrowed at the boundary.
7
7
  *
@@ -66,8 +66,8 @@ const appleHostShims = () => {
66
66
  let sdk;
67
67
  let plugin;
68
68
  try {
69
- sdk = require_(sdkSpecifier);
70
- plugin = require_(pluginSpecifier);
69
+ sdk = packageLoader.load(sdkSpecifier);
70
+ plugin = packageLoader.load(pluginSpecifier);
71
71
  }
72
72
  catch (error) {
73
73
  const requested = process.env.GEATSC2_APPLE_SDK !== undefined || process.env.GEATSC2_APPLE_PLUGIN !== undefined;
@@ -104,6 +104,13 @@ export const appleBridgeMetadata = () => {
104
104
  return loadedMetadata ?? {};
105
105
  };
106
106
  let loadedMarkers = null;
107
+ /** The package entry the CLI loaded is the one this host's tables come from -- see `plugins/host-package.ts`. */
108
+ export const adoptApplePackage = (file) => {
109
+ packageLoader.adopt(file);
110
+ loaded = undefined;
111
+ loadedMetadata = null;
112
+ loadedMarkers = null;
113
+ };
107
114
  /** The directory of the package a resolved entry point belongs to, by its own manifest. */
108
115
  const packageRootOf = (entry, name) => {
109
116
  let directory = dirname(entry);
@@ -159,7 +166,7 @@ export const appleModuleMarkers = () => {
159
166
  if (used.size === 0)
160
167
  return markers;
161
168
  const specifier = process.env.GEATSC2_APPLE_SDK ?? '@geastack/apple';
162
- const root = packageRootOf(require_.resolve(specifier), specifier);
169
+ const root = packageRootOf(packageLoader.resolve(specifier), specifier);
163
170
  if (root === null)
164
171
  return markers;
165
172
  const manifest = JSON.parse(readFileSync(resolvePath(root, 'package.json'), 'utf8'));
@@ -243,7 +250,7 @@ export const appleClassConstructorParameters = (className) => {
243
250
  */
244
251
  export const writeAppleBridgeArtifacts = (metadataPath, outDir) => {
245
252
  const pluginSpecifier = process.env.GEATSC2_APPLE_PLUGIN ?? '@geastack/geatsc-plugin-apple-native';
246
- const plugin = require_(pluginSpecifier);
253
+ const plugin = packageLoader.load(pluginSpecifier);
247
254
  const factory = plugin.appleNativePlugin;
248
255
  if (typeof factory !== 'function')
249
256
  throw new Error(`'${pluginSpecifier}' exports no appleNativePlugin function`);
@@ -145,6 +145,8 @@ export interface GeaHostClass {
145
145
  readonly construct?: string;
146
146
  readonly factory?: string;
147
147
  }
148
+ /** The package entry the CLI loaded is the one this host's tables come from -- see `plugins/host-package.ts`. */
149
+ export declare const adoptGeaPackage: (file: string) => void;
148
150
  /**
149
151
  * The host type table, from the package that owns it.
150
152
  *
@@ -1,6 +1,6 @@
1
- import { createRequire } from 'node:module';
1
+ import { createHostPackageLoader } from '../host-package.js';
2
2
  import { templateArity } from '../../targets/cpp/host/host-members.js';
3
- const require_ = createRequire(import.meta.url);
3
+ const packageLoader = createHostPackageLoader(import.meta.url);
4
4
  /**
5
5
  * `nativeTypes`, narrowed at the boundary.
6
6
  *
@@ -22,6 +22,13 @@ const nativeTypesOf = (value) => {
22
22
  let loaded;
23
23
  let loadedSlice = null;
24
24
  let loadedInterop = null;
25
+ /** The package entry the CLI loaded is the one this host's tables come from -- see `plugins/host-package.ts`. */
26
+ export const adoptGeaPackage = (file) => {
27
+ packageLoader.adopt(file);
28
+ loaded = undefined;
29
+ loadedSlice = null;
30
+ loadedInterop = null;
31
+ };
25
32
  /**
26
33
  * The host type table, from the package that owns it.
27
34
  *
@@ -62,7 +69,7 @@ export const geaNativeTypes = () => {
62
69
  const specifier = requested ?? '@geastack/geatsc-plugin-gea/host-shims';
63
70
  let shims;
64
71
  try {
65
- shims = require_(specifier);
72
+ shims = packageLoader.load(specifier);
66
73
  }
67
74
  catch (error) {
68
75
  // Not installed is a configuration; anything else is a defect. The
@@ -375,7 +382,7 @@ export const geaCanvasInterop = () => {
375
382
  : requested.endsWith(shimSuffix)
376
383
  ? `${requested.slice(0, requested.length - shimSuffix.length)}cpp-ir`
377
384
  : requested;
378
- const loaded = require_(specifier);
385
+ const loaded = packageLoader.load(specifier);
379
386
  const generate = loaded
380
387
  .directCanvasInteropSource;
381
388
  if (typeof generate !== 'function') {