@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1
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/CLAUDE.md +87 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +212 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +692 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +326 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +672 -0
- package/dist/shim/notebook/knob.js +151 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +510 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +214 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +807 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +62 -4
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/experiment-frame — THE fail-loud guard for a host that embeds a
|
|
5
|
+
* canonical experiment page in an iframe.
|
|
6
|
+
*
|
|
7
|
+
* ── Why this module exists (the false-200 trap, observed 2026-07-28) ─────────
|
|
8
|
+
* A dev/prod vite host answers an UNKNOWN path with its SPA fallback: HTTP 200,
|
|
9
|
+
* `text/html`, the host app's OWN `index.html`. So when an experiment id stops
|
|
10
|
+
* existing (a retirement), `GET /frqtl/experiments/<gone>/index.html` does not
|
|
11
|
+
* 404 — it returns the SITE. The host's hero iframe then embeds the host inside
|
|
12
|
+
* itself, recursively: the nav bar renders three times, the stage is blank, and
|
|
13
|
+
* EVERY automated signal is clean (0 HTTP 4xx/5xx, 0 `requestfailed`, 0
|
|
14
|
+
* `pageerror`). A missing experiment degraded SILENTLY into a self-embed.
|
|
15
|
+
*
|
|
16
|
+
* This module makes that impossible to miss. It does NOT trust the HTTP status
|
|
17
|
+
* (which lies) — it inspects the DOCUMENT that actually loaded and asserts it is
|
|
18
|
+
* a canonical experiment page, by two independent signals:
|
|
19
|
+
*
|
|
20
|
+
* 1. POSITIVE PROOF — the boot record markers. Every canonical experiment
|
|
21
|
+
* page boots through `_shared/boot.js`, whose boot record writes
|
|
22
|
+
* `EXPERIMENT_PAGE_MARKERS` on its own <html> while the page's module script
|
|
23
|
+
* evaluates — before the document reaches `readyState === 'complete'`. No
|
|
24
|
+
* other document in this repo writes them. Their presence is what "an
|
|
25
|
+
* experiment page booted" MEANS; their absence at document-complete is a
|
|
26
|
+
* hard failure.
|
|
27
|
+
* 2. POSITIVE IDENTIFICATION of the impostor — the host SPA shell root
|
|
28
|
+
* (`HOST_SHELL_ROOT_SELECTOR`, the `<div id="root">` in
|
|
29
|
+
* `apps/ide/index.html`). A document that
|
|
30
|
+
* has the shell root and none of the markers IS the SPA fallback, and the
|
|
31
|
+
* guard says so by name rather than reporting a generic timeout.
|
|
32
|
+
*
|
|
33
|
+
* A 200 cannot fool either signal: the check reads the loaded document, and the
|
|
34
|
+
* SPA shell provably lacks every marker.
|
|
35
|
+
*
|
|
36
|
+
* ── Layer 1: API contract ───────────────────────────────────────────────────
|
|
37
|
+
* classifyFrameProbe(probe) — PURE verdict function (no DOM). Unit-testable.
|
|
38
|
+
* probeExperimentFrame(el) — DOM adapter: iframe element → probe bag.
|
|
39
|
+
* watchExperimentFrame(el,o) — poll + paint a visible overlay on failure.
|
|
40
|
+
*
|
|
41
|
+
* ── Layer 2: physics behavior ───────────────────────────────────────────────
|
|
42
|
+
* None. This is host-side plumbing: it observes whether a document booted and
|
|
43
|
+
* never touches the engine, a universe, or a tick.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
'use strict';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The <html> attributes the boot record (`lib/frqtl/experiments/_shared/boot.js`
|
|
50
|
+
* `createBootRecord`) writes the moment a page's boot begins — during the page's
|
|
51
|
+
* module-script evaluation, so both exist by the time the document reaches
|
|
52
|
+
* `readyState === 'complete'`, regardless of whether the engine has finished
|
|
53
|
+
* fetching WASM.
|
|
54
|
+
*
|
|
55
|
+
* This is the ONE list; a consumer asserts against it, never against a
|
|
56
|
+
* hand-picked attribute of its own.
|
|
57
|
+
*
|
|
58
|
+
* @type {ReadonlyArray<string>}
|
|
59
|
+
*/
|
|
60
|
+
export var EXPERIMENT_PAGE_MARKERS = Object.freeze([
|
|
61
|
+
'data-egpt-boot-phase',
|
|
62
|
+
'data-egpt-boot-progress'
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The mount point the host SPA shell renders React into (`apps/ide/index.html`).
|
|
67
|
+
* Its presence inside an experiment iframe is the fingerprint of the vite SPA
|
|
68
|
+
* fallback having answered the request with the host's own page.
|
|
69
|
+
* @type {string}
|
|
70
|
+
*/
|
|
71
|
+
export var HOST_SHELL_ROOT_SELECTOR = '#root';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The verdict vocabulary. `ok` and `pending` are non-failures; everything else
|
|
75
|
+
* is a FAIL-LOUD outcome the host must surface visibly.
|
|
76
|
+
* @type {Readonly<Record<string,string>>}
|
|
77
|
+
*/
|
|
78
|
+
export var FRAME_VERDICTS = Object.freeze({
|
|
79
|
+
OK: 'ok',
|
|
80
|
+
PENDING: 'pending',
|
|
81
|
+
CROSS_ORIGIN: 'cross-origin',
|
|
82
|
+
SPA_SHELL: 'spa-shell',
|
|
83
|
+
NOT_EXPERIMENT: 'not-an-experiment-page'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** Default grace period before a not-yet-complete document is called failed. */
|
|
87
|
+
export var DEFAULT_FRAME_TIMEOUT_MS = 10000;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* classifyFrameProbe(probe) — the PURE verdict function.
|
|
91
|
+
*
|
|
92
|
+
* @param {{
|
|
93
|
+
* src?: string, // the URL the host ASKED for
|
|
94
|
+
* href?: string, // the URL the frame's document actually has
|
|
95
|
+
* sameOrigin?: boolean, // false when reading contentWindow threw
|
|
96
|
+
* readyState?: string, // the frame document's readyState
|
|
97
|
+
* markers?: string[], // which EXPERIMENT_PAGE_MARKERS were found
|
|
98
|
+
* hasHostShellRoot?: boolean,
|
|
99
|
+
* elapsedMs?: number,
|
|
100
|
+
* timeoutMs?: number
|
|
101
|
+
* }} probe
|
|
102
|
+
* @returns {{verdict:string, ok:boolean, done:boolean, message:string}}
|
|
103
|
+
* `done` is false only for `pending` (keep polling).
|
|
104
|
+
*/
|
|
105
|
+
export function classifyFrameProbe(probe) {
|
|
106
|
+
var p = probe || {};
|
|
107
|
+
var src = p.src || '(unknown src)';
|
|
108
|
+
var href = typeof p.href === 'string' ? p.href : '';
|
|
109
|
+
var found = Array.isArray(p.markers) ? p.markers : [];
|
|
110
|
+
var timeout = typeof p.timeoutMs === 'number' ? p.timeoutMs : DEFAULT_FRAME_TIMEOUT_MS;
|
|
111
|
+
var elapsed = typeof p.elapsedMs === 'number' ? p.elapsedMs : 0;
|
|
112
|
+
|
|
113
|
+
// A cross-origin frame cannot be inspected at all — say so rather than
|
|
114
|
+
// silently trusting it. The /frqtl mount is same-origin BY DESIGN (that is
|
|
115
|
+
// the precondition every host-side runner grab already relies on), so this
|
|
116
|
+
// verdict means the host built the wrong URL.
|
|
117
|
+
if (p.sameOrigin === false) {
|
|
118
|
+
return _verdict(FRAME_VERDICTS.CROSS_ORIGIN, false, true,
|
|
119
|
+
'the experiment frame is CROSS-ORIGIN, so the host cannot verify what loaded.\n' +
|
|
120
|
+
'requested: ' + src + '\n' +
|
|
121
|
+
'Experiment pages must be served from the host\'s own origin (the /frqtl mount).');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A frame that has not navigated to the requested document yet (about:blank
|
|
125
|
+
// between src assignments) is PENDING, never a failure.
|
|
126
|
+
if (href === '' || href === 'about:blank') {
|
|
127
|
+
return _pendingOrTimeout(elapsed, timeout, src,
|
|
128
|
+
'the frame is still on about:blank — it never navigated to the requested URL.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// POSITIVE PROOF: every boot marker present ⇒ a canonical experiment page.
|
|
132
|
+
var missing = EXPERIMENT_PAGE_MARKERS.filter(function (m) { return found.indexOf(m) === -1; });
|
|
133
|
+
if (missing.length === 0) {
|
|
134
|
+
return _verdict(FRAME_VERDICTS.OK, true, true, 'experiment page booted: ' + src);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// POSITIVE IDENTIFICATION of the impostor. The SPA shell verdict does NOT
|
|
138
|
+
// wait for the timeout: a document carrying the host's own React root and
|
|
139
|
+
// none of the boot markers is definitively not an experiment page, whatever
|
|
140
|
+
// status code delivered it.
|
|
141
|
+
if (p.hasHostShellRoot === true) {
|
|
142
|
+
return _verdict(FRAME_VERDICTS.SPA_SHELL, false, true,
|
|
143
|
+
'THE FALSE-200 TRAP: the request returned HTTP 200 text/html, but the document is the\n' +
|
|
144
|
+
'HOST\'S OWN SPA SHELL (it has ' + HOST_SHELL_ROOT_SELECTOR + ' and none of the experiment-page\n' +
|
|
145
|
+
'boot markers [' + EXPERIMENT_PAGE_MARKERS.join(', ') + ']).\n' +
|
|
146
|
+
'requested: ' + src + '\n' +
|
|
147
|
+
'loaded: ' + href + '\n' +
|
|
148
|
+
'The dev server answers an UNKNOWN path with its SPA fallback, so a MISSING experiment id\n' +
|
|
149
|
+
'looks like a successful load and this host ends up embedding ITSELF, recursively.\n' +
|
|
150
|
+
'Fix the id at its single declaration site — do not add a fallback id here.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Document finished loading and it is neither an experiment page nor the
|
|
154
|
+
// recognised shell — still a hard failure, reported honestly.
|
|
155
|
+
if (p.readyState === 'complete') {
|
|
156
|
+
return _verdict(FRAME_VERDICTS.NOT_EXPERIMENT, false, true,
|
|
157
|
+
'the frame finished loading but it is NOT a canonical experiment page.\n' +
|
|
158
|
+
'requested: ' + src + '\n' +
|
|
159
|
+
'loaded: ' + href + '\n' +
|
|
160
|
+
'missing boot markers: [' + missing.join(', ') + ']\n' +
|
|
161
|
+
'A canonical experiment page boots through _shared/boot.js, whose boot record writes all of\n' +
|
|
162
|
+
'[' + EXPERIMENT_PAGE_MARKERS.join(', ') + '] on its <html> as the boot begins.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return _pendingOrTimeout(elapsed, timeout, src,
|
|
166
|
+
'missing boot markers: [' + missing.join(', ') + '] (document readyState=' +
|
|
167
|
+
(p.readyState || 'unknown') + ').');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _verdict(verdict, ok, done, message) {
|
|
171
|
+
return { verdict: verdict, ok: ok, done: done, message: '[experiment-frame] ' + message };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _pendingOrTimeout(elapsed, timeout, src, detail) {
|
|
175
|
+
if (elapsed >= timeout) {
|
|
176
|
+
return _verdict(FRAME_VERDICTS.NOT_EXPERIMENT, false, true,
|
|
177
|
+
'the frame never became a canonical experiment page within ' + timeout + 'ms.\n' +
|
|
178
|
+
'requested: ' + src + '\n' + detail);
|
|
179
|
+
}
|
|
180
|
+
return _verdict(FRAME_VERDICTS.PENDING, false, false, 'waiting for ' + src + ' — ' + detail);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* probeExperimentFrame(iframeEl, opts) — the DOM adapter. Reads the frame's own
|
|
185
|
+
* document (same-origin) and returns the probe bag `classifyFrameProbe` consumes.
|
|
186
|
+
*
|
|
187
|
+
* @param {HTMLIFrameElement} iframeEl
|
|
188
|
+
* @param {{elapsedMs?:number, timeoutMs?:number}} [opts]
|
|
189
|
+
* @returns {object} the probe bag.
|
|
190
|
+
*/
|
|
191
|
+
export function probeExperimentFrame(iframeEl, opts) {
|
|
192
|
+
var o = opts || {};
|
|
193
|
+
var probe = {
|
|
194
|
+
src: iframeEl && iframeEl.src ? iframeEl.src : '',
|
|
195
|
+
href: '',
|
|
196
|
+
sameOrigin: true,
|
|
197
|
+
readyState: undefined,
|
|
198
|
+
markers: [],
|
|
199
|
+
hasHostShellRoot: false,
|
|
200
|
+
elapsedMs: o.elapsedMs,
|
|
201
|
+
timeoutMs: o.timeoutMs
|
|
202
|
+
};
|
|
203
|
+
var cw = null;
|
|
204
|
+
try {
|
|
205
|
+
cw = iframeEl && iframeEl.contentWindow;
|
|
206
|
+
if (!cw) { return probe; } // not attached yet → PENDING
|
|
207
|
+
probe.href = (cw.location && cw.location.href) || '';
|
|
208
|
+
var doc = cw.document;
|
|
209
|
+
probe.readyState = doc ? doc.readyState : undefined;
|
|
210
|
+
var root = doc && doc.documentElement;
|
|
211
|
+
probe.markers = EXPERIMENT_PAGE_MARKERS.filter(function (m) {
|
|
212
|
+
return !!(root && root.hasAttribute && root.hasAttribute(m));
|
|
213
|
+
});
|
|
214
|
+
probe.hasHostShellRoot = !!(doc && doc.querySelector &&
|
|
215
|
+
doc.querySelector(HOST_SHELL_ROOT_SELECTOR));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// A same-origin read that throws means the document is cross-origin.
|
|
218
|
+
probe.sameOrigin = false;
|
|
219
|
+
}
|
|
220
|
+
return probe;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* paintFrameGuardOverlay(hostEl, message) — the VISIBLE failure. Mirrors the
|
|
225
|
+
* empty page's own `_egptPaintWalkthroughError` styling so a guard failure and a
|
|
226
|
+
* recipe failure read as one family.
|
|
227
|
+
*
|
|
228
|
+
* @param {Element} hostEl — the element to overlay (the iframe's wrapper).
|
|
229
|
+
* @param {string} message
|
|
230
|
+
* @returns {Element} the overlay node.
|
|
231
|
+
*/
|
|
232
|
+
export function paintFrameGuardOverlay(hostEl, message) {
|
|
233
|
+
var host = hostEl || (typeof document !== 'undefined' ? document.body : null);
|
|
234
|
+
if (!host) { return null; }
|
|
235
|
+
var existing = host.querySelector && host.querySelector('[data-role="experiment-frame-error"]');
|
|
236
|
+
if (existing) { existing.parentNode.removeChild(existing); }
|
|
237
|
+
var box = host.ownerDocument.createElement('div');
|
|
238
|
+
box.setAttribute('data-role', 'experiment-frame-error');
|
|
239
|
+
box.style.cssText =
|
|
240
|
+
'position:absolute;inset:0;z-index:9999;padding:16px;overflow:auto;' +
|
|
241
|
+
'white-space:pre-wrap;background:#1a0000;color:#ff6b6b;border:2px solid #b6161c;' +
|
|
242
|
+
'font:12px/1.55 ui-monospace,Menlo,Consolas,monospace;';
|
|
243
|
+
box.textContent = message;
|
|
244
|
+
if (host.style && (!host.style.position || host.style.position === 'static')) {
|
|
245
|
+
host.style.position = 'relative';
|
|
246
|
+
}
|
|
247
|
+
host.appendChild(box);
|
|
248
|
+
return box;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* watchExperimentFrame(iframeEl, opts) — poll the frame until it is proven to be
|
|
253
|
+
* a canonical experiment page, or FAIL LOUD (console.error + a visible overlay +
|
|
254
|
+
* `onError`).
|
|
255
|
+
*
|
|
256
|
+
* @param {HTMLIFrameElement} iframeEl
|
|
257
|
+
* @param {{
|
|
258
|
+
* overlayHost?: Element, // defaults to iframeEl.parentNode
|
|
259
|
+
* timeoutMs?: number,
|
|
260
|
+
* pollMs?: number,
|
|
261
|
+
* onOk?: function(Window):void,
|
|
262
|
+
* onError?: function({verdict:string,message:string}):void
|
|
263
|
+
* }} [opts]
|
|
264
|
+
* @returns {function():void} stop() — cancels the watch (call from useEffect cleanup).
|
|
265
|
+
*/
|
|
266
|
+
export function watchExperimentFrame(iframeEl, opts) {
|
|
267
|
+
var o = opts || {};
|
|
268
|
+
var timeout = typeof o.timeoutMs === 'number' ? o.timeoutMs : DEFAULT_FRAME_TIMEOUT_MS;
|
|
269
|
+
var pollMs = typeof o.pollMs === 'number' ? o.pollMs : 150;
|
|
270
|
+
var started = Date.now();
|
|
271
|
+
var stopped = false;
|
|
272
|
+
var timer = setInterval(tick, pollMs);
|
|
273
|
+
tick();
|
|
274
|
+
|
|
275
|
+
function tick() {
|
|
276
|
+
if (stopped) { return; }
|
|
277
|
+
var probe = probeExperimentFrame(iframeEl, { elapsedMs: Date.now() - started, timeoutMs: timeout });
|
|
278
|
+
var result = classifyFrameProbe(probe);
|
|
279
|
+
if (!result.done) { return; }
|
|
280
|
+
stop();
|
|
281
|
+
if (result.ok) {
|
|
282
|
+
if (typeof o.onOk === 'function') { o.onOk(iframeEl.contentWindow); }
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (typeof console !== 'undefined' && console.error) { console.error(result.message); }
|
|
286
|
+
paintFrameGuardOverlay(o.overlayHost || (iframeEl && iframeEl.parentNode), result.message);
|
|
287
|
+
if (typeof o.onError === 'function') { o.onError(result); }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function stop() {
|
|
291
|
+
if (stopped) { return; }
|
|
292
|
+
stopped = true;
|
|
293
|
+
clearInterval(timer);
|
|
294
|
+
}
|
|
295
|
+
return stop;
|
|
296
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ast-schema.js — Expression AST WIRE CONTRACT for FRQTL op-programs.
|
|
5
|
+
*
|
|
6
|
+
* This module is the cross-engine authority for op-program AST node shapes,
|
|
7
|
+
* operator vocabularies, and evaluation conventions. It is read-contract-shaped
|
|
8
|
+
* (like lib/shim/frame-data SECTION_KEYS) — it exports node SHAPES and vocab
|
|
9
|
+
* LISTS; it does NOT contain any parsing or evaluation logic.
|
|
10
|
+
*
|
|
11
|
+
* Architecture: lib/shim/expr/ (this file) ← imported by lib/frqtl/objects/
|
|
12
|
+
* sugar (JS-side parser → ast-schema shapes)
|
|
13
|
+
* and by the dual-native evaluators in the JS reference and Rust engines.
|
|
14
|
+
* Arrow-legality: lib/frqtl/objects imports lib/shim (downward — legal).
|
|
15
|
+
*
|
|
16
|
+
* Gate-2 architect binding (2026-06-21, slice 3):
|
|
17
|
+
* • Rust NEVER parses — AST-only payload; this schema is the data shape.
|
|
18
|
+
* • NODE_KINDS and vocab lists are the conformance iteration surface
|
|
19
|
+
* (binding item 3 — conformance iterates these lists).
|
|
20
|
+
* • Integer-first evaluation convention is documented here (binding item 4).
|
|
21
|
+
* • Named refs compile to integer handles JS-side (binding item 5) — this
|
|
22
|
+
* schema defines the Ref shape ({handle:number, prop:string}).
|
|
23
|
+
*
|
|
24
|
+
* HOW TO READ THIS FILE
|
|
25
|
+
* ─────────────────────
|
|
26
|
+
* An ExprAST is one of:
|
|
27
|
+
*
|
|
28
|
+
* Lit { kind:'Lit', value: number }
|
|
29
|
+
* A numeric literal. Floating-point JSON number; rounded to integer
|
|
30
|
+
* by integer-first eval where the target property is integer-domain.
|
|
31
|
+
*
|
|
32
|
+
* Ref { kind:'Ref', handle: number, prop: string }
|
|
33
|
+
* Read handle.prop at eval time. handle is an integer index into the
|
|
34
|
+
* scope's object table (compiled JS-side — NOT a string name).
|
|
35
|
+
* Special handles: HANDLE_FRAME / HANDLE_SELF / HANDLE_UNIVERSE (below).
|
|
36
|
+
*
|
|
37
|
+
* Bin { kind:'Bin', op: BIN_OP, a: ExprAST, b: ExprAST }
|
|
38
|
+
* Binary arithmetic. INTEGER-FIRST: '/' is integer division (truncate
|
|
39
|
+
* toward zero). Result of '/' is Math.trunc(a/b) in JS, a/b as i64
|
|
40
|
+
* truncated in Rust. Never produces a float in integer-domain props.
|
|
41
|
+
*
|
|
42
|
+
* Cmp { kind:'Cmp', op: CMP_OP, a: ExprAST, b: ExprAST }
|
|
43
|
+
* Comparison. Returns 1 (true) or 0 (false) — integer, not boolean.
|
|
44
|
+
* Required for Cond tests and guard expressions.
|
|
45
|
+
*
|
|
46
|
+
* Cond { kind:'Cond', test: ExprAST, then: ExprAST, else: ExprAST }
|
|
47
|
+
* Conditional: if test != 0 then 'then' else 'else'.
|
|
48
|
+
* test is truthy when non-zero (follows from Cmp returning 0/1).
|
|
49
|
+
*
|
|
50
|
+
* Fn { kind:'Fn', name: FN_NAME, args: ExprAST[] }
|
|
51
|
+
* Built-in function. min/max accept two args; floor accepts one.
|
|
52
|
+
* INTEGER-FIRST: floor(x) is already integer in integer-domain
|
|
53
|
+
* contexts; on float-domain props it applies Math.floor / Rust floor.
|
|
54
|
+
*
|
|
55
|
+
* StrLit { kind:'StrLit', value: string }
|
|
56
|
+
* A string literal — valid ONLY as the `to` value of a `set` op
|
|
57
|
+
* whose target is `<handle>.data.<key>` (a string-map write).
|
|
58
|
+
* MUST throw (JS) / panic (Rust) if used as a child of Bin, Cmp,
|
|
59
|
+
* Fn, or Cond.test — eval_expr stays numeric-pure (INTEGER-FIRST
|
|
60
|
+
* ruling; Gate-2 2026-06-21 item 2). The op-apply layer
|
|
61
|
+
* (_runOpProgram / apply_set_op) detects StrLit before calling
|
|
62
|
+
* eval_expr and routes it to the string-map writer.
|
|
63
|
+
*
|
|
64
|
+
* OP-PROGRAM SHAPE
|
|
65
|
+
* ─────────────────
|
|
66
|
+
* { on: TRIGGER, ops: [ { set: '<handle>.<prop>', to: ExprAST }, ... ] }
|
|
67
|
+
*
|
|
68
|
+
* TRIGGER:
|
|
69
|
+
* 'collision' — ops run per colliding frame (scope: frame, self, universe)
|
|
70
|
+
* 'tick' — ops run per LO per tick (scope: self, named refs, universe)
|
|
71
|
+
*
|
|
72
|
+
* set target format: '<integer-handle>.<prop-name>'
|
|
73
|
+
* The JS sugar compiles readable names ('frame', 'self', 'emitterA', …)
|
|
74
|
+
* to integer handles before the AST crosses the boundary. Rust parses
|
|
75
|
+
* '<number>.<string>' — no string-name registry on the Rust side.
|
|
76
|
+
*
|
|
77
|
+
* EVALUATION SCOPE
|
|
78
|
+
* ─────────────────
|
|
79
|
+
* on:'collision':
|
|
80
|
+
* HANDLE_FRAME (0) — the colliding frame (Frame properties)
|
|
81
|
+
* HANDLE_SELF (1) — this LO / emitter (LO/emitter properties)
|
|
82
|
+
* HANDLE_UNIVERSE (2) — universe read-only (tick, activeQuanta)
|
|
83
|
+
*
|
|
84
|
+
* on:'tick':
|
|
85
|
+
* HANDLE_SELF (1) — this LO/emitter
|
|
86
|
+
* HANDLE_UNIVERSE (2) — universe
|
|
87
|
+
* 3, 4, 5, … — named refs (other registered objects by handle)
|
|
88
|
+
*
|
|
89
|
+
* INTEGER-FIRST EVALUATION CONVENTION (binding item 4)
|
|
90
|
+
* ──────────────────────────────────────────────────────
|
|
91
|
+
* All arithmetic operates on the raw numeric values from the scope.
|
|
92
|
+
* '/' is INTEGER DIVISION: Math.trunc(a / b) in JS; (a as i64) / (b as i64)
|
|
93
|
+
* in Rust. Division by zero → 0 (not an error; matches discrete physics).
|
|
94
|
+
* '==' / '<' / '>' return 1 (true) or 0 (false) — integer.
|
|
95
|
+
* 'floor' on an already-integer value is a no-op (included for completeness).
|
|
96
|
+
* NO silent float result is written into an integer-domain property (x, y, vx,
|
|
97
|
+
* vy, mass, count, etc.). The 'set' op applies Math.round() / Rust f64.round()
|
|
98
|
+
* before writing integer-domain props identified in the Frame / LO tables.
|
|
99
|
+
*
|
|
100
|
+
* ADVERSARIAL CASES (conformance must cover these — binding item 4):
|
|
101
|
+
* Bin('/', Lit(7), Lit(2)) → 3 (truncate, not round or floor)
|
|
102
|
+
* Bin('/', Lit(-7), Lit(2)) → -3 (truncate toward zero, not floor: -4)
|
|
103
|
+
* Fn('floor', [Lit(7.9)]) → 7 (floor, not truncate: same for positives,
|
|
104
|
+
* differs for negatives: floor(-7.9)=-8 ≠ trunc(-7.9)=-7)
|
|
105
|
+
* Cmp('==', Lit(1), Lit(1)) → 1
|
|
106
|
+
* Cmp('==', Lit(1), Lit(2)) → 0
|
|
107
|
+
* Cmp('<', Lit(3), Lit(5)) → 1
|
|
108
|
+
* Cmp('>', Lit(5), Lit(3)) → 1
|
|
109
|
+
* nested Cond(Cmp…, Lit, Lit)
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
// ── Node kind names ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* NODE_KINDS — the complete, ordered list of expression AST node kinds.
|
|
118
|
+
*
|
|
119
|
+
* Conformance iterates this list to assert coverage (binding item 3):
|
|
120
|
+
* every kind must have at least one representative scenario in
|
|
121
|
+
* expr_program_scenarios.js.
|
|
122
|
+
*
|
|
123
|
+
* THIS LIST IS CLOSED AT 7 — do NOT add structural op kinds here.
|
|
124
|
+
* Structural op kinds live in OP_KINDS (below). The two lists are orthogonal.
|
|
125
|
+
*
|
|
126
|
+
* NOTE on StrLit (item 7): StrLit is ONLY valid as the `to` value of a `set`
|
|
127
|
+
* op targeting `<handle>.data.<key>` (a string-map write). Using StrLit as a
|
|
128
|
+
* child of Bin, Cmp, Fn, or Cond.test MUST throw/panic — eval_expr is
|
|
129
|
+
* numeric-pure (INTEGER-FIRST ruling, Gate-2 2026-06-21 item 2).
|
|
130
|
+
*
|
|
131
|
+
* @type {string[]}
|
|
132
|
+
*/
|
|
133
|
+
var NODE_KINDS = Object.freeze([
|
|
134
|
+
'Lit', // numeric literal
|
|
135
|
+
'Ref', // property read via handle
|
|
136
|
+
'Bin', // binary arithmetic
|
|
137
|
+
'Cmp', // comparison → 0/1
|
|
138
|
+
'Cond', // conditional
|
|
139
|
+
'Fn', // built-in function
|
|
140
|
+
'StrLit' // string literal — ONLY valid as `to` in data.<key> set ops
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* OP_KINDS — the complete, ordered list of op-program STATEMENT kinds.
|
|
145
|
+
*
|
|
146
|
+
* These are the kinds of ops that can appear in program.ops[].
|
|
147
|
+
* They are DISTINCT from the value-grammar node kinds in NODE_KINDS.
|
|
148
|
+
*
|
|
149
|
+
* §2e (2026-06-24): OP_KINDS is closed at ['set'].
|
|
150
|
+
* addFrames / subtractFrames / addDimension were NEVER implemented as op-kinds
|
|
151
|
+
* in the Rust engine (OpProgram = ops: Vec<SetOp>; SetOp = {set, to}). Their
|
|
152
|
+
* structural analogs are engine METHODS, not op-program statements:
|
|
153
|
+
* - Frame injection → emitters / makeLeafFrame / createFrameFromRect / insert
|
|
154
|
+
* - Frame removal → set 0.is_alive=0 + dead-parent GC
|
|
155
|
+
* - Dimension alloc → universe.addDimension(n) / add_dimension engine method
|
|
156
|
+
*
|
|
157
|
+
* Gate-2 architect binding (2026-06-21, §10.8 B3):
|
|
158
|
+
* • B3.2 — 'collect' is NOT a 2nd op. It compiles to:
|
|
159
|
+
* { kind:'set', set:'1.count', to: Bin('+', Ref(HANDLE_SELF,'count'), Lit(1)) }
|
|
160
|
+
* i.e. a plain 'set' op on the LO's 'count' property. No special sink.
|
|
161
|
+
*
|
|
162
|
+
* Op shapes:
|
|
163
|
+
* set — { kind:'set', set:'<handle>.<prop>', to: ExprAST }
|
|
164
|
+
*
|
|
165
|
+
* Conformance iterates this list (binding item 3):
|
|
166
|
+
* every kind must have at least one scenario in control_loop_scenarios.js.
|
|
167
|
+
*
|
|
168
|
+
* @type {string[]}
|
|
169
|
+
*/
|
|
170
|
+
var OP_KINDS = Object.freeze([
|
|
171
|
+
'set' // property write: routes through setFrameProperty / setObjectProperty
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
// ── Operator vocabularies ──────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* BIN_OPS — binary arithmetic operators.
|
|
178
|
+
* '/' is INTEGER DIVISION (truncate toward zero) — see convention above.
|
|
179
|
+
*
|
|
180
|
+
* Conformance must cover all four.
|
|
181
|
+
* @type {string[]}
|
|
182
|
+
*/
|
|
183
|
+
var BIN_OPS = Object.freeze(['+', '-', '*', '/']);
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* CMP_OPS — comparison operators. All return 1 or 0.
|
|
187
|
+
* Conformance must cover all three.
|
|
188
|
+
* @type {string[]}
|
|
189
|
+
*/
|
|
190
|
+
var CMP_OPS = Object.freeze(['<', '>', '==']);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* FN_NAMES — built-in function names.
|
|
194
|
+
* min/max: 2 args. floor: 1 arg. random: 0 args.
|
|
195
|
+
* Conformance must cover all four.
|
|
196
|
+
*
|
|
197
|
+
* NOTE on 'random' (G2, 2026-06-24):
|
|
198
|
+
* Fn('random', []) draws one value from the engine's single Mulberry32 stream.
|
|
199
|
+
* It is a SIDE-EFFECTING draw — each eval call advances the RNG by one step.
|
|
200
|
+
* JS eval: closes over the engine's _rng() function (already in scope in
|
|
201
|
+
* _runOpProgram).
|
|
202
|
+
* Rust eval: eval_expr receives &mut rng so the draw advances the shared stream.
|
|
203
|
+
* Draw order: the ops array is evaluated left-to-right (op 0 before op 1 etc.)
|
|
204
|
+
* and each Fn('random',[]) in a `to` expression is evaluated in tree order
|
|
205
|
+
* (left child before right child for Bin/Fn). This is deterministic.
|
|
206
|
+
* RNG discipline: velocity set-ops MUST appear LAST in the op-program ops array
|
|
207
|
+
* (after any ABSORB/COUNT/MARK/BLOCK ops) so that random() draws land at the
|
|
208
|
+
* same point in the global RNG sequence as the native RANDOM_* dispatch did
|
|
209
|
+
* (which ran AFTER action dispatch in the collision loop).
|
|
210
|
+
*
|
|
211
|
+
* NOTE on 'intersects' (G2b, 2026-06-25):
|
|
212
|
+
* Fn('intersects', [Lit(h0), Lit(h1)]) is a NATIVE GEOMETRY CALL — not numeric
|
|
213
|
+
* arithmetic. It calls the native Rectangle::intersects primitive on the
|
|
214
|
+
* scope-resolved rects for handle h0 and h1 (reading left/right/top/bottom from
|
|
215
|
+
* the scope bag for each handle). Returns 1 (true) or 0 (false).
|
|
216
|
+
*
|
|
217
|
+
* In practice args are always [Lit(HANDLE_FRAME), Lit(HANDLE_SELF)] (frame vs LO).
|
|
218
|
+
* Compiled exclusively by actionCompiler._buildTriggerExpr — do NOT construct
|
|
219
|
+
* manually.
|
|
220
|
+
*
|
|
221
|
+
* This is the canonical overlap test for op-programs (replaces the native
|
|
222
|
+
* overlaps/past_wall gate that was deleted from process_large_object_collisions in
|
|
223
|
+
* G2b). The JS reference (EGPTfrqtl.js) is FROZEN — intersects is a WASM-canonical
|
|
224
|
+
* addition only (JS reference keeps the old native gate).
|
|
225
|
+
*
|
|
226
|
+
* Draw discipline: intersects draws ZERO RNG values (deterministic geometry).
|
|
227
|
+
* @type {string[]}
|
|
228
|
+
*/
|
|
229
|
+
var FN_NAMES = Object.freeze(['min', 'max', 'floor', 'random', 'intersects']);
|
|
230
|
+
|
|
231
|
+
// ── Trigger vocabulary ─────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* TRIGGERS — valid values for program.on.
|
|
235
|
+
* @type {string[]}
|
|
236
|
+
*/
|
|
237
|
+
var TRIGGERS = Object.freeze(['collision', 'tick']);
|
|
238
|
+
|
|
239
|
+
// ── Reserved scope handles ─────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* HANDLE_FRAME — scope handle for the colliding frame in on:'collision' programs.
|
|
243
|
+
* Reads Frame properties (§2.3 table: x, y, vx, vy, mass, …).
|
|
244
|
+
* @type {number}
|
|
245
|
+
*/
|
|
246
|
+
var HANDLE_FRAME = 0;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* HANDLE_SELF — scope handle for the owning LO/emitter.
|
|
250
|
+
* Reads LO descriptor properties (rect, count, …) or emitter properties.
|
|
251
|
+
* @type {number}
|
|
252
|
+
*/
|
|
253
|
+
var HANDLE_SELF = 1;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* HANDLE_UNIVERSE — scope handle for universe read-only state.
|
|
257
|
+
* Properties: 'tick' (u64), 'activeQuanta' (usize).
|
|
258
|
+
* @type {number}
|
|
259
|
+
*/
|
|
260
|
+
var HANDLE_UNIVERSE = 2;
|
|
261
|
+
|
|
262
|
+
// ── Node shape helpers (JS-side construction) ──────────────────────────────────
|
|
263
|
+
//
|
|
264
|
+
// These are OPTIONAL helpers for constructing AST nodes from JS sugar.
|
|
265
|
+
// The shapes they produce are the canonical wire format — Rust deserializes
|
|
266
|
+
// the same JSON structure. Using these helpers is not required; you may
|
|
267
|
+
// construct the object literal directly.
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Lit(n) — numeric literal node.
|
|
271
|
+
* @param {number} value
|
|
272
|
+
* @returns {{ kind:'Lit', value:number }}
|
|
273
|
+
*/
|
|
274
|
+
function Lit(value) {
|
|
275
|
+
return { kind: 'Lit', value: value };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Ref(handle, prop) — property read node.
|
|
280
|
+
* @param {number} handle — integer scope handle (HANDLE_FRAME / HANDLE_SELF / HANDLE_UNIVERSE / …)
|
|
281
|
+
* @param {string} prop — property name
|
|
282
|
+
* @returns {{ kind:'Ref', handle:number, prop:string }}
|
|
283
|
+
*/
|
|
284
|
+
function Ref(handle, prop) {
|
|
285
|
+
return { kind: 'Ref', handle: handle, prop: prop };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Bin(op, a, b) — binary arithmetic node.
|
|
290
|
+
* op must be a member of BIN_OPS.
|
|
291
|
+
* @param {string} op
|
|
292
|
+
* @param {*} a — ExprAST
|
|
293
|
+
* @param {*} b — ExprAST
|
|
294
|
+
* @returns {{ kind:'Bin', op:string, a:*, b:* }}
|
|
295
|
+
*/
|
|
296
|
+
function Bin(op, a, b) {
|
|
297
|
+
return { kind: 'Bin', op: op, a: a, b: b };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Cmp(op, a, b) — comparison node. Returns 1 or 0.
|
|
302
|
+
* op must be a member of CMP_OPS.
|
|
303
|
+
* @param {string} op
|
|
304
|
+
* @param {*} a — ExprAST
|
|
305
|
+
* @param {*} b — ExprAST
|
|
306
|
+
* @returns {{ kind:'Cmp', op:string, a:*, b:* }}
|
|
307
|
+
*/
|
|
308
|
+
function Cmp(op, a, b) {
|
|
309
|
+
return { kind: 'Cmp', op: op, a: a, b: b };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Cond(test, then_, else_) — conditional node.
|
|
314
|
+
* @param {*} test — ExprAST; truthy when non-zero
|
|
315
|
+
* @param {*} then_ — ExprAST; result when test != 0
|
|
316
|
+
* @param {*} else_ — ExprAST; result when test == 0
|
|
317
|
+
* @returns {{ kind:'Cond', test:*, then:*, else:* }}
|
|
318
|
+
*/
|
|
319
|
+
function Cond(test, then_, else_) {
|
|
320
|
+
return { kind: 'Cond', test: test, then: then_, else: else_ };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Fn(name, args) — built-in function node.
|
|
325
|
+
* name must be a member of FN_NAMES.
|
|
326
|
+
* @param {string} name
|
|
327
|
+
* @param {Array<*>} args — ExprAST[]
|
|
328
|
+
* @returns {{ kind:'Fn', name:string, args:Array }}
|
|
329
|
+
*/
|
|
330
|
+
function Fn(name, args) {
|
|
331
|
+
return { kind: 'Fn', name: name, args: args };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* StrLit(value) — string literal node (NODE_KINDS item 7).
|
|
336
|
+
*
|
|
337
|
+
* ONLY valid as the `to` value of a `set` op whose target is
|
|
338
|
+
* `<handle>.data.<key>` (a string-map write into the frame bubble).
|
|
339
|
+
*
|
|
340
|
+
* ILLEGAL as a child of Bin / Cmp / Fn / Cond.test — eval_expr is
|
|
341
|
+
* numeric-pure. The op-apply layer detects StrLit before calling eval_expr.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} value
|
|
344
|
+
* @returns {{ kind:'StrLit', value:string }}
|
|
345
|
+
*/
|
|
346
|
+
function StrLit(value) {
|
|
347
|
+
return { kind: 'StrLit', value: value };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Exports ────────────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
export {
|
|
353
|
+
NODE_KINDS,
|
|
354
|
+
OP_KINDS,
|
|
355
|
+
BIN_OPS,
|
|
356
|
+
CMP_OPS,
|
|
357
|
+
FN_NAMES,
|
|
358
|
+
TRIGGERS,
|
|
359
|
+
HANDLE_FRAME,
|
|
360
|
+
HANDLE_SELF,
|
|
361
|
+
HANDLE_UNIVERSE,
|
|
362
|
+
Lit,
|
|
363
|
+
Ref,
|
|
364
|
+
Bin,
|
|
365
|
+
Cmp,
|
|
366
|
+
Cond,
|
|
367
|
+
Fn,
|
|
368
|
+
StrLit
|
|
369
|
+
};
|