@geastack/compiler 1.0.16 → 1.0.18

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 (48) 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/lower-operands.d.ts +10 -1
  7. package/dist/ir/lower-operands.js +58 -2
  8. package/dist/ir/lower.js +12 -3
  9. package/dist/ir/reflection-demand.js +5 -0
  10. package/dist/plugins/apple/host.d.ts +2 -0
  11. package/dist/plugins/apple/host.js +13 -6
  12. package/dist/plugins/apple/jsx.js +20 -2
  13. package/dist/plugins/gea/host.d.ts +2 -0
  14. package/dist/plugins/gea/host.js +11 -4
  15. package/dist/plugins/host-package.d.ts +37 -0
  16. package/dist/plugins/host-package.js +28 -0
  17. package/dist/plugins/load.js +15 -0
  18. package/dist/plugins/webgl/host.d.ts +9 -3
  19. package/dist/plugins/webgl/host.js +15 -6
  20. package/dist/plugins/webgl/plugin.js +4 -1
  21. package/dist/representation/embedded-carriers.d.ts +22 -0
  22. package/dist/representation/embedded-carriers.js +105 -0
  23. package/dist/semantics/model/operands.d.ts +17 -0
  24. package/dist/semantics/model/operations.d.ts +28 -1
  25. package/dist/semantics/normalize/producers/allocations.js +2 -0
  26. package/dist/semantics/normalize/producers/bindings.js +5 -1
  27. package/dist/semantics/normalize/producers/boundary.d.ts +1 -0
  28. package/dist/semantics/normalize/producers/boundary.js +6 -2
  29. package/dist/semantics/normalize/producers/class-lifecycle.js +3 -0
  30. package/dist/semantics/normalize/producers/control.js +2 -2
  31. package/dist/semantics/normalize/producers/erasure.d.ts +7 -0
  32. package/dist/semantics/normalize/producers/erasure.js +19 -0
  33. package/dist/semantics/normalize/producers/exact-arms.d.ts +12 -0
  34. package/dist/semantics/normalize/producers/exact-arms.js +12 -0
  35. package/dist/semantics/normalize/producers/invocations.js +33 -1
  36. package/dist/targets/cpp/class-properties/emit-class-properties.js +4 -4
  37. package/dist/targets/cpp/conversions.js +63 -1
  38. package/dist/targets/cpp/emit-callable.js +5 -5
  39. package/dist/targets/cpp/emit-context.d.ts +38 -1
  40. package/dist/targets/cpp/emit-context.js +28 -2
  41. package/dist/targets/cpp/emit-narrowing.d.ts +1 -1
  42. package/dist/targets/cpp/emit-narrowing.js +47 -1
  43. package/dist/targets/cpp/emit-record-view.js +31 -0
  44. package/dist/targets/cpp/emit.d.ts +3 -3
  45. package/dist/targets/cpp/emit.js +11 -5
  46. package/dist/targets/cpp/translation-unit.js +24 -15
  47. package/package.json +1 -1
  48. package/src/targets/cpp/runtime/gea_runtime.h +123 -44
@@ -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);
@@ -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');
@@ -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`);
@@ -74,6 +74,23 @@ const claimedElement = (node, carriers) => {
74
74
  * question is asked of it rather than of a list of class names kept here.
75
75
  */
76
76
  const childAdderOf = (carrier, members) => members.has(`${carrier}.addArrangedSubview`) ? 'addArrangedSubview' : 'addSubview';
77
+ /**
78
+ * The view a parent's children are added TO, which is not always the parent.
79
+ *
80
+ * A `UIVisualEffectView` (and a table or collection cell) owns a `contentView`
81
+ * and takes every subview through it; adding one to the effect view itself is
82
+ * a UIKit assertion, so the app aborts at launch inside `_addSubview:`. The
83
+ * member table again decides: a view that has both a `contentView` and its
84
+ * own `addSubview` is one whose content view is where children belong. A
85
+ * window has a `contentView` but no `addSubview`, so it is left as it is.
86
+ *
87
+ * The receiver is asserted non-null. `NSBox` declares its content view
88
+ * nullable (it can be unset), and the checker would otherwise refuse
89
+ * `box.contentView.addSubview(...)` for a box that has just been constructed
90
+ * and cannot be without one; on `UIVisualEffectView`, whose content view is
91
+ * not nullable, the assertion is a no-op the checker accepts.
92
+ */
93
+ const childReceiverOf = (view, carrier, members) => members.has(`${carrier}.contentView`) && members.has(`${carrier}.addSubview`) ? `${view}.contentView!` : view;
77
94
  /** The source text of a node, exactly as the author wrote it. */
78
95
  const sourceTextOf = (node, file) => node.getText(file);
79
96
  /**
@@ -220,15 +237,16 @@ const elementExpressionText = (claimed, file, carriers, members, nextName) => {
220
237
  statements.push(setter ?? `${view}.${attribute.name.text} = ${value};`);
221
238
  }
222
239
  const adder = childAdderOf(carrier, members);
240
+ const receiver = childReceiverOf(view, carrier, members);
223
241
  for (const child of claimed.children) {
224
242
  const nested = claimedElement(child, carriers);
225
243
  if (nested) {
226
- statements.push(`${view}.${adder}(${elementExpressionText(nested, file, carriers, members, nextName)});`);
244
+ statements.push(`${receiver}.${adder}(${elementExpressionText(nested, file, carriers, members, nextName)});`);
227
245
  continue;
228
246
  }
229
247
  if (ts.isJsxExpression(child)) {
230
248
  if (child.expression)
231
- statements.push(`${view}.${adder}(${sourceTextOf(child.expression, file)});`);
249
+ statements.push(`${receiver}.${adder}(${sourceTextOf(child.expression, file)});`);
232
250
  continue;
233
251
  }
234
252
  if (ts.isJsxText(child)) {
@@ -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') {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * How a built-in host adapter loads the package that states its tables.
3
+ *
4
+ * Each adapter reads its host's own statement -- gea's `host-shims` and
5
+ * `cpp-ir`, Apple's SDK fixture and plugin, the ANGLE plugin -- by package
6
+ * name, which resolves from THIS compiler's `node_modules`. That copy and the
7
+ * checkout a build names with `--plugin <path>` are two spellings of one
8
+ * package, and they drift: the compiler's is an `npm pack` snapshot, the path
9
+ * the build passes is the sibling checkout. A table added to the checkout then
10
+ * never reached a compile that named the checkout explicitly, and the failure
11
+ * surfaced far downstream with nothing saying why (an ambient-type
12
+ * realization stated by the checkout fell through to another host's by-name
13
+ * protocols; an absent global stated by the checkout stayed live and emitted
14
+ * lib.dom's whole `Window`).
15
+ *
16
+ * The stated path is the authority. When `plugins/load.ts` answers a legacy
17
+ * package entry with its built-in adapter, the adapter ADOPTS that file:
18
+ * every later load resolves from the file's own package first -- a package
19
+ * self-reference through its `exports`, or its own dependencies -- and falls
20
+ * back to the compiler's copy only for a specifier the stated package cannot
21
+ * see at all (Apple's SDK is a peer the plugin package does not carry).
22
+ * `MODULE_NOT_FOUND` is the only failure the fallback absorbs: a package that
23
+ * resolves and then fails to load is a defect wherever it lives, and the
24
+ * adapters' own two-failures rule (`gea/host.ts`) still tells it apart.
25
+ *
26
+ * Adoption must precede every read. An adapter table computed at module
27
+ * initialization would freeze the compiler's copy before the CLI ever saw
28
+ * `--plugin`, so the adapters read their tables lazily and `adopt` resets
29
+ * whatever they cached.
30
+ */
31
+ export interface HostPackageLoader {
32
+ readonly load: (specifier: string) => unknown;
33
+ readonly resolve: (specifier: string) => string;
34
+ /** Resolve from this file's package first; the caller resets its own caches. */
35
+ readonly adopt: (file: string) => void;
36
+ }
37
+ export declare const createHostPackageLoader: (ownUrl: string) => HostPackageLoader;