@geastack/compiler 1.0.16 → 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.
- package/dist/conversion/nodes.d.ts +31 -0
- package/dist/conversion/nodes.js +46 -2
- package/dist/conversion/record-view.d.ts +24 -0
- package/dist/conversion/record-view.js +54 -0
- package/dist/ir/certify.js +23 -1
- package/dist/ir/lower-operands.d.ts +10 -1
- package/dist/ir/lower-operands.js +58 -2
- package/dist/ir/lower.js +12 -3
- package/dist/ir/reflection-demand.js +5 -0
- package/dist/plugins/apple/host.d.ts +2 -0
- package/dist/plugins/apple/host.js +13 -6
- package/dist/plugins/gea/host.d.ts +2 -0
- package/dist/plugins/gea/host.js +11 -4
- package/dist/plugins/host-package.d.ts +37 -0
- package/dist/plugins/host-package.js +28 -0
- package/dist/plugins/load.js +15 -0
- package/dist/plugins/webgl/host.d.ts +9 -3
- package/dist/plugins/webgl/host.js +15 -6
- package/dist/plugins/webgl/plugin.js +4 -1
- package/dist/representation/embedded-carriers.d.ts +22 -0
- package/dist/representation/embedded-carriers.js +105 -0
- package/dist/semantics/model/operands.d.ts +17 -0
- package/dist/semantics/model/operations.d.ts +28 -1
- package/dist/semantics/normalize/producers/allocations.js +2 -0
- package/dist/semantics/normalize/producers/bindings.js +5 -1
- package/dist/semantics/normalize/producers/boundary.d.ts +1 -0
- package/dist/semantics/normalize/producers/boundary.js +6 -2
- package/dist/semantics/normalize/producers/class-lifecycle.js +3 -0
- package/dist/semantics/normalize/producers/control.js +2 -2
- package/dist/semantics/normalize/producers/erasure.d.ts +7 -0
- package/dist/semantics/normalize/producers/erasure.js +19 -0
- package/dist/semantics/normalize/producers/exact-arms.d.ts +12 -0
- package/dist/semantics/normalize/producers/exact-arms.js +12 -0
- package/dist/semantics/normalize/producers/invocations.js +33 -1
- package/dist/targets/cpp/class-properties/emit-class-properties.js +4 -4
- package/dist/targets/cpp/conversions.js +63 -1
- package/dist/targets/cpp/emit-callable.js +5 -5
- package/dist/targets/cpp/emit-context.d.ts +38 -1
- package/dist/targets/cpp/emit-context.js +28 -2
- package/dist/targets/cpp/emit-narrowing.d.ts +1 -1
- package/dist/targets/cpp/emit-narrowing.js +47 -1
- package/dist/targets/cpp/emit-record-view.js +31 -0
- package/dist/targets/cpp/emit.d.ts +3 -3
- package/dist/targets/cpp/emit.js +11 -5
- package/dist/targets/cpp/translation-unit.js +24 -15
- package/package.json +1 -1
- 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;
|
package/dist/conversion/nodes.js
CHANGED
|
@@ -86,6 +86,50 @@ export const createConversionNodes = (input) => {
|
|
|
86
86
|
coercions.set(id, node);
|
|
87
87
|
return node;
|
|
88
88
|
};
|
|
89
|
-
const
|
|
90
|
-
|
|
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) {
|
package/dist/ir/certify.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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
|
|
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 =
|
|
70
|
-
plugin =
|
|
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(
|
|
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 =
|
|
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
|
*
|
package/dist/plugins/gea/host.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHostPackageLoader } from '../host-package.js';
|
|
2
2
|
import { templateArity } from '../../targets/cpp/host/host-members.js';
|
|
3
|
-
const
|
|
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 =
|
|
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 =
|
|
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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const absent = (error) => error.code === 'MODULE_NOT_FOUND';
|
|
3
|
+
export const createHostPackageLoader = (ownUrl) => {
|
|
4
|
+
const own = createRequire(ownUrl);
|
|
5
|
+
let adopted = null;
|
|
6
|
+
const attempt = (run) => {
|
|
7
|
+
const bases = adopted === null ? [own] : [adopted, own];
|
|
8
|
+
let missing = null;
|
|
9
|
+
for (const base of bases) {
|
|
10
|
+
try {
|
|
11
|
+
return run(base);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!absent(error))
|
|
15
|
+
throw error;
|
|
16
|
+
missing = error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw missing;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
load: (specifier) => attempt((base) => base(specifier)),
|
|
23
|
+
resolve: (specifier) => attempt((base) => base.resolve(specifier)),
|
|
24
|
+
adopt: (file) => {
|
|
25
|
+
adopted = createRequire(file);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|