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
@@ -11,13 +11,14 @@ use swc_core::ecma::ast::{BinaryOp, Expr, Stmt, UpdateOp};
11
11
  use super::access::{self, Key};
12
12
  use super::plan::{self, Case, Expression, FunctionPlan, Name, Property, Statement, Target};
13
13
  use super::run::{Evaluation, Flow, Frame, Run, Stop};
14
- use super::semantics;
14
+ use super::{compile, semantics};
15
+ use crate::vm::HashMap;
15
16
  use crate::vm::output::Term;
16
- use crate::vm::value::{Env, Value};
17
+ use crate::vm::value::{Env, Scope, Value};
17
18
 
18
19
  /// Maximum handler loop count. Valid constants require fewer iterations; exceeding it indicates
19
20
  /// invalid instruction bytes.
20
- const MAXIMUM_ITERATIONS: usize = 1 << 16;
21
+ pub(super) const MAXIMUM_ITERATIONS: usize = 1 << 16;
21
22
 
22
23
  impl<'ast> Run<'ast, '_> {
23
24
  pub(super) fn statement(&mut self, statement: &'ast Stmt) -> Evaluation<'ast, Flow<'ast>> {
@@ -28,31 +29,96 @@ impl<'ast> Run<'ast, '_> {
28
29
  self.evaluate(&plan::scoped_expression(expr), &mut [])
29
30
  }
30
31
 
32
+ /// Calls a slot plan. Its scope holds the locals nested functions mention, inside `closed`,
33
+ /// the scope the function captured, if any.
31
34
  pub(super) fn call_plan(
32
35
  &mut self,
33
36
  plan: &FunctionPlan<'ast>,
37
+ closed: Option<&Scope<'ast>>,
34
38
  this: Value<'ast>,
35
- mut arguments: Vec<Value<'ast>>,
39
+ arguments: Vec<Value<'ast>>,
40
+ ) -> Evaluation<'ast, Value<'ast>> {
41
+ let mut locals = self.enter_plan(plan, closed, this, arguments);
42
+ let result = compile::run_all(&plan.code, self, &mut locals).map(returned);
43
+ self.frames.pop();
44
+ self.release_locals(locals);
45
+ result
46
+ }
47
+
48
+ /// Calls a handler's slot plan as [`Run::call_plan`] does, with no captured scope. Before
49
+ /// each top-level statement, `before` sees the statement's index and the locals.
50
+ pub(super) fn call_handler(
51
+ &mut self,
52
+ plan: &FunctionPlan<'ast>,
53
+ this: Value<'ast>,
54
+ arguments: Vec<Value<'ast>>,
55
+ before: &mut dyn FnMut(&Self, usize, &Locals<'ast>),
36
56
  ) -> Evaluation<'ast, Value<'ast>> {
57
+ let mut locals = self.enter_plan(plan, None, this, arguments);
58
+ let result = self.handler_statements(plan, 0, &mut locals, before);
59
+ self.release_locals(locals);
60
+ result
61
+ }
62
+
63
+ /// Binds a slot plan's parameters and pushes its frame. Returns the call's locals.
64
+ #[inline]
65
+ fn enter_plan(
66
+ &mut self,
67
+ plan: &FunctionPlan<'ast>,
68
+ closed: Option<&Scope<'ast>>,
69
+ this: Value<'ast>,
70
+ mut arguments: Vec<Value<'ast>>,
71
+ ) -> Vec<Option<Value<'ast>>> {
37
72
  let mut locals = self.spare.locals.pop().unwrap_or_default();
38
73
  locals.resize(plan.locals, None);
39
74
  if let Some(slot) = plan.arguments {
40
75
  locals[slot] = Some(Value::array(arguments.clone()));
41
76
  }
77
+ let scope = Env::scope(HashMap::default(), closed.cloned());
42
78
  let mut values = arguments.drain(..);
43
- for &slot in &plan.parameters {
44
- locals[slot] = Some(values.next().unwrap_or(Value::Undefined));
79
+ for parameter in &plan.parameters {
80
+ let value = values.next().unwrap_or(Value::Undefined);
81
+ match *parameter {
82
+ Name::Local(slot, _) => locals[slot] = Some(value),
83
+ Name::Global(name) => {
84
+ scope.borrow_mut().variables.insert(name.clone(), value);
85
+ }
86
+ }
45
87
  }
46
88
  drop(values);
47
89
  self.spare.arguments.push(arguments);
48
- let result = self.body(Frame::new(this), &plan.body, &mut locals);
90
+ self.frames.push(Frame { this, scope });
91
+ locals
92
+ }
93
+
94
+ /// Runs a handler's top-level statements from `start` in the frame the run holds, then
95
+ /// leaves that frame, as [`Run::body`] does.
96
+ pub(super) fn handler_statements(
97
+ &mut self,
98
+ plan: &FunctionPlan<'ast>,
99
+ start: usize,
100
+ locals: &mut Locals<'ast>,
101
+ before: &mut dyn FnMut(&Self, usize, &Locals<'ast>),
102
+ ) -> Evaluation<'ast, Value<'ast>> {
103
+ let mut flow = Ok(Flow::Normal);
104
+ for (index, statement) in plan.code.iter().enumerate().skip(start) {
105
+ before(self, index, locals);
106
+ flow = statement(self, locals);
107
+ if !matches!(flow, Ok(Flow::Normal)) {
108
+ break;
109
+ }
110
+ }
111
+ self.frames.pop();
112
+ flow.map(returned)
113
+ }
114
+
115
+ /// Returns a call's locals to the spare buffers.
116
+ fn release_locals(&mut self, mut locals: Vec<Option<Value<'ast>>>) {
49
117
  // Numeric locals need no destructor call.
50
- let assigned = locals.drain(..).flatten();
51
- for local in assigned {
118
+ for local in locals.drain(..).flatten() {
52
119
  local.discard();
53
120
  }
54
121
  self.spare.locals.push(locals);
55
- result
56
122
  }
57
123
 
58
124
  /// Runs a function body in `frame`.
@@ -65,10 +131,7 @@ impl<'ast> Run<'ast, '_> {
65
131
  self.frames.push(frame);
66
132
  let flow = self.execute_all(body, locals);
67
133
  self.frames.pop();
68
- match flow? {
69
- Flow::Return(value) => Ok(value),
70
- _ => Ok(Value::Undefined),
71
- }
134
+ flow.map(returned)
72
135
  }
73
136
 
74
137
  fn execute_all(
@@ -86,7 +149,7 @@ impl<'ast> Run<'ast, '_> {
86
149
  }
87
150
 
88
151
  /// Checks whether `name` has already been declared in the current function.
89
- fn declared(&self, name: Name<'ast>, locals: &Locals<'ast>) -> bool {
152
+ pub(super) fn declared(&self, name: Name<'ast>, locals: &Locals<'ast>) -> bool {
90
153
  match name {
91
154
  Name::Local(slot, _) => locals[slot].is_some(),
92
155
  Name::Global(name) => self.frame().scope.borrow().variables.contains_key(name),
@@ -94,7 +157,7 @@ impl<'ast> Run<'ast, '_> {
94
157
  }
95
158
 
96
159
  /// Declares `name` with `value`.
97
- fn bind(&mut self, name: Name<'ast>, value: Value<'ast>, locals: &mut Locals<'ast>) {
160
+ pub(super) fn bind(&mut self, name: Name<'ast>, value: Value<'ast>, locals: &mut Locals<'ast>) {
98
161
  match name {
99
162
  Name::Local(slot, _) => locals[slot] = Some(value),
100
163
  Name::Global(name) => self.declare(name.clone(), value),
@@ -130,25 +193,14 @@ impl<'ast> Run<'ast, '_> {
130
193
  handler,
131
194
  finalizer,
132
195
  } => {
133
- let mut outcome = self.execute_all(block, locals);
134
- if let Some(handler) = handler
135
- && let Err(stop) = &mut outcome
136
- && let Stop::Throw(exception) = &mut **stop
137
- {
138
- let exception = mem::replace(exception, Value::Undefined);
139
- if let Some(parameter) = parameter {
140
- self.bind(*parameter, exception, locals);
141
- }
142
- outcome = self.execute_all(handler, locals);
143
- }
144
- // A jump from `finally` overrides the previous completion of the block.
145
- if let Some(finalizer) = finalizer {
146
- match self.execute_all(finalizer, locals)? {
147
- Flow::Normal => {}
148
- flow => return Ok(flow),
149
- }
150
- }
151
- return outcome;
196
+ return self.attempt(
197
+ block.as_slice(),
198
+ *parameter,
199
+ handler.as_deref(),
200
+ finalizer.as_deref(),
201
+ locals,
202
+ Self::execute_all,
203
+ );
152
204
  }
153
205
  Statement::Block(statements) => return self.execute_all(statements, locals),
154
206
  Statement::If(test, yes, no) => {
@@ -216,6 +268,39 @@ impl<'ast> Run<'ast, '_> {
216
268
  Ok(Flow::Normal)
217
269
  }
218
270
 
271
+ /// Runs a `try` statement whose bodies `run` executes: `block`, then `handler` with an
272
+ /// exception bound to `parameter`, then `finalizer`.
273
+ #[inline]
274
+ pub(super) fn attempt<Body: ?Sized>(
275
+ &mut self,
276
+ block: &Body,
277
+ parameter: Option<Name<'ast>>,
278
+ handler: Option<&Body>,
279
+ finalizer: Option<&Body>,
280
+ locals: &mut Locals<'ast>,
281
+ run: impl Fn(&mut Self, &Body, &mut Locals<'ast>) -> Evaluation<'ast, Flow<'ast>>,
282
+ ) -> Evaluation<'ast, Flow<'ast>> {
283
+ let mut outcome = run(self, block, locals);
284
+ if let Some(handler) = handler
285
+ && let Err(stop) = &mut outcome
286
+ && let Stop::Throw(exception) = &mut **stop
287
+ {
288
+ let exception = mem::replace(exception, Value::Undefined);
289
+ if let Some(parameter) = parameter {
290
+ self.bind(parameter, exception, locals);
291
+ }
292
+ outcome = run(self, handler, locals);
293
+ }
294
+ // A jump from `finally` overrides the previous completion of the block.
295
+ if let Some(finalizer) = finalizer {
296
+ match run(self, finalizer, locals)? {
297
+ Flow::Normal => {}
298
+ flow => return Ok(flow),
299
+ }
300
+ }
301
+ outcome
302
+ }
303
+
219
304
  /// Runs from the first case strictly equal to `discriminant`, or else from the default.
220
305
  fn switch(
221
306
  &mut self,
@@ -504,35 +589,52 @@ impl<'ast> Run<'ast, '_> {
504
589
  locals: &mut Locals<'ast>,
505
590
  ) -> Evaluation<'ast, Value<'ast>> {
506
591
  let target = self.locate(target, locals)?;
507
- let value = match operator {
508
- None => self.evaluate(right, locals)?,
509
- Some(BinaryOp::LogicalAnd | BinaryOp::LogicalOr | BinaryOp::NullishCoalescing) => {
510
- return Err(Stop::unsupported("a logical assignment"));
511
- }
512
- // The script reapplies a new rolling mask to the program counter or key. The
513
- // interpreter keeps that mask at zero; see `crate::vm::registers`.
514
- Some(_) if matches!(&target, Location::Member(Value::Registers, Key::Name(_))) => {
515
- self.evaluate(right, locals)?;
516
- return self.read(&target, locals);
592
+ let Some(operator) = operator else {
593
+ let value = self.evaluate(right, locals)?;
594
+ self.write(target, value.clone(), locals)?;
595
+ return Ok(value);
596
+ };
597
+ self.compound(target, operator, locals, |run, locals| {
598
+ run.evaluate(right, locals)
599
+ })
600
+ }
601
+
602
+ /// Assigns to `target` with `operator`, as in `target += right`, where `right` evaluates
603
+ /// the right side.
604
+ #[inline]
605
+ pub(super) fn compound(
606
+ &mut self,
607
+ target: Location<'ast>,
608
+ operator: BinaryOp,
609
+ locals: &mut Locals<'ast>,
610
+ right: impl FnOnce(&mut Self, &mut Locals<'ast>) -> Evaluation<'ast, Value<'ast>>,
611
+ ) -> Evaluation<'ast, Value<'ast>> {
612
+ if matches!(
613
+ operator,
614
+ BinaryOp::LogicalAnd | BinaryOp::LogicalOr | BinaryOp::NullishCoalescing
615
+ ) {
616
+ return Err(Stop::unsupported("a logical assignment"));
617
+ }
618
+ // The script reapplies a new rolling mask to the program counter or key. The
619
+ // interpreter keeps that mask at zero; see `crate::vm::registers`.
620
+ if matches!(&target, Location::Member(Value::Registers, Key::Name(_))) {
621
+ right(self, locals)?;
622
+ return self.read(&target, locals);
623
+ }
624
+ // The target's old value is read before the right side runs.
625
+ let old = self.read(&target, locals)?;
626
+ let right = right(self, locals)?;
627
+ // Release the old value before overwriting it, allowing `s += c` to append in place.
628
+ match &target {
629
+ Location::Name(Name::Local(slot, _)) if locals[*slot].is_some() => {
630
+ locals[*slot] = Some(Value::Undefined);
517
631
  }
518
- // The target's old value is read before the right side runs.
519
- Some(operator) => {
520
- let old = self.read(&target, locals)?;
521
- let right = self.evaluate(right, locals)?;
522
- // Release the old value before overwriting it, allowing `s += c` to append in
523
- // place.
524
- match &target {
525
- Location::Name(Name::Local(slot, _)) if locals[*slot].is_some() => {
526
- locals[*slot] = Some(Value::Undefined);
527
- }
528
- Location::Name(name) => {
529
- Env::assign(&self.frame().scope, name.atom(), Value::Undefined);
530
- }
531
- Location::Member(..) => {}
532
- }
533
- binary_value(operator, old, right)
632
+ Location::Name(name) => {
633
+ Env::assign(&self.frame().scope, name.atom(), Value::Undefined);
534
634
  }
535
- };
635
+ Location::Member(..) => {}
636
+ }
637
+ let value = binary_value(operator, old, right);
536
638
  self.write(target, value.clone(), locals)?;
537
639
  Ok(value)
538
640
  }
@@ -549,18 +651,27 @@ impl<'ast> Run<'ast, '_> {
549
651
  if let Target::Name(Name::Local(slot, _)) = target
550
652
  && let Some(Value::Number(number)) = &mut locals[*slot]
551
653
  {
552
- let change = updated(operator, prefix, *number);
553
- *number = change.written;
554
- return Ok(Value::Number(change.value));
654
+ return Ok(Value::Number(step(operator, prefix, number)));
555
655
  }
556
656
  let target = self.locate(target, locals)?;
657
+ self.update_location(target, operator, prefix, locals)
658
+ }
659
+
660
+ /// Applies `++` or `--` to the number at `target`.
661
+ pub(super) fn update_location(
662
+ &mut self,
663
+ target: Location<'ast>,
664
+ operator: UpdateOp,
665
+ prefix: bool,
666
+ locals: &mut Locals<'ast>,
667
+ ) -> Evaluation<'ast, Value<'ast>> {
557
668
  let old = self.read(&target, locals)?;
558
- let old = old
669
+ let mut number = old
559
670
  .number()
560
671
  .ok_or_else(|| Stop::unsupported(&format!("incrementing {}", old.expr())))?;
561
- let change = updated(operator, prefix, old);
562
- self.write(target, Value::Number(change.written), locals)?;
563
- Ok(Value::Number(change.value))
672
+ let value = step(operator, prefix, &mut number);
673
+ self.write(target, Value::Number(number), locals)?;
674
+ Ok(Value::Number(value))
564
675
  }
565
676
 
566
677
  /// Evaluates `&&`, `||`, or `??`, evaluating `right` only when needed.
@@ -574,6 +685,22 @@ impl<'ast> Run<'ast, '_> {
574
685
  locals: &mut Locals<'ast>,
575
686
  ) -> Evaluation<'ast, Value<'ast>> {
576
687
  let left = self.evaluate(left, locals)?;
688
+ self.short_circuit(operator, left, pure, locals, |run, locals| {
689
+ run.evaluate(right, locals)
690
+ })
691
+ }
692
+
693
+ /// Finishes `&&`, `||`, or `??` after its left side, calling `right` to evaluate the right
694
+ /// side only when needed.
695
+ #[inline]
696
+ pub(super) fn short_circuit(
697
+ &mut self,
698
+ operator: BinaryOp,
699
+ left: Value<'ast>,
700
+ pure: bool,
701
+ locals: &mut Locals<'ast>,
702
+ right: impl Fn(&mut Self, &mut Locals<'ast>) -> Evaluation<'ast, Value<'ast>>,
703
+ ) -> Evaluation<'ast, Value<'ast>> {
577
704
  // Condition controlling right-operand evaluation and the branch direction that
578
705
  // evaluates it.
579
706
  let (test, runs_right) = match operator {
@@ -588,7 +715,7 @@ impl<'ast> Run<'ast, '_> {
588
715
  // An effect-free right operand may become a symbolic term only if evaluating it
589
716
  // cannot introduce an exception on a skipped path.
590
717
  let effects = self.effects.len();
591
- if let Ok(right) = self.evaluate(right, locals) {
718
+ if let Ok(right) = right(self, locals) {
592
719
  debug_assert_eq!(
593
720
  effects,
594
721
  self.effects.len(),
@@ -602,7 +729,7 @@ impl<'ast> Run<'ast, '_> {
602
729
  }
603
730
  }
604
731
  if self.decide(&test) == runs_right {
605
- self.evaluate(right, locals)
732
+ right(self, locals)
606
733
  } else {
607
734
  Ok(left)
608
735
  }
@@ -624,7 +751,7 @@ impl<'ast> Run<'ast, '_> {
624
751
  })
625
752
  }
626
753
 
627
- fn read(
754
+ pub(super) fn read(
628
755
  &mut self,
629
756
  target: &Location<'ast>,
630
757
  locals: &Locals<'ast>,
@@ -641,7 +768,7 @@ impl<'ast> Run<'ast, '_> {
641
768
 
642
769
  /// Writes declared locals directly, since they are the most common assignment target.
643
770
  #[inline]
644
- fn write(
771
+ pub(super) fn write(
645
772
  &mut self,
646
773
  target: Location<'ast>,
647
774
  value: Value<'ast>,
@@ -667,7 +794,7 @@ impl<'ast> Run<'ast, '_> {
667
794
 
668
795
  /// Clones a value, using a fast path for numbers.
669
796
  #[inline(always)]
670
- fn copied<'a>(value: &Value<'a>) -> Value<'a> {
797
+ pub(super) fn copied<'a>(value: &Value<'a>) -> Value<'a> {
671
798
  match value {
672
799
  Value::Number(number) => Value::Number(*number),
673
800
  value => value.clone(),
@@ -676,21 +803,21 @@ fn copied<'a>(value: &Value<'a>) -> Value<'a> {
676
803
 
677
804
  /// Extracts the value from a [`Run::number`] result.
678
805
  #[inline(always)]
679
- fn number_value(number: Result<f64, Value<'_>>) -> Value<'_> {
806
+ pub(super) fn number_value(number: Result<f64, Value<'_>>) -> Value<'_> {
680
807
  number.map_or_else(|value| value, Value::Number)
681
808
  }
682
809
 
683
810
  /// Slot-indexed call locals; `None` until declared or written.
684
- type Locals<'ast> = [Option<Value<'ast>>];
811
+ pub(super) type Locals<'ast> = [Option<Value<'ast>>];
685
812
 
686
- enum Location<'ast> {
813
+ pub(super) enum Location<'ast> {
687
814
  Name(Name<'ast>),
688
815
  Member(Value<'ast>, Key<'ast, 'ast>),
689
816
  }
690
817
 
691
818
  /// Applies [`semantics::binary_value`] to numeric operands without coercion or symbolic terms.
692
819
  #[inline(always)]
693
- fn binary_value<'a>(operator: BinaryOp, left: Value<'a>, right: Value<'a>) -> Value<'a> {
820
+ pub(super) fn binary_value<'a>(operator: BinaryOp, left: Value<'a>, right: Value<'a>) -> Value<'a> {
694
821
  let (&Value::Number(a), &Value::Number(b)) = (&left, &right) else {
695
822
  return semantics::binary_value(operator, left, right);
696
823
  };
@@ -710,24 +837,24 @@ fn binary_value<'a>(operator: BinaryOp, left: Value<'a>, right: Value<'a>) -> Va
710
837
  })
711
838
  }
712
839
 
713
- /// Values produced and stored by an increment or decrement.
714
- struct Updated {
715
- /// The number written back.
716
- written: f64,
717
- /// Expression result: the new number for prefix form, or the old number for postfix form.
718
- value: f64,
719
- }
720
-
721
- /// Computes the result and stored value for `++` or `--` applied to `old`.
840
+ /// Applies `++` or `--` to `number` in place. Returns the expression's result: the new number
841
+ /// in prefix form, or the old number in postfix form.
722
842
  #[inline(always)]
723
- fn updated(operator: UpdateOp, prefix: bool, old: f64) -> Updated {
724
- let new = match operator {
843
+ pub(super) fn step(operator: UpdateOp, prefix: bool, number: &mut f64) -> f64 {
844
+ let old = *number;
845
+ *number = match operator {
725
846
  UpdateOp::PlusPlus => old + 1.0,
726
847
  UpdateOp::MinusMinus => old - 1.0,
727
848
  };
728
- Updated {
729
- written: new,
730
- value: if prefix { new } else { old },
849
+ if prefix { *number } else { old }
850
+ }
851
+
852
+ /// The result of a call whose body completed with `flow`.
853
+ #[inline]
854
+ fn returned(flow: Flow<'_>) -> Value<'_> {
855
+ match flow {
856
+ Flow::Return(value) => value,
857
+ _ => Value::Undefined,
731
858
  }
732
859
  }
733
860