diff --git a/src/js_parser/fold.rs b/src/js_parser/fold.rs index 085318aaa987..8f2f272bb2c5 100644 --- a/src/js_parser/fold.rs +++ b/src/js_parser/fold.rs @@ -471,6 +471,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O && !identifier_opts.is_delete_target() && identifier_opts.assign_target() == js_ast::AssignTarget::None && !identifier_opts.is_call_target() + && !identifier_opts.is_template_tag() { let prop: &G::Property = &obj.properties.slice()[0]; if let (Some(value), Some(key)) = (prop.value, prop.key) { diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 91e474677f7f..717b7498f75e 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -444,6 +444,7 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { // syntactic constructs as appropriate. pub(crate) stmt_expr_value: js_ast::ExprData, pub(crate) call_target: js_ast::ExprData, + pub(crate) template_tag: js_ast::ExprData, pub(crate) delete_target: js_ast::ExprData, pub(crate) loop_body: js_ast::StmtData, pub(crate) module_scope: js_ast::StoreRef, @@ -2646,6 +2647,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } js_ast::ExprData::ETemplate(mut e) => { if let Some(tag) = e.tag.as_mut() { + // Don't substitute something into a template tag that could change "this" + match replacement.data { + js_ast::ExprData::EDot(_) | js_ast::ExprData::EIndex(_) => { + if matches!(tag.data, js_ast::ExprData::EIdentifier(id) if id.ref_.eql(r#ref)) + { + break 'outer; + } + } + _ => {} + } match self.substitute_single_use_symbol_in_expr( *tag, r#ref, @@ -8712,6 +8723,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O allow_in: true, call_target: null_expr_data(), + template_tag: null_expr_data(), delete_target: null_expr_data(), stmt_expr_value: null_expr_data(), loop_body: null_stmt_data(), diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index c5f674097102..2baa2b7846fc 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -993,9 +993,9 @@ impl AsyncPrefixExpression { } // Packed u8 — assign_target:u2, is_delete_target:b1, -// was_originally_identifier:b1, is_call_target:b1, _padding:u3 (LSB-first). -// Not all-bool (assign_target is a 2-bit enum), so per PORTING.md we use a -// transparent u8 with manual shift accessors. +// was_originally_identifier:b1, is_call_target:b1, is_template_tag:b1, +// _padding:u2 (LSB-first). Not all-bool (assign_target is a 2-bit enum), +// so per PORTING.md we use a transparent u8 with manual shift accessors. #[repr(transparent)] #[derive(Clone, Copy, Default, PartialEq, Eq)] pub struct IdentifierOpts(u8); @@ -1005,6 +1005,7 @@ impl IdentifierOpts { const IS_DELETE_TARGET: u8 = 1 << 2; const WAS_ORIGINALLY_IDENTIFIER: u8 = 1 << 3; const IS_CALL_TARGET: u8 = 1 << 4; + const IS_TEMPLATE_TAG: u8 = 1 << 5; #[inline] pub(crate) const fn assign_target(self) -> js_ast::AssignTarget { @@ -1032,6 +1033,10 @@ impl IdentifierOpts { pub(crate) const fn is_call_target(self) -> bool { self.0 & Self::IS_CALL_TARGET != 0 } + #[inline] + pub(crate) const fn is_template_tag(self) -> bool { + self.0 & Self::IS_TEMPLATE_TAG != 0 + } // Builder-style helpers (this stays a packed u8 rather than a // named-field struct). @@ -1059,6 +1064,11 @@ impl IdentifierOpts { self.0 = (self.0 & !Self::IS_CALL_TARGET) | ((v as u8) << 4); self } + #[inline] + pub(crate) const fn with_is_template_tag(mut self, v: bool) -> Self { + self.0 = (self.0 & !Self::IS_TEMPLATE_TAG) | ((v as u8) << 5); + self + } } pub(crate) fn statement_cares_about_scope(stmt: &Stmt) -> bool { diff --git a/src/js_parser/visit/visit_binary.rs b/src/js_parser/visit/visit_binary.rs index 0b7d11afe789..6d854cd354d4 100644 --- a/src/js_parser/visit/visit_binary.rs +++ b/src/js_parser/visit/visit_binary.rs @@ -99,6 +99,11 @@ pub struct BinaryExpressionVisitor { /// Input for visiting the left child pub(crate) left_in: ExprIn, + + /// Captured in `check_and_prepare` (before visiting `left`) so a nested + /// call/tagged-template inside `left` can't clobber the pointer match. + pub(crate) is_call_target: bool, + pub(crate) is_template_tag: bool, } impl BinaryExpressionVisitor { @@ -112,11 +117,10 @@ impl BinaryExpressionVisitor { // invariant is encapsulated there. The borrow is on the `v.e` field // only, so `v.loc` reads below split-borrow cleanly. let e_handle: StoreRef = v.e; - let e_ptr: *mut E::Binary = e_handle.as_ptr(); + let is_call_target = v.is_call_target; + let is_template_tag = v.is_template_tag; let e_ = &mut *v.e; - let is_call_target = - matches!(p.call_target, ExprData::EBinary(ptr) if core::ptr::eq(ptr.as_ptr(), e_ptr)); let was_anonymous_named_expr = e_.right.is_anonymous_named(); let prev_decorator_class_name = p.decorator_class_name; @@ -225,7 +229,9 @@ impl BinaryExpressionVisitor { } else { // The left operand has no side effects, but we need to preserve // the comma operator semantics when used as a call target - if is_call_target && e_.right.has_value_for_this_in_call() { + if (is_call_target || is_template_tag) + && e_.right.has_value_for_this_in_call() + { // Keep the comma expression to strip "this" binding e_.left = Expr { data: prefill::data::ZERO, @@ -369,7 +375,10 @@ impl BinaryExpressionVisitor { // "(null ?? fn)()" => "fn()" // "(null ?? this.fn)" => "this.fn" // "(null ?? this.fn)()" => "(0, this.fn)()" - if is_call_target && e_.right.has_value_for_this_in_call() { + // "(null ?? this.fn)`x`" => "(0, this.fn)`x`" + if (is_call_target || is_template_tag) + && e_.right.has_value_for_this_in_call() + { return Expr::join_with_comma( Expr { data: ExprData::ENumber(E::Number::new(0.0)), @@ -392,7 +401,9 @@ impl BinaryExpressionVisitor { // "(0 || fn)()" => "fn()" // "(0 || this.fn)" => "this.fn" // "(0 || this.fn)()" => "(0, this.fn)()" - if is_call_target && e_.right.has_value_for_this_in_call() { + // "(0 || this.fn)`x`" => "(0, this.fn)`x`" + if (is_call_target || is_template_tag) && e_.right.has_value_for_this_in_call() + { return Expr::join_with_comma( Expr { data: prefill::data::ZERO, @@ -414,7 +425,10 @@ impl BinaryExpressionVisitor { // "(1 && fn)()" => "fn()" // "(1 && this.fn)" => "this.fn" // "(1 && this.fn)()" => "(0, this.fn)()" - if is_call_target && e_.right.has_value_for_this_in_call() { + // "(1 && this.fn)`x`" => "(0, this.fn)`x`" + if (is_call_target || is_template_tag) + && e_.right.has_value_for_this_in_call() + { return Expr::join_with_comma( Expr { data: prefill::data::ZERO, @@ -711,6 +725,17 @@ impl BinaryExpressionVisitor { if let Some(obj) = dot.target.data.e_object() { if obj.properties.len_u32() == 0 { if dot.name != b"__proto__" { + if (is_call_target || is_template_tag) + && e_.right.has_value_for_this_in_call() + { + return Expr::join_with_comma( + Expr { + data: prefill::data::ZERO, + loc: e_.left.loc, + }, + e_.right, + ); + } return e_.right; } } @@ -777,6 +802,12 @@ impl BinaryExpressionVisitor { _ => {} } + let e_ptr: *mut E::Binary = e_handle.as_ptr(); + v.is_call_target = + matches!(p.call_target, ExprData::EBinary(ptr) if core::ptr::eq(ptr.as_ptr(), e_ptr)); + v.is_template_tag = + matches!(p.template_tag, ExprData::EBinary(ptr) if core::ptr::eq(ptr.as_ptr(), e_ptr)); + v.left_in = ExprIn { assign_target: Op::Code::binary_assign_target(e_.op), ..ExprIn::default() diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index 0d5c834fde53..4197c943a45c 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -692,8 +692,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let expr = *e; let _ = in_; let mut e_ = expr.data.e_template().expect("infallible: variant checked"); - if e_.tag.is_some() { - p.visit_expr(e_.tag.as_mut().unwrap()); + if let Some(tag) = e_.tag.as_mut() { + p.template_tag = tag.data; + p.visit_expr(tag); } // Visit the interpolation values before the macro dispatch below: its @@ -820,6 +821,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O e: e_, loc: expr.loc, left_in: ExprIn::default(), + is_call_target: false, + is_template_tag: false, }; // Everything uses a single stack to reduce allocation overhead. This stack @@ -867,6 +870,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O e: left_binary.unwrap(), loc: left.loc, left_in: ExprIn::default(), + is_call_target: false, + is_template_tag: false, }; } @@ -885,6 +890,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let expr = *e; let mut e_ = expr.data.e_index().expect("infallible: variant checked"); let is_call_target = matches!(p.call_target, Data::EIndex(ct) if core::ptr::eq(&raw const *e_, &raw const *ct)); + let is_template_tag = matches!(p.template_tag, Data::EIndex(tt) if core::ptr::eq(&raw const *e_, &raw const *tt)); let is_delete_target = matches!(p.delete_target, Data::EIndex(dt) if core::ptr::eq(&raw const *e_, &raw const *dt)); // "a['b']" => "a.b" @@ -905,6 +911,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if is_call_target { p.call_target = dot.data; } + if is_template_tag { + p.template_tag = dot.data; + } if is_delete_target { p.delete_target = dot.data; } @@ -1020,6 +1029,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if is_call_target { p.call_target = dot.data; } + if is_template_tag { + p.template_tag = dot.data; + } if is_delete_target { p.delete_target = dot.data; } @@ -1041,7 +1053,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O unwrapped.loc, IdentifierOpts::default() .with_is_call_target(is_call_target) - // .is_template_tag = is_template_tag, + .with_is_template_tag(is_template_tag) .with_is_delete_target(is_delete_target) .with_assign_target(in_.assign_target), ) { @@ -1057,7 +1069,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let target = e_.target.unwrap_inlined(); let index = e_.index.unwrap_inlined(); - if p.options.features.minify_syntax { + // `[obj.m][0]` / `"s"[n]` are property references into a temporary; + // folding them to a value in tag position would rebind `this`. + if p.options.features.minify_syntax && !is_template_tag { if let Some(number) = index.data.as_e_number() { if number.value() >= 0.0 && number.value() < (usize::MAX as f64) @@ -1335,6 +1349,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut e_ = expr.data.e_dot().expect("infallible: variant checked"); let is_delete_target = matches!(p.delete_target, Data::EDot(dt) if core::ptr::eq(&raw const *e_, &raw const *dt)); let is_call_target = matches!(p.call_target, Data::EDot(ct) if core::ptr::eq(&raw const *e_, &raw const *ct)); + let is_template_tag = matches!(p.template_tag, Data::EDot(tt) if core::ptr::eq(&raw const *e_, &raw const *tt)); // `p.define: &'a Define` is `Copy`; hoist so the `dots.get` borrow is // tied to `'a`, not `&*p`, and `&mut self` helpers below can be called @@ -1427,9 +1442,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O e_.name_loc, IdentifierOpts::default() .with_is_call_target(is_call_target) + .with_is_template_tag(is_template_tag) .with_assign_target(in_.assign_target) .with_is_delete_target(is_delete_target), - // .is_template_tag = p.template_tag != null, ) { *e = _expr; return; @@ -1460,6 +1475,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut e_ = e.data.e_if().expect("infallible: variant checked"); let is_call_target = matches!(p.call_target, Data::EIf(ct) if core::ptr::eq(&raw const *e_, &raw const *ct)); + let is_template_tag = matches!(p.template_tag, Data::EIf(tt) if core::ptr::eq(&raw const *e_, &raw const *tt)); let prev_in_branch = p.in_branch_condition; p.in_branch_condition = true; @@ -1483,24 +1499,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.visit_expr(&mut e_.no); p.is_control_flow_dead = old; - if side_effects.side_effects == SideEffects::CouldHaveSideEffects { - *e = SideEffects::simplify_unused_expr(p, e_.test) - .unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc)) - .join_with_comma(e_.yes); - return; - } - // "(1 ? fn : 2)()" => "fn()" // "(1 ? this.fn : 2)" => "this.fn" // "(1 ? this.fn : 2)()" => "(0, this.fn)()" - if is_call_target && e_.yes.has_value_for_this_in_call() { - *e = p - .new_expr(E::Number::new(0.0), e_.test.loc) - .join_with_comma(e_.yes); - return; + // "(1 ? this.fn : 2)`x`" => "(0, this.fn)`x`" + let mut left = if side_effects.side_effects == SideEffects::CouldHaveSideEffects { + SideEffects::simplify_unused_expr(p, e_.test) + .unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc)) + } else { + p.new_expr(E::Missing {}, e_.test.loc) + }; + if left.is_missing() + && (is_call_target || is_template_tag) + && e_.yes.has_value_for_this_in_call() + { + left = p.new_expr(E::Number::new(0.0), e_.test.loc); } - - *e = e_.yes; + *e = left.join_with_comma(e_.yes); return; } else { // "false ? dead : live" @@ -1511,23 +1526,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.visit_expr(&mut e_.no); // "(a, false) ? b : c" => "a, c" - if side_effects.side_effects == SideEffects::CouldHaveSideEffects { - *e = SideEffects::simplify_unused_expr(p, e_.test) + // "(0 ? 1 : fn)()" => "fn()" + // "(0 ? 1 : this.fn)" => "this.fn" + // "(0 ? 1 : this.fn)()" => "(0, this.fn)()" + // "(0 ? 1 : this.fn)`x`" => "(0, this.fn)`x`" + let mut left = if side_effects.side_effects == SideEffects::CouldHaveSideEffects { + SideEffects::simplify_unused_expr(p, e_.test) .unwrap_or_else(|| p.new_expr(E::Missing {}, e_.test.loc)) - .join_with_comma(e_.no); - return; - } - - // "(1 ? fn : 2)()" => "fn()" - // "(1 ? this.fn : 2)" => "this.fn" - // "(1 ? this.fn : 2)()" => "(0, this.fn)()" - if is_call_target && e_.no.has_value_for_this_in_call() { - *e = p - .new_expr(E::Number::new(0.0), e_.test.loc) - .join_with_comma(e_.no); - return; + } else { + p.new_expr(E::Missing {}, e_.test.loc) + }; + if left.is_missing() + && (is_call_target || is_template_tag) + && e_.no.has_value_for_this_in_call() + { + left = p.new_expr(E::Number::new(0.0), e_.test.loc); } - *e = e_.no; + *e = left.join_with_comma(e_.no); return; } } diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index ab204c146078..a0d046eca8b7 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -48,7 +48,10 @@ bun_core::declare_scope!(cache, visible); /// bindings from the compiled bytecode after the module-loader rewrite, so the /// record no longer carries them; blobs written in the old numbering must not /// be read back. -const EXPECTED_VERSION: u32 = 24; +/// Version 25: `p.template_tag` is tracked during the visit pass, so wrapper +/// folds (`(0, a.b)`, `cond ? a.b : x`, `a.b ?? x`, `[a.b][0]`, `{f:a.b}.f`) +/// no longer strip the indirection when the result is a tagged-template tag. +const EXPECTED_VERSION: u32 = 25; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index d72bcc6fddcd..53e920cdfdb8 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -3919,6 +3919,104 @@ console.log(foo, array); expectPrinted("(0, func())", "func()"); }); + it("tagged-template tag folds preserve `this`", () => { + const expectPrinted = (code, out) => { + expect(parsed(code, true, true, transpilerMinifySyntax)).toBe(out); + }; + + // A tagged template binds `this` the same way a call does: folding a + // wrapper away so that a member expression lands directly in tag + // position would change the receiver. These match the call-target + // cases above, emitting `(0, obj.m)` to strip `this`. + expectPrinted("(0, obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("(0, obj[k])`x`", "(0, obj[k])`x`"); + expectPrinted("(1 ? obj.m : 0)`x`", "(0, obj.m)`x`"); + expectPrinted("(0 ? 0 : obj.m)`x`", "(0, obj.m)`x`"); + // Statically-known test whose side-effect class is `CouldHaveSideEffects` + // but simplifies away entirely: the fold must still keep `(0, obj.m)`. + expectPrinted("(typeof x ? obj.m : 0)`x`", "(0, obj.m)`x`"); + expectPrinted("(typeof x ? obj.m : 0)()", "(0, obj.m)()"); + expectPrinted("(typeof x && 0 ? 0 : obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("(typeof x && 0 ? 0 : obj.m)()", "(0, obj.m)()"); + // When the test leaves a real side effect behind, the resulting comma + // already strips `this`; no extra `0,` is needed. + expectPrinted("(f() || 1 ? obj.m : 0)`x`", "(f(), obj.m)`x`"); + expectPrinted("(null ?? obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("(1 && obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("(0 || obj.m)`x`", "(0, obj.m)`x`"); + // A call/tagged-template nested inside the comma's left operand must not + // clobber the outer binary's call/tag-position capture. + expectPrinted("((() => f`a`), obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("((true ? 0 : f`a`), obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("((() => f()), obj.m)()", "(0, obj.m)()"); + // The `{}.x ??= v` / `{}.x ||= v` HMR fold also needs the guard. + expectPrinted("({}.x ??= obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("({}.x ??= obj.m)()", "(0, obj.m)()"); + expectPrinted("({}.x ||= obj.m)`x`", "(0, obj.m)`x`"); + expectPrinted("({}.x ??= fn)`x`", "fn`x`"); + // Single-use-symbol inlining must not move a member expression into + // identifier tag position (mirrors the existing call-target guard). + const subst = src => parsed(src, true, false, transpilerMinifySyntax); + expect(subst("function f(obj) { let x = obj.m; return x`t`; }")).toBe( + "function f(obj) {\n let x = obj.m;\n return x`t`;\n}", + ); + expect(subst("function f(obj) { let x = obj[k]; return x`t`; }")).toBe( + "function f(obj) {\n let x = obj[k];\n return x`t`;\n}", + ); + expect(subst("function f(obj) { let x = obj.m; return x; }")).toBe("function f(obj) {\n return obj.m;\n}"); + // Property-access folds that replace a reference with its value bail + // out entirely in tag position. + expectPrinted("[obj.m][0]`x`", "[obj.m][0]`x`"); + expectPrinted("({ m: obj.m }).m`x`", "{ m: obj.m }.m`x`"); + expectPrinted('({ m: obj.m })["m"]`x`', "{ m: obj.m }.m`x`"); + + // Still folded when the wrapped value carries no `this`. + expectPrinted("(0, fn)`x`", "fn`x`"); + expectPrinted("(1 ? fn : 0)`x`", "fn`x`"); + expectPrinted("(null ?? fn)`x`", "fn`x`"); + expectPrinted("(1 && fn)`x`", "fn`x`"); + expectPrinted("(0 || fn)`x`", "fn`x`"); + + // Still folded outside call/tag position. + expectPrinted("(0, obj.m)", "obj.m"); + expectPrinted("[obj.m][0]", "obj.m"); + expectPrinted("({ m: obj.m }).m", "obj.m"); + }); + + it("tagged-template tag `this` matches node at runtime", async () => { + const src = ` + var obj = { m() { return this === obj; } }; + var x = 1, f = () => 0; + console.log(JSON.stringify([ + (0, obj.m)\`x\`, + (1 ? obj.m : 0)\`x\`, + (0 ? 0 : obj.m)\`x\`, + (typeof x ? obj.m : 0)\`x\`, + (typeof x ? obj.m : 0)(), + (typeof x && 0 ? 0 : obj.m)\`x\`, + (null ?? obj.m)\`x\`, + (true && obj.m)\`x\`, + (false || obj.m)\`x\`, + ((() => f\`a\`), obj.m)\`x\`, + ((() => f()), obj.m)(), + ({}.x ??= obj.m)\`x\`, + ({}.x ??= obj.m)(), + (function(){ let y = obj.m; return y\`t\`; })(), + [obj.m][0]\`x\`, + ({ m: obj.m }).m\`x\`, + ({ m: obj.m })["m"]\`x\`, + obj.m\`x\`, + ])); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", src], env: bunEnv, stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe( + "[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true]\n", + ); + expect(exitCode).toBe(0); + }); + it("constant folding", () => { const expectPrinted = (code, out) => { expect(parsed(code, true, true, transpilerMinifySyntax)).toBe(out);