Skip to content

Commit cd10aa9

Browse files
Add ES2020+ -> ES2019 syntax repair polyfill for Chakra
Babylon Native's Chakra-based builds parse only up to ES2019; modern Babylon.js playground snippets routinely use optional chaining (?.), nullish coalescing (??), numeric separators (1_000_000), and logical assignment (||= &&= ??=), causing 26 visual tests to fail with SyntaxError at the eval() site in validation_native.js. Add a lightweight regex-based syntax-repair polyfill loaded before babylon.max.js. The polyfill exposes __bnTranspileES2019(code) as a top-level global var. validation_native.js's new evalWithFallback helper catches SyntaxError on the first eval and retries once with the repaired source. Engines that already accept the source (V8, JSC) never hit the retry path. The polyfill is syntax-only (parse-time fix), not semantic - ?. is rewritten to . so a null target throws TypeError at runtime instead of short-circuiting to undefined. This is a deliberate trade-off: parse failures are 100% blocking; runtime TypeErrors are diagnosable and at worst surface a different bug that was previously hidden. Strip excludeFromAutomaticTesting from idx 403 (custom-handling-of-materials-for-render-target-pass) which now passes end-to-end. Updated reasons for the other 25 entries to describe their post-polyfill state: 9 hit runtime errors after parse repair, 12 hit asset/module gaps, 4 hit ES2022 syntax not covered by the polyfill (e.g. private class fields).
1 parent 5c4081e commit cd10aa9

5 files changed

Lines changed: 163 additions & 29 deletions

File tree

Apps/Playground/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ set(DEPENDENCIES
1313
"../Dependencies/recast.js")
1414

1515
set(SCRIPTS
16+
"Scripts/es2019_transpile.js"
1617
"Scripts/experience.js"
1718
"Scripts/playground_runner.js"
1819
"Scripts/validation_native.js"

Apps/Playground/Scripts/config.json

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,7 +1553,7 @@
15531553
"playgroundId": "#8NTR5X#8",
15541554
"replace": "//options//, roundtrip = true;",
15551555
"excludeFromAutomaticTesting": true,
1556-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
1556+
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
15571557
"referenceImage": "glTFSerializerTextureExport.png"
15581558
},
15591559
{
@@ -2340,7 +2340,7 @@
23402340
"title": "Material Plugin",
23412341
"playgroundId": "#22HT5Z#10",
23422342
"excludeFromAutomaticTesting": true,
2343-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
2343+
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
23442344
"referenceImage": "materialPlugin.png"
23452345
},
23462346
{
@@ -2427,7 +2427,7 @@
24272427
"playgroundId": "#PCMH7A#2",
24282428
"renderCount": 120,
24292429
"excludeFromAutomaticTesting": true,
2430-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
2430+
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
24312431
"referenceImage": "fluidBoxSphere.png"
24322432
},
24332433
{
@@ -2812,8 +2812,6 @@
28122812
"title": "custom-handling-of-materials-for-render-target-pass",
28132813
"playgroundId": "#FIVL25#21",
28142814
"renderCount": 60,
2815-
"excludeFromAutomaticTesting": true,
2816-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
28172815
"referenceImage": "custom-handling-of-materials-for-render-target-pass.png"
28182816
},
28192817
{
@@ -2907,7 +2905,7 @@
29072905
"renderCount": 2,
29082906
"replace": "//options//, modelIndex = 2;",
29092907
"excludeFromAutomaticTesting": true,
2910-
"reason": "Test crashes or hangs on Babylon Native",
2908+
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
29112909
"referenceImage": "computeMaxExtents-BoxAnimated.png"
29122910
},
29132911
{
@@ -2916,7 +2914,7 @@
29162914
"renderCount": 2,
29172915
"replace": "//options//, modelIndex = 4; animationIndex = 0;",
29182916
"excludeFromAutomaticTesting": true,
2919-
"reason": "Test crashes or hangs on Babylon Native",
2917+
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
29202918
"referenceImage": "computeMaxExtents-Fox0.png"
29212919
},
29222920
{
@@ -2925,7 +2923,7 @@
29252923
"renderCount": 2,
29262924
"replace": "//options//, modelIndex = 4; animationIndex = 1;",
29272925
"excludeFromAutomaticTesting": true,
2928-
"reason": "Test crashes or hangs on Babylon Native",
2926+
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
29292927
"referenceImage": "computeMaxExtents-Fox1.png"
29302928
},
29312929
{
@@ -2934,7 +2932,7 @@
29342932
"renderCount": 2,
29352933
"replace": "//options//, modelIndex = 5;",
29362934
"excludeFromAutomaticTesting": true,
2937-
"reason": "Test crashes or hangs on Babylon Native",
2935+
"reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.",
29382936
"referenceImage": "computeMaxExtents-MorphStressTest.png"
29392937
},
29402938
{
@@ -3047,7 +3045,7 @@
30473045
"title": "FrameGraph nrge custom rendering",
30483046
"playgroundId": "#1QCA2M#35",
30493047
"excludeFromAutomaticTesting": true,
3050-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3048+
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
30513049
"referenceImage": "FrameGraph-nrge-custom-rendering.png"
30523050
},
30533051
{
@@ -3061,14 +3059,14 @@
30613059
"title": "FrameGraph nrge glow layer",
30623060
"playgroundId": "#IG8NRC#84",
30633061
"excludeFromAutomaticTesting": true,
3064-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3062+
"reason": "Parse repaired via ES2019 polyfill; runtime fails: cubemap files not defined (asset gap).",
30653063
"referenceImage": "FrameGraph-nrge-glow-layer.png"
30663064
},
30673065
{
30683066
"title": "FrameGraph nrge highlight layer",
30693067
"playgroundId": "#QZYNMK#3",
30703068
"excludeFromAutomaticTesting": true,
3071-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3069+
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
30723070
"referenceImage": "FrameGraph-nrge-highlight-layer.png"
30733071
},
30743072
{
@@ -3126,7 +3124,7 @@
31263124
"title": "FrameGraph nrge rig camera",
31273125
"playgroundId": "#ATL1CS#19",
31283126
"excludeFromAutomaticTesting": true,
3129-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3127+
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
31303128
"referenceImage": "FrameGraph-nrge-rig-camera.png"
31313129
},
31323130
{
@@ -3153,7 +3151,7 @@
31533151
"title": "FrameGraph nrge volumetric lighting",
31543152
"playgroundId": "#3VH0AC#2",
31553153
"excludeFromAutomaticTesting": true,
3156-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3154+
"reason": "Parse repaired via ES2019 polyfill; runtime hangs (FrameGraph volumetric lighting).",
31573155
"referenceImage": "FrameGraph-nrge-volumetric-lighting.png"
31583156
},
31593157
{
@@ -3180,14 +3178,14 @@
31803178
"title": "FrameGraph nrge transmission",
31813179
"playgroundId": "#ZNTBN2#10",
31823180
"excludeFromAutomaticTesting": true,
3183-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3181+
"reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).",
31843182
"referenceImage": "FrameGraph-nrge-transmission.png"
31853183
},
31863184
{
31873185
"title": "FrameGraph nrge selection outline layer",
31883186
"playgroundId": "#ADUC74#1",
31893187
"excludeFromAutomaticTesting": true,
3190-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3188+
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).",
31913189
"referenceImage": "FrameGraph-nrge-selection-outline-layer.png"
31923190
},
31933191
{
@@ -3295,7 +3293,7 @@
32953293
"title": "FrameGraph volumetric lighting",
32963294
"playgroundId": "#3VH0AC",
32973295
"excludeFromAutomaticTesting": true,
3298-
"reason": "Test crashes or hangs on Babylon Native",
3296+
"reason": "Parse repaired via ES2019 polyfill; runtime TypeError (FrameGraph volumetric lighting).",
32993297
"referenceImage": "FrameGraph-volumetric-lighting.png"
33003298
},
33013299
{
@@ -3316,14 +3314,14 @@
33163314
"title": "FrameGraph selection outline",
33173315
"playgroundId": "#E1F0GP#4",
33183316
"excludeFromAutomaticTesting": true,
3319-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3317+
"reason": "Parse repaired via ES2019 polyfill; runtime fails: Unknown error opening URL.",
33203318
"referenceImage": "FrameGraph-selection-outline.png"
33213319
},
33223320
{
33233321
"title": "Render target texture with clustered lights",
33243322
"playgroundId": "#1QCA2M#11",
33253323
"excludeFromAutomaticTesting": true,
3326-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3324+
"reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).",
33273325
"referenceImage": "Render-target-texture-with-clustered-lights.png"
33283326
},
33293327
{
@@ -3449,14 +3447,14 @@
34493447
"title": "Sponza Clustered Lighting",
34503448
"playgroundId": "#CSCJO2#17",
34513449
"excludeFromAutomaticTesting": true,
3452-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3450+
"reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).",
34533451
"referenceImage": "sponza-clustered-lighting.png"
34543452
},
34553453
{
34563454
"title": "Sponza Clustered Lighting (2 viewports)",
34573455
"playgroundId": "#CSCJO2#20",
34583456
"excludeFromAutomaticTesting": true,
3459-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3457+
"reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).",
34603458
"referenceImage": "sponza-clustered-lighting-viewports.png"
34613459
},
34623460
{
@@ -3537,28 +3535,28 @@
35373535
"title": "Atmosphere Day",
35383536
"playgroundId": "#VO1Z0C#39",
35393537
"excludeFromAutomaticTesting": true,
3540-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3538+
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
35413539
"referenceImage": "atmosphere-day.png"
35423540
},
35433541
{
35443542
"title": "Atmosphere Day (Planet Origin)",
35453543
"playgroundId": "#VO1Z0C#40",
35463544
"excludeFromAutomaticTesting": true,
3547-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3545+
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
35483546
"referenceImage": "atmosphere-day.png"
35493547
},
35503548
{
35513549
"title": "Atmosphere Day (Ray Marching)",
35523550
"playgroundId": "#VO1Z0C#41",
35533551
"excludeFromAutomaticTesting": true,
3554-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3552+
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
35553553
"referenceImage": "atmosphere-day-ray-marching.png"
35563554
},
35573555
{
35583556
"title": "Atmosphere Sunset",
35593557
"playgroundId": "#VO1Z0C#42",
35603558
"excludeFromAutomaticTesting": true,
3561-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
3559+
"reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.",
35623560
"referenceImage": "atmosphere-sunset.png"
35633561
},
35643562
{
@@ -4249,7 +4247,7 @@
42494247
"renderCount": 180,
42504248
"useLargeWorldRendering": true,
42514249
"excludeFromAutomaticTesting": true,
4252-
"reason": "Test crashes or hangs on Babylon Native",
4250+
"reason": "Parse repaired via ES2019 polyfill; runtime fails: 'HK' (Havok) global not defined.",
42534251
"referenceImage": "Havok-FloatingOrigin-Multi-Region.png"
42544252
},
42554253
{
@@ -4652,7 +4650,7 @@
46524650
"title": "Selection outline layer with instances",
46534651
"playgroundId": "#UR9706#0",
46544652
"excludeFromAutomaticTesting": true,
4655-
"reason": "Pixel comparison fails (more than 20% pixels differ)",
4653+
"reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue).",
46564654
"referenceImage": "Selection-outline-layer-with-instances.png"
46574655
},
46584656
{
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// es2019_transpile.js
2+
//
3+
// Lightweight regex-based ES2020+ -> ES2019 syntax repair for engines that
4+
// lack optional chaining (?.), nullish coalescing (??), and numeric
5+
// separators (1_000_000).
6+
//
7+
// This is a SYNTAX REPAIR, not a full transpile. It rewrites parse-time
8+
// failing tokens to the closest legal ES2019 expressions so that the host
9+
// engine accepts the code. Where native ES2020+ semantics differ from the
10+
// rewritten form, the rewritten form runs the "happy path" -- assumes the
11+
// target value is present. If the target is null at runtime the rewritten
12+
// code throws a TypeError where native ?. would have short-circuited to
13+
// undefined. This is an intentional trade-off: it produces valid syntax
14+
// (the prerequisite for the test running at all) while preserving the
15+
// common case behaviour. Tests that rely on the nullish-short-circuit
16+
// semantics may surface runtime errors that were previously hidden by
17+
// the parse failure -- a strict improvement for debuggability.
18+
//
19+
// Transforms applied:
20+
// * 1_000_000 -> 1000000 (strip numeric separators)
21+
// * a?.b -> a.b (optional chaining -> required chaining)
22+
// * a?.[x] -> a[x]
23+
// * a?.() -> a()
24+
// * a ?? b -> (a != null ? a : b)
25+
//
26+
// Not handled (still cause parse failures on Chakra):
27+
// * Logical assignment ||= &&= ??=
28+
// * Class private fields #name
29+
// * BigInt literals 1n
30+
//
31+
// String/regex/comment literals containing ? . sequences are not skipped,
32+
// but in Babylon snippet code such occurrences are rare.
33+
//
34+
// On engines with native ES2020+ support (V8, JSC) this code is never
35+
// invoked because the caller only retries with this transform after
36+
// initial eval throws a SyntaxError.
37+
//
38+
// Public API: top-level global function __bnTranspileES2019(code) -> string.
39+
// Chakra in Babylon Native does not define `self` or `globalThis`, so we
40+
// install via a top-level `var` declaration which becomes a property of
41+
// the script-global object across all our supported engines.
42+
var __bnTranspileES2019 = (function () {
43+
"use strict";
44+
45+
function stripNumericSeparators(code) {
46+
// Decimal / float: 1_000_000, 1.234_5, 1_000e3
47+
var out = code.replace(/\b(\d[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?)\b/g, function (m) {
48+
return m.indexOf("_") === -1 ? m : m.replace(/_/g, "");
49+
});
50+
// Hex / octal / binary: 0xFFFF_FFFF, 0o7_5, 0b1010_0101
51+
out = out.replace(/\b(0[xXoObB][0-9A-Fa-f_]+)\b/g, function (m) {
52+
return m.indexOf("_") === -1 ? m : m.replace(/_/g, "");
53+
});
54+
return out;
55+
}
56+
57+
function transformLogicalAssignment(code) {
58+
// a ||= b -> (a || (a = b))
59+
// a &&= b -> (a && (a = b))
60+
// a ??= b -> (a != null ? a : (a = b))
61+
// Only matches simple LHS (identifier, optional .prop / [idx] chain) to keep
62+
// the rewrite syntactically safe; complex LHS expressions are left alone.
63+
var lhs = "(?:[A-Za-z_$][\\w$]*)(?:\\.[A-Za-z_$][\\w$]*|\\[[^\\]\\n]*\\])*";
64+
var rhs = "[^;\\n]+";
65+
var prev = null, out = code, guard = 0;
66+
while (prev !== out && guard++ < 10) {
67+
prev = out;
68+
out = out
69+
.replace(new RegExp("(" + lhs + ")\\s*\\?\\?=\\s*(" + rhs + ")", "g"),
70+
"$1 = ($1 != null ? $1 : ($2))")
71+
.replace(new RegExp("(" + lhs + ")\\s*\\|\\|=\\s*(" + rhs + ")", "g"),
72+
"$1 = ($1 || ($2))")
73+
.replace(new RegExp("(" + lhs + ")\\s*&&=\\s*(" + rhs + ")", "g"),
74+
"$1 = ($1 && ($2))");
75+
}
76+
return out;
77+
}
78+
79+
function transformOptionalChaining(code) {
80+
var prev = null, out = code, guard = 0;
81+
while (prev !== out && guard++ < 50) {
82+
prev = out;
83+
out = out
84+
.replace(/\?\.(?=\()/g, "")
85+
.replace(/\?\.(?=\[)/g, "")
86+
.replace(/\?\./g, ".");
87+
}
88+
return out;
89+
}
90+
91+
function transformNullishCoalescing(code) {
92+
var prev = null, out = code, guard = 0;
93+
while (prev !== out && guard++ < 50) {
94+
prev = out;
95+
out = out.replace(
96+
/(\b[A-Za-z_$][\w$.]*(?:\[[^\]]*\])?)\s*\?\?\s*(\b[A-Za-z_$][\w$.]*(?:\[[^\]]*\])?|"[^"]*"|'[^']*'|\d+(?:\.\d+)?|true|false|null|undefined)/g,
97+
"($1 != null ? $1 : $2)"
98+
);
99+
}
100+
return out;
101+
}
102+
103+
function transpileES2019(code) {
104+
if (typeof code !== "string") { return code; }
105+
var out = code;
106+
out = stripNumericSeparators(out);
107+
out = transformLogicalAssignment(out);
108+
out = transformOptionalChaining(out);
109+
out = transformNullishCoalescing(out);
110+
return out;
111+
}
112+
113+
return transpileES2019;
114+
})();

Apps/Playground/Scripts/validation_native.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,26 @@
4545
done(false);
4646
}
4747

48+
// Run `eval(src)` directly. If the host engine throws a SyntaxError
49+
// (e.g. Chakra rejecting ?. ?? or numeric separators) and the
50+
// __bnTranspileES2019 helper is available, retry once with an ES2019
51+
// syntax-repaired version of the source. Re-throws on any other error.
52+
function evalWithFallback(src, test) {
53+
try {
54+
return eval(src);
55+
} catch (e) {
56+
if (e instanceof SyntaxError && typeof __bnTranspileES2019 === "function") {
57+
const repaired = __bnTranspileES2019(src);
58+
if (repaired !== src) {
59+
const title = test && test.title ? test.title : "(unknown)";
60+
console.log("Retrying '" + title + "' after ES2019 syntax repair (host engine lacks ES2020+ parse support).");
61+
return eval(repaired);
62+
}
63+
}
64+
throw e;
65+
}
66+
}
67+
4868
// Per-run counters surfaced as a final summary line on exit.
4969
let ranCount = 0;
5070
let passedCount = 0;
@@ -344,7 +364,7 @@
344364
}
345365
}
346366

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

349369
if (currentScene.then) {
350370
// Handle if createScene returns a promise
@@ -415,7 +435,7 @@
415435
}
416436
}
417437

418-
currentScene = eval(scriptToRun + test.functionToCall + "(engine)");
438+
currentScene = evalWithFallback(scriptToRun + test.functionToCall + "(engine)", test);
419439
processCurrentScene(test, renderImage, done, compareFunction);
420440
}
421441
catch (e) {

0 commit comments

Comments
 (0)