@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
@@ -17,7 +17,7 @@ import { createCppDocumentBuilder, emptyCppFacts, render, spliceRendered } from
17
17
  import { beginUnionAliasing, endUnionAliasing } from './types.js';
18
18
  import { cppConstructedThunkName, cppConstructThunkName, cppFormalName, cppReceiverName, cppThunkName, emitBody, isCppEmitBlockedError } from './emit.js';
19
19
  import { cppCaptureFieldName, cppCaptureReceiverFieldName, cppEnvironmentLocalName, cppEnvironmentParamName, cppEnvironmentSlotName, cppEnvironmentStructName, effectiveAbiOf, symbolKeyDefinitions, templateObjectDefinitions } from './emit-context.js';
20
- import { censusClassStaticFieldStorage, censusLazyArrowFields, classBoxable, cppFieldInitializerStatements, publishBoxableClasses, publishClassStaticFieldStorage, publishLazyArrowFieldPlans } from './class-layout.js';
20
+ import { censusClassStaticFieldStorage, censusConstantFieldInitializers, censusLazyArrowFields, classBoxable, cppFieldInitializerStatements, publishBoxableClasses, publishClassStaticFieldStorage, publishConstantFieldInitializers, publishLazyArrowFieldPlans } from './class-layout.js';
21
21
  import { classStaticFieldStorageRows, cppRecordDeclarations, cppStructNameOf, declaredFieldRepresentationOf, declaredRecordFieldOf, recordAccessorBodiesOf, representationNamesMintedStruct, withUnreadParametersUnnamed } from './records.js';
22
22
  import { prototypeReadHooks, virtualMethodEmission, virtualMethodFamiliesOf } from './virtual-methods.js';
23
23
  import { virtualDispatchVerdictOf } from '../../projection/dispatch.js';
@@ -27,7 +27,7 @@ import { classesConstructedUnobservably, functionsIgnoringTheirReceiver } from '
27
27
  import { renderJsonStructDeclarations } from './emit-json.js';
28
28
  import { alignedValueText } from './emit-narrowing.js';
29
29
  import { recordLayoutPolicyOf } from '../../projection/fields.js';
30
- import { cppAbiParameterType, cppAbiType, cppStringLiteral, cppStringViewLiteral, cppBodyName, cppCallableDeclarationTagName, cppCommonJsModuleName, cppCommonJsRecordName, cppNarrowedIntegerType, cppRefcountedReceiver, cppBoxedType, cppClassName, cppConstructName, cppGlobalName, cppInitializeName, cppRecordFieldName, cppRecordStructName, cppResultTypeOf, cppTypeOf, sanitizeForCppIdentifier } from './types.js';
30
+ import { cppAbiParameterType, cppAbiType, cppStringLiteral, cppBodyName, cppCallableDeclarationTagName, cppCommonJsModuleName, cppCommonJsRecordName, cppNarrowedIntegerType, cppRefcountedReceiver, cppBoxedType, cppClassName, cppConstructName, cppGlobalName, cppInitializeName, cppRecordFieldName, cppRecordStructName, cppResultTypeOf, cppTypeOf, sanitizeForCppIdentifier } from './types.js';
31
31
  import { buildHostMethodAliasIndex } from './host/host-method-aliases.js';
32
32
  import { cppRecursiveContainerDeclarations, cppRecursiveContainerTraceEdges } from './recursive-containers.js';
33
33
  /**
@@ -562,7 +562,7 @@ const signatureOf = (body, captures, narrowed = new Set(), narrowedResult = fals
562
562
  const formals = bodyFormalsOf(body.sourceOwner, effectiveAbi, admission, narrowed, borrowsReceiver(body), borrowed);
563
563
  return `${result} ${name}(${formals.join(', ')})`;
564
564
  };
565
- const thunkOf = (body, captures, narrowed, narrowedResult, linkage, preserveFunctionFacts) => {
565
+ const thunkOf = (body, captures, narrowed, narrowedResult, linkage) => {
566
566
  if (!body.abi)
567
567
  return null;
568
568
  const abi = body.abi;
@@ -601,17 +601,11 @@ const thunkOf = (body, captures, narrowed, narrowedResult, linkage, preserveFunc
601
601
  const construct = constructThunkOf(body, abi, hasEnvironment, linkage);
602
602
  return {
603
603
  prototype: [`${signature};`, ...(construct === null ? [] : [construct.prototype])].join('\n'),
604
- definition: [
605
- `${signature} { ${statement} }`,
606
- ...(!preserveFunctionFacts || body.functionSource === undefined
607
- ? []
608
- : [
609
- `[[maybe_unused]] static const bool ${cppThunkName(body.sourceOwner)}_source = ` +
610
- `gea::CallableObject<${cppAbiType(abi)}>::registerSource<&${cppThunkName(body.sourceOwner)}>(` +
611
- `${cppStringViewLiteral(body.functionName ?? '')}, ${body.functionLength ?? 0}, ${cppStringViewLiteral(body.functionSource)});`
612
- ]),
613
- ...(construct === null ? [] : [construct.definition])
614
- ].join('\n')
604
+ // No `registerSource` beside the thunk any more: the function's
605
+ // `name`/`length`/source text register at the sites that mint a function
606
+ // object from it (`emit-context.ts`'s `cppThunkEntryText`), so a function
607
+ // nothing reaches is not pinned into the binary by its own registration.
608
+ definition: [`${signature} { ${statement} }`, ...(construct === null ? [] : [construct.definition])].join('\n')
615
609
  };
616
610
  };
617
611
  /**
@@ -1395,6 +1389,13 @@ export const renderTranslationUnit = (input) => {
1395
1389
  // actual reader in this same function -- `cppRecordDeclarations` itself --
1396
1390
  // so it cannot be published after, the way that one is.
1397
1391
  publishLazyArrowFieldPlans(input.classes, censusLazyArrowFields(input.bodies, input.classes, captures));
1392
+ // Published alongside it, and read by the same constructor emission: which
1393
+ // field initializers are a single constant, so a store that writes what
1394
+ // value-initialization already wrote can be dropped. Passed the plugin's own
1395
+ // `reactive.fields` rather than the settled `celledFields`, which
1396
+ // `cppRecordDeclarations` has not produced yet -- see the census's own
1397
+ // comment for why that is the right superset to refuse.
1398
+ publishConstantFieldInitializers(input.classes, censusConstantFieldInitializers(input.bodies, input.classes, input.hosts.reactive.fields));
1398
1399
  const structs = cppRecordDeclarations(input.plan, deriver, input.classes, input.hosts.reactive, boundRecordFields, structMembers, narrowedStorage.slots, input.wellKnownSymbols, perFile, input.reflection, emissionRepresentations, input.physicalClasses ?? input.classes, (body) => captures.of(body).kind === 'ok', fixedFieldStateConstant, singleEvaluationClasses);
1399
1400
  const recursiveContainers = cppRecursiveContainerDeclarations(input.plan, emissionRepresentations);
1400
1401
  // Real storage for every `ClassName.KEY = value` site the whole program
@@ -1632,9 +1633,24 @@ export const renderTranslationUnit = (input) => {
1632
1633
  }
1633
1634
  return false;
1634
1635
  });
1636
+ // The facts every mint site of a function registers (`cppThunkEntryText`),
1637
+ // spelled once per body here rather than looked up through the thunk. Empty
1638
+ // when the census above found no reader, so `&thunk` alone is stored.
1639
+ const functionFacts = new Map();
1640
+ if (preserveFunctionFacts)
1641
+ for (const body of input.bodies) {
1642
+ if (isRegionId(body.sourceOwner) || body.abi === null || body.functionSource === undefined)
1643
+ continue;
1644
+ functionFacts.set(body.sourceOwner, {
1645
+ abiType: cppAbiType(body.abi),
1646
+ name: body.functionName ?? '',
1647
+ length: body.functionLength ?? 0,
1648
+ source: body.functionSource
1649
+ });
1650
+ }
1635
1651
  const thunks = new Map();
1636
1652
  for (const body of input.bodies) {
1637
- const thunk = thunkOf(body, captures, formalsNarrowedIn(body.sourceOwner), resultNarrowedIn(body.sourceOwner), linkage, preserveFunctionFacts);
1653
+ const thunk = thunkOf(body, captures, formalsNarrowedIn(body.sourceOwner), resultNarrowedIn(body.sourceOwner), linkage);
1638
1654
  if (thunk)
1639
1655
  thunks.set(body, thunk);
1640
1656
  }
@@ -1706,7 +1722,7 @@ export const renderTranslationUnit = (input) => {
1706
1722
  continue;
1707
1723
  }
1708
1724
  try {
1709
- const sections = emitBody(body, input.placements, input.classes, hosts, deriver, input.wellKnownSymbols, captures, symbolKeys, templateObjects, directCallables, virtuals.dispatched, narrowedStorage.factsOf(body.sourceOwner), formalsNarrowedIn(body.sourceOwner), repeatedConstructors, dyingArguments, instantiation, (callable) => abiByBody.get(String(callable)) ?? null, hostMethodAliases, callableMemberCandidates, borrowableMemberBodies, stableBorrowEntries, printerDrift, input.conversions, selectionHelpers, callableIdentityDemand, nativeIntegrityRestricted, fixedFieldStateConstant);
1725
+ const sections = emitBody(body, input.placements, input.classes, hosts, deriver, input.wellKnownSymbols, captures, symbolKeys, templateObjects, directCallables, virtuals.dispatched, narrowedStorage.factsOf(body.sourceOwner), formalsNarrowedIn(body.sourceOwner), repeatedConstructors, dyingArguments, instantiation, (callable) => abiByBody.get(String(callable)) ?? null, functionFacts, hostMethodAliases, callableMemberCandidates, borrowableMemberBodies, stableBorrowEntries, printerDrift, input.conversions, selectionHelpers, callableIdentityDemand, nativeIntegrityRestricted, fixedFieldStateConstant);
1710
1726
  const stableEntry = stableBorrowEntries.get(cppBodyName(body.sourceOwner));
1711
1727
  const commonJsScope = commonJsOwner === null || commonJsOwner.nativeRecord
1712
1728
  ? []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/compiler",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "The geastack TypeScript-to-C++ compiler: one semantic spine, no source-shaped authority, no boxing of statically typed values.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -67,7 +67,7 @@
67
67
  "check:contracts": "npm run build && node test/contracts.mjs"
68
68
  },
69
69
  "dependencies": {
70
- "@geastack/node-compat": "^1.0.7",
70
+ "@geastack/node-compat": "^1.0.12",
71
71
  "typescript": "^5.9.3"
72
72
  },
73
73
  "devDependencies": {
@@ -77,9 +77,9 @@
77
77
  "prettier": "^3.8.3"
78
78
  },
79
79
  "peerDependencies": {
80
- "@geastack/apple": ">=0.1.0",
81
- "@geastack/geatsc-plugin-apple-native": ">=0.1.0",
82
- "@geastack/geatsc-plugin-gea": ">=0.1.0"
80
+ "@geastack/apple": ">=0.2.8",
81
+ "@geastack/geatsc-plugin-apple-native": ">=0.1.3",
82
+ "@geastack/geatsc-plugin-gea": ">=0.1.6"
83
83
  },
84
84
  "peerDependenciesMeta": {
85
85
  "@geastack/apple": {
@@ -89,8 +89,46 @@ inline Value ordinaryToPrimitive(const Value& value, ToPrimitiveHint hint) {
89
89
  host::throwRuntimeError("TypeError", "Cannot convert object to primitive value");
90
90
  }
91
91
 
92
+ /**
93
+ * ToPrimitive of a boxed typed array, which is always its ToString.
94
+ *
95
+ * A typed array is a native carrier with no dynamic property surface: it has no
96
+ * `@@toPrimitive`, its `valueOf` is Object's and returns the object, and
97
+ * nothing a program can do installs either on it. So 7.1.1 lands on
98
+ * `%TypedArray%.prototype.toString` for every hint, and that is knowable from
99
+ * the payload type alone -- without it the generic path below asks the box for
100
+ * `@@toPrimitive` and the box, having no field table, refuses by name. A view
101
+ * whose host brand states its own ToString (Node's Buffer) answers with that.
102
+ */
103
+ template <typename Element>
104
+ inline bool boxedTypedArrayToStringAs(const Value& value, std::string& out) {
105
+ using Handle = gea::Ref<TypedArray<Element>>;
106
+ if (value.payloadType() != detail::payloadTypeTagFor<Handle>()) return false;
107
+ const Handle& view = value.as<Handle>();
108
+ if constexpr (std::is_same_v<Element, std::uint8_t>) {
109
+ if (view) {
110
+ if (const detail::HostViewToString render = detail::hostViewToStringFor(view->hostBrand())) {
111
+ out = render(*view);
112
+ return true;
113
+ }
114
+ }
115
+ }
116
+ out = runtime::array::join(view);
117
+ return true;
118
+ }
119
+
120
+ inline bool boxedTypedArrayToString(const Value& value, std::string& out) {
121
+ if (value.tag() != Value::Tag::Object || value.isProxy()) return false;
122
+ return boxedTypedArrayToStringAs<std::uint8_t>(value, out) || boxedTypedArrayToStringAs<std::int8_t>(value, out) ||
123
+ boxedTypedArrayToStringAs<ClampedUint8>(value, out) || boxedTypedArrayToStringAs<std::int16_t>(value, out) ||
124
+ boxedTypedArrayToStringAs<std::uint16_t>(value, out) || boxedTypedArrayToStringAs<std::int32_t>(value, out) ||
125
+ boxedTypedArrayToStringAs<std::uint32_t>(value, out) || boxedTypedArrayToStringAs<float>(value, out) ||
126
+ boxedTypedArrayToStringAs<double>(value, out);
127
+ }
128
+
92
129
  inline Value dynamicToPrimitive(const Value& value, ToPrimitiveHint hint = ToPrimitiveHint::Default) {
93
130
  if (!isObjectValue(value)) return value;
131
+ if (std::string text; boxedTypedArrayToString(value, text)) return Value::box(Value::Tag::String, std::move(text));
94
132
  const Value exotic = value.getProperty(PropertyKey::symbol(wellKnownSymbol(detail::WellKnownSymbol::ToPrimitive)));
95
133
  if (exotic.tag() != Value::Tag::Undefined && exotic.tag() != Value::Tag::Null) {
96
134
  if (exotic.tag() != Value::Tag::Function) host::throwRuntimeError("TypeError", "@@toPrimitive is not callable");
@@ -3170,6 +3170,25 @@ struct CallableObject<Result(Arguments...)> {
3170
3170
  return true;
3171
3171
  }
3172
3172
 
3173
+ // The same registration, performed where a function OBJECT is minted from
3174
+ // the thunk rather than by a namespace-scope initializer beside the thunk.
3175
+ // A namespace-scope registration is a static initializer that takes the
3176
+ // thunk's address, and a static initializer with a side effect is a GC
3177
+ // root: neither `--gc-sections` nor LTO may drop it, so it kept every
3178
+ // emitted function -- and its source text -- in the binary whether or not
3179
+ // anything reached it. One `EventEmitter` boxing a listener turned the
3180
+ // registry on for a whole program, and that pinned all 384 functions of a
3181
+ // raw HTTP server. Here the registration is reachable only from a site
3182
+ // that creates the function object, which is the only site that can later
3183
+ // observe its `name`/`length`/source. The function-local static is one
3184
+ // per `Entry`, so several minting sites of one declaration share a single
3185
+ // registration, and a site costs one guard-variable load per mint.
3186
+ template <Invoke Entry>
3187
+ static Invoke entryWithFacts(std::string_view name, std::size_t length, std::string_view text) {
3188
+ static const SourceRegistration registration{Entry, name, length, text, nullptr};
3189
+ return Entry;
3190
+ }
3191
+
3173
3192
  // An adapter (`dropArguments` and its siblings below) shares ONE thunk
3174
3193
  // pointer across every closure it wraps, so the facts it reports cannot
3175
3194
  // live in this registration -- they belong to whichever `Source` closure
@@ -5649,6 +5668,7 @@ class TypedArray {
5649
5668
  */
5650
5669
  void setHostBrand(const void* brand) { hostBrand_ = brand; }
5651
5670
  bool hasHostBrand(const void* brand) const { return brand != nullptr && hostBrand_ == brand; }
5671
+ const void* hostBrand() const { return hostBrand_; }
5652
5672
 
5653
5673
  /**
5654
5674
  * ECMA-262 23.2.3.26 `%TypedArray%.prototype.set`, over a source view of any
@@ -5833,6 +5853,41 @@ class TypedArray {
5833
5853
  const void* hostBrand_ = nullptr;
5834
5854
  };
5835
5855
 
5856
+ /**
5857
+ * What a host brand's views answer to ToString.
5858
+ *
5859
+ * A brand is identity only (see `setHostBrand`), which is enough while the
5860
+ * view's type is known: `buffer.toString()` lowers to the host's own function.
5861
+ * Behind a box it is not. `body + chunk` in a `'data'` listener -- where Node
5862
+ * declares `chunk` as `any` and hands out a Buffer -- is ToPrimitive over an
5863
+ * erased byte view, and the generic answer for a byte view (23.2.3.32, the
5864
+ * comma-joined elements) is not Buffer's, which decodes the bytes as UTF-8.
5865
+ * The host that minted the brand is the only party that knows that, so it
5866
+ * states it here once, keyed by the same opaque address.
5867
+ *
5868
+ * `GEA_HOST_VIEW_TO_STRING` lets a host header register only when this runtime
5869
+ * has the table, so it still builds against a runtime that predates it.
5870
+ */
5871
+ #define GEA_HOST_VIEW_TO_STRING 1
5872
+ namespace detail {
5873
+ using HostViewToString = std::string (*)(const TypedArray<std::uint8_t>&);
5874
+ inline std::vector<std::pair<const void*, HostViewToString>>& hostViewToStrings() {
5875
+ static std::vector<std::pair<const void*, HostViewToString>> table;
5876
+ return table;
5877
+ }
5878
+ inline bool registerHostViewToString(const void* brand, HostViewToString render) {
5879
+ hostViewToStrings().emplace_back(brand, render);
5880
+ return true;
5881
+ }
5882
+ inline HostViewToString hostViewToStringFor(const void* brand) {
5883
+ if (brand == nullptr) return nullptr;
5884
+ for (const auto& [known, render] : hostViewToStrings()) {
5885
+ if (known == brand) return render;
5886
+ }
5887
+ return nullptr;
5888
+ }
5889
+ } // namespace detail
5890
+
5836
5891
  namespace runtime::atomics {
5837
5892
 
5838
5893
  template <typename T>
@@ -6544,6 +6599,32 @@ struct MaxAlignOf<T, Rest...> {
6544
6599
  static constexpr std::size_t value = alignof(T) > MaxAlignOf<Rest...>::value ? alignof(T) : MaxAlignOf<Rest...>::value;
6545
6600
  };
6546
6601
 
6602
+ /**
6603
+ * Whether an arm may be the one a default-constructed union holds.
6604
+ *
6605
+ * A default-constructed `TaggedUnion` has to hold SOMETHING, and which arm it
6606
+ * picks is arbitrary -- the emitter only ever produces one to declare an SSA
6607
+ * slot that is assigned before it is read. Arm 0 is not always a legal choice:
6608
+ * `FunctionValue` deliberately has no default (a Function arm holding a
6609
+ * non-function is the bug its constructors exist to catch), so picking it
6610
+ * aborts a program that has done nothing wrong. Arms opt out here and the
6611
+ * default walks past them.
6612
+ */
6613
+ template <typename Arm>
6614
+ struct UnionDefaultArm : std::true_type {};
6615
+
6616
+ /** The first arm that may be default-constructed; 0 when none opts in. */
6617
+ template <typename... Arms>
6618
+ struct DefaultUnionArmIndex {
6619
+ static constexpr std::size_t value = [] {
6620
+ constexpr bool allowed[] = {UnionDefaultArm<Arms>::value...};
6621
+ for (std::size_t index = 0; index < sizeof...(Arms); ++index) {
6622
+ if (allowed[index]) return index;
6623
+ }
6624
+ return static_cast<std::size_t>(0);
6625
+ }();
6626
+ };
6627
+
6547
6628
  /** Lifetime dispatch over a closed arm set, by index: a raw byte buffer holds whichever arm is live, so construct/copy/move/destroy need a hand-written vtable-by-recursion in place of what the language generates for a real union of non-trivial arms. */
6548
6629
  template <typename... Arms>
6549
6630
  struct TaggedUnionOps;
@@ -6588,7 +6669,9 @@ class TaggedUnion {
6588
6669
  }(std::index_sequence_for<Arms...>{});
6589
6670
  }
6590
6671
 
6591
- TaggedUnion() : index_(0) { detail::TaggedUnionOps<Arms...>::defaultConstruct(0, &storage_); }
6672
+ TaggedUnion() : index_(detail::DefaultUnionArmIndex<Arms...>::value) {
6673
+ detail::TaggedUnionOps<Arms...>::defaultConstruct(index_, &storage_);
6674
+ }
6592
6675
 
6593
6676
  TaggedUnion(const TaggedUnion& other) : index_(other.index_) {
6594
6677
  detail::TaggedUnionOps<Arms...>::copyConstruct(index_, &storage_, &other.storage_);
@@ -10021,6 +10104,19 @@ class FunctionValue : public Value {
10021
10104
  }
10022
10105
  };
10023
10106
 
10107
+ namespace detail {
10108
+ /**
10109
+ * Never the arm a default-constructed union holds.
10110
+ *
10111
+ * `FunctionValue()` aborts on purpose, so a union with a Function arm first
10112
+ * could not be declared at all: `gea_union_N v1;` -- the ordinary way the
10113
+ * emitter opens an SSA slot it assigns further down -- killed the process
10114
+ * before the assignment ran. `node:net`'s `Socket.pipe` is one such union.
10115
+ */
10116
+ template <>
10117
+ struct UnionDefaultArm<FunctionValue> : std::false_type {};
10118
+ }
10119
+
10024
10120
  namespace runtime {
10025
10121
  namespace string {
10026
10122
  inline std::size_t utf16Length(const std::string& value);
@@ -14682,6 +14778,50 @@ inline const Ref<DynamicObject>& Value::functionProperties() const {
14682
14778
 
14683
14779
  Value dynamicFunctionPrototypeGet(const PropertyKey& key);
14684
14780
 
14781
+ namespace detail {
14782
+ /**
14783
+ * The own, non-method surface of a boxed typed array: `length`, `byteLength`,
14784
+ * `byteOffset` and the element at a canonical numeric index (ECMA-262 10.4.5,
14785
+ * 23.2.3).
14786
+ *
14787
+ * A typed array reaches a box wherever a library types it `any` -- Node's
14788
+ * `'data'` chunk is the measured case, and `total += chunk.length` is what a
14789
+ * byte-counting handler does with it. The payload type names the element type
14790
+ * exactly, so these four are answerable from the box with no table. Every other
14791
+ * key keeps the refusal below: a method (`slice`, `toString`) needs a callable
14792
+ * this runtime does not mint for an erased view, and answering `undefined`
14793
+ * would be the plausible wrong answer.
14794
+ */
14795
+ template <typename Element>
14796
+ inline bool boxedTypedArrayOwnAs(const Value& value, const PropertyKey& key, Value& out) {
14797
+ using Handle = gea::Ref<TypedArray<Element>>;
14798
+ if (value.payloadType() != payloadTypeTagFor<Handle>()) return false;
14799
+ const Handle& view = value.as<Handle>();
14800
+ if (!view || key.isSymbol()) return false;
14801
+ std::size_t index = 0;
14802
+ if (arrayIndexOfKey(key, index)) {
14803
+ // 10.4.5.15: an index past the end reads `undefined`, never the prototype.
14804
+ out = index < view->size() ? Value::box(Value::Tag::Number, static_cast<double>((*view)[index])) : Value();
14805
+ return true;
14806
+ }
14807
+ if (key.isNumericSource()) return false;
14808
+ const std::string& name = key.text();
14809
+ if (name == "length") out = Value::box(Value::Tag::Number, static_cast<double>(view->size()));
14810
+ else if (name == "byteLength") out = Value::box(Value::Tag::Number, view->byteLength());
14811
+ else if (name == "byteOffset") out = Value::box(Value::Tag::Number, view->byteOffset());
14812
+ else return false;
14813
+ return true;
14814
+ }
14815
+
14816
+ inline bool boxedTypedArrayOwn(const Value& value, const PropertyKey& key, Value& out) {
14817
+ return boxedTypedArrayOwnAs<std::uint8_t>(value, key, out) || boxedTypedArrayOwnAs<std::int8_t>(value, key, out) ||
14818
+ boxedTypedArrayOwnAs<ClampedUint8>(value, key, out) || boxedTypedArrayOwnAs<std::int16_t>(value, key, out) ||
14819
+ boxedTypedArrayOwnAs<std::uint16_t>(value, key, out) || boxedTypedArrayOwnAs<std::int32_t>(value, key, out) ||
14820
+ boxedTypedArrayOwnAs<std::uint32_t>(value, key, out) || boxedTypedArrayOwnAs<float>(value, key, out) ||
14821
+ boxedTypedArrayOwnAs<double>(value, key, out);
14822
+ }
14823
+ } // namespace detail
14824
+
14685
14825
  inline Value Value::getProperty(const PropertyKey& key) const {
14686
14826
  return getProperty(key, *this);
14687
14827
  }
@@ -14739,6 +14879,8 @@ inline Value Value::getProperty(const PropertyKey& key, const Value& receiver) c
14739
14879
  if (tag_ == Tag::Object) {
14740
14880
  Value method;
14741
14881
  if (detail::boxedPromiseMethod(*this, key, method)) return method;
14882
+ Value own;
14883
+ if (detail::boxedTypedArrayOwn(*this, key, own)) return own;
14742
14884
  }
14743
14885
  if (tag_ == Tag::Object || tag_ == Tag::Function) detail::refuseOpaquePropertyAccess("a property read", key);
14744
14886
  // Every remaining box is a primitive, whose properties all live on a
@@ -18207,6 +18349,31 @@ template <typename T>
18207
18349
  throw gea::Value::box(gea::Value::Tag::String, std::string("InternalError: entered a branch the compiler proved unreachable"));
18208
18350
  }
18209
18351
 
18352
+ /**
18353
+ * The exact-arm projection of a tagged union (`conversion/nodes.ts`'s
18354
+ * `exactArmFor`): the payload of arm `Index`, or a `TypeError` when the value
18355
+ * holds another arm.
18356
+ *
18357
+ * Emitted inside a body whose declaration states `@gea-exact-arms`, and at a
18358
+ * value the program's own `as T` states the arm of where the census has no
18359
+ * sound per-arm answer (`lower-operands.ts`'s `assertedArmEntry`). The
18360
+ * alternative the compiler would otherwise render is a per-arm dispatch that
18361
+ * CONVERTS every arm into the target -- for a callable target, an adapter that
18362
+ * boxes the typed arm's parameters to feed a generic listener -- and the tag
18363
+ * is the author stating that the guard around the cast already excluded those
18364
+ * arms. A `TypeError` rather than an abort because a wrong arm here is the
18365
+ * program's contract violated (`server.on('request', genericHandler)`), not
18366
+ * the compiler's, and the program may want to catch it.
18367
+ */
18368
+ template <std::size_t Index, typename... Arms>
18369
+ typename gea::TaggedUnion<Arms...>::template ArmType<Index> exactArm(const gea::TaggedUnion<Arms...>& value) {
18370
+ if (!value.template is<Index>()) {
18371
+ throwRuntimeError("TypeError", "Value is not the union arm this operation requires (arm " + std::to_string(Index) +
18372
+ " expected, arm " + std::to_string(value.index()) + " held)");
18373
+ }
18374
+ return value.template get<Index>();
18375
+ }
18376
+
18210
18377
  } // namespace gea::host
18211
18378
 
18212
18379
  namespace gea::runtime::string {
@@ -24927,6 +25094,77 @@ inline void recordNodeSubscription(int nodeId, std::function<void()> release);
24927
25094
  template <typename Node>
24928
25095
  gea::Ref<bool> nodeLiveToken(const Node& node);
24929
25096
 
25097
+ /**
25098
+ * Whether the engine dispatches `name` off-tree. `rotary` (the encoder dial)
25099
+ * always: it has no element target, the runtime hands each detent to the one
25100
+ * document-level listener chain (`dispatchDocumentRotary`), and
25101
+ * `Tree::setEventListener` drops "rotary" as a type it never walks the tree
25102
+ * for -- so an `onRotary` delegated to the body was registered nowhere and the
25103
+ * dial did nothing, silently. `keydown` is off-tree on anything but an
25104
+ * `<input>`: the runtime hands a key to the focused input's own listener (which
25105
+ * bubbles to the body) and then to the document-level chain. The same rule the
25106
+ * gea plugin's `cpp-mounted-lowering.ts` applies to its direct-root listeners.
25107
+ */
25108
+ template <typename Node>
25109
+ bool isDocumentLevelEvent(const Node& node, const std::string& name) {
25110
+ if (name == "rotary") return true;
25111
+ if (name != "keydown") return false;
25112
+ const char* tag = node.tagName();
25113
+ if (!tag) return true;
25114
+ std::string lowered(tag);
25115
+ for (char& character : lowered) character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
25116
+ return lowered != "input";
25117
+ }
25118
+
25119
+ /**
25120
+ * Register `invoke` for `name` on behalf of `node`: the one place both engine
25121
+ * `addNodeListener` overloads bind through.
25122
+ *
25123
+ * A document-level event goes straight to `Document::addEventListener`, which
25124
+ * chains it onto the off-tree singleton: no `containsNode` gate, since there is
25125
+ * no target to contain. The chain has no per-listener removal, so the handler
25126
+ * is gated on the node's live token instead -- a rebuilt subtree leaves an
25127
+ * inert closure behind rather than a second generation of the handler.
25128
+ *
25129
+ * Everything else is delegated to the BODY and gated by `containsNode`; see
25130
+ * the no-argument `addNodeListener` for why, and `recordNodeSubscription` for
25131
+ * how it is released with the node.
25132
+ */
25133
+ template <typename Node, typename Invoke>
25134
+ void bindNodeListener(Node& node, const std::string& name, Invoke invoke) {
25135
+ const auto target = node;
25136
+ auto run = [target, invoke](gea::framework::events::PointerEvent& event) mutable {
25137
+ const auto previousTarget = event.currentTarget;
25138
+ const auto previousTargetId = event.currentTargetId;
25139
+ const auto previousPhase = event.eventPhase;
25140
+ event.currentTarget = gea::framework::events::EventTarget(target.id());
25141
+ event.currentTargetId = target.id();
25142
+ event.eventPhase = event.targetId == target.id() ? gea::framework::events::EventPhase::AtTarget
25143
+ : gea::framework::events::EventPhase::Bubbling;
25144
+ invoke(event);
25145
+ event.currentTarget = previousTarget;
25146
+ event.currentTargetId = previousTargetId;
25147
+ event.eventPhase = previousPhase;
25148
+ };
25149
+ if (isDocumentLevelEvent(node, name)) {
25150
+ auto alive = nodeLiveToken(node);
25151
+ gea::embedded::ui::Document::instance().addEventListener(
25152
+ name.c_str(), [alive, run](gea::framework::events::PointerEvent& event) mutable {
25153
+ if (!*alive) return;
25154
+ run(event);
25155
+ });
25156
+ return;
25157
+ }
25158
+ const auto listenerId = gea::embedded::ui::Document::instance().body().addEventListener(
25159
+ name.c_str(), [target, run](gea::framework::events::PointerEvent& event) mutable {
25160
+ if (!gea::embedded::ui::Tree::instance().containsNode(target.id(), event.targetId)) return;
25161
+ run(event);
25162
+ });
25163
+ recordNodeSubscription(target.id(), [name, listenerId]() {
25164
+ gea::embedded::ui::Document::instance().body().removeEventListener(name.c_str(), listenerId);
25165
+ });
25166
+ }
25167
+
24930
25168
  /**
24931
25169
  * `onClick={() => ...}` on the real engine: the handler is copied into the
24932
25170
  * listener the engine holds, and the event it hands back is dropped -- a
@@ -24969,26 +25207,7 @@ void addNodeListener(Node& node, const std::string& type, const gea::CallableObj
24969
25207
  // flipped the unit twice and the whole UI stayed in Celsius, with nothing
24970
25208
  // logged and every node still rendering correctly.
24971
25209
  const gea::CallableObject<Result()> held = handler;
24972
- const auto target = node;
24973
- const std::string name = eventTypeOf(type);
24974
- const auto listenerId = gea::embedded::ui::Document::instance().body().addEventListener(
24975
- name.c_str(), [held, target](gea::framework::events::PointerEvent& event) mutable {
24976
- if (!gea::embedded::ui::Tree::instance().containsNode(target.id(), event.targetId)) return;
24977
- const auto previousTarget = event.currentTarget;
24978
- const auto previousTargetId = event.currentTargetId;
24979
- const auto previousPhase = event.eventPhase;
24980
- event.currentTarget = gea::framework::events::EventTarget(target.id());
24981
- event.currentTargetId = target.id();
24982
- event.eventPhase = event.targetId == target.id() ? gea::framework::events::EventPhase::AtTarget
24983
- : gea::framework::events::EventPhase::Bubbling;
24984
- held.call();
24985
- event.currentTarget = previousTarget;
24986
- event.currentTargetId = previousTargetId;
24987
- event.eventPhase = previousPhase;
24988
- });
24989
- recordNodeSubscription(target.id(), [name, listenerId]() {
24990
- gea::embedded::ui::Document::instance().body().removeEventListener(name.c_str(), listenerId);
24991
- });
25210
+ bindNodeListener(node, eventTypeOf(type), [held](gea::framework::events::PointerEvent&) mutable { held.call(); });
24992
25211
  }
24993
25212
 
24994
25213
  /**
@@ -25018,31 +25237,14 @@ void addNodeListener(Node& node, const std::string& type, const gea::CallableObj
25018
25237
  template <typename Node, typename Result, typename Argument>
25019
25238
  void addNodeListener(Node& node, const std::string& type, const gea::CallableObject<Result(Argument)>& handler) {
25020
25239
  const gea::CallableObject<Result(Argument)> held = handler;
25021
- const auto target = node;
25022
25240
  const std::string name = eventTypeOf(type);
25023
- const auto listenerId = gea::embedded::ui::Document::instance().body().addEventListener(
25024
- name.c_str(), [held, target, name](gea::framework::events::PointerEvent& event) mutable {
25025
- if (!gea::embedded::ui::Tree::instance().containsNode(target.id(), event.targetId)) return;
25026
- const auto previousTarget = event.currentTarget;
25027
- const auto previousTargetId = event.currentTargetId;
25028
- const auto previousPhase = event.eventPhase;
25029
- event.currentTarget = gea::framework::events::EventTarget(target.id());
25030
- event.currentTargetId = target.id();
25031
- event.eventPhase = event.targetId == target.id() ? gea::framework::events::EventPhase::AtTarget
25032
- : gea::framework::events::EventPhase::Bubbling;
25033
- if constexpr (std::is_constructible_v<std::decay_t<Argument>, const gea::framework::events::PointerEvent&>) {
25034
- held.call(static_cast<std::decay_t<Argument>>(event));
25035
- } else {
25036
- std::fprintf(stderr, "gea: the handler for \"%s\" declares an event parameter, which no host table states a carrier for\n", name.c_str());
25037
- gea::detail::abortAfterFlush();
25038
- }
25039
- event.currentTarget = previousTarget;
25040
- event.currentTargetId = previousTargetId;
25041
- event.eventPhase = previousPhase;
25042
- });
25043
- // Owned by `node`, for the reason the no-argument overload above states.
25044
- recordNodeSubscription(target.id(), [name, listenerId]() {
25045
- gea::embedded::ui::Document::instance().body().removeEventListener(name.c_str(), listenerId);
25241
+ bindNodeListener(node, name, [held, name](gea::framework::events::PointerEvent& event) mutable {
25242
+ if constexpr (std::is_constructible_v<std::decay_t<Argument>, const gea::framework::events::PointerEvent&>) {
25243
+ held.call(static_cast<std::decay_t<Argument>>(event));
25244
+ } else {
25245
+ std::fprintf(stderr, "gea: the handler for \"%s\" declares an event parameter, which no host table states a carrier for\n", name.c_str());
25246
+ gea::detail::abortAfterFlush();
25247
+ }
25046
25248
  });
25047
25249
  }
25048
25250