cfsolve 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +10 -9
  4. data/UPSTREAM.md +6 -6
  5. data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  6. data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  7. data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
  8. data/lib/cfsolve/version.rb +2 -2
  9. data/vendor/cfsolve/README.md +0 -35
  10. data/vendor/cfsolve/examples/build_profile.rs +17 -15
  11. data/vendor/cfsolve/examples/capture_session.rs +37 -0
  12. data/vendor/cfsolve/examples/check_answers.rs +66 -0
  13. data/vendor/cfsolve/examples/compare_session.rs +190 -0
  14. data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
  15. data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  16. data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  17. data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
  18. data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
  19. data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
  20. data/vendor/cfsolve/src/capture/profile.rs +7 -18
  21. data/vendor/cfsolve/src/capture/recorded.rs +42 -7
  22. data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
  23. data/vendor/cfsolve/src/challenge/installed.rs +122 -271
  24. data/vendor/cfsolve/src/challenge/payload.rs +107 -5
  25. data/vendor/cfsolve/src/challenge/state.rs +98 -16
  26. data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
  27. data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
  28. data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
  29. data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
  30. data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
  31. data/vendor/cfsolve/src/http/client.rs +11 -0
  32. data/vendor/cfsolve/src/js/ast.rs +12 -3
  33. data/vendor/cfsolve/src/js/objects.rs +9 -2
  34. data/vendor/cfsolve/src/listing/fold.rs +0 -83
  35. data/vendor/cfsolve/src/listing/query.rs +25 -244
  36. data/vendor/cfsolve/src/listing.rs +0 -1
  37. data/vendor/cfsolve/src/session/initial.rs +14 -5
  38. data/vendor/cfsolve/src/session/probes.rs +160 -125
  39. data/vendor/cfsolve/src/session/timing.rs +58 -53
  40. data/vendor/cfsolve/src/session/widget.rs +5 -6
  41. data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
  42. data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
  43. data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
  44. data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
  45. data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
  46. data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
  47. data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
  48. data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
  49. data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
  50. data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
  51. data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
  52. data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
  53. data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
  54. data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
  55. data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
  56. data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
  57. data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
  58. data/vendor/cfsolve/src/stage2/probes.rs +0 -1
  59. data/vendor/cfsolve/src/stage2/records.rs +4 -13
  60. data/vendor/cfsolve/src/stage2/reference.json +1 -1
  61. data/vendor/cfsolve/src/stage2/reference.rs +98 -100
  62. data/vendor/cfsolve/src/stage2/registry.rs +41 -34
  63. data/vendor/cfsolve/src/stage2/replay.rs +182 -254
  64. data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
  65. data/vendor/cfsolve/src/stage2/trace.rs +18 -0
  66. data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
  67. data/vendor/cfsolve/src/stage2.rs +2 -1
  68. data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
  69. data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
  70. data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
  71. data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
  72. data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
  73. data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
  74. data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
  75. data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
  76. data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
  77. data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
  78. data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
  79. data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
  80. data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
  81. data/vendor/cfsolve/src/vm/interpret.rs +7 -2
  82. data/vendor/cfsolve/src/vm/machine.rs +6 -2
  83. data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
  84. data/vendor/cfsolve/src/vm/session.rs +36 -14
  85. data/vendor/cfsolve/src/vm/value.rs +13 -0
  86. data/vendor/cfsolve/src/vm.rs +1 -0
  87. data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
  88. metadata +13 -6
  89. data/vendor/cfsolve/src/listing/normalize.rs +0 -264
  90. data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
  91. data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
  92. data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
  93. data/vendor/cfsolve/src/stage2/session_names.rs +0 -470
@@ -0,0 +1,350 @@
1
+ //! Native versions of pure functions the script defines, such as the BLAKE3 compression its
2
+ //! string decryption runs. A script function is replaced only after it returns the native
3
+ //! result on test inputs. Calls with arguments a native version does not accept are
4
+ //! interpreted.
5
+
6
+ use std::ptr;
7
+
8
+ use swc_core::ecma::ast::{Function, Lit};
9
+ use swc_core::ecma::visit::{Visit, VisitWith};
10
+
11
+ use super::copies::Copies;
12
+ use super::run::Run;
13
+ use crate::js;
14
+ use crate::vm::HashMap;
15
+ use crate::vm::session::Session;
16
+ use crate::vm::value::{Callable, Value};
17
+
18
+ /// A native function equivalent to a script function.
19
+ #[derive(Clone, Copy)]
20
+ pub(crate) enum Intrinsic {
21
+ /// `compress(cv, block, counter, length, flags)`: overwrites the 8 words of `cv` with the
22
+ /// BLAKE3 compression of the 16-word `block`.
23
+ Blake3Compress,
24
+ }
25
+
26
+ /// Script functions with native equivalents, by function address.
27
+ pub(crate) type Intrinsics = HashMap<usize, Intrinsic>;
28
+
29
+ /// The address identifying `function` in [`Intrinsics`].
30
+ pub(super) fn address(function: &Function) -> usize {
31
+ ptr::from_ref(function).addr()
32
+ }
33
+
34
+ /// Finds the script functions `session` can run natively. Tests functions that take five
35
+ /// parameters and mention the BLAKE3 initialization vector.
36
+ pub(crate) fn intrinsics(session: &Session<'_>) -> Intrinsics {
37
+ session
38
+ .functions
39
+ .values()
40
+ .filter(|function| function.params.len() == 5 && mentions(function, IV[0]))
41
+ .filter(|function| compresses_like_native(session, function))
42
+ .map(|function| (address(function), Intrinsic::Blake3Compress))
43
+ .collect()
44
+ }
45
+
46
+ impl Intrinsic {
47
+ /// Runs the native version. Returns `None` if the arguments are outside what it reproduces
48
+ /// exactly, leaving the call to the interpreter.
49
+ pub(super) fn call<'ast>(self, arguments: &[Value<'ast>]) -> Option<Value<'ast>> {
50
+ match self {
51
+ Self::Blake3Compress => {
52
+ let [
53
+ Value::Array(cv),
54
+ Value::Array(block),
55
+ counter,
56
+ length,
57
+ flags,
58
+ ] = arguments
59
+ else {
60
+ return None;
61
+ };
62
+ let output = {
63
+ let (cv, block) = (cv.borrow(), block.borrow());
64
+ compress(
65
+ &words(cv.get(..8)?)?,
66
+ &words(block.get(..16)?)?,
67
+ Counter::of(counter.as_number()?),
68
+ js::int32(length.as_number()?) as u32,
69
+ js::int32(flags.as_number()?) as u32,
70
+ )
71
+ };
72
+ let mut cv = cv.borrow_mut();
73
+ for (slot, word) in cv.iter_mut().zip(output) {
74
+ // The script stores the results of `^`, which are signed.
75
+ *slot = Value::Number(f64::from(word as i32));
76
+ }
77
+ Some(Value::Undefined)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ impl Value<'_> {
84
+ fn as_number(&self) -> Option<f64> {
85
+ match self {
86
+ Self::Number(number) => Some(*number),
87
+ _ => None,
88
+ }
89
+ }
90
+ }
91
+
92
+ /// Converts state or message words. The script first adds three words before truncating, which
93
+ /// matches wrapping addition only while the sum is exact, so larger or fractional words are
94
+ /// rejected.
95
+ fn words<const N: usize>(values: &[Value<'_>]) -> Option<[u32; N]> {
96
+ let mut words = [0; N];
97
+ for (word, value) in words.iter_mut().zip(values) {
98
+ let number = value.as_number()?;
99
+ if number.fract() != 0.0 || number.abs() >= 2f64.powi(51) {
100
+ return None;
101
+ }
102
+ *word = js::int32(number) as u32;
103
+ }
104
+ Some(words)
105
+ }
106
+
107
+ /// A block counter split as the script splits it: `counter | 0` and
108
+ /// `counter / 4294967296 | 0`.
109
+ #[derive(Clone, Copy)]
110
+ struct Counter {
111
+ low: u32,
112
+ high: u32,
113
+ }
114
+
115
+ impl Counter {
116
+ fn of(counter: f64) -> Self {
117
+ Self {
118
+ low: js::int32(counter) as u32,
119
+ high: js::int32(counter / 4_294_967_296.0) as u32,
120
+ }
121
+ }
122
+ }
123
+
124
+ const IV: [u32; 8] = [
125
+ 0x6a09_e667,
126
+ 0xbb67_ae85,
127
+ 0x3c6e_f372,
128
+ 0xa54f_f53a,
129
+ 0x510e_527f,
130
+ 0x9b05_688c,
131
+ 0x1f83_d9ab,
132
+ 0x5be0_cd19,
133
+ ];
134
+
135
+ /// Message word order of each round after the first, relative to the previous round.
136
+ const PERMUTATION: [usize; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
137
+
138
+ /// The BLAKE3 compression function, truncated to the chaining value.
139
+ fn compress(
140
+ cv: &[u32; 8],
141
+ block: &[u32; 16],
142
+ counter: Counter,
143
+ length: u32,
144
+ flags: u32,
145
+ ) -> [u32; 8] {
146
+ let mut state = [0; 16];
147
+ state[..8].copy_from_slice(cv);
148
+ state[8..12].copy_from_slice(&IV[..4]);
149
+ state[12..].copy_from_slice(&[counter.low, counter.high, length, flags]);
150
+ let mut message = *block;
151
+ for round in 0..7 {
152
+ if round > 0 {
153
+ message = PERMUTATION.map(|index| message[index]);
154
+ }
155
+ mix(&mut state, [0, 4, 8, 12], message[0], message[1]);
156
+ mix(&mut state, [1, 5, 9, 13], message[2], message[3]);
157
+ mix(&mut state, [2, 6, 10, 14], message[4], message[5]);
158
+ mix(&mut state, [3, 7, 11, 15], message[6], message[7]);
159
+ mix(&mut state, [0, 5, 10, 15], message[8], message[9]);
160
+ mix(&mut state, [1, 6, 11, 12], message[10], message[11]);
161
+ mix(&mut state, [2, 7, 8, 13], message[12], message[13]);
162
+ mix(&mut state, [3, 4, 9, 14], message[14], message[15]);
163
+ }
164
+ std::array::from_fn(|index| state[index] ^ state[index + 8])
165
+ }
166
+
167
+ /// The BLAKE3 quarter-round `G` on state words `a`, `b`, `c` and `d`.
168
+ fn mix(state: &mut [u32; 16], [a, b, c, d]: [usize; 4], x: u32, y: u32) {
169
+ state[a] = state[a].wrapping_add(state[b]).wrapping_add(x);
170
+ state[d] = (state[d] ^ state[a]).rotate_right(16);
171
+ state[c] = state[c].wrapping_add(state[d]);
172
+ state[b] = (state[b] ^ state[c]).rotate_right(12);
173
+ state[a] = state[a].wrapping_add(state[b]).wrapping_add(y);
174
+ state[d] = (state[d] ^ state[a]).rotate_right(8);
175
+ state[c] = state[c].wrapping_add(state[d]);
176
+ state[b] = (state[b] ^ state[c]).rotate_right(7);
177
+ }
178
+
179
+ /// Whether `function` contains the numeric literal `number`.
180
+ fn mentions(function: &Function, number: u32) -> bool {
181
+ struct Finder {
182
+ number: f64,
183
+ found: bool,
184
+ }
185
+
186
+ impl Visit for Finder {
187
+ fn visit_lit(&mut self, literal: &Lit) {
188
+ self.found |= matches!(literal, Lit::Num(found) if found.value == self.number);
189
+ }
190
+ }
191
+
192
+ let mut finder = Finder {
193
+ number: f64::from(number),
194
+ found: false,
195
+ };
196
+ function.visit_with(&mut finder);
197
+ finder.found
198
+ }
199
+
200
+ /// Whether interpreting `function` gives the result of [`Intrinsic::Blake3Compress`] on every
201
+ /// test input, returning nothing and recording no effects.
202
+ fn compresses_like_native<'ast>(session: &Session<'ast>, function: &'ast Function) -> bool {
203
+ TEST_INPUTS.iter().all(|input| {
204
+ let native = input.arguments();
205
+ if Intrinsic::Blake3Compress.call(&native).is_none() {
206
+ return false;
207
+ }
208
+ // Clones share the arrays, so `interpreted` shows what the script wrote.
209
+ let interpreted = input.arguments();
210
+ let mut run = Run::new(session, &[], Vec::new(), Some(Copies::default()));
211
+ let callable = Callable::Script {
212
+ name: None,
213
+ function,
214
+ scope: None,
215
+ };
216
+ let outcome = run.invoke(&callable, Value::Undefined, interpreted.clone());
217
+ let same = matches!(outcome, Ok(Value::Undefined))
218
+ && run.effects.is_empty()
219
+ && numbers(&interpreted[0]) == numbers(&native[0]);
220
+ for scope in run.scopes() {
221
+ scope.borrow_mut().variables.clear();
222
+ }
223
+ same
224
+ })
225
+ }
226
+
227
+ /// Arguments for [`Intrinsic::Blake3Compress`], covering signed and unsigned words and a counter
228
+ /// above 32 bits.
229
+ struct TestInput {
230
+ cv: [f64; 8],
231
+ block: [f64; 16],
232
+ counter: f64,
233
+ length: f64,
234
+ flags: f64,
235
+ }
236
+
237
+ const TEST_INPUTS: [TestInput; 2] = [
238
+ TestInput {
239
+ cv: [
240
+ 3_144_134_277.0,
241
+ -1_521_486_534.0,
242
+ 7.0,
243
+ 4_294_967_295.0,
244
+ 1_359_893_119.0,
245
+ 0.0,
246
+ -1.0,
247
+ 2_147_483_648.0,
248
+ ],
249
+ block: [
250
+ 1.0,
251
+ 2.0,
252
+ 3.0,
253
+ 255.0,
254
+ 65_536.0,
255
+ -2.0,
256
+ 1_779_033_703.0,
257
+ 3_000_000_000.0,
258
+ 12.0,
259
+ -2_147_483_648.0,
260
+ 99.0,
261
+ 100_000.0,
262
+ 31.0,
263
+ 4_000_000_000.0,
264
+ 5.0,
265
+ 6.0,
266
+ ],
267
+ counter: 4_294_967_301.0,
268
+ length: 64.0,
269
+ flags: 11.0,
270
+ },
271
+ TestInput {
272
+ cv: [
273
+ 1_779_033_703.0,
274
+ 3_144_134_277.0,
275
+ 1_013_904_242.0,
276
+ 2_773_480_762.0,
277
+ 1_359_893_119.0,
278
+ 2_600_822_924.0,
279
+ 528_734_635.0,
280
+ 1_541_459_225.0,
281
+ ],
282
+ block: [
283
+ 827_605_328.0,
284
+ 17.0,
285
+ 0.0,
286
+ 0.0,
287
+ -5.0,
288
+ 0.0,
289
+ 0.0,
290
+ 1_234.0,
291
+ 0.0,
292
+ 0.0,
293
+ 0.0,
294
+ 0.0,
295
+ 0.0,
296
+ 0.0,
297
+ 0.0,
298
+ 0.0,
299
+ ],
300
+ counter: 3.0,
301
+ length: 13.0,
302
+ flags: 1.0,
303
+ },
304
+ ];
305
+
306
+ impl TestInput {
307
+ fn arguments<'ast>(&self) -> Vec<Value<'ast>> {
308
+ let array =
309
+ |numbers: &[f64]| Value::array(numbers.iter().copied().map(Value::Number).collect());
310
+ vec![
311
+ array(&self.cv),
312
+ array(&self.block),
313
+ Value::Number(self.counter),
314
+ Value::Number(self.length),
315
+ Value::Number(self.flags),
316
+ ]
317
+ }
318
+ }
319
+
320
+ fn numbers(value: &Value<'_>) -> Option<Vec<f64>> {
321
+ let Value::Array(elements) = value else {
322
+ return None;
323
+ };
324
+ elements.borrow().iter().map(Value::as_number).collect()
325
+ }
326
+
327
+ #[cfg(test)]
328
+ mod tests {
329
+ use super::*;
330
+
331
+ /// BLAKE3 of the empty input is one compression of an empty block with the chunk-start,
332
+ /// chunk-end and root flags.
333
+ #[test]
334
+ fn compresses_like_blake3() {
335
+ let digest = compress(&IV, &[0; 16], Counter::of(0.0), 0, 1 | 2 | 8);
336
+ let bytes: Vec<u8> = digest.iter().flat_map(|word| word.to_le_bytes()).collect();
337
+ let hex: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
338
+ assert_eq!(
339
+ hex,
340
+ "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
341
+ );
342
+ }
343
+
344
+ #[test]
345
+ fn rejects_inexact_words() {
346
+ let mut arguments = TEST_INPUTS[0].arguments();
347
+ arguments[1] = Value::array(vec![Value::Number(0.5); 16]);
348
+ assert!(Intrinsic::Blake3Compress.call(&arguments).is_none());
349
+ }
350
+ }
@@ -1,6 +1,7 @@
1
1
  //! Compiles JavaScript into plans run by [`super::evaluate`]. Slot plans store function locals
2
- //! in numbered slots and cannot support closures capturing those locals. Scoped plans resolve
3
- //! names through scopes. Unsupported syntax becomes a node that stops evaluation if reached.
2
+ //! in numbered slots, except locals that nested functions mention, which stay in the call's
3
+ //! scope for those functions to capture. Scoped plans resolve all names through scopes.
4
+ //! Unsupported syntax becomes a node that stops evaluation if reached.
4
5
 
5
6
  #[cfg(test)]
6
7
  use std::cell::Cell;
@@ -8,15 +9,17 @@ use std::cell::RefCell;
8
9
  use std::ptr;
9
10
  use std::rc::Rc;
10
11
 
12
+ use rustc_hash::FxHashSet as HashSet;
11
13
  use swc_core::atoms::{Atom, atom};
12
14
  use swc_core::ecma::ast::{
13
- ArrowExpr, AssignTarget, BinaryOp, BlockStmtOrExpr, Callee, CatchClause, Class, Decl,
14
- DoWhileStmt, Expr, ExprOrSpread, FnDecl, Function, Ident, Lit, MemberExpr, MemberProp, Pat,
15
- Prop, PropName, PropOrSpread, SimpleAssignTarget, Stmt, UnaryOp, UpdateOp, VarDecl,
15
+ ArrowExpr, AssignTarget, BinaryOp, BlockStmt, BlockStmtOrExpr, Callee, CatchClause, Class,
16
+ Decl, DoWhileStmt, Expr, ExprOrSpread, FnDecl, Function, Ident, Lit, MemberExpr, MemberProp,
17
+ Pat, Prop, PropName, PropOrSpread, SimpleAssignTarget, Stmt, UnaryOp, UpdateOp, VarDecl,
16
18
  VarDeclKind, VarDeclOrExpr, VarDeclarator, WhileStmt,
17
19
  };
18
20
  use swc_core::ecma::visit::{Visit, VisitWith};
19
21
 
22
+ use super::compile::{self, Code};
20
23
  use super::semantics;
21
24
  use crate::vm::HashMap;
22
25
  use crate::vm::value::Value;
@@ -65,28 +68,35 @@ impl<'ast> Plans<'ast> {
65
68
  }
66
69
 
67
70
  pub(super) struct FunctionPlan<'ast> {
68
- pub(super) parameters: Vec<usize>,
71
+ /// Where each parameter is bound: a slot, or the call's scope if a nested function
72
+ /// mentions it.
73
+ pub(super) parameters: Vec<Name<'ast>>,
69
74
  /// The slot of `arguments`, if the body reads it.
70
75
  pub(super) arguments: Option<usize>,
71
76
  pub(super) locals: usize,
72
- pub(super) body: Vec<Statement<'ast>>,
77
+ /// The body, compiled into closures.
78
+ pub(super) code: Vec<Code<'ast>>,
73
79
  }
74
80
 
75
81
  impl<'ast> FunctionPlan<'ast> {
76
82
  /// Attempts to compile a slot plan. Returns `None` to request a scoped plan for unsupported
77
83
  /// syntax, parenthesized callees, computed object keys, logical assignments, parameter
78
- /// patterns, variables named `arguments`, or nested functions capturing locals.
84
+ /// patterns, variables named `arguments`, or arrows reading `arguments`.
79
85
  pub(super) fn compile(function: &'ast Function) -> Option<Self> {
80
- let mut compiler = PlanCompiler::default();
86
+ let body = function.body.as_ref()?;
87
+ let mut compiler = PlanCompiler {
88
+ captured: captured(body),
89
+ ..PlanCompiler::default()
90
+ };
81
91
  let mut parameters = Vec::with_capacity(function.params.len());
82
92
  for parameter in &function.params {
83
93
  let name = &parameter.pat.as_ident()?.sym;
84
94
  if name == "arguments" {
85
95
  return None;
86
96
  }
87
- parameters.push(compiler.slot(name));
97
+ compiler.local(name);
98
+ parameters.push(compiler.name(name));
88
99
  }
89
- let body = function.body.as_ref()?;
90
100
  body.visit_with(&mut compiler);
91
101
  let body = compiler.body(&body.stmts);
92
102
  if compiler.rejected {
@@ -96,7 +106,7 @@ impl<'ast> FunctionPlan<'ast> {
96
106
  parameters,
97
107
  arguments: compiler.locals.get(&atom!("arguments")).copied(),
98
108
  locals: compiler.locals.len(),
99
- body,
109
+ code: compile::statements(&body),
100
110
  })
101
111
  }
102
112
  }
@@ -126,6 +136,8 @@ pub(super) fn scoped_arrow(arrow: &ArrowExpr) -> Vec<Statement<'_>> {
126
136
  struct PlanCompiler {
127
137
  /// Slots of a slot plan's locals; empty for scoped plans.
128
138
  locals: HashMap<Atom, usize>,
139
+ /// Names nested functions mention. Locals with these names stay in the call's scope.
140
+ captured: HashSet<Atom>,
129
141
  /// Whether compilation needs a scoped plan. Ignored when compiling scoped plans.
130
142
  rejected: bool,
131
143
  }
@@ -133,32 +145,31 @@ struct PlanCompiler {
133
145
  impl Visit for PlanCompiler {
134
146
  fn visit_var_declarator(&mut self, declaration: &VarDeclarator) {
135
147
  if let Pat::Ident(name) = &declaration.name {
136
- self.slot(&name.sym);
148
+ self.local(&name.sym);
137
149
  }
138
150
  declaration.visit_children_with(self);
139
151
  }
140
152
 
141
153
  fn visit_ident(&mut self, name: &Ident) {
142
154
  if name.sym == "arguments" {
143
- self.slot(&name.sym);
155
+ self.local(&name.sym);
144
156
  }
145
157
  }
146
158
 
147
159
  /// Treat a nested function declaration as a local binding in the enclosing function.
148
160
  fn visit_fn_decl(&mut self, declaration: &FnDecl) {
149
- self.slot(&declaration.ident.sym);
161
+ self.local(&declaration.ident.sym);
150
162
  }
151
163
 
152
164
  /// Treat a `catch` parameter as a function local in the execution plan.
153
165
  fn visit_catch_clause(&mut self, clause: &CatchClause) {
154
166
  if let Some(Pat::Ident(name)) = &clause.param {
155
- self.slot(&name.sym);
167
+ self.local(&name.sym);
156
168
  }
157
169
  clause.visit_children_with(self);
158
170
  }
159
171
 
160
- // Compile a nested function only if it captures no outer locals (see
161
- // [`PlanCompiler::closes_over_nothing`]). Its own locals remain separate.
172
+ // A nested function's own locals remain separate.
162
173
  fn visit_function(&mut self, _: &Function) {}
163
174
  fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}
164
175
  fn visit_class(&mut self, _: &Class) {}
@@ -166,12 +177,36 @@ impl Visit for PlanCompiler {
166
177
 
167
178
  /// Every name a nested function mentions, at any depth.
168
179
  #[derive(Default)]
169
- struct Names(Vec<Atom>);
180
+ struct Names(HashSet<Atom>);
170
181
 
171
182
  impl Visit for Names {
172
183
  fn visit_ident(&mut self, name: &Ident) {
173
- self.0.push(name.sym.clone());
184
+ self.0.insert(name.sym.clone());
185
+ }
186
+ }
187
+
188
+ /// The names that functions, arrows, and classes nested in `body` mention, except `arguments`,
189
+ /// which functions shadow and arrows reading it are rejected for.
190
+ fn captured(body: &BlockStmt) -> HashSet<Atom> {
191
+ struct Nested(Names);
192
+
193
+ impl Visit for Nested {
194
+ fn visit_function(&mut self, function: &Function) {
195
+ function.visit_with(&mut self.0);
196
+ }
197
+ fn visit_arrow_expr(&mut self, arrow: &ArrowExpr) {
198
+ arrow.visit_with(&mut self.0);
199
+ }
200
+ fn visit_class(&mut self, class: &Class) {
201
+ class.visit_with(&mut self.0);
202
+ }
174
203
  }
204
+
205
+ let mut nested = Nested(Names::default());
206
+ body.visit_with(&mut nested);
207
+ let Nested(Names(mut names)) = nested;
208
+ names.remove(&atom!("arguments"));
209
+ names
175
210
  }
176
211
 
177
212
  pub(super) struct Case<'ast> {
@@ -291,31 +326,22 @@ impl<'ast> Name<'ast> {
291
326
  }
292
327
 
293
328
  impl PlanCompiler {
294
- fn slot(&mut self, name: &Atom) -> usize {
295
- let next = self.locals.len();
296
- *self.locals.entry(name.clone()).or_insert(next)
297
- }
298
-
299
- /// Checks that a nested function captures no locals from the function being compiled.
300
- /// Slot plans do not put these locals in the scope chain. Ordinary functions shadow
301
- /// `arguments`; arrows inherit it and must not read it.
302
- fn closes_over_nothing(&self, nested: &impl VisitWith<Names>, arrow: bool) -> bool {
303
- let mut names = Names::default();
304
- nested.visit_with(&mut names);
305
- names
306
- .0
307
- .iter()
308
- .all(|name| !self.locals.contains_key(name) && !(arrow && name == "arguments"))
329
+ /// Gives local `name` a slot unless a nested function mentions it.
330
+ fn local(&mut self, name: &Atom) {
331
+ if !self.captured.contains(name) {
332
+ let next = self.locals.len();
333
+ self.locals.entry(name.clone()).or_insert(next);
334
+ }
309
335
  }
310
336
 
311
- /// Plans a function body, binding function declarations before executing statements.
312
- /// Declarations beside the VM class are planned as statements and resolved as globals.
337
+ /// Plans a function body or block, binding its function declarations before executing
338
+ /// statements, as JavaScript hoists them to the start of the body or block. Declarations
339
+ /// beside the VM class are planned as statements and resolved as globals.
313
340
  fn body<'a>(&mut self, statements: &'a [Stmt]) -> Vec<Statement<'a>> {
314
341
  let mut plan = Vec::with_capacity(statements.len());
315
342
  for statement in statements {
316
343
  if let Stmt::Decl(Decl::Fn(declaration)) = statement {
317
344
  let name = &declaration.ident.sym;
318
- self.rejected |= !self.closes_over_nothing(&*declaration.function, false);
319
345
  plan.push(Statement::Declare(vec![Declaration {
320
346
  name: self.name(name),
321
347
  init: Some(Expression::Function(Some(name), &declaration.function)),
@@ -341,7 +367,6 @@ impl PlanCompiler {
341
367
  // Declarations do not bind their names; the names resolve as globals.
342
368
  Stmt::Decl(Decl::Fn(_) | Decl::Class(_)) | Stmt::Empty(_) => Statement::Empty,
343
369
  Stmt::Decl(_) => Statement::Expression(self.unsupported("a declaration")),
344
- // A block shares its function's scope, so its function declarations bind at its start.
345
370
  Stmt::Block(block) => Statement::Block(self.body(&block.stmts)),
346
371
  Stmt::If(branch) => Statement::If(
347
372
  self.expression(&branch.test),
@@ -406,13 +431,13 @@ impl PlanCompiler {
406
431
  }
407
432
  };
408
433
  Statement::Try {
409
- block: self.statements(&attempt.block.stmts),
434
+ block: self.body(&attempt.block.stmts),
410
435
  parameter,
411
- handler: handler.map(|handler| self.statements(&handler.body.stmts)),
436
+ handler: handler.map(|handler| self.body(&handler.body.stmts)),
412
437
  finalizer: attempt
413
438
  .finalizer
414
439
  .as_ref()
415
- .map(|body| self.statements(&body.stmts)),
440
+ .map(|body| self.body(&body.stmts)),
416
441
  }
417
442
  }
418
443
  Stmt::ForIn(_) | Stmt::ForOf(_) => {
@@ -665,12 +690,14 @@ impl PlanCompiler {
665
690
  .collect(),
666
691
  ),
667
692
  Expr::Fn(function) => {
668
- self.rejected |= !self.closes_over_nothing(&function.function, false);
669
693
  let name = function.ident.as_ref().map(|ident| &ident.sym);
670
694
  Expression::Function(name, &function.function)
671
695
  }
696
+ // Arrows inherit `arguments`, which slot plans keep out of scopes.
672
697
  Expr::Arrow(arrow) => {
673
- self.rejected |= !self.closes_over_nothing(arrow, true);
698
+ let mut names = Names::default();
699
+ arrow.visit_with(&mut names);
700
+ self.rejected |= names.0.contains(&atom!("arguments"));
674
701
  Expression::Arrow(arrow)
675
702
  }
676
703
  expression => self.unsupported(match expression {