* feat: add QuickJS as alternative JS engine for flow expression evaluation Add rquickjs as an optional alternative to deno_core for evaluating JavaScript expressions in flow transformations. QuickJS offers ~8-16x faster startup times for simple expressions, making it ideal for evaluating many small expressions in flows. Key changes: - Add new `quickjs` feature flag for windmill-worker - Implement js_eval_quickjs.rs with true async Rust callbacks for variable(), resource(), and results.xxx access (no pre-fetching) - Share expression transformation logic (replace_with_await, replace_with_await_result) between both implementations - Add USE_QUICKJS_FOR_FLOW_EVAL env var to switch engines at runtime - When only quickjs feature is enabled (no deno_core), QuickJS is automatically used - Add comprehensive parity tests comparing QuickJS and deno_core output Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * all * quickjs * quickjs * all * all * all --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
12 KiB
QuickJS Migration - Potential Breaking Changes Analysis
Summary
This document details the comprehensive investigation into potential breaking changes when migrating flow expressions from Deno Core (V8) to QuickJS.
1. Areas Already Tested (60+ Parity Tests)
The following areas have comprehensive parity tests in js_eval_parity_tests.rs:
- Arithmetic operations: +, -, *, /, %, **
- Comparison operators: ===, !==, >, <, >=, <=, ==, !=
- Logical operators: &&, ||, !, ??, ?.
- Bitwise operators: &, |, ^, ~, <<, >>, >>>
- Object operations: property access, spread, destructuring, Object.keys/values/entries
- Array operations: map, filter, reduce, find, some, every, slice, flat, etc.
- String operations: split, replace, includes, startsWith, trim, etc.
- Template literals: ${} interpolation
- Optional chaining: ?. for properties, methods, computed properties
- Nullish coalescing: ??
- Try-catch blocks
- Arrow functions
- Destructuring
- Date operations (with fixed dates)
- JSON.parse/stringify
- Math functions
- Set and Map operations
- Regular expressions (basic patterns)
- flow_input, flow_env, previous_result access
- Error extraction logic (from parallel results)
2. Potential Breaking Changes Identified
2.1 Number Handling Edge Cases (MEDIUM RISK)
Implementation Difference:
// QuickJS json_to_js:
if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
Ok(Value::new_int(ctx.clone(), i as i32))
} else {
Ok(Value::new_float(ctx.clone(), i as f64))
}
Potential Issues:
- Numbers outside i32 range (-2147483648 to 2147483647) are stored as floats
- Large integers (between i32::MAX and 2^53) might lose precision
- Timestamps (e.g., 1704067200000) are typically in this range
Test Case Needed:
// Numbers just above i32::MAX
2147483648 + 1 // i32::MAX + 2
9007199254740991 - 1 // Near MAX_SAFE_INTEGER
2.2 Object Property Order (LOW RISK)
Implementation Difference:
- QuickJS:
obj.props::<String, Value>()iteration order - V8: Guaranteed insertion order for string keys
Potential Impact:
Object.keys(),Object.values(),Object.entries()order might differ- Object spread
{...obj}order might differ
Mitigated by:
- JSON comparison in tests normalizes order
- Most flow expressions don't depend on property order
2.3 Missing Browser/Deno APIs (MEDIUM RISK)
APIs NOT available in QuickJS:
atob()/btoa()- Base64 encoding/decodingTextEncoder/TextDecoderfetch()(not relevant for expressions)Blob,ArrayBuffer(limited support)Intl.*- Internationalization APIsconsole.log()- No effect (not breaking, just no output)
Expressions that would break:
atob("SGVsbG8=") // Would throw: atob is not defined
btoa("Hello") // Would throw: btoa is not defined
new TextEncoder().encode("test") // Would throw
"test".toLocaleUpperCase('tr-TR') // Might behave differently
2.4 Regular Expression Differences (LOW RISK)
QuickJS RegExp limitations:
- No
dflag (indices) - No lookbehind assertions
(?<=...)and(?<!...) - No named capture groups
(?<name>...)
Expressions that might break:
"test123".match(/(?<=test)\d+/) // Lookbehind not supported
/(?<name>\w+)/.exec("test")?.groups?.name // Named groups not supported
2.5 Prototype Method Availability (LOW RISK)
Methods that might differ:
Array.prototype.at()- ES2022String.prototype.at()- ES2022Object.hasOwn()- ES2022String.prototype.replaceAll()- ES2021
Test Case:
[1,2,3].at(-1) // Might not exist
"hello".at(-1) // Might not exist
2.6 NaN/Infinity/Special Values (LOW RISK)
Implementation:
// QuickJS js_to_json:
if let Some(n) = serde_json::Number::from_f64(f) {
return Ok(serde_json::Value::Number(n));
} else {
return Ok(serde_json::Value::Null); // NaN, Infinity -> null
}
Both engines convert NaN/Infinity to null in JSON, so this is consistent.
2.7 Fallback for Unsupported Types (LOW RISK)
QuickJS fallback:
// Fallback
Ok(serde_json::Value::String("[object]".to_string()))
Types that would trigger this:
- Symbol
- WeakMap/WeakRef
- Generator objects
- Custom objects with non-enumerable properties only
2.8 Date Object Timezone Handling (MEDIUM RISK)
Potential Issue:
new Date()without arguments uses system time- Timezone-dependent methods might vary
Safe patterns (already tested):
new Date('2024-01-15T00:00:00.000Z').getUTCFullYear() // OK - UTC methods
Date.parse('2024-01-15T00:00:00.000Z') // OK - explicit timezone
Risky patterns:
new Date().toLocaleDateString() // Timezone dependent
new Date().getHours() // Timezone dependent
3. Edge Cases NOT Currently Tested
3.1 Very Large Numbers
9007199254740991 // MAX_SAFE_INTEGER
9007199254740992 // MAX_SAFE_INTEGER + 1 (loses precision)
2147483648 // i32::MAX + 1
3.2 Negative Zero
-0 === 0 // true
Object.is(-0, 0) // false
1/-0 // -Infinity
3.3 Sparse Arrays
const arr = [1, , 3] // Hole at index 1
arr.map(x => x * 2) // Holes might be handled differently
arr.filter(x => true) // Holes might be skipped or preserved
3.4 Unicode Edge Cases
"🎉".length // 2 (surrogate pairs)
"🎉".split('') // Might differ
[..."🎉"] // Might differ
"café" === "café" // NFC vs NFD normalization
3.5 Prototype Chain
const obj = Object.create({ inherited: 1 });
obj.own = 2;
Object.keys(obj) // Should only return ['own']
3.6 Getter/Setter Properties
const obj = {
get prop() { return 42; },
set prop(v) { }
};
obj.prop // Should return 42
3.7 Circular References
const obj = { a: 1 };
obj.self = obj;
JSON.stringify(obj) // Should throw in both
3.8 Array-like Objects
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
Array.from(arrayLike) // Should work in both
4. Recommended Additional Tests
High Priority (Add to parity tests):
- Large integers (i32 boundary, MAX_SAFE_INTEGER boundary)
Array.prototype.at()andString.prototype.at()- Sparse arrays with holes
- Emoji/surrogate pair handling
- Object property order verification
Medium Priority:
- Getter/setter access
- Prototype chain behavior
- Array-like object conversion
- Error message format differences
Low Priority (Unlikely to be used in expressions):
- WeakMap/WeakSet
- Generators
- Symbols
- Proxy edge cases
5. Known Safe Patterns
These patterns are safe to use and have been verified:
- All arithmetic and comparison operators
- All standard array methods (map, filter, reduce, etc.)
- All standard string methods
- Object spread and destructuring
- Optional chaining and nullish coalescing
- Template literals
- Arrow functions
- Try-catch blocks
flow_input,flow_env,previous_result,resultsaccess- JSON operations
- Date operations with UTC methods
- Regular expressions (basic patterns without lookbehind)
6. Test Coverage Summary
Unit Parity Tests (114 tests in js_eval_parity_tests.rs)
- Basic arithmetic, comparison, logical, and bitwise operators
- Object operations: property access, spread, destructuring
- Array operations: map, filter, reduce, find, some, every, slice, flat, etc.
- String operations: all standard methods
- Template literals with complex expressions
- Optional chaining and nullish coalescing
- Set and Map operations
- JSON parse/stringify
- Date operations with UTC methods
- Error handling with try-catch
- Large integer handling (i32 boundaries, timestamps, MAX_SAFE_INTEGER)
- Unicode and special characters
- Type coercion
Flow Engine Parity Tests (19 tests in flow_engine_parity.rs)
All tests pass with both Deno Core and QuickJS:
- Linear flow with input transforms -
results.a.propertyaccess - For-loop with complex iterator -
results.a.users.filter(...) - Branch conditions -
results.a.status === 'premium' && results.a.score >= 90 - Previous result aggregation -
previous_result.value,results.a.value + results.b.value - Nested complexity - Deep result access across loop iterations
- Parallel for-loops - Multiple concurrent iterations
- Skip-if expressions - Conditional step execution
- Object transformations - Complex data manipulation
- Template literals -
\Status: ${results.a.status}`` - Optional chaining -
results.a.user?.name,results.a?.missing?.value ?? 'default' - Flow env access -
flow_env.CONFIG.apiUrl - Combined flow_input and flow_env
- Results optional chaining - Deep optional chaining with results proxy
- Large integers - Timestamps, i32 boundaries through results
- Unicode and emoji - Strings with unicode through flow results
- Complex array operations - Sort, filter/map chains, reduce through results
- Multiline expressions - Multi-statement expressions with semicolons and return
- Spread operators -
{...results.a.config},[...results.a.tags] - Nested for-loop results access - Accessing outer step results from inner loops
7. Conclusion
The QuickJS migration is safe for the vast majority of flow expressions. Comprehensive testing shows:
- 133 total parity tests pass (114 unit + 19 flow engine)
- All tests pass with both Deno Core and QuickJS
- No behavioral differences detected in production-like scenarios
ES2022+ Method Support (All SUPPORTED in both engines):
Tested and verified to work identically:
Array.prototype.at()- ES2022 ✅String.prototype.at()- ES2022 ✅Object.hasOwn()- ES2022 ✅String.prototype.replaceAll()- ES2021 ✅Array.prototype.findLast()- ES2023 ✅Array.prototype.findLastIndex()- ES2023 ✅Array.prototype.toSorted()- ES2023 ✅Array.prototype.toReversed()- ES2023 ✅Array.prototype.toSpliced()- ES2023 ✅Array.prototype.with()- ES2023 ✅Object.groupBy()- ES2024 ✅
Regex Feature Support (All SUPPORTED in both engines):
- Lookbehind assertions
(?<=...)✅ - Negative lookbehind
(?<!...)✅ - Named capture groups
(?<name>...)✅ dflag (indices) ✅
Browser API Parity (Both engines return undefined):
These APIs are NOT available in either engine (consistent behavior):
atob/btoa- Both returntypeof === "undefined"✅TextEncoder/TextDecoder- Both returntypeof === "undefined"✅URL/URLSearchParams- Both returntypeof === "undefined"✅
BREAKING CHANGE IDENTIFIED:
Intl API - ONLY breaking change found:
- Deno Core:
typeof Intl === "object"(available) - QuickJS:
typeof Intl === "undefined"(NOT available)
Expressions using these will FAIL with QuickJS:
new Intl.NumberFormat('en-US').format(1234567.89)new Intl.DateTimeFormat('en-US').format(new Date())num.toLocaleString('de-DE')date.toLocaleDateString('fr-FR')
Mitigation: Search production logs for Intl usage in flow expressions before migration.
Recommendations:
- ✅ Run the parity tests to verify current implementation (132 unit tests + 19 flow engine tests pass)
- ✅ Add tests for edge cases (large integers, optional chaining, spread, multiline)
- ✅ Test ES2022+ methods - All supported (Array.at, Object.hasOwn, etc.)
- ✅ Test regex features - All supported (lookbehind, named groups)
- ⚠️ Search production for
Intlusage - Only confirmed breaking change - Run
USE_QUICKJS_FOR_FLOW_EVAL=1in staging before full production rollout - Consider adding
Intlpolyfill to QuickJS if production usage is found
Commands to Run Tests:
# Run all parity tests (132 tests)
cargo test --features deno_core,quickjs -p windmill-worker -- parity_
# Run flow engine tests with both engines (19 tests)
cargo test --features deno_core -p windmill --test flow_engine_parity
USE_QUICKJS_FOR_FLOW_EVAL=1 cargo test --features deno_core,quickjs -p windmill --test flow_engine_parity