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
@@ -3,8 +3,7 @@
3
3
  use std::cell::RefCell;
4
4
  use std::rc::Rc;
5
5
 
6
- use swc_core::atoms::Atom;
7
-
6
+ use super::frozen::{self, Frozen};
8
7
  use crate::vm::HashMap;
9
8
  use crate::vm::value::{Callable, Env, Scope, Value};
10
9
 
@@ -17,135 +16,248 @@ pub(super) struct Copies<'ast> {
17
16
  }
18
17
 
19
18
  /// Deep-copies mutable state in `value`. Uses original addresses in `copies` to preserve shared
20
- /// references and avoid copying twice.
21
- pub(super) fn copy<'a>(value: &Value<'a>, copies: &mut Copies<'a>) -> Value<'a> {
22
- let address = match value {
23
- Value::Array(elements) => Rc::as_ptr(elements).addr(),
24
- Value::Object(properties) => Rc::as_ptr(properties).addr(),
25
- Value::Set(members) => Rc::as_ptr(members).addr(),
26
- Value::Map(entries) => Rc::as_ptr(entries).addr(),
27
- Value::Function(callable) => Rc::as_ptr(callable).addr(),
28
- _ => return value.clone(),
29
- };
30
- if let Some(copy) = copies.values.get(&address) {
31
- return copy.clone();
32
- }
33
- match value {
34
- Value::Array(elements) => copy_elements(elements, address, copies, Value::Array, copy),
35
- Value::Set(members) => copy_elements(members, address, copies, Value::Set, copy),
36
- Value::Map(entries) => copy_elements(
37
- entries,
38
- address,
39
- copies,
40
- Value::Map,
41
- |(key, value), copies| (copy(key, copies), copy(value, copies)),
42
- ),
43
- Value::Object(properties) => copy_elements(
44
- properties,
45
- address,
46
- copies,
47
- Value::Object,
48
- |(name, value), copies| (name.clone(), copy(value, copies)),
49
- ),
50
- Value::Function(callable) => {
51
- let copied = match &**callable {
52
- Callable::Script {
53
- name,
54
- function,
55
- scope: Some(scope),
56
- } => match copy_scope(scope, copies) {
57
- // The function captures no mutable state.
58
- None => return value.clone(),
59
- scope => Callable::Script {
60
- name: name.clone(),
19
+ /// references and avoid copying twice. Objects in `frozen` stay shared.
20
+ pub(super) fn copy<'a>(
21
+ value: &Value<'a>,
22
+ copies: &mut Copies<'a>,
23
+ frozen: Option<&Frozen>,
24
+ ) -> Value<'a> {
25
+ Isolation { copies, frozen }.copy(value)
26
+ }
27
+
28
+ /// The deep copy shared by [`copy`] and [`Fork`], which differ in the scopes they copy.
29
+ trait Copier<'ast>: Sized {
30
+ /// Shared session objects, which are not copied.
31
+ fn frozen(&self) -> Option<&Frozen>;
32
+ /// Copies by original address.
33
+ fn values(&mut self) -> &mut HashMap<usize, Value<'ast>>;
34
+ /// Copies a function's captured scope, or returns `None` if it needs no copy.
35
+ fn captured(&mut self, scope: &Scope<'ast>) -> Option<Scope<'ast>>;
36
+
37
+ fn copy(&mut self, value: &Value<'ast>) -> Value<'ast> {
38
+ if let Some(frozen) = self.frozen()
39
+ && let Some(address) = frozen::address(value)
40
+ && frozen.contains(address)
41
+ {
42
+ return value.clone();
43
+ }
44
+ let address = match (value, frozen::address(value)) {
45
+ (Value::Function(callable), _) => Rc::as_ptr(callable).addr(),
46
+ (_, Some(address)) => address,
47
+ (_, None) => return value.clone(),
48
+ };
49
+ if let Some(copy) = self.values().get(&address) {
50
+ return copy.clone();
51
+ }
52
+ match value {
53
+ Value::Array(elements) => self.elements(elements, address, Value::Array, Self::copy),
54
+ Value::Set(members) => self.elements(members, address, Value::Set, Self::copy),
55
+ Value::Map(entries) => {
56
+ self.elements(entries, address, Value::Map, |copier, (key, value)| {
57
+ (copier.copy(key), copier.copy(value))
58
+ })
59
+ }
60
+ Value::Object(properties) => self.elements(
61
+ properties,
62
+ address,
63
+ Value::Object,
64
+ |copier, (name, value)| (name.clone(), copier.copy(value)),
65
+ ),
66
+ Value::Function(callable) => {
67
+ let copied = match &**callable {
68
+ Callable::Script {
69
+ name,
61
70
  function,
62
- scope,
71
+ scope: Some(scope),
72
+ } => match self.captured(scope) {
73
+ // The function captures no mutable state.
74
+ None => return value.clone(),
75
+ scope => Callable::Script {
76
+ name: name.clone(),
77
+ function,
78
+ scope,
79
+ },
80
+ },
81
+ Callable::Script { scope: None, .. }
82
+ | Callable::Builtin(_)
83
+ | Callable::Standard(_) => return value.clone(),
84
+ Callable::Arrow { arrow, this, scope } => Callable::Arrow {
85
+ arrow,
86
+ this: self.copy(this),
87
+ scope: scope.as_ref().and_then(|scope| self.captured(scope)),
88
+ },
89
+ Callable::Call(target) => Callable::Call(self.copy(target)),
90
+ Callable::Apply(target) => Callable::Apply(self.copy(target)),
91
+ Callable::Bind(target) => Callable::Bind(self.copy(target)),
92
+ Callable::Bound {
93
+ target,
94
+ this,
95
+ arguments,
96
+ } => Callable::Bound {
97
+ target: self.copy(target),
98
+ this: self.copy(this),
99
+ arguments: arguments
100
+ .iter()
101
+ .map(|argument| self.copy(argument))
102
+ .collect(),
63
103
  },
64
- },
65
- Callable::Script { scope: None, .. }
66
- | Callable::Builtin(_)
67
- | Callable::Standard(_) => return value.clone(),
68
- Callable::Arrow { arrow, this, scope } => Callable::Arrow {
69
- arrow,
70
- this: copy(this, copies),
71
- scope: scope.as_ref().and_then(|scope| copy_scope(scope, copies)),
72
- },
73
- Callable::Call(target) => Callable::Call(copy(target, copies)),
74
- Callable::Apply(target) => Callable::Apply(copy(target, copies)),
75
- Callable::Bind(target) => Callable::Bind(copy(target, copies)),
76
- Callable::Bound {
77
- target,
78
- this,
79
- arguments,
80
- } => Callable::Bound {
81
- target: copy(target, copies),
82
- this: copy(this, copies),
83
- arguments: arguments
84
- .iter()
85
- .map(|argument| copy(argument, copies))
86
- .collect(),
87
- },
88
- };
89
- // A captured value may refer back to this function and copy it first.
90
- if let Some(copy) = copies.values.get(&address) {
91
- return copy.clone();
104
+ };
105
+ // A captured value may refer back to this function and copy it first.
106
+ if let Some(copy) = self.values().get(&address) {
107
+ return copy.clone();
108
+ }
109
+ let copied = Value::function(copied);
110
+ self.values().insert(address, copied.clone());
111
+ copied
92
112
  }
93
- let copied = Value::function(copied);
94
- copies.values.insert(address, copied.clone());
95
- copied
113
+ _ => unreachable!("only objects have addresses"),
96
114
  }
97
- _ => unreachable!("only objects have addresses"),
115
+ }
116
+
117
+ /// Copies the elements of an array, set, map, or object with `copy_element`.
118
+ fn elements<T>(
119
+ &mut self,
120
+ elements: &RefCell<Vec<T>>,
121
+ address: usize,
122
+ wrap: fn(Rc<RefCell<Vec<T>>>) -> Value<'ast>,
123
+ copy_element: impl Fn(&mut Self, &T) -> T,
124
+ ) -> Value<'ast> {
125
+ // Cache before copying elements because they may refer back to this object.
126
+ let copied = Rc::new(RefCell::new(Vec::new()));
127
+ self.values().insert(address, wrap(copied.clone()));
128
+ let elements: Vec<T> = elements
129
+ .borrow()
130
+ .iter()
131
+ .map(|element| copy_element(self, element))
132
+ .collect();
133
+ *copied.borrow_mut() = elements;
134
+ wrap(copied)
98
135
  }
99
136
  }
100
137
 
101
- /// Copies the elements of an array, set, or object with `copy_element`.
102
- fn copy_elements<'a, T>(
103
- elements: &RefCell<Vec<T>>,
104
- address: usize,
105
- copies: &mut Copies<'a>,
106
- wrap: fn(Rc<RefCell<Vec<T>>>) -> Value<'a>,
107
- copy_element: impl Fn(&T, &mut Copies<'a>) -> T,
108
- ) -> Value<'a> {
109
- // Cache before copying elements because they may refer back to this object.
110
- let copied = Rc::new(RefCell::new(Vec::new()));
111
- copies.values.insert(address, wrap(copied.clone()));
112
- let elements: Vec<T> = elements
113
- .borrow()
114
- .iter()
115
- .map(|element| copy_element(element, copies))
116
- .collect();
117
- *copied.borrow_mut() = elements;
118
- wrap(copied)
138
+ /// The state of a [`copy`].
139
+ struct Isolation<'copies, 'frozen, 'ast> {
140
+ copies: &'copies mut Copies<'ast>,
141
+ frozen: Option<&'frozen Frozen>,
119
142
  }
120
143
 
121
- /// Copies `scope` and its parents. Omits chains with no variables: completed session calls
122
- /// cannot add variables to those scopes.
123
- fn copy_scope<'a>(scope: &Scope<'a>, copies: &mut Copies<'a>) -> Option<Scope<'a>> {
124
- let address = Rc::as_ptr(scope).addr();
125
- if let Some(copy) = copies.scopes.get(&address) {
126
- return copy.clone();
144
+ impl<'ast> Copier<'ast> for Isolation<'_, '_, 'ast> {
145
+ fn frozen(&self) -> Option<&Frozen> {
146
+ self.frozen
147
+ }
148
+
149
+ fn values(&mut self) -> &mut HashMap<usize, Value<'ast>> {
150
+ &mut self.copies.values
127
151
  }
128
- let original = scope.borrow();
129
- if original.variables.is_empty() {
130
- let copied = original
152
+
153
+ /// Copies `scope` and its parents. Omits chains with no variables: completed session calls
154
+ /// cannot add variables to those scopes.
155
+ fn captured(&mut self, scope: &Scope<'ast>) -> Option<Scope<'ast>> {
156
+ let address = Rc::as_ptr(scope).addr();
157
+ if let Some(copy) = self.copies.scopes.get(&address) {
158
+ return copy.clone();
159
+ }
160
+ let original = scope.borrow();
161
+ if original.variables.is_empty() {
162
+ let copied = original
163
+ .outer
164
+ .as_ref()
165
+ .and_then(|outer| self.captured(outer));
166
+ self.copies.scopes.insert(address, copied.clone());
167
+ return copied;
168
+ }
169
+ // Cache before copying variables because they may hold functions capturing this scope.
170
+ let copied = Env::scope(HashMap::default(), None);
171
+ self.copies.scopes.insert(address, Some(copied.clone()));
172
+ let outer = original
131
173
  .outer
132
174
  .as_ref()
133
- .and_then(|outer| copy_scope(outer, copies));
134
- copies.scopes.insert(address, copied.clone());
135
- return copied;
175
+ .and_then(|outer| self.captured(outer));
176
+ let variables = original
177
+ .variables
178
+ .iter()
179
+ .map(|(name, value)| (name.clone(), self.copy(value)))
180
+ .collect();
181
+ *copied.borrow_mut() = Env { variables, outer };
182
+ Some(copied)
183
+ }
184
+ }
185
+
186
+ /// Deep-copies a run's state for another path to resume from (see
187
+ /// [`super::run::Snapshot`]). Unlike [`copy`], it copies every scope, since the run may still
188
+ /// add variables to them, and it clones maps before copying their values so they keep the
189
+ /// original's layout and iteration order.
190
+ pub(super) struct Fork<'frozen, 'ast> {
191
+ frozen: Option<&'frozen Frozen>,
192
+ values: HashMap<usize, Value<'ast>>,
193
+ scopes: HashMap<usize, Scope<'ast>>,
194
+ /// Every scope copied, for breaking cycles when the copy is dropped.
195
+ pub(super) copied: Vec<Scope<'ast>>,
196
+ }
197
+
198
+ impl<'ast> Copier<'ast> for Fork<'_, 'ast> {
199
+ fn frozen(&self) -> Option<&Frozen> {
200
+ self.frozen
201
+ }
202
+
203
+ fn values(&mut self) -> &mut HashMap<usize, Value<'ast>> {
204
+ &mut self.values
205
+ }
206
+
207
+ fn captured(&mut self, scope: &Scope<'ast>) -> Option<Scope<'ast>> {
208
+ Some(self.scope(scope))
209
+ }
210
+ }
211
+
212
+ impl<'frozen, 'ast> Fork<'frozen, 'ast> {
213
+ pub(super) fn new(frozen: Option<&'frozen Frozen>) -> Self {
214
+ Self {
215
+ frozen,
216
+ values: HashMap::default(),
217
+ scopes: HashMap::default(),
218
+ copied: Vec::new(),
219
+ }
220
+ }
221
+
222
+ pub(super) fn value(&mut self, value: &Value<'ast>) -> Value<'ast> {
223
+ self.copy(value)
224
+ }
225
+
226
+ pub(super) fn scope(&mut self, scope: &Scope<'ast>) -> Scope<'ast> {
227
+ let address = Rc::as_ptr(scope).addr();
228
+ if let Some(copy) = self.scopes.get(&address) {
229
+ return copy.clone();
230
+ }
231
+ // Cache before copying variables because they may hold functions capturing this scope.
232
+ let copied = Env::scope(HashMap::default(), None);
233
+ self.scopes.insert(address, copied.clone());
234
+ self.copied.push(copied.clone());
235
+ let original = scope.borrow();
236
+ let outer = original.outer.as_ref().map(|outer| self.scope(outer));
237
+ let variables = self.map(&original.variables);
238
+ *copied.borrow_mut() = Env { variables, outer };
239
+ copied
240
+ }
241
+
242
+ /// Copies a map's values, keeping its layout.
243
+ pub(super) fn map<K: Clone>(
244
+ &mut self,
245
+ map: &HashMap<K, Value<'ast>>,
246
+ ) -> HashMap<K, Value<'ast>> {
247
+ let mut copied = map.clone();
248
+ for value in copied.values_mut() {
249
+ *value = self.copy(value);
250
+ }
251
+ copied
252
+ }
253
+
254
+ /// Copies the run's [`Copies`], mapping session objects to copies of their run copies.
255
+ pub(super) fn copies(&mut self, copies: &Copies<'ast>) -> Copies<'ast> {
256
+ let values = self.map(&copies.values);
257
+ let mut scopes = copies.scopes.clone();
258
+ for scope in scopes.values_mut().flatten() {
259
+ *scope = self.scope(scope);
260
+ }
261
+ Copies { values, scopes }
136
262
  }
137
- // Cache before copying variables because they may hold functions capturing this scope.
138
- let copied = Env::scope(HashMap::default(), None);
139
- copies.scopes.insert(address, Some(copied.clone()));
140
- let outer = original
141
- .outer
142
- .as_ref()
143
- .and_then(|outer| copy_scope(outer, copies));
144
- let variables: HashMap<Atom, Value<'a>> = original
145
- .variables
146
- .iter()
147
- .map(|(name, value)| (name.clone(), copy(value, copies)))
148
- .collect();
149
- *copied.borrow_mut() = Env { variables, outer };
150
- Some(copied)
151
263
  }
@@ -4,17 +4,58 @@ use swc_core::atoms::Atom;
4
4
  use swc_core::ecma::ast::BinaryOp;
5
5
 
6
6
  use crate::js::ast;
7
- use crate::vm::extract;
8
7
  use crate::vm::interpret::plan::FunctionPlan;
9
8
  use crate::vm::interpret::run::Stop;
10
9
  use crate::vm::interpret::{Run, semantics};
11
- use crate::vm::output::Term;
12
- use crate::vm::session::Session;
10
+ use crate::vm::output::{Effect, Position, Term};
11
+ use crate::vm::session::{Layout, Session};
13
12
  use crate::vm::value::{Callable, Value};
13
+ use crate::vm::{disassemble, extract};
14
+
15
+ #[test]
16
+ fn concrete_register_stack_keeps_its_exception_continuation() {
17
+ let parsed = ast::parse_iframe_javascript("function f(stack) { stack.push([42, 9, [], 0]); }")
18
+ .expect("the source parses");
19
+ let mut session = Session::empty();
20
+ session.functions = extract::functions(&parsed.script.body);
21
+ session.layout = Layout {
22
+ pc: 240,
23
+ key: 241,
24
+ program: 242,
25
+ cells: 243,
26
+ };
27
+ session.initial = vec![(7, Value::array(Vec::new()))];
28
+ let mut run = Run::method(&session);
29
+ run.start_at(0, 0);
30
+ let stack = Value::array(Vec::new());
31
+ run.registers.insert(7, stack.clone());
32
+ let function = Callable::Script {
33
+ name: None,
34
+ function: session.functions[&Atom::from("f")],
35
+ scope: None,
36
+ };
37
+ let result = run.invoke(&function, Value::Undefined, vec![stack]);
38
+ let path = run.finish(result).expect("the call completes");
39
+ let catches = |frame: &Term| {
40
+ matches!(frame, Term::Array(values)
41
+ if disassemble::catch_frame(values) == Some(Position::new(42, 9)))
42
+ };
43
+ assert!(
44
+ path.effects.iter().any(|effect| matches!(effect,
45
+ Effect::Set(7, Term::Array(frames)) if frames.iter().any(catches))),
46
+ "the pushed catch frame is kept as a register effect: {:?}",
47
+ path.effects
48
+ );
49
+ }
14
50
 
15
51
  /// Compares results, exceptions, symbolic effects, branches, and global writes. Each execution
16
- /// has independent mutable state.
17
- fn compare(source: &str, arguments: Vec<Value<'static>>, decisions: Vec<bool>, slots: bool) {
52
+ /// has independent mutable state. Returns the shared outcome, starting with the result.
53
+ fn compare(
54
+ source: &str,
55
+ arguments: Vec<Value<'static>>,
56
+ decisions: Vec<bool>,
57
+ slots: bool,
58
+ ) -> String {
18
59
  // Leak the script text so arguments can be prepared before parsing it.
19
60
  let parsed: &_ = Box::leak(Box::new(ast::parse_iframe_javascript(source).unwrap()));
20
61
  let mut session = Session::empty();
@@ -56,7 +97,9 @@ fn compare(source: &str, arguments: Vec<Value<'static>>, decisions: Vec<bool>, s
56
97
  run.effects, run.branches.decisions
57
98
  )
58
99
  };
59
- assert_eq!(execute(false), execute(true), "{source}");
100
+ let outcome = execute(false);
101
+ assert_eq!(outcome, execute(true), "{source}");
102
+ outcome
60
103
  }
61
104
 
62
105
  #[test]
@@ -122,13 +165,37 @@ fn branches_and_loops_match() {
122
165
  }
123
166
  }
124
167
 
168
+ #[test]
169
+ fn functions_declared_in_blocks_are_bound_on_entry() {
170
+ // A function declared after a block's `return` is callable from the start of the block,
171
+ // including one that reads the enclosing function's locals.
172
+ for (source, slots, result) in [
173
+ (
174
+ "function f(a) { { return g(a) + 1; function g(x) { return x * 3; } } }",
175
+ true,
176
+ "return Const(Number(7.0))",
177
+ ),
178
+ (
179
+ "function f(a) { { var b = 4; return g(1); function g(x) { return a + b + x; } } }",
180
+ true,
181
+ "return Const(Number(7.0))",
182
+ ),
183
+ (
184
+ "function f(a) { try { return g(a); function g(x) { return x + 1; } } finally {} }",
185
+ true,
186
+ "return Const(Number(3.0))",
187
+ ),
188
+ ] {
189
+ let outcome = compare(source, vec![Value::Number(2.0)], vec![], slots);
190
+ assert!(outcome.starts_with(result), "{source}: {outcome}");
191
+ }
192
+ }
193
+
125
194
  #[test]
126
195
  fn rejected_functions_use_scoped_plans() {
127
196
  for source in [
128
197
  "function f(a) { return (function() { return a; })(); }",
129
- "function f(a) { var g = () => a; return g(); }",
130
198
  "function f(a) { var g = () => arguments[0]; return g(); }",
131
- "function f(a) { var g = function() { return a; }; return g(); }",
132
199
  "function f(arguments) { return arguments; }",
133
200
  "function f(a) { return {[a]: 1}; }",
134
201
  "function f(a) { try { return a; } catch ({ e }) { return 0; } }",
@@ -137,6 +204,27 @@ fn rejected_functions_use_scoped_plans() {
137
204
  }
138
205
  }
139
206
 
207
+ /// Locals that nested functions mention stay in the call's scope, and both the outer
208
+ /// function and its closures keep slot plans.
209
+ #[test]
210
+ fn closures_share_captured_locals() {
211
+ for source in [
212
+ "function f(a) { var g = () => a; return g(); }",
213
+ "function f(a) { var g = function() { return a; }; return g(); }",
214
+ "function f(a) { var s = 0; function g(x) { return k + x; } var k = a * 3; s = g(1) + g(2); return s; }",
215
+ "function f(a) { var n = 0; var inc = function() { n += a; return n; }; inc(); inc(); return n; }",
216
+ "function f(a) { var mk = function(b) { return function(c) { return a + b + c; }; }; return mk(1)(2); }",
217
+ "function f(a) { a = a + 1; var g = function() { return a; }; a = a * 10; return g(); }",
218
+ "function f(a) { var t = [a]; var g = function(i) { return t[i] + outer; }; return g(0); }",
219
+ "function f(a) { function r(n) { return n ? r(n - 1) + a : 0; } return r(3); }",
220
+ "function f(a) { var s = ''; for (var i = 0; i < a; i++) { s += c(i); } return s; function c(j) { var x = j * 2; return k[x]; } var k; }",
221
+ "function f(a) { var k = 'abcdef', s = ''; for (var i = 0; i < a; i++) { s += c(i); } return s; function c(j) { var x = j * 2; return k[x]; } }",
222
+ "function f(a) { var o = {}; o.h = function(x, y) { return x ^ y; }; var h = o.h; return h(a, 6); }",
223
+ ] {
224
+ compare(source, vec![Value::Number(2.0)], vec![], true);
225
+ }
226
+ }
227
+
140
228
  #[test]
141
229
  fn numeric_operands_and_effects_match() {
142
230
  for source in [