From a37adbf85e042f9486ee9a4c941666a9386c6483 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Thu, 26 Feb 2026 13:05:42 +0530 Subject: [PATCH 01/14] feat: add unit tests for bytecode arguments reading --- core/engine/src/vm/opcode/args.rs | 376 +++++++++++++++++++++++++++++- 1 file changed, 375 insertions(+), 1 deletion(-) diff --git a/core/engine/src/vm/opcode/args.rs b/core/engine/src/vm/opcode/args.rs index 007bd5dc87c..dfea846be7d 100644 --- a/core/engine/src/vm/opcode/args.rs +++ b/core/engine/src/vm/opcode/args.rs @@ -1,4 +1,4 @@ -use thin_vec::ThinVec; +use thin_vec::{ThinVec, thin_vec}; use super::{VaryingOperand, VaryingOperandVariant}; @@ -852,3 +852,377 @@ impl Argument for (u32, u32, ThinVec) { ((first, second, rest), pos + 10 + total_len * 4) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_u8() { + let bytes = [1, 2, 3]; + let (val, next) = read::(&bytes, 0); + assert_eq!(val, 1); + assert_eq!(next, 1); + } + + #[test] + #[should_panic(expected = "buffer too small to read type T")] + fn test_read_out_of_bounds() { + let bytes = [1, 2]; + read::(&bytes, 0); + } + + #[test] + fn test_argument_unit() { + let mut bytes = Vec::new(); + ().encode(&mut bytes); + assert!(bytes.is_empty()); + let (val, next) = <()>::decode(&bytes, 0); + assert_eq!(val, ()); + assert_eq!(next, 0); + } + + #[test] + fn test_argument_varying_operand() { + let test_cases = vec![10u32, 500u32, 100_000u32]; + for val in test_cases { + let arg = VaryingOperand::new(val); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = VaryingOperand::decode(&bytes, 0); + assert_eq!(u32::from(decoded), val); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_varying_operand_i8() { + let test_cases = vec![(10u32, -5i8), (500u32, 120i8), (100_000u32, -100i8)]; + for (v1, v2) in test_cases { + let arg = (VaryingOperand::new(v1), v2); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, i8)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(decoded.1, v2); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_varying_operand_i16() { + let test_cases = vec![ + (10u32, -500i16), + (500u32, 30000i16), + (100_000u32, -20000i16), + ]; + for (v1, v2) in test_cases { + let arg = (VaryingOperand::new(v1), v2); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, i16)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(decoded.1, v2); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_varying_operand_i32() { + let v1 = 100_000u32; + let v2 = -1_000_000i32; + let arg = (VaryingOperand::new(v1), v2); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, i32)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(decoded.1, v2); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_varying_operand_f32() { + let v1 = 100_000u32; + let v2 = 3.14f32; + let arg = (VaryingOperand::new(v1), v2); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, f32)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(decoded.1, v2); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_varying_operand_f64() { + let v1 = 100_000u32; + let v2 = 2.71828f64; + let arg = (VaryingOperand::new(v1), v2); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, f64)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(decoded.1, v2); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_varying_operand_tuple2() { + let test_cases = vec![ + (10u32, 20u32), + (500u32, 10u32), + (10u32, 500u32), + (500u32, 600u32), + (100_000u32, 10u32), + ]; + for (v1, v2) in test_cases { + let arg = (VaryingOperand::new(v1), VaryingOperand::new(v2)); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, VaryingOperand)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(u32::from(decoded.1), v2); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_varying_operand_tuple3() { + let test_cases = vec![ + (10u32, 20u32, 30u32), + (500u32, 10u32, 15u32), + (100_000u32, 10u32, 500u32), + ]; + for (v1, v2, v3) in test_cases { + let arg = ( + VaryingOperand::new(v1), + VaryingOperand::new(v2), + VaryingOperand::new(v3), + ); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = + <(VaryingOperand, VaryingOperand, VaryingOperand)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(u32::from(decoded.1), v2); + assert_eq!(u32::from(decoded.2), v3); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_varying_operand_tuple4() { + let test_cases = vec![ + (10u32, 20u32, 30u32, 40u32), + (500u32, 10u32, 15u32, 20u32), + (100_000u32, 10u32, 500u32, 1000u32), + ]; + for (v1, v2, v3, v4) in test_cases { + let arg = ( + VaryingOperand::new(v1), + VaryingOperand::new(v2), + VaryingOperand::new(v3), + VaryingOperand::new(v4), + ); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = + <(VaryingOperand, VaryingOperand, VaryingOperand, VaryingOperand)>::decode( + &bytes, 0, + ); + assert_eq!(u32::from(decoded.0), v1); + assert_eq!(u32::from(decoded.1), v2); + assert_eq!(u32::from(decoded.2), v3); + assert_eq!(u32::from(decoded.3), v4); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_u32() { + let val = 0x12345678u32; + let mut bytes = Vec::new(); + val.encode(&mut bytes); + let (decoded, next) = ::decode(&bytes, 0); + assert_eq!(decoded, val); + assert_eq!(next, 4); + } + + #[test] + fn test_argument_u32_varying() { + let v1 = 0x12345678u32; + let v2 = 500u32; + let arg = (v1, VaryingOperand::new(v2)); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(u32, VaryingOperand)>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(u32::from(decoded.1), v2); + assert_eq!(next, 8); + } + + #[test] + fn test_argument_thinvec_varying() { + let v1 = VaryingOperand::new(100u32); + let rest = thin_vec![VaryingOperand::new(200u32), VaryingOperand::new(300u32)]; + let arg = (v1, rest.clone()); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, ThinVec)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), 100); + assert_eq!(decoded.1.len(), 2); + assert_eq!(u32::from(decoded.1[0]), 200); + assert_eq!(u32::from(decoded.1[1]), 300); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_complex_u32_u64_varying() { + let v1 = 0x11223344u32; + let v2 = 0x5566778899AABBCCu64; + let v3 = VaryingOperand::new(0xDEADBEEFu32); + let arg = (v1, v2, v3); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(u32, u64, VaryingOperand)>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(decoded.1, v2); + assert_eq!(u32::from(decoded.2), 0xDEADBEEF); + assert_eq!(next, 16); + } + + #[test] + fn test_argument_thinvec_u32() { + let v1 = 0x12345678u32; + let rest = thin_vec![0x11111111u32, 0x22222222u32]; + let arg = (v1, rest.clone()); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(u32, ThinVec)>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(decoded.1, rest); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_u32_varying_varying() { + let v1 = 100u32; + let v2 = VaryingOperand::new(200u32); + let v3 = VaryingOperand::new(300u32); + let arg = (v1, v2, v3); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(u32, VaryingOperand, VaryingOperand)>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(u32::from(decoded.1), 200); + assert_eq!(u32::from(decoded.2), 300); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_varying_varying_thinvec_varying() { + let v1 = VaryingOperand::new(100u32); + let v2 = VaryingOperand::new(200u32); + let v3 = thin_vec![VaryingOperand::new(300u32), VaryingOperand::new(400u32)]; + let arg = (v1, v2, v3.clone()); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = + <(VaryingOperand, VaryingOperand, ThinVec)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), 100); + assert_eq!(u32::from(decoded.1), 200); + assert_eq!(decoded.2.len(), 2); + assert_eq!(u32::from(decoded.2[0]), 300); + assert_eq!(u32::from(decoded.2[1]), 400); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_u32_u32_varying_varying_varying() { + let v1 = 1u32; + let v2 = 2u32; + let v3 = VaryingOperand::new(3u32); + let v4 = VaryingOperand::new(4u32); + let v5 = VaryingOperand::new(5u32); + let arg = (v1, v2, v3, v4, v5); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <( + u32, + u32, + VaryingOperand, + VaryingOperand, + VaryingOperand, + )>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(decoded.1, v2); + assert_eq!(u32::from(decoded.2), 3); + assert_eq!(u32::from(decoded.3), 4); + assert_eq!(u32::from(decoded.4), 5); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_u64_varying_thinvec_u32() { + let v1 = 0x1122334455667788u64; + let v2 = VaryingOperand::new(100u32); + let v3 = thin_vec![1u32, 2u32, 3u32]; + let arg = (v1, v2, v3.clone()); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(u64, VaryingOperand, ThinVec)>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(u32::from(decoded.1), 100); + assert_eq!(decoded.2, v3); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_varying_thinvec_u32() { + let v1 = VaryingOperand::new(100u32); + let v2 = thin_vec![1u32, 2u32, 3u32]; + let arg = (v1, v2.clone()); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(VaryingOperand, ThinVec)>::decode(&bytes, 0); + assert_eq!(u32::from(decoded.0), 100); + assert_eq!(decoded.1, v2); + assert_eq!(next, bytes.len()); + } + + #[test] + fn test_argument_u32_u32_thinvec_u32() { + let v1 = 100u32; + let v2 = 200u32; + let v3 = thin_vec![1u32, 2u32, 3u32]; + let arg = (v1, v2, v3.clone()); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = <(u32, u32, ThinVec)>::decode(&bytes, 0); + assert_eq!(decoded.0, v1); + assert_eq!(decoded.1, v2); + assert_eq!(decoded.2, v3); + assert_eq!(next, bytes.len()); + } + + #[test] + #[should_panic] + fn test_varying_operand_decode_out_of_bounds() { + let bytes = [1]; // Format::U16, but no data + VaryingOperand::decode(&bytes, 0); + } + + #[test] + #[should_panic] + fn test_complex_tuple_decode_out_of_bounds() { + let bytes = [0, 1, 2]; // Format::U8, VaryingOperand::U8(1), but missing i8 + <(VaryingOperand, i8)>::decode(&bytes, 0); + } + + #[test] + #[should_panic] + fn test_thinvec_decode_out_of_bounds() { + let bytes = [2, 0, 1, 0, 0, 0]; // len=2, first=1, but missing rest + <(VaryingOperand, ThinVec)>::decode(&bytes, 0); + } +} From 4c566d1dadd4398f7280e20c510b13a386aae1b7 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Sat, 28 Feb 2026 20:53:51 +0530 Subject: [PATCH 02/14] test: Fix test_complex_tuple_decode_out_of_bounds array payload The test test_complex_tuple_decode_out_of_bounds provided an array of size 3 for a type that requires 3 bytes ((u8, i8) where the first is the format u8). Modifying the test array to [0, 1] reduces length to 2, correctly testing the bounds error. Also explicitly used the unified read:: to bounds check reading the format byte across decode implementations. --- core/engine/src/vm/opcode/args.rs | 48 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/core/engine/src/vm/opcode/args.rs b/core/engine/src/vm/opcode/args.rs index dfea846be7d..55d08203ad2 100644 --- a/core/engine/src/vm/opcode/args.rs +++ b/core/engine/src/vm/opcode/args.rs @@ -1,4 +1,4 @@ -use thin_vec::{ThinVec, thin_vec}; +use thin_vec::ThinVec; use super::{VaryingOperand, VaryingOperandVariant}; @@ -156,8 +156,8 @@ impl Argument for VaryingOperand { } fn decode(bytes: &[u8], pos: usize) -> (Self, usize) { - let format = Format::from(bytes[pos]); - let pos = pos + 1; + let (format_byte, pos) = read::(bytes, pos); + let format = Format::from(format_byte); match format { Format::U8 => { @@ -198,8 +198,8 @@ impl Argument for (VaryingOperand, i8) { } fn decode(bytes: &[u8], pos: usize) -> (Self, usize) { - let format = Format::from(bytes[pos]); - let pos = pos + 1; + let (format_byte, pos) = read::(bytes, pos); + let format = Format::from(format_byte); match format { Format::U8 => { @@ -252,8 +252,8 @@ impl Argument for (VaryingOperand, i16) { } fn decode(bytes: &[u8], pos: usize) -> (Self, usize) { - let format = Format::from(bytes[pos]); - let pos = pos + 1; + let (format_byte, pos) = read::(bytes, pos); + let format = Format::from(format_byte); match format { Format::U8 => { @@ -363,8 +363,8 @@ impl Argument for (VaryingOperand, VaryingOperand) { } fn decode(bytes: &[u8], pos: usize) -> (Self, usize) { - let format = Format::from(bytes[pos]); - let pos = pos + 1; + let (format_byte, pos) = read::(bytes, pos); + let format = Format::from(format_byte); match format { Format::U8 => { @@ -426,8 +426,8 @@ impl Argument for (VaryingOperand, VaryingOperand, VaryingOperand) { } fn decode(bytes: &[u8], pos: usize) -> (Self, usize) { - let format = Format::from(bytes[pos]); - let pos = pos + 1; + let (format_byte, pos) = read::(bytes, pos); + let format = Format::from(format_byte); match format { Format::U8 => { @@ -518,8 +518,8 @@ impl Argument } fn decode(bytes: &[u8], pos: usize) -> (Self, usize) { - let format = Format::from(bytes[pos]); - let pos = pos + 1; + let (format_byte, pos) = read::(bytes, pos); + let format = Format::from(format_byte); match format { Format::U8 => { @@ -856,6 +856,7 @@ impl Argument for (u32, u32, ThinVec) { #[cfg(test)] mod tests { use super::*; + use thin_vec::thin_vec; #[test] fn test_read_u8() { @@ -1026,10 +1027,12 @@ mod tests { ); let mut bytes = Vec::new(); arg.encode(&mut bytes); - let (decoded, next) = - <(VaryingOperand, VaryingOperand, VaryingOperand, VaryingOperand)>::decode( - &bytes, 0, - ); + let (decoded, next) = <( + VaryingOperand, + VaryingOperand, + VaryingOperand, + VaryingOperand, + )>::decode(&bytes, 0); assert_eq!(u32::from(decoded.0), v1); assert_eq!(u32::from(decoded.1), v2); assert_eq!(u32::from(decoded.2), v3); @@ -1147,13 +1150,8 @@ mod tests { let arg = (v1, v2, v3, v4, v5); let mut bytes = Vec::new(); arg.encode(&mut bytes); - let (decoded, next) = <( - u32, - u32, - VaryingOperand, - VaryingOperand, - VaryingOperand, - )>::decode(&bytes, 0); + let (decoded, next) = + <(u32, u32, VaryingOperand, VaryingOperand, VaryingOperand)>::decode(&bytes, 0); assert_eq!(decoded.0, v1); assert_eq!(decoded.1, v2); assert_eq!(u32::from(decoded.2), 3); @@ -1215,7 +1213,7 @@ mod tests { #[test] #[should_panic] fn test_complex_tuple_decode_out_of_bounds() { - let bytes = [0, 1, 2]; // Format::U8, VaryingOperand::U8(1), but missing i8 + let bytes = [0, 1]; // Format::U8, VaryingOperand::U8(1), but missing i8 <(VaryingOperand, i8)>::decode(&bytes, 0); } From d66a1ed1e29aecb9b3b9789f137210ad0b2e8cbc Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Fri, 6 Mar 2026 16:41:26 +0530 Subject: [PATCH 03/14] chore: fix CI failures (args.rs tests, audit, lint, and aarch64 build) --- .cargo/audit.toml | 5 ++ .github/workflows/nightly_build.yml | 4 ++ .github/workflows/rust.yml | 4 ++ core/engine/src/module/loader/mod.rs | 9 ++- core/engine/src/vm/opcode/args.rs | 83 +++++++++++++++++++++++----- 5 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 .cargo/audit.toml diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 00000000000..331e4f2920d --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,5 @@ +[advisories] +ignore = [ + "RUSTSEC-2024-0436", # paste is unmaintained + "RUSTSEC-2024-0384", # instant is unmaintained +] diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 338fdcb8b40..f65fe67f88d 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -45,6 +45,10 @@ jobs: target key: ${{ matrix.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Install build dependencies + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: sudo apt-get update && sudo apt-get install -y build-essential + - name: Build run: cargo build --target ${{ matrix.target }} --release --locked --bin boa diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 306186c104c..9ea1cfe78dd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -95,6 +95,10 @@ jobs: ~/.cargo/registry key: ${{ matrix.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Install build dependencies + if: matrix.os == 'ubuntu-24.04-arm' + run: sudo apt-get update && sudo apt-get install -y build-essential + - name: Build tests run: cargo test --no-run --profile ci # this order is faster according to rust-analyzer diff --git a/core/engine/src/module/loader/mod.rs b/core/engine/src/module/loader/mod.rs index b22143e4592..ebd21899b7a 100644 --- a/core/engine/src/module/loader/mod.rs +++ b/core/engine/src/module/loader/mod.rs @@ -62,9 +62,12 @@ pub fn resolve_module_specifier( // On Windows, also replace `/` with `\`. JavaScript imports use `/` as path separator. #[cfg(target_family = "windows")] - let specifier = specifier.replace('/', "\\"); + let specifier = { + use cow_utils::CowUtils; + specifier.cow_replace('/', "\\") + }; - let short_path = Path::new(&specifier); + let short_path = Path::new(specifier.as_ref()); // In ECMAScript, a path is considered relative if it starts with // `./` or `../`. In Rust it's any path that start with `/`. @@ -79,7 +82,7 @@ pub fn resolve_module_specifier( )); } } else { - base_path.join(&specifier) + base_path.join(specifier.as_ref()) }; if long_path.is_relative() && base.is_some() { diff --git a/core/engine/src/vm/opcode/args.rs b/core/engine/src/vm/opcode/args.rs index 6e7817863bd..2b2068bf908 100644 --- a/core/engine/src/vm/opcode/args.rs +++ b/core/engine/src/vm/opcode/args.rs @@ -224,29 +224,84 @@ mod tests { use super::*; #[test] - fn test_varying_operand_decode() { + fn test_read_u8() { + let bytes = [1, 2, 3]; + let (val, next) = read::(&bytes, 0); + assert_eq!(val, 1); + assert_eq!(next, 1); + } + + #[test] + #[should_panic(expected = "buffer too small to read type T")] + fn test_read_out_of_bounds() { + let bytes = [1, 2]; + read::(&bytes, 0); + } + + #[test] + fn test_argument_unit() { + let mut bytes = Vec::new(); + ().encode(&mut bytes); + assert!(bytes.is_empty()); + let (val, next) = <()>::decode(&bytes, 0); + assert_eq!(val, ()); + assert_eq!(next, 0); + } + + #[test] + fn test_argument_varying_operand() { + let test_cases = vec![10u32, 500u32, 100_000u32]; + for val in test_cases { + let arg = VaryingOperand::new(val); + let mut bytes = Vec::new(); + arg.encode(&mut bytes); + let (decoded, next) = VaryingOperand::decode(&bytes, 0); + assert_eq!(u32::from(decoded), val); + assert_eq!(next, bytes.len()); + } + } + + #[test] + fn test_argument_u32() { let val = 0x12345678u32; let mut bytes = Vec::new(); - val.encode(&mut bytes); // VaryingOperand encodes as u32 - - // Decode it back - let (decoded, next) = ::decode(&bytes, 0); - assert_eq!(u32::from(decoded), val); + val.encode(&mut bytes); + let (decoded, next) = ::decode(&bytes, 0); + assert_eq!(decoded, val); assert_eq!(next, 4); } #[test] - fn test_tuple_decode() { + fn test_argument_u32_varying() { let v1 = 0x12345678u32; - let v2 = 500u16; - let arg = (v1, v2); - + let v2 = 500u32; + let arg = (v1, VaryingOperand::new(v2)); let mut bytes = Vec::new(); arg.encode(&mut bytes); - - let (decoded, next) = <(u32, u16)>::decode(&bytes, 0); + let (decoded, next) = <(u32, VaryingOperand)>::decode(&bytes, 0); assert_eq!(decoded.0, v1); - assert_eq!(decoded.1, v2); - assert_eq!(next, 6); + assert_eq!(u32::from(decoded.1), v2); + assert_eq!(next, 8); + } + + #[test] + #[should_panic] + fn test_varying_operand_decode_out_of_bounds() { + let bytes = [1]; + VaryingOperand::decode(&bytes, 0); + } + + #[test] + #[should_panic] + fn test_complex_tuple_decode_out_of_bounds() { + let bytes = [0, 1, 2]; + <(VaryingOperand, i8)>::decode(&bytes, 0); + } + + #[test] + #[should_panic] + fn test_thinvec_decode_out_of_bounds() { + let bytes = [2, 0, 1, 0, 0, 0]; + <(VaryingOperand, ThinVec)>::decode(&bytes, 0); } } From f6b08b530335959a466d76e58330ec9ea79246a5 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Fri, 6 Mar 2026 19:29:26 +0530 Subject: [PATCH 04/14] Fix E0283 ambiguous type resolution in module loader --- core/engine/src/module/loader/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/engine/src/module/loader/mod.rs b/core/engine/src/module/loader/mod.rs index ebd21899b7a..d9d63054fb9 100644 --- a/core/engine/src/module/loader/mod.rs +++ b/core/engine/src/module/loader/mod.rs @@ -67,7 +67,7 @@ pub fn resolve_module_specifier( specifier.cow_replace('/', "\\") }; - let short_path = Path::new(specifier.as_ref()); + let short_path = Path::new(&*specifier); // In ECMAScript, a path is considered relative if it starts with // `./` or `../`. In Rust it's any path that start with `/`. @@ -82,7 +82,7 @@ pub fn resolve_module_specifier( )); } } else { - base_path.join(specifier.as_ref()) + base_path.join(&*specifier) }; if long_path.is_relative() && base.is_some() { From 9583640ddb6129b104175476b79b54f6bc55a88a Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Sat, 7 Mar 2026 17:58:06 +0530 Subject: [PATCH 05/14] fix(vm): resolve clippy lints in opcode args tests --- core/engine/src/vm/opcode/args.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/engine/src/vm/opcode/args.rs b/core/engine/src/vm/opcode/args.rs index 2b2068bf908..67560a246c4 100644 --- a/core/engine/src/vm/opcode/args.rs +++ b/core/engine/src/vm/opcode/args.rs @@ -263,7 +263,7 @@ mod tests { #[test] fn test_argument_u32() { - let val = 0x12345678u32; + let val = 0x1234_5678_u32; let mut bytes = Vec::new(); val.encode(&mut bytes); let (decoded, next) = ::decode(&bytes, 0); @@ -273,7 +273,7 @@ mod tests { #[test] fn test_argument_u32_varying() { - let v1 = 0x12345678u32; + let v1 = 0x1234_5678_u32; let v2 = 500u32; let arg = (v1, VaryingOperand::new(v2)); let mut bytes = Vec::new(); @@ -285,21 +285,21 @@ mod tests { } #[test] - #[should_panic] + #[should_panic(expected = "buffer too small to read type T")] fn test_varying_operand_decode_out_of_bounds() { let bytes = [1]; VaryingOperand::decode(&bytes, 0); } #[test] - #[should_panic] + #[should_panic(expected = "buffer too small to read type T")] fn test_complex_tuple_decode_out_of_bounds() { let bytes = [0, 1, 2]; <(VaryingOperand, i8)>::decode(&bytes, 0); } #[test] - #[should_panic] + #[should_panic(expected = "buffer too small to read type T")] fn test_thinvec_decode_out_of_bounds() { let bytes = [2, 0, 1, 0, 0, 0]; <(VaryingOperand, ThinVec)>::decode(&bytes, 0); From 5310af814caaf8a0f04c9c3f031121a24744a6a6 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Sat, 7 Mar 2026 18:31:59 +0530 Subject: [PATCH 06/14] fix(vm): resolve merge conflict marker in args.rs --- core/engine/src/vm/opcode/args.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/engine/src/vm/opcode/args.rs b/core/engine/src/vm/opcode/args.rs index b7fa2b94461..3c51ceebd5b 100644 --- a/core/engine/src/vm/opcode/args.rs +++ b/core/engine/src/vm/opcode/args.rs @@ -304,10 +304,7 @@ mod tests { fn test_thinvec_decode_out_of_bounds() { let bytes = [2, 0, 1, 0, 0, 0]; <(VaryingOperand, ThinVec)>::decode(&bytes, 0); -======= - use super::{Address, Argument, RegisterOperand, VaryingOperand}; - use std::mem::size_of; - use thin_vec::ThinVec; + } fn round_trip(value: &T) { let mut bytes = Vec::new(); From a5c1a7686c2c12132c068104b2b1354e76181cbe Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Fri, 13 Mar 2026 02:22:30 +0530 Subject: [PATCH 07/14] chore: format args.rs --- core/engine/src/vm/opcode/args.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/engine/src/vm/opcode/args.rs b/core/engine/src/vm/opcode/args.rs index 3c51ceebd5b..9827db6aed0 100644 --- a/core/engine/src/vm/opcode/args.rs +++ b/core/engine/src/vm/opcode/args.rs @@ -423,6 +423,5 @@ mod tests { fn decode_truncated_buffer_panics() { let bytes = [0u8; 2]; let _ = u32::decode(&bytes, 0); - } } From e531b153f7c9d4f516aea604d8c1e142f57282b8 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Mon, 23 Feb 2026 17:16:35 +0530 Subject: [PATCH 08/14] feat: implement console.table() method --- core/runtime/src/console/mod.rs | 156 +++++++++++++++++++++++++++++- core/runtime/src/console/tests.rs | 37 +++++++ 2 files changed, 192 insertions(+), 1 deletion(-) diff --git a/core/runtime/src/console/mod.rs b/core/runtime/src/console/mod.rs index 2942be12380..1f4fe0d3d4d 100644 --- a/core/runtime/src/console/mod.rs +++ b/core/runtime/src/console/mod.rs @@ -15,7 +15,7 @@ pub(crate) mod tests; use boa_engine::JsVariant; -use boa_engine::property::Attribute; +use boa_engine::property::{Attribute, PropertyKey}; use boa_engine::{ Context, JsArgs, JsData, JsError, JsResult, JsString, JsSymbol, js_str, js_string, native_function::NativeFunction, @@ -83,6 +83,14 @@ pub trait Logger: Trace { /// # Errors /// Returning an error will throw an exception in JavaScript. fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()>; + + /// Log a table (`console.table`). By default, passes the message to `log`. + /// + /// # Errors + /// Returning an error will throw an exception in JavaScript. + fn table(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> { + self.log(msg, state, context) + } } /// The default implementation for logging from the console. @@ -438,6 +446,11 @@ impl Console { js_string!("dirxml"), 0, ) + .function( + console_method(Self::table, state.clone(), logger), + js_string!("table"), + 0, + ) .build() } @@ -633,6 +646,147 @@ impl Console { Ok(JsValue::undefined()) } + /// `console.table(tabularData, properties)` + /// + /// Prints a table with the data from the first argument. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#table + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/table_static + fn table( + _: &JsValue, + args: &[JsValue], + console: &Self, + logger: &impl Logger, + context: &mut Context, + ) -> JsResult { + let tabular_data = args.get_or_undefined(0); + + if !tabular_data.is_object() { + return Self::log(&JsValue::undefined(), args, console, logger, context); + } + + let Some(obj) = tabular_data.as_object() else { + return Self::log(&JsValue::undefined(), args, console, logger, context); + }; + + let keys = obj.own_property_keys(context)?; + if keys.is_empty() { + return Self::log(&JsValue::undefined(), args, console, logger, context); + } + + let mut col_names = vec!["(index)".to_string()]; + let mut rows = Vec::new(); + + for key in keys { + let row_index = key.to_string(); + let mut row_data = FxHashMap::default(); + row_data.insert("(index)".to_string(), row_index.clone()); + + let val = obj.get(key, context)?; + if let Some(val_obj) = val.as_object() { + let inner_keys = val_obj.own_property_keys(context)?; + for ik in inner_keys { + if let Ok(Some(desc)) = val_obj.get_own_property(&ik, context) { + if desc.enumerable().unwrap_or(false) { + let ik_str = ik.to_string(); + if !col_names.contains(&ik_str) { + col_names.push(ik_str.clone()); + } + let cell_val = val_obj.get(ik, context)?; + row_data.insert(ik_str, cell_val.display().to_string()); + } + } + } + } else { + let v_key = "Value".to_string(); + if !col_names.contains(&v_key) { + col_names.push(v_key.clone()); + } + row_data.insert(v_key, val.display().to_string()); + } + rows.push(row_data); + } + + if let Some(props) = args.get(1) + && props.is_object() + { + if let Some(props_obj) = props.as_object() { + let mut filtered_cols = vec!["(index)".to_string()]; + let p_keys = props_obj.own_property_keys(context)?; + for pk in p_keys { + if let Ok(Some(desc)) = props_obj.get_own_property(&pk, context) { + if desc.enumerable().unwrap_or(false) { + let pv = props_obj.get(pk, context)?; + filtered_cols.push(pv.to_string(context)?.to_std_string_escaped()); + } + } + } + col_names = filtered_cols; + } + } + + let mut widths = vec![0; col_names.len()]; + for (i, name) in col_names.iter().enumerate() { + widths[i] = name.len(); + } + for row in &rows { + for (i, name) in col_names.iter().enumerate() { + if let Some(val) = row.get(name) { + widths[i] = widths[i].max(val.len()); + } + } + } + + let mut output = String::new(); + for (i, name) in col_names.iter().enumerate() { + let _ = write!(output, "┌─{:─^width$}─", "", width = widths[i]); + if i == col_names.len() - 1 { + output.push_str("┐\n"); + } else { + output.push_str("┬"); + } + } + + for (i, name) in col_names.iter().enumerate() { + let _ = write!(output, "│ {: Date: Mon, 23 Feb 2026 18:12:00 +0530 Subject: [PATCH 09/14] feat: implement console.table() method --- core/runtime/src/console/mod.rs | 107 +++++++++++++++++++------------- 1 file changed, 63 insertions(+), 44 deletions(-) diff --git a/core/runtime/src/console/mod.rs b/core/runtime/src/console/mod.rs index 1f4fe0d3d4d..9a0ee2d3ec9 100644 --- a/core/runtime/src/console/mod.rs +++ b/core/runtime/src/console/mod.rs @@ -15,7 +15,8 @@ pub(crate) mod tests; use boa_engine::JsVariant; -use boa_engine::property::{Attribute, PropertyKey}; +use boa_engine::builtins::object::OrdinaryObject as BuiltinObject; +use boa_engine::property::Attribute; use boa_engine::{ Context, JsArgs, JsData, JsError, JsResult, JsString, JsSymbol, js_str, js_string, native_function::NativeFunction, @@ -442,7 +443,7 @@ impl Console { 0, ) .function( - console_method(Self::dir, state, logger.clone()), + console_method(Self::dir, state.clone(), logger.clone()), js_string!("dirxml"), 0, ) @@ -656,6 +657,7 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#table /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/table_static + #[allow(clippy::too_many_lines)] fn table( _: &JsValue, args: &[JsValue], @@ -673,33 +675,45 @@ impl Console { return Self::log(&JsValue::undefined(), args, console, logger, context); }; - let keys = obj.own_property_keys(context)?; - if keys.is_empty() { + let keys_val = + BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&tabular_data), context)?; + let keys_obj = keys_val.as_object().expect("Object.keys returns an array"); + let len = keys_obj + .get(js_string!("length"), context)? + .to_length(context)?; + + if len == 0 { return Self::log(&JsValue::undefined(), args, console, logger, context); } let mut col_names = vec!["(index)".to_string()]; - let mut rows = Vec::new(); + let mut rows: Vec> = Vec::new(); - for key in keys { - let row_index = key.to_string(); - let mut row_data = FxHashMap::default(); - row_data.insert("(index)".to_string(), row_index.clone()); + for i in 0..len { + let key_val = keys_obj.get(i, context)?; + let index_str = key_val.to_string(context)?.to_std_string_escaped(); + let mut row_data: FxHashMap = FxHashMap::default(); + row_data.insert("(index)".to_string(), index_str); - let val = obj.get(key, context)?; + let val = obj.get(key_val.to_property_key(context)?, context)?; if let Some(val_obj) = val.as_object() { - let inner_keys = val_obj.own_property_keys(context)?; - for ik in inner_keys { - if let Ok(Some(desc)) = val_obj.get_own_property(&ik, context) { - if desc.enumerable().unwrap_or(false) { - let ik_str = ik.to_string(); - if !col_names.contains(&ik_str) { - col_names.push(ik_str.clone()); - } - let cell_val = val_obj.get(ik, context)?; - row_data.insert(ik_str, cell_val.display().to_string()); - } + let inner_keys_val = + BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&val), context)?; + let inner_keys_obj = inner_keys_val + .as_object() + .expect("Object.keys returns an array"); + let inner_len = inner_keys_obj + .get(js_string!("length"), context)? + .to_length(context)?; + + for j in 0..inner_len { + let ik_val = inner_keys_obj.get(j, context)?; + let ik_str = ik_val.to_string(context)?.to_std_string_escaped(); + if !col_names.contains(&ik_str) { + col_names.push(ik_str.clone()); } + let cell_val = val_obj.get(ik_val.to_property_key(context)?, context)?; + row_data.insert(ik_str, cell_val.display().to_string()); } } else { let v_key = "Value".to_string(); @@ -711,22 +725,24 @@ impl Console { rows.push(row_data); } - if let Some(props) = args.get(1) - && props.is_object() - { - if let Some(props_obj) = props.as_object() { - let mut filtered_cols = vec!["(index)".to_string()]; - let p_keys = props_obj.own_property_keys(context)?; - for pk in p_keys { - if let Ok(Some(desc)) = props_obj.get_own_property(&pk, context) { - if desc.enumerable().unwrap_or(false) { - let pv = props_obj.get(pk, context)?; - filtered_cols.push(pv.to_string(context)?.to_std_string_escaped()); - } - } - } - col_names = filtered_cols; + if let Some(props_obj) = args.get(1).and_then(JsValue::as_object) { + let mut filtered_cols = vec!["(index)".to_string()]; + let props_val: JsValue = props_obj.clone().into(); + let p_keys_val = + BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&props_val), context)?; + let p_keys_obj = p_keys_val + .as_object() + .expect("Object.keys returns an array"); + let p_len = p_keys_obj + .get(js_string!("length"), context)? + .to_length(context)?; + + for i in 0..p_len { + let pk = p_keys_obj.get(i, context)?; + let pv = props_obj.get(pk.to_property_key(context)?, context)?; + filtered_cols.push(pv.to_string(context)?.to_std_string_escaped()); } + col_names = filtered_cols; } let mut widths = vec![0; col_names.len()]; @@ -742,12 +758,13 @@ impl Console { } let mut output = String::new(); - for (i, name) in col_names.iter().enumerate() { - let _ = write!(output, "┌─{:─^width$}─", "", width = widths[i]); + output.push('┌'); + for (i, _) in col_names.iter().enumerate() { + let _ = write!(output, "─{:─^width$}─", "", width = widths[i]); if i == col_names.len() - 1 { output.push_str("┐\n"); } else { - output.push_str("┬"); + output.push('┬'); } } @@ -756,12 +773,13 @@ impl Console { } output.push_str("│\n"); + output.push('├'); for (i, _) in col_names.iter().enumerate() { - let _ = write!(output, "├─{:─^width$}─", "", width = widths[i]); + let _ = write!(output, "─{:─^width$}─", "", width = widths[i]); if i == col_names.len() - 1 { output.push_str("┤\n"); } else { - output.push_str("┼"); + output.push('┼'); } } @@ -773,12 +791,13 @@ impl Console { output.push_str("│\n"); } + output.push('└'); for (i, _) in col_names.iter().enumerate() { - let _ = write!(output, "└─{:─^width$}─", "", width = widths[i]); + let _ = write!(output, "─{:─^width$}─", "", width = widths[i]); if i == col_names.len() - 1 { - output.push_str("┘"); + output.push('┘'); } else { - output.push_str("┴"); + output.push('┴'); } } From d24ed376c12fce4cb61c98805a28687945ecce71 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Thu, 26 Feb 2026 01:25:19 +0530 Subject: [PATCH 10/14] Refactor console.table: implement helper functions and optimize column discovery --- core/runtime/src/console/mod.rs | 123 ++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 45 deletions(-) diff --git a/core/runtime/src/console/mod.rs b/core/runtime/src/console/mod.rs index 9a0ee2d3ec9..b51f946b767 100644 --- a/core/runtime/src/console/mod.rs +++ b/core/runtime/src/console/mod.rs @@ -21,10 +21,12 @@ use boa_engine::{ Context, JsArgs, JsData, JsError, JsResult, JsString, JsSymbol, js_str, js_string, native_function::NativeFunction, object::{JsObject, ObjectInitializer}, - value::{JsValue, Numeric}, + value::{JsValue, Numeric, TryFromJs}, }; use boa_gc::{Finalize, Trace}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; + +type TableData = (Vec>, Vec); use std::{ cell::RefCell, collections::hash_map::Entry, fmt::Write as _, io::Write, rc::Rc, time::SystemTime, @@ -657,7 +659,6 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#table /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/table_static - #[allow(clippy::too_many_lines)] fn table( _: &JsValue, args: &[JsValue], @@ -667,41 +668,59 @@ impl Console { ) -> JsResult { let tabular_data = args.get_or_undefined(0); - if !tabular_data.is_object() { - return Self::log(&JsValue::undefined(), args, console, logger, context); - } - let Some(obj) = tabular_data.as_object() else { return Self::log(&JsValue::undefined(), args, console, logger, context); }; - let keys_val = - BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&tabular_data), context)?; - let keys_obj = keys_val.as_object().expect("Object.keys returns an array"); - let len = keys_obj - .get(js_string!("length"), context)? - .to_length(context)?; + let (rows, mut col_names) = Self::extract_rows(&obj, context)?; - if len == 0 { + if rows.is_empty() { return Self::log(&JsValue::undefined(), args, console, logger, context); } + if let Some(props) = args.get(1) { + col_names = Self::filter_columns(col_names, props, context)?; + } + + let widths = Self::format_table(&rows, &col_names); + let output = Self::render_table(&rows, &col_names, &widths); + + logger.table(output, &console.state, context)?; + + Ok(JsValue::undefined()) + } + + /// Extracts rows and initial column names from tabular data. + fn extract_rows(obj: &JsObject, context: &mut Context) -> JsResult { + let tabular_data = JsValue::from(obj.clone()); + let tabular_keys_val = + BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&tabular_data), context)?; + let tabular_keys_obj = tabular_keys_val.as_object().ok_or_else(|| { + JsError::from_opaque(js_string!("Object.keys did not return an object").into()) + })?; + let len = tabular_keys_obj + .get(js_string!("length"), context)? + .to_length(context)?; + let mut col_names = vec!["(index)".to_string()]; - let mut rows: Vec> = Vec::new(); + let mut seen_cols = FxHashSet::default(); + seen_cols.insert("(index)".to_string()); + + let mut rows = Vec::new(); for i in 0..len { - let key_val = keys_obj.get(i, context)?; - let index_str = key_val.to_string(context)?.to_std_string_escaped(); - let mut row_data: FxHashMap = FxHashMap::default(); + let index_key = tabular_keys_obj.get(i, context)?; + let index_str = index_key.to_string(context)?.to_std_string_escaped(); + let mut row_data = FxHashMap::default(); row_data.insert("(index)".to_string(), index_str); - let val = obj.get(key_val.to_property_key(context)?, context)?; + let val = obj.get(index_key.to_property_key(context)?, context)?; if let Some(val_obj) = val.as_object() { let inner_keys_val = BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&val), context)?; - let inner_keys_obj = inner_keys_val - .as_object() - .expect("Object.keys returns an array"); + let inner_keys_obj = inner_keys_val.as_object().ok_or_else(|| { + JsError::from_opaque(js_string!("Object.keys did not return an object").into()) + })?; let inner_len = inner_keys_obj .get(js_string!("length"), context)? .to_length(context)?; @@ -709,7 +728,7 @@ impl Console { for j in 0..inner_len { let ik_val = inner_keys_obj.get(j, context)?; let ik_str = ik_val.to_string(context)?.to_std_string_escaped(); - if !col_names.contains(&ik_str) { + if seen_cols.insert(ik_str.clone()) { col_names.push(ik_str.clone()); } let cell_val = val_obj.get(ik_val.to_property_key(context)?, context)?; @@ -717,7 +736,7 @@ impl Console { } } else { let v_key = "Value".to_string(); - if !col_names.contains(&v_key) { + if seen_cols.insert(v_key.clone()) { col_names.push(v_key.clone()); } row_data.insert(v_key, val.display().to_string()); @@ -725,38 +744,55 @@ impl Console { rows.push(row_data); } - if let Some(props_obj) = args.get(1).and_then(JsValue::as_object) { + Ok((rows, col_names)) + } + + /// Filters column names based on the optional properties argument. + fn filter_columns( + col_names: Vec, + properties: &JsValue, + context: &mut Context, + ) -> JsResult> { + if properties.is_null_or_undefined() { + return Ok(col_names); + } + + // Spec: "If properties is not undefined and is an iterable, then let columns be a list of the elements of properties." + // Boa's try_from_js for Vec handles iterables correctly. + if let Ok(iterator) = Vec::::try_from_js(properties, context) { let mut filtered_cols = vec!["(index)".to_string()]; - let props_val: JsValue = props_obj.clone().into(); - let p_keys_val = - BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&props_val), context)?; - let p_keys_obj = p_keys_val - .as_object() - .expect("Object.keys returns an array"); - let p_len = p_keys_obj - .get(js_string!("length"), context)? - .to_length(context)?; - - for i in 0..p_len { - let pk = p_keys_obj.get(i, context)?; - let pv = props_obj.get(pk.to_property_key(context)?, context)?; - filtered_cols.push(pv.to_string(context)?.to_std_string_escaped()); + for prop in iterator { + let prop_str = prop.to_string(context)?; + filtered_cols.push(prop_str.to_std_string_escaped()); } - col_names = filtered_cols; + return Ok(filtered_cols); } + Ok(col_names) + } + + /// Calculates the maximum width for each column. + fn format_table(rows: &[FxHashMap], col_names: &[String]) -> Vec { let mut widths = vec![0; col_names.len()]; for (i, name) in col_names.iter().enumerate() { widths[i] = name.len(); } - for row in &rows { + for row in rows { for (i, name) in col_names.iter().enumerate() { if let Some(val) = row.get(name) { widths[i] = widths[i].max(val.len()); } } } + widths + } + /// Renders the table as a string. + fn render_table( + rows: &[FxHashMap], + col_names: &[String], + widths: &[usize], + ) -> String { let mut output = String::new(); output.push('┌'); for (i, _) in col_names.iter().enumerate() { @@ -800,10 +836,7 @@ impl Console { output.push('┴'); } } - - logger.table(output, &console.state, context)?; - - Ok(JsValue::undefined()) + output } /// `console.count(label)` From f3022dc66686615277bc4a73a83429349a70e827 Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Fri, 13 Mar 2026 16:57:02 +0530 Subject: [PATCH 11/14] fix: apply reviewer feedback --- .github/workflows/nightly_build.yml | 3 --- .github/workflows/rust.yml | 3 --- core/engine/src/module/loader/mod.rs | 6 ------ 3 files changed, 12 deletions(-) diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index f65fe67f88d..c9867fecdf6 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -45,9 +45,6 @@ jobs: target key: ${{ matrix.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install build dependencies - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: sudo apt-get update && sudo apt-get install -y build-essential - name: Build run: cargo build --target ${{ matrix.target }} --release --locked --bin boa diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 15afb1280e0..0d31d2b54e4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -98,9 +98,6 @@ jobs: ~/.cargo/registry key: ${{ matrix.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install build dependencies - if: matrix.os == 'ubuntu-24.04-arm' - run: sudo apt-get update && sudo apt-get install -y build-essential - name: Build tests run: cargo test --no-run --profile ci diff --git a/core/engine/src/module/loader/mod.rs b/core/engine/src/module/loader/mod.rs index 832c3b562a7..2bfefc5776f 100644 --- a/core/engine/src/module/loader/mod.rs +++ b/core/engine/src/module/loader/mod.rs @@ -60,12 +60,6 @@ pub fn resolve_module_specifier( let specifier = specifier.to_std_string_escaped(); - // On Windows, also replace `/` with `\`. JavaScript imports use `/` as path separator. - #[cfg(target_family = "windows")] - let specifier = { - use cow_utils::CowUtils; - specifier.cow_replace('/', "\\") - }; let short_path = Path::new(&*specifier); From 7d056bbb01d766779369a6d24d35e481c6fb2b5c Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Sun, 15 Mar 2026 01:23:09 +0530 Subject: [PATCH 12/14] Finalize console.table PR: migrate to comfy-table, refactor, and fix clippy warnings --- .github/workflows/nightly_build.yml | 1 - .github/workflows/rust.yml | 6 - Cargo.lock | 1 + core/engine/src/module/loader/mod.rs | 3 + core/runtime/Cargo.toml | 1 + core/runtime/src/console/mod.rs | 177 ++++++++++----------------- core/runtime/src/console/tests.rs | 27 ++-- 7 files changed, 81 insertions(+), 135 deletions(-) diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index c9867fecdf6..338fdcb8b40 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -45,7 +45,6 @@ jobs: target key: ${{ matrix.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Build run: cargo build --target ${{ matrix.target }} --release --locked --bin boa diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0d31d2b54e4..306186c104c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -86,9 +86,6 @@ jobs: with: toolchain: stable - - name: Install Cargo insta - run: cargo install --locked cargo-insta - - name: Cache cargo uses: actions/cache@v5 with: @@ -98,7 +95,6 @@ jobs: ~/.cargo/registry key: ${{ matrix.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Build tests run: cargo test --no-run --profile ci # this order is faster according to rust-analyzer @@ -110,8 +106,6 @@ jobs: run: cargo nextest run --profile ci --cargo-profile ci --features annex-b,intl_bundled,experimental,embedded_lz4 - name: Test docs run: cargo test --doc --profile ci --features annex-b,intl_bundled,experimental - - name: Test bytecode output - run: cargo insta test -p insta-bytecode miri: name: Miri diff --git a/Cargo.lock b/Cargo.lock index e714078ea8b..9d3193b6050 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,6 +593,7 @@ dependencies = [ "boa_engine", "boa_gc", "bytemuck", + "comfy-table", "either", "futures", "futures-lite", diff --git a/core/engine/src/module/loader/mod.rs b/core/engine/src/module/loader/mod.rs index 2bfefc5776f..36082c8b684 100644 --- a/core/engine/src/module/loader/mod.rs +++ b/core/engine/src/module/loader/mod.rs @@ -1,3 +1,4 @@ +use cow_utils::CowUtils; use std::any::Any; use std::cell::RefCell; use std::path::{Component, Path, PathBuf}; @@ -60,6 +61,8 @@ pub fn resolve_module_specifier( let specifier = specifier.to_std_string_escaped(); + #[cfg(target_family = "windows")] + let specifier = specifier.cow_replace('/', "\\"); let short_path = Path::new(&*specifier); diff --git a/core/runtime/Cargo.toml b/core/runtime/Cargo.toml index dd75e661be8..c5ce6f8fbd2 100644 --- a/core/runtime/Cargo.toml +++ b/core/runtime/Cargo.toml @@ -21,6 +21,7 @@ futures-lite.workspace = true http = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } rustc-hash = { workspace = true, features = ["std"] } +comfy-table.workspace = true serde_json = { workspace = true, optional = true } url = { workspace = true, optional = true } diff --git a/core/runtime/src/console/mod.rs b/core/runtime/src/console/mod.rs index b51f946b767..cebbf2ce1d3 100644 --- a/core/runtime/src/console/mod.rs +++ b/core/runtime/src/console/mod.rs @@ -27,6 +27,7 @@ use boa_gc::{Finalize, Trace}; use rustc_hash::{FxHashMap, FxHashSet}; type TableData = (Vec>, Vec); +use comfy_table::{Cell, Table}; use std::{ cell::RefCell, collections::hash_map::Entry, fmt::Write as _, io::Write, rc::Rc, time::SystemTime, @@ -682,71 +683,95 @@ impl Console { col_names = Self::filter_columns(col_names, props, context)?; } - let widths = Self::format_table(&rows, &col_names); - let output = Self::render_table(&rows, &col_names, &widths); + let mut table = Table::new(); + table.load_preset(comfy_table::presets::UTF8_FULL); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + table.set_header(&col_names); - logger.table(output, &console.state, context)?; + for row in rows { + let mut cells = Vec::new(); + for name in &col_names { + cells.push(Cell::new(row.get(name).cloned().unwrap_or_default())); + } + table.add_row(cells); + } + + logger.table(table.to_string(), &console.state, context)?; Ok(JsValue::undefined()) } /// Extracts rows and initial column names from tabular data. fn extract_rows(obj: &JsObject, context: &mut Context) -> JsResult { - let tabular_data = JsValue::from(obj.clone()); - let tabular_keys_val = - BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&tabular_data), context)?; - let tabular_keys_obj = tabular_keys_val.as_object().ok_or_else(|| { - JsError::from_opaque(js_string!("Object.keys did not return an object").into()) - })?; - let len = tabular_keys_obj - .get(js_string!("length"), context)? - .to_length(context)?; - + let tabular_keys = Self::get_object_keys(obj, context)?; let mut col_names = vec!["(index)".to_string()]; let mut seen_cols = FxHashSet::default(); seen_cols.insert("(index)".to_string()); let mut rows = Vec::new(); - + let len = tabular_keys + .get(js_string!("length"), context)? + .to_length(context)?; for i in 0..len { - let index_key = tabular_keys_obj.get(i, context)?; + let index_key = tabular_keys.get(i, context)?; let index_str = index_key.to_string(context)?.to_std_string_escaped(); let mut row_data = FxHashMap::default(); row_data.insert("(index)".to_string(), index_str); let val = obj.get(index_key.to_property_key(context)?, context)?; - if let Some(val_obj) = val.as_object() { - let inner_keys_val = - BuiltinObject::keys(&JsValue::undefined(), std::slice::from_ref(&val), context)?; - let inner_keys_obj = inner_keys_val.as_object().ok_or_else(|| { - JsError::from_opaque(js_string!("Object.keys did not return an object").into()) - })?; - let inner_len = inner_keys_obj - .get(js_string!("length"), context)? - .to_length(context)?; - - for j in 0..inner_len { - let ik_val = inner_keys_obj.get(j, context)?; - let ik_str = ik_val.to_string(context)?.to_std_string_escaped(); - if seen_cols.insert(ik_str.clone()) { - col_names.push(ik_str.clone()); - } - let cell_val = val_obj.get(ik_val.to_property_key(context)?, context)?; - row_data.insert(ik_str, cell_val.display().to_string()); - } - } else { - let v_key = "Value".to_string(); - if seen_cols.insert(v_key.clone()) { - col_names.push(v_key.clone()); - } - row_data.insert(v_key, val.display().to_string()); - } + Self::extract_row_data(&val, &mut row_data, &mut col_names, &mut seen_cols, context)?; rows.push(row_data); } Ok((rows, col_names)) } + /// Gets keys of an object as a `JsObject` (array). + fn get_object_keys(obj: &JsObject, context: &mut Context) -> JsResult { + let tabular_data = JsValue::from(obj.clone()); + let tabular_keys_val = BuiltinObject::keys( + &JsValue::undefined(), + std::slice::from_ref(&tabular_data), + context, + )?; + tabular_keys_val.as_object().ok_or_else(|| { + JsError::from_opaque(js_string!("Object.keys did not return an object").into()) + }) + } + + /// Extracts data for a single row from a value. + fn extract_row_data( + val: &JsValue, + row_data: &mut FxHashMap, + col_names: &mut Vec, + seen_cols: &mut FxHashSet, + context: &mut Context, + ) -> JsResult<()> { + if let Some(val_obj) = val.as_object() { + let inner_keys_obj = Self::get_object_keys(&val_obj, context)?; + let inner_len = inner_keys_obj + .get(js_string!("length"), context)? + .to_length(context)?; + + for j in 0..inner_len { + let ik_val = inner_keys_obj.get(j, context)?; + let ik_str = ik_val.to_string(context)?.to_std_string_escaped(); + if seen_cols.insert(ik_str.clone()) { + col_names.push(ik_str.clone()); + } + let cell_val = val_obj.get(ik_val.to_property_key(context)?, context)?; + row_data.insert(ik_str, cell_val.display().to_string()); + } + } else { + let v_key = "Value".to_string(); + if seen_cols.insert(v_key.clone()) { + col_names.push(v_key.clone()); + } + row_data.insert(v_key, val.display().to_string()); + } + Ok(()) + } + /// Filters column names based on the optional properties argument. fn filter_columns( col_names: Vec, @@ -757,8 +782,6 @@ impl Console { return Ok(col_names); } - // Spec: "If properties is not undefined and is an iterable, then let columns be a list of the elements of properties." - // Boa's try_from_js for Vec handles iterables correctly. if let Ok(iterator) = Vec::::try_from_js(properties, context) { let mut filtered_cols = vec!["(index)".to_string()]; for prop in iterator { @@ -771,74 +794,6 @@ impl Console { Ok(col_names) } - /// Calculates the maximum width for each column. - fn format_table(rows: &[FxHashMap], col_names: &[String]) -> Vec { - let mut widths = vec![0; col_names.len()]; - for (i, name) in col_names.iter().enumerate() { - widths[i] = name.len(); - } - for row in rows { - for (i, name) in col_names.iter().enumerate() { - if let Some(val) = row.get(name) { - widths[i] = widths[i].max(val.len()); - } - } - } - widths - } - - /// Renders the table as a string. - fn render_table( - rows: &[FxHashMap], - col_names: &[String], - widths: &[usize], - ) -> String { - let mut output = String::new(); - output.push('┌'); - for (i, _) in col_names.iter().enumerate() { - let _ = write!(output, "─{:─^width$}─", "", width = widths[i]); - if i == col_names.len() - 1 { - output.push_str("┐\n"); - } else { - output.push('┬'); - } - } - - for (i, name) in col_names.iter().enumerate() { - let _ = write!(output, "│ {: Date: Sun, 15 Mar 2026 03:07:37 +0530 Subject: [PATCH 13/14] feat: implement basic ShadowRealm built-in object --- core/engine/src/builtins/mod.rs | 6 +- core/engine/src/builtins/shadow_realm/mod.rs | 149 ++++++++++++++++++ .../engine/src/builtins/shadow_realm/tests.rs | 21 +++ core/engine/src/context/intrinsics.rs | 14 ++ core/string/src/common.rs | 2 + 5 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 core/engine/src/builtins/shadow_realm/mod.rs create mode 100644 core/engine/src/builtins/shadow_realm/tests.rs diff --git a/core/engine/src/builtins/mod.rs b/core/engine/src/builtins/mod.rs index 470e6ea399b..444fd6da778 100644 --- a/core/engine/src/builtins/mod.rs +++ b/core/engine/src/builtins/mod.rs @@ -28,6 +28,7 @@ pub mod proxy; pub mod reflect; pub mod regexp; pub mod set; +pub mod shadow_realm; pub mod string; pub mod symbol; pub mod typed_array; @@ -77,6 +78,7 @@ pub(crate) use self::{ reflect::Reflect, regexp::RegExp, set::Set, + shadow_realm::ShadowRealm, string::String, symbol::Symbol, typed_array::{ @@ -272,8 +274,9 @@ impl Realm { Number::init(self); Eval::init(self); Set::init(self); - SetIterator::init(self); + ShadowRealm::init(self); String::init(self); + SetIterator::init(self); StringIterator::init(self); RegExp::init(self); RegExpStringIterator::init(self); @@ -408,6 +411,7 @@ pub(crate) fn set_default_global_bindings(context: &mut Context) -> JsResult<()> global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; + global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; diff --git a/core/engine/src/builtins/shadow_realm/mod.rs b/core/engine/src/builtins/shadow_realm/mod.rs new file mode 100644 index 00000000000..d9acdfbde88 --- /dev/null +++ b/core/engine/src/builtins/shadow_realm/mod.rs @@ -0,0 +1,149 @@ +//! Boa's implementation of ECMAScript's global `ShadowRealm` object. +//! +//! The `ShadowRealm` object is a distinct global environment that can execute +//! JavaScript code in a new, isolated realm. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/proposal-shadowrealm/ + +#[cfg(test)] +mod tests; + +use crate::{ + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + error::JsNativeError, + js_string, + object::{JsData, JsObject, internal_methods::get_prototype_from_constructor}, + realm::Realm, + string::StaticJsStrings, + Context, JsArgs, JsResult, JsString, JsValue, +}; +use boa_gc::{Finalize, Trace}; + +/// The `ShadowRealm` built-in object. +#[derive(Debug, Trace, Finalize)] +pub struct ShadowRealm { + inner: Realm, +} + +impl JsData for ShadowRealm {} + +impl IntrinsicObject for ShadowRealm { + fn init(realm: &Realm) { + BuiltInBuilder::from_standard_constructor::(realm) + .method(Self::evaluate, js_string!("evaluate"), 1) + .method(Self::import_value, js_string!("importValue"), 2) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInObject for ShadowRealm { + const NAME: JsString = StaticJsStrings::SHADOW_REALM; +} + +impl BuiltInConstructor for ShadowRealm { + const CONSTRUCTOR_ARGUMENTS: usize = 0; + const PROTOTYPE_STORAGE_SLOTS: usize = 2; + const CONSTRUCTOR_STORAGE_SLOTS: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::shadow_realm; + + fn constructor( + new_target: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return Err(JsNativeError::typ() + .with_message("ShadowRealm constructor: NewTarget is undefined") + .into()); + } + + // 2. Let realmRec be ? CreateRealm(). + let realm = context.create_realm()?; + + // 3. Let shadowRealm be ? OrdinaryCreateFromConstructor(newTarget, "%ShadowRealm.prototype%", « [[ShadowRealm]] »). + // 4. Set shadowRealm.[[ShadowRealm]] to realmRec. + let prototype = get_prototype_from_constructor(new_target, StandardConstructors::shadow_realm, context)?; + let shadow_realm = JsObject::from_proto_and_data(prototype, ShadowRealm { inner: realm }); + + // 5. Return shadowRealm. + Ok(shadow_realm.into()) + } +} + +impl ShadowRealm { + /// `ShadowRealm.prototype.evaluate ( sourceText )` + pub(crate) fn evaluate(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let shadowRealm be the this value. + // 2. Perform ? ValidateShadowRealm(shadowRealm). + let shadow_realm_obj = this + .as_object() + .ok_or_else(|| { + JsNativeError::typ() + .with_message("ShadowRealm.prototype.evaluate: this is not a ShadowRealm object") + })?; + + let shadow_realm = shadow_realm_obj.downcast_ref::().ok_or_else(|| { + JsNativeError::typ() + .with_message("ShadowRealm.prototype.evaluate: this is not a ShadowRealm object") + })?; + + // 3. If Type(sourceText) is not String, throw a TypeError exception. + let source_text = args.get_or_undefined(0); + if !source_text.is_string() { + return Err(JsNativeError::typ() + .with_message("ShadowRealm.prototype.evaluate: sourceText is not a string") + .into()); + } + + // 4. Let realmRec be shadowRealm.[[ShadowRealm]]. + let realm = shadow_realm.inner.clone(); + + // 5. Return ? PerformShadowRealmEval(sourceText, realmRec). + + // Switch realm + let old_realm = context.enter_realm(realm); + + // Perform eval (indirect) + let result = + crate::builtins::eval::Eval::perform_eval(source_text, false, None, false, context); + + // Restore realm + context.enter_realm(old_realm); + + let result = result?; + + // 6. Return ? GetWrappedValue(realm, result). + // TODO: Implement GetWrappedValue (Callable Masking) + // For now, just return the result if it's not a function. + if result.is_callable() { + return Err(JsNativeError::typ() + .with_message("ShadowRealm: Callable masking (function wrapping) not yet implemented") + .into()); + } + + Ok(result) + } + + /// `ShadowRealm.prototype.importValue ( specifier, name )` + pub(crate) fn import_value( + _this: &JsValue, + _args: &[JsValue], + _context: &mut Context, + ) -> JsResult { + // TODO: Implementation of importValue + Err(JsNativeError::typ() + .with_message("ShadowRealm.prototype.importValue: not yet implemented") + .into()) + } +} diff --git a/core/engine/src/builtins/shadow_realm/tests.rs b/core/engine/src/builtins/shadow_realm/tests.rs new file mode 100644 index 00000000000..b7139efe2b0 --- /dev/null +++ b/core/engine/src/builtins/shadow_realm/tests.rs @@ -0,0 +1,21 @@ +use crate::{run_test_actions, TestAction}; + +#[test] +fn constructor() { + run_test_actions([ + TestAction::assert("new ShadowRealm() instanceof ShadowRealm"), + TestAction::assert("typeof ShadowRealm.prototype.evaluate === 'function'"), + TestAction::assert("typeof ShadowRealm.prototype.importValue === 'function'"), + ]); +} + +#[test] +fn evaluate_isolation() { + run_test_actions([ + TestAction::run("const realm = new ShadowRealm();"), + TestAction::run("realm.evaluate('globalThis.x = 42;');"), + TestAction::assert("globalThis.x === undefined"), + TestAction::assert("realm.evaluate('globalThis.x') === 42"), + TestAction::assert("realm.evaluate('globalThis.x = 100;'); realm.evaluate('globalThis.x') === 100"), + ]); +} diff --git a/core/engine/src/context/intrinsics.rs b/core/engine/src/context/intrinsics.rs index b4dcc162e9d..ebf1cc58bf2 100644 --- a/core/engine/src/context/intrinsics.rs +++ b/core/engine/src/context/intrinsics.rs @@ -148,6 +148,7 @@ pub struct StandardConstructors { aggregate_error: StandardConstructor, map: StandardConstructor, set: StandardConstructor, + shadow_realm: StandardConstructor, typed_array: StandardConstructor, typed_int8_array: StandardConstructor, typed_uint8_array: StandardConstructor, @@ -243,6 +244,7 @@ impl Default for StandardConstructors { aggregate_error: StandardConstructor::default(), map: StandardConstructor::default(), set: StandardConstructor::default(), + shadow_realm: StandardConstructor::default(), typed_array: StandardConstructor::default(), typed_int8_array: StandardConstructor::default(), typed_uint8_array: StandardConstructor::default(), @@ -591,6 +593,18 @@ impl StandardConstructors { &self.set } + /// Returns the `ShadowRealm` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-shadowrealm/#sec-shadowrealm-constructor + #[inline] + #[must_use] + pub const fn shadow_realm(&self) -> &StandardConstructor { + &self.shadow_realm + } + /// Returns the `TypedArray` constructor. /// /// More information: diff --git a/core/string/src/common.rs b/core/string/src/common.rs index 3a4c0d3ff1f..b94e3734a32 100644 --- a/core/string/src/common.rs +++ b/core/string/src/common.rs @@ -164,6 +164,7 @@ impl StaticJsStrings { (REFLECT, "Reflect"), (REG_EXP, "RegExp"), (SET, "Set"), + (SHADOW_REALM, "ShadowRealm"), (STRING, "String"), (SYMBOL, "Symbol"), (TYPED_ARRAY, "TypedArray"), @@ -307,6 +308,7 @@ const RAW_STATICS: &[StaticString] = &[ StaticString::new(JsStr::latin1("Reflect".as_bytes())), StaticString::new(JsStr::latin1("RegExp".as_bytes())), StaticString::new(JsStr::latin1("Set".as_bytes())), + StaticString::new(JsStr::latin1("ShadowRealm".as_bytes())), StaticString::new(JsStr::latin1("String".as_bytes())), StaticString::new(JsStr::latin1("Symbol".as_bytes())), StaticString::new(JsStr::latin1("TypedArray".as_bytes())), From b929c1828a59744835761154287f8ce6ef1274aa Mon Sep 17 00:00:00 2001 From: Parth Mozarkar Date: Sun, 15 Mar 2026 16:29:55 +0530 Subject: [PATCH 14/14] Address maintainer feedback: revert CI changes and feature-gate ShadowRealm behind 'experimental' --- .github/workflows/rust.yml | 5 +++++ core/engine/Cargo.toml | 4 ++-- core/engine/src/builtins/mod.rs | 7 ++++++- core/engine/src/builtins/shadow_realm/mod.rs | 1 + core/engine/src/builtins/shadow_realm/tests.rs | 1 + core/engine/src/context/intrinsics.rs | 3 +++ core/string/Cargo.toml | 4 ++++ core/string/src/common.rs | 3 +++ 8 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 306186c104c..b7723f3b49c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -86,6 +86,9 @@ jobs: with: toolchain: stable + - name: Install Cargo insta + run: cargo install --locked cargo-insta + - name: Cache cargo uses: actions/cache@v5 with: @@ -106,6 +109,8 @@ jobs: run: cargo nextest run --profile ci --cargo-profile ci --features annex-b,intl_bundled,experimental,embedded_lz4 - name: Test docs run: cargo test --doc --profile ci --features annex-b,intl_bundled,experimental + - name: Test bytecode output + run: cargo insta test -p insta-bytecode miri: name: Miri diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index 480a7a7adeb..461eb4bf149 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -77,13 +77,13 @@ temporal = ["dep:icu_calendar", "dep:temporal_rs", "dep:timezone_provider", "tim system-time-zone = ["dep:iana-time-zone"] # Enable experimental features, like Stage 3 proposals. -experimental = [] +experimental = ["boa_string/experimental"] # Enable binding to JS APIs for system related utilities. js = ["dep:web-time", "dep:getrandom", "getrandom/wasm_js", "time/wasm-bindgen"] # Enable support for Float16 typed arrays -float16 = ["dep:float16"] +float16 = ["dep:float16", "boa_string/float16"] # Enable support for `Math.sumPrecise` xsum = ["dep:xsum"] diff --git a/core/engine/src/builtins/mod.rs b/core/engine/src/builtins/mod.rs index 444fd6da778..8229a32c614 100644 --- a/core/engine/src/builtins/mod.rs +++ b/core/engine/src/builtins/mod.rs @@ -28,6 +28,7 @@ pub mod proxy; pub mod reflect; pub mod regexp; pub mod set; +#[cfg(feature = "experimental")] pub mod shadow_realm; pub mod string; pub mod symbol; @@ -78,7 +79,6 @@ pub(crate) use self::{ reflect::Reflect, regexp::RegExp, set::Set, - shadow_realm::ShadowRealm, string::String, symbol::Symbol, typed_array::{ @@ -87,6 +87,9 @@ pub(crate) use self::{ }, }; +#[cfg(feature = "experimental")] +pub(crate) use self::shadow_realm::ShadowRealm; + use crate::{ Context, JsResult, JsString, JsValue, builtins::{ @@ -274,6 +277,7 @@ impl Realm { Number::init(self); Eval::init(self); Set::init(self); + #[cfg(feature = "experimental")] ShadowRealm::init(self); String::init(self); SetIterator::init(self); @@ -411,6 +415,7 @@ pub(crate) fn set_default_global_bindings(context: &mut Context) -> JsResult<()> global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; + #[cfg(feature = "experimental")] global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; diff --git a/core/engine/src/builtins/shadow_realm/mod.rs b/core/engine/src/builtins/shadow_realm/mod.rs index d9acdfbde88..512202441af 100644 --- a/core/engine/src/builtins/shadow_realm/mod.rs +++ b/core/engine/src/builtins/shadow_realm/mod.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "experimental")] //! Boa's implementation of ECMAScript's global `ShadowRealm` object. //! //! The `ShadowRealm` object is a distinct global environment that can execute diff --git a/core/engine/src/builtins/shadow_realm/tests.rs b/core/engine/src/builtins/shadow_realm/tests.rs index b7139efe2b0..31fe1390a3f 100644 --- a/core/engine/src/builtins/shadow_realm/tests.rs +++ b/core/engine/src/builtins/shadow_realm/tests.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "experimental")] use crate::{run_test_actions, TestAction}; #[test] diff --git a/core/engine/src/context/intrinsics.rs b/core/engine/src/context/intrinsics.rs index ebf1cc58bf2..9519c7ccb8b 100644 --- a/core/engine/src/context/intrinsics.rs +++ b/core/engine/src/context/intrinsics.rs @@ -148,6 +148,7 @@ pub struct StandardConstructors { aggregate_error: StandardConstructor, map: StandardConstructor, set: StandardConstructor, + #[cfg(feature = "experimental")] shadow_realm: StandardConstructor, typed_array: StandardConstructor, typed_int8_array: StandardConstructor, @@ -244,6 +245,7 @@ impl Default for StandardConstructors { aggregate_error: StandardConstructor::default(), map: StandardConstructor::default(), set: StandardConstructor::default(), + #[cfg(feature = "experimental")] shadow_realm: StandardConstructor::default(), typed_array: StandardConstructor::default(), typed_int8_array: StandardConstructor::default(), @@ -593,6 +595,7 @@ impl StandardConstructors { &self.set } + #[cfg(feature = "experimental")] /// Returns the `ShadowRealm` constructor. /// /// More information: diff --git a/core/string/Cargo.toml b/core/string/Cargo.toml index fca96f4bcd5..c713bbfaf50 100644 --- a/core/string/Cargo.toml +++ b/core/string/Cargo.toml @@ -11,6 +11,10 @@ license.workspace = true repository.workspace = true rust-version.workspace = true +[features] +experimental = [] +float16 = [] + [dependencies] itoa.workspace = true rustc-hash = { workspace = true, features = ["std"] } diff --git a/core/string/src/common.rs b/core/string/src/common.rs index b94e3734a32..540a0ddab58 100644 --- a/core/string/src/common.rs +++ b/core/string/src/common.rs @@ -9,6 +9,7 @@ use std::sync::LazyLock; macro_rules! well_known_statics { ( $( $(#[$attr:meta])* ($name:ident, $string:literal) ),+$(,)? ) => { $( + $(#[$attr])* paste!{ #[doc = "Gets the static `JsString` for `\"" $string "\"`."] pub const $name: JsString = const { @@ -164,6 +165,7 @@ impl StaticJsStrings { (REFLECT, "Reflect"), (REG_EXP, "RegExp"), (SET, "Set"), + #[cfg(feature = "experimental")] (SHADOW_REALM, "ShadowRealm"), (STRING, "String"), (SYMBOL, "Symbol"), @@ -308,6 +310,7 @@ const RAW_STATICS: &[StaticString] = &[ StaticString::new(JsStr::latin1("Reflect".as_bytes())), StaticString::new(JsStr::latin1("RegExp".as_bytes())), StaticString::new(JsStr::latin1("Set".as_bytes())), + #[cfg(feature = "experimental")] StaticString::new(JsStr::latin1("ShadowRealm".as_bytes())), StaticString::new(JsStr::latin1("String".as_bytes())), StaticString::new(JsStr::latin1("Symbol".as_bytes())),