Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Apps/Playground/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ set(DEPENDENCIES
"../Dependencies/recast.js")

set(SCRIPTS
"Scripts/es2019_transpile.js"
"Scripts/experience.js"
"Scripts/playground_runner.js"
"Scripts/validation_native.js"
Expand Down
52 changes: 25 additions & 27 deletions Apps/Playground/Scripts/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1553,7 +1553,7 @@
"playgroundId": "#8NTR5X#8",
"replace": "//options//, roundtrip = true;",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
"referenceImage": "glTFSerializerTextureExport.png"
},
{
Expand Down Expand Up @@ -2340,7 +2340,7 @@
"title": "Material Plugin",
"playgroundId": "#22HT5Z#10",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
"referenceImage": "materialPlugin.png"
},
{
Expand Down Expand Up @@ -2427,7 +2427,7 @@
"playgroundId": "#PCMH7A#2",
"renderCount": 120,
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
"referenceImage": "fluidBoxSphere.png"
},
{
Expand Down Expand Up @@ -2812,8 +2812,6 @@
"title": "custom-handling-of-materials-for-render-target-pass",
"playgroundId": "#FIVL25#21",
"renderCount": 60,
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"referenceImage": "custom-handling-of-materials-for-render-target-pass.png"
},
{
Expand Down Expand Up @@ -2907,7 +2905,7 @@
"renderCount": 2,
"replace": "//options//, modelIndex = 2;",
"excludeFromAutomaticTesting": true,
"reason": "Test crashes or hangs on Babylon Native",
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
"referenceImage": "computeMaxExtents-BoxAnimated.png"
},
{
Expand All @@ -2916,7 +2914,7 @@
"renderCount": 2,
"replace": "//options//, modelIndex = 4; animationIndex = 0;",
"excludeFromAutomaticTesting": true,
"reason": "Test crashes or hangs on Babylon Native",
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
"referenceImage": "computeMaxExtents-Fox0.png"
},
{
Expand All @@ -2925,7 +2923,7 @@
"renderCount": 2,
"replace": "//options//, modelIndex = 4; animationIndex = 1;",
"excludeFromAutomaticTesting": true,
"reason": "Test crashes or hangs on Babylon Native",
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
"referenceImage": "computeMaxExtents-Fox1.png"
},
{
Expand All @@ -2934,7 +2932,7 @@
"renderCount": 2,
"replace": "//options//, modelIndex = 5;",
"excludeFromAutomaticTesting": true,
"reason": "Test crashes or hangs on Babylon Native",
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
"referenceImage": "computeMaxExtents-MorphStressTest.png"
},
{
Expand Down Expand Up @@ -3047,7 +3045,7 @@
"title": "FrameGraph nrge custom rendering",
"playgroundId": "#1QCA2M#35",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
"referenceImage": "FrameGraph-nrge-custom-rendering.png"
},
{
Expand All @@ -3061,14 +3059,14 @@
"title": "FrameGraph nrge glow layer",
"playgroundId": "#IG8NRC#84",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails: cubemap files not defined (asset gap).",
"referenceImage": "FrameGraph-nrge-glow-layer.png"
},
{
"title": "FrameGraph nrge highlight layer",
"playgroundId": "#QZYNMK#3",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
"referenceImage": "FrameGraph-nrge-highlight-layer.png"
},
{
Expand Down Expand Up @@ -3126,7 +3124,7 @@
"title": "FrameGraph nrge rig camera",
"playgroundId": "#ATL1CS#19",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
"referenceImage": "FrameGraph-nrge-rig-camera.png"
},
{
Expand All @@ -3153,7 +3151,7 @@
"title": "FrameGraph nrge volumetric lighting",
"playgroundId": "#3VH0AC#2",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime hangs (FrameGraph volumetric lighting).",
"referenceImage": "FrameGraph-nrge-volumetric-lighting.png"
},
{
Expand All @@ -3180,14 +3178,14 @@
"title": "FrameGraph nrge transmission",
"playgroundId": "#ZNTBN2#10",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
"referenceImage": "FrameGraph-nrge-transmission.png"
},
{
"title": "FrameGraph nrge selection outline layer",
"playgroundId": "#ADUC74#1",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
"referenceImage": "FrameGraph-nrge-selection-outline-layer.png"
},
{
Expand Down Expand Up @@ -3295,7 +3293,7 @@
"title": "FrameGraph volumetric lighting",
"playgroundId": "#3VH0AC",
"excludeFromAutomaticTesting": true,
"reason": "Test crashes or hangs on Babylon Native",
"reason": "Parse repaired via ES2019 polyfill; runtime TypeError (FrameGraph volumetric lighting).",
"referenceImage": "FrameGraph-volumetric-lighting.png"
},
{
Expand All @@ -3316,14 +3314,14 @@
"title": "FrameGraph selection outline",
"playgroundId": "#E1F0GP#4",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails: Unknown error opening URL.",
"referenceImage": "FrameGraph-selection-outline.png"
},
{
"title": "Render target texture with clustered lights",
"playgroundId": "#1QCA2M#11",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).",
"referenceImage": "Render-target-texture-with-clustered-lights.png"
},
{
Expand Down Expand Up @@ -3449,14 +3447,14 @@
"title": "Sponza Clustered Lighting",
"playgroundId": "#CSCJO2#17",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).",
"referenceImage": "sponza-clustered-lighting.png"
},
{
"title": "Sponza Clustered Lighting (2 viewports)",
"playgroundId": "#CSCJO2#20",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).",
"referenceImage": "sponza-clustered-lighting-viewports.png"
},
{
Expand Down Expand Up @@ -3537,28 +3535,28 @@
"title": "Atmosphere Day",
"playgroundId": "#VO1Z0C#39",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
"referenceImage": "atmosphere-day.png"
},
{
"title": "Atmosphere Day (Planet Origin)",
"playgroundId": "#VO1Z0C#40",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
"referenceImage": "atmosphere-day.png"
},
{
"title": "Atmosphere Day (Ray Marching)",
"playgroundId": "#VO1Z0C#41",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
"referenceImage": "atmosphere-day-ray-marching.png"
},
{
"title": "Atmosphere Sunset",
"playgroundId": "#VO1Z0C#42",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
"referenceImage": "atmosphere-sunset.png"
},
{
Expand Down Expand Up @@ -4249,7 +4247,7 @@
"renderCount": 180,
"useLargeWorldRendering": true,
"excludeFromAutomaticTesting": true,
"reason": "Test crashes or hangs on Babylon Native",
"reason": "Parse repaired via ES2019 polyfill; runtime fails: 'HK' (Havok) global not defined.",
"referenceImage": "Havok-FloatingOrigin-Multi-Region.png"
},
{
Expand Down Expand Up @@ -4652,7 +4650,7 @@
"title": "Selection outline layer with instances",
"playgroundId": "#UR9706#0",
"excludeFromAutomaticTesting": true,
"reason": "Pixel comparison fails (more than 20% pixels differ)",
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue).",
"referenceImage": "Selection-outline-layer-with-instances.png"
},
{
Expand Down
114 changes: 114 additions & 0 deletions Apps/Playground/Scripts/es2019_transpile.js
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we doing this with a custom script rather than just using babel or swc or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. Honest assessment of the tradeoff:

  • The custom regex polyfill runs inside the Chakra host at parse-failure time, so a Node-side build tool (babel CLI / swc CLI) wasn't directly usable. The runtime-in-host constraint left us with @babel/standalone (~1 MB shipped + parsed by Chakra at startup), swc-wasm (Chakra doesn't support WASM), or this regex stopgap.
  • The regex is semantically incomplete — ?. rewrites to . so null?.foo throws TypeError instead of returning undefined. That's 9 of the 26 affected tests still excluded because of this divergence, plus 4 more for ES2022 private fields. So this polyfill unlocks only 1 test out of 26.
  • The cost/benefit of "+114 lines for +1 test, with a semantic footgun baked in" is too thin to justify cementing the regex approach.

Closing this PR. Filed #1711 to investigate @babel/standalone properly (measure the actual size/startup cost, prototype, compare against alternatives, decide). If that lands, it would unlock the 9 ?. tests + 4 ES2022 tests correctly, which is a much better story than what this PR achieves.

Thanks for pushing back.

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// es2019_transpile.js
//
// Lightweight regex-based ES2020+ -> ES2019 syntax repair for engines that
// lack optional chaining (?.), nullish coalescing (??), and numeric
// separators (1_000_000).
//
// This is a SYNTAX REPAIR, not a full transpile. It rewrites parse-time
// failing tokens to the closest legal ES2019 expressions so that the host
// engine accepts the code. Where native ES2020+ semantics differ from the
// rewritten form, the rewritten form runs the "happy path" -- assumes the
// target value is present. If the target is null at runtime the rewritten
// code throws a TypeError where native ?. would have short-circuited to
// undefined. This is an intentional trade-off: it produces valid syntax
// (the prerequisite for the test running at all) while preserving the
// common case behaviour. Tests that rely on the nullish-short-circuit
// semantics may surface runtime errors that were previously hidden by
// the parse failure -- a strict improvement for debuggability.
//
// Transforms applied:
// * 1_000_000 -> 1000000 (strip numeric separators)
// * a?.b -> a.b (optional chaining -> required chaining)
// * a?.[x] -> a[x]
// * a?.() -> a()
// * a ?? b -> (a != null ? a : b)
//
// Not handled (still cause parse failures on Chakra):
// * Logical assignment ||= &&= ??=
// * Class private fields #name
// * BigInt literals 1n
//
// String/regex/comment literals containing ? . sequences are not skipped,
// but in Babylon snippet code such occurrences are rare.
//
// On engines with native ES2020+ support (V8, JSC) this code is never
// invoked because the caller only retries with this transform after
// initial eval throws a SyntaxError.
//
// Public API: top-level global function __bnTranspileES2019(code) -> string.
// Chakra in Babylon Native does not define `self` or `globalThis`, so we
// install via a top-level `var` declaration which becomes a property of
// the script-global object across all our supported engines.
var __bnTranspileES2019 = (function () {
"use strict";

function stripNumericSeparators(code) {
// Decimal / float: 1_000_000, 1.234_5, 1_000e3
var out = code.replace(/\b(\d[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?)\b/g, function (m) {
return m.indexOf("_") === -1 ? m : m.replace(/_/g, "");
});
// Hex / octal / binary: 0xFFFF_FFFF, 0o7_5, 0b1010_0101
out = out.replace(/\b(0[xXoObB][0-9A-Fa-f_]+)\b/g, function (m) {
return m.indexOf("_") === -1 ? m : m.replace(/_/g, "");
});
return out;
}

function transformLogicalAssignment(code) {
// a ||= b -> (a || (a = b))
// a &&= b -> (a && (a = b))
// a ??= b -> (a != null ? a : (a = b))
// Only matches simple LHS (identifier, optional .prop / [idx] chain) to keep
// the rewrite syntactically safe; complex LHS expressions are left alone.
var lhs = "(?:[A-Za-z_$][\\w$]*)(?:\\.[A-Za-z_$][\\w$]*|\\[[^\\]\\n]*\\])*";
var rhs = "[^;\\n]+";
var prev = null, out = code, guard = 0;
while (prev !== out && guard++ < 10) {
prev = out;
out = out
.replace(new RegExp("(" + lhs + ")\\s*\\?\\?=\\s*(" + rhs + ")", "g"),
"$1 = ($1 != null ? $1 : ($2))")
.replace(new RegExp("(" + lhs + ")\\s*\\|\\|=\\s*(" + rhs + ")", "g"),
"$1 = ($1 || ($2))")
.replace(new RegExp("(" + lhs + ")\\s*&&=\\s*(" + rhs + ")", "g"),
"$1 = ($1 && ($2))");
}
return out;
}

function transformOptionalChaining(code) {
var prev = null, out = code, guard = 0;
while (prev !== out && guard++ < 50) {
prev = out;
out = out
.replace(/\?\.(?=\()/g, "")
.replace(/\?\.(?=\[)/g, "")
.replace(/\?\./g, ".");
}
return out;
}

function transformNullishCoalescing(code) {
var prev = null, out = code, guard = 0;
while (prev !== out && guard++ < 50) {
prev = out;
out = out.replace(
/(\b[A-Za-z_$][\w$.]*(?:\[[^\]]*\])?)\s*\?\?\s*(\b[A-Za-z_$][\w$.]*(?:\[[^\]]*\])?|"[^"]*"|'[^']*'|\d+(?:\.\d+)?|true|false|null|undefined)/g,
"($1 != null ? $1 : $2)"
);
}
return out;
}

function transpileES2019(code) {
if (typeof code !== "string") { return code; }
var out = code;
out = stripNumericSeparators(out);
out = transformLogicalAssignment(out);
out = transformOptionalChaining(out);
out = transformNullishCoalescing(out);
return out;
}

return transpileES2019;
})();
24 changes: 22 additions & 2 deletions Apps/Playground/Scripts/validation_native.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@
done(false);
}

// Run `eval(src)` directly. If the host engine throws a SyntaxError
// (e.g. Chakra rejecting ?. ?? or numeric separators) and the
// __bnTranspileES2019 helper is available, retry once with an ES2019
// syntax-repaired version of the source. Re-throws on any other error.
function evalWithFallback(src, test) {
try {
return eval(src);
} catch (e) {
if (e instanceof SyntaxError && typeof __bnTranspileES2019 === "function") {
const repaired = __bnTranspileES2019(src);
if (repaired !== src) {
const title = test && test.title ? test.title : "(unknown)";
console.log("Retrying '" + title + "' after ES2019 syntax repair (host engine lacks ES2020+ parse support).");
return eval(repaired);
}
}
throw e;
}
}

// Per-run counters surfaced as a final summary line on exit.
let ranCount = 0;
let passedCount = 0;
Expand Down Expand Up @@ -344,7 +364,7 @@
}
}

currentScene = eval(code + "\r\ncreateScene(engine)");
currentScene = evalWithFallback(code + "\r\ncreateScene(engine)", test);

if (currentScene.then) {
// Handle if createScene returns a promise
Expand Down Expand Up @@ -415,7 +435,7 @@
}
}

currentScene = eval(scriptToRun + test.functionToCall + "(engine)");
currentScene = evalWithFallback(scriptToRun + test.functionToCall + "(engine)", test);
processCurrentScene(test, renderImage, done, compareFunction);
}
catch (e) {
Expand Down
Loading
Loading