nanvm_lib/vm/lambda/mod.rs
1//! The three continuation types of `fjs/edag/README.md`'s Chains section,
2//! one per combination of its two bits — a receiver is live (P), a
3//! short-circuit region is open (O) — that is not a node boundary. A
4//! chain is opened by a node's entry point on [`Any`] (`dot`, `option_dot`,
5//! `option_call` in `vm/any`), continued by one method per step the README
6//! allows in that state, and closed by `end` or `end_call`, which alone
7//! answer the chain's `Result`. A step the README does not allow is a
8//! method that does not exist, so a spelling that disagrees with the
9//! README does not compile.
10//!
11//! None of the three is `Clone`, and every method consumes `self`: a
12//! continuation "cannot be lifted out as a shared node", so a chain is used
13//! once, and `#[must_use]` makes a chain used zero times a warning. What a
14//! terminal answers is an ordinary `Result<Any<A>, Any<A>>`, shareable as
15//! any value is — neither bit is ever the result of an `exp`.
16//!
17//! A step's order is *guard, thunk, operation*: a guarded step looks at the
18//! current value for nullishness first and skips its thunk on it; every
19//! callable validation follows argument evaluation, and a computed key
20//! precedes its property read. A member call resolves its property and
21//! runtime callee before forcing arguments, retaining lookup failures;
22//! resolving a noncallable value does not validate it yet.
23//!
24//! A property step holds its receiver and key, [`Member`], and the read
25//! waits for the exit: `end` reads, and a call exit resolves the callee —
26//! an object's own property or an array's element first, then the
27//! function the runtime value's `dot` answers for the receiver, then the
28//! receiver type's built-in member function ([`method`](method::method))
29//! for a name without a body, then the `TypeError` for calling `undefined`. A call step on a member
30//! takes the runtime value for that, `&runtime`, the parameter every
31//! compiled module receives (`fjs/js/runtime/README.md`, "How NaNVM finds
32//! it"); a step on a plain value needs none. No user function reads
33//! `this`, so `Function::call` takes no receiver and never will: the
34//! receiver is consumed by the built-in and handed to nothing. Which
35//! built-ins the table answers is `nanvm/lib/todo/member-functions.md`.
36
37use crate::vm::{Any, IVm};
38
39pub(crate) mod member;
40mod method;
41mod number;
42mod string;
43
44/// The shared runtime-only pairs and the completeness table `method`'s test
45/// checks it against, generated by `fjs/nanvm/methods` — a `#[path]`, since a
46/// `gen.` name is not a Rust identifier.
47#[path = "gen.methods.rs"]
48mod methods_table;
49
50pub(crate) use member::Member;
51
52/// What a chain's live state can do, whatever it holds: a bare value, the
53/// state after a call step or a `?.()` node, or a [`Member`], the receiver
54/// and key of a property step whose read waits for the exit.
55pub(crate) trait Live<A: IVm>: Sized {
56 /// The chain's current value: the value itself, or the property read.
57 fn value(self) -> Any<A>;
58 /// The guard of a `?.()` step: whether what the call would call is
59 /// nullish, or the throw of deciding, a member's body deciding for its
60 /// receiver, which the runtime value's `dot` answers.
61 fn is_nullish(&self, runtime: &Any<A>) -> Result<bool, Any<A>>;
62 /// Resolve before arguments, retaining lookup failures while postponing
63 /// callable validation. Bare values have no member lookup to perform.
64 fn resolve(&self, runtime: &Any<A>) -> Result<(), Any<A>>;
65 /// The call, its arguments evaluated: `Any::call` of a value, and a
66 /// member's resolution — the receiver's own property, the function
67 /// the runtime value's `dot` answers, the native for a name without a
68 /// body, the `TypeError`.
69 fn call(self, runtime: &Any<A>, args: Any<A>) -> Result<Any<A>, Any<A>>;
70}
71
72impl<A: IVm> Live<A> for Any<A> {
73 fn value(self) -> Any<A> {
74 self
75 }
76 fn is_nullish(&self, _runtime: &Any<A>) -> Result<bool, Any<A>> {
77 Ok(Any::is_nullish(self))
78 }
79 fn resolve(&self, _runtime: &Any<A>) -> Result<(), Any<A>> {
80 Ok(())
81 }
82 fn call(self, _runtime: &Any<A>, args: Any<A>) -> Result<Any<A>, Any<A>> {
83 Any::call(self, args)
84 }
85}
86
87impl<A: IVm> Live<A> for Member<A> {
88 fn value(self) -> Any<A> {
89 self.read()
90 }
91 fn is_nullish(&self, runtime: &Any<A>) -> Result<bool, Any<A>> {
92 Member::is_nullish(self, runtime)
93 }
94 fn resolve(&self, runtime: &Any<A>) -> Result<(), Any<A>> {
95 self.is_nullish(runtime).map(|_| ())
96 }
97 fn call(self, runtime: &Any<A>, args: Any<A>) -> Result<Any<A>, Any<A>> {
98 Member::call(self, runtime, args)
99 }
100}
101
102/// The state of a chain inside an open region: live, skipped by a guard, or
103/// thrown. Skipped and thrown pass through every step; `end` tells them
104/// apart.
105pub(crate) enum Region<A: IVm, T: Live<A>> {
106 Live(T),
107 Skipped,
108 Thrown(Any<A>),
109}
110
111impl<A: IVm, T: Live<A>> From<Result<T, Any<A>>> for Region<A, T> {
112 fn from(r: Result<T, Any<A>>) -> Self {
113 match r {
114 Ok(v) => Region::Live(v),
115 Err(e) => Region::Thrown(e),
116 }
117 }
118}
119
120impl<A: IVm, T: Live<A>> Region<A, T> {
121 /// The region closed: a skipped chain is `undefined`, the value a
122 /// short-circuit answers.
123 fn end(self) -> Result<Any<A>, Any<A>> {
124 match self {
125 Region::Live(v) => Ok(v.value()),
126 Region::Skipped => Ok(Any::undefined()),
127 Region::Thrown(e) => Err(e),
128 }
129 }
130 /// An unguarded step: `op` over a live state, nothing otherwise.
131 fn step<U: Live<A>>(self, op: impl FnOnce(T) -> Result<U, Any<A>>) -> Region<A, U> {
132 match self {
133 Region::Live(v) => op(v).into(),
134 Region::Skipped => Region::Skipped,
135 Region::Thrown(e) => Region::Thrown(e),
136 }
137 }
138 /// `|()`: resolve a member, force the arguments, then call. Callable
139 /// validation follows arguments; lookup failure precedes them.
140 fn call(
141 self,
142 runtime: &Any<A>,
143 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
144 ) -> Region<A, Any<A>> {
145 self.step(|v| {
146 v.resolve(runtime)
147 .and_then(|()| args())
148 .and_then(|a| v.call(runtime, a))
149 })
150 }
151 /// `|.`: the key forced, then the step over the current value as the
152 /// receiver, whose nullish throw therefore comes second.
153 pub(crate) fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> Region<A, Member<A>> {
154 self.step(|v| key().and_then(|k| Member::new(v.value(), k)))
155 }
156 /// A guard: the region skipped where the live state is nullish, thrown
157 /// where deciding throws, and otherwise `step` over it; nothing for a
158 /// region already skipped or thrown. A member decides with the runtime
159 /// value, whose `dot` may answer a callee for its receiver.
160 fn guarded<U: Live<A>>(
161 self,
162 runtime: &Any<A>,
163 step: impl FnOnce(Self) -> Region<A, U>,
164 ) -> Region<A, U> {
165 match self {
166 Region::Live(v) => match v.is_nullish(runtime) {
167 Err(e) => Region::Thrown(e),
168 Ok(true) => Region::Skipped,
169 Ok(false) => step(Region::Live(v)),
170 },
171 Region::Skipped => Region::Skipped,
172 Region::Thrown(e) => Region::Thrown(e),
173 }
174 }
175 /// `|?.()`: a nullish callee skips the rest of the chain, its
176 /// arguments untouched; any other is called.
177 pub(crate) fn option_call(
178 self,
179 runtime: &Any<A>,
180 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
181 ) -> Region<A, Any<A>> {
182 // The guard already performed the preargument resolution.
183 self.guarded(runtime, |live| {
184 live.step(|v| args().and_then(|a| v.call(runtime, a)))
185 })
186 }
187}
188
189impl<A: IVm> Region<A, Any<A>> {
190 /// The `?.` node's own step, on a plain value: a nullish receiver skips
191 /// the key and the rest of the chain; any other takes the step `|.`
192 /// takes. The guard is the value's own nullishness, which asks no
193 /// runtime.
194 pub(crate) fn option_dot(
195 self,
196 key: impl FnOnce() -> Result<Any<A>, Any<A>>,
197 ) -> Region<A, Member<A>> {
198 match self {
199 Region::Live(v) if Any::is_nullish(&v) => Region::Skipped,
200 live => live.dot(key),
201 }
202 }
203 /// `|()` on a plain value, the state after a call step or a `?.()`
204 /// node: `Any::call`, which asks no runtime.
205 fn call_value(self, args: impl FnOnce() -> Result<Any<A>, Any<A>>) -> Region<A, Any<A>> {
206 self.step(|v| args().and_then(|a| Any::call(v, a)))
207 }
208 /// `?.()` on a plain value, the node's own step and a `|?.()` step on
209 /// the state after a call: a nullish callee skips, any other is called.
210 pub(crate) fn option_call_value(
211 self,
212 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
213 ) -> Region<A, Any<A>> {
214 match self {
215 Region::Live(v) if Any::is_nullish(&v) => Region::Skipped,
216 live => live.call_value(args),
217 }
218 }
219}
220
221/// A receiver is live and no region is open: the state after a `.` node,
222/// `a.b`. Its steps are `|()` (`end_call`) and `|?.()` (`option_call`);
223/// `end` is the bare node, the read with its receiver dropped — the one
224/// property read `nanvm-lib` has, `dot(a, key).end()`.
225///
226/// No region, so no skipped state: the interior is the step's own
227/// `Result`, its nullish-receiver throw waiting for a terminal to surface
228/// it — `a.b(...c)` on a nullish `a` throws at the access with `c`
229/// untouched, which is why `end_call` takes its arguments as a thunk.
230#[must_use]
231pub struct PropertyLambda<A: IVm>(pub(crate) Result<Member<A>, Any<A>>);
232
233impl<A: IVm> PropertyLambda<A> {
234 /// No continuation: `a.b`.
235 pub fn end(self) -> Result<Any<A>, Any<A>> {
236 self.0.map(Member::read)
237 }
238 /// `|()`, terminal: `a.b(...args)`, the property called on its
239 /// receiver — an own property, or a built-in member function. No
240 /// region is open, so this is also what a `|!()` would be — "there is
241 /// no bit to clear".
242 pub fn end_call(
243 self,
244 runtime: &Any<A>,
245 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
246 ) -> Result<Any<A>, Any<A>> {
247 // A `Result`-sourced region is never skipped, so `end` is exact.
248 Region::from(self.0).call(runtime, args).end()
249 }
250 /// `|()` of an intrinsic's helper, `slice(s, a, b)`: the member call
251 /// that asks no runtime, the own property else the native else the
252 /// `TypeError`, since no body defines the name
253 /// ([`Member::call_native`]).
254 pub fn end_call_native(
255 self,
256 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
257 ) -> Result<Any<A>, Any<A>> {
258 self.0.and_then(|m| args().and_then(|a| m.call_native(a)))
259 }
260 /// `|?.()`: `a.b?.(...args)`, opening a region.
261 pub fn option_call(
262 self,
263 runtime: &Any<A>,
264 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
265 ) -> OptionLambda<A> {
266 OptionLambda(Region::from(self.0).option_call(runtime, args))
267 }
268}
269
270/// A region is open and no receiver is live: the state after a `?.()` node,
271/// `a?.(...b)`, or a `|()` or `|?.()` step inside a region. Its steps are
272/// `|()` (`call`) and `|.` (`dot`); no `|?.()`, and no `|!()` — there is no
273/// receiver for either to justify.
274#[must_use]
275pub struct OptionLambda<A: IVm>(pub(crate) Region<A, Any<A>>);
276
277impl<A: IVm> OptionLambda<A> {
278 /// No continuation: the region closes, a skipped chain is `undefined`.
279 pub fn end(self) -> Result<Any<A>, Any<A>> {
280 self.0.end()
281 }
282 /// `|()`: call the current value, inside the region.
283 pub fn call(self, args: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionLambda<A> {
284 OptionLambda(self.0.call_value(args))
285 }
286 /// `|.`: a property of the current value, inside the region; the value
287 /// becomes the receiver.
288 pub fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionPropertyLambda<A> {
289 OptionPropertyLambda(self.0.dot(key))
290 }
291}
292
293/// Both bits live: the state after a `?.` node, `a?.b`, or a `|.` step
294/// inside a region. Every step is legal here: `|()` (`call`), `|.` (`dot`),
295/// `|?.()` (`option_call`) and `|!()` (`end_call`).
296#[must_use]
297pub struct OptionPropertyLambda<A: IVm>(pub(crate) Region<A, Member<A>>);
298
299impl<A: IVm> OptionPropertyLambda<A> {
300 /// No continuation: `(a?.b)`, the region closed with nothing after it.
301 pub fn end(self) -> Result<Any<A>, Any<A>> {
302 self.0.end()
303 }
304 /// `|()`: `a?.b(...args)`, the property called on its receiver, skipped
305 /// with the region.
306 pub fn call(
307 self,
308 runtime: &Any<A>,
309 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
310 ) -> OptionLambda<A> {
311 OptionLambda(self.0.call(runtime, args))
312 }
313 /// `|.`: `a?.b.c`, skipped with the region.
314 pub fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionPropertyLambda<A> {
315 OptionPropertyLambda(self.0.dot(key))
316 }
317 /// `|?.()`: `a?.b?.(...args)`, its own guard on the callee.
318 pub fn option_call(
319 self,
320 runtime: &Any<A>,
321 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
322 ) -> OptionLambda<A> {
323 OptionLambda(self.0.option_call(runtime, args))
324 }
325 /// `|!()`, terminal: `(a?.b)(...args)`. The parentheses close the
326 /// region first, so a skipped chain is `undefined` here and the call
327 /// happens regardless — the arguments are evaluated, then `undefined`
328 /// is called and throws. A live chain calls the property on its
329 /// receiver; a thrown chain stays thrown, arguments untouched.
330 pub fn end_call(
331 self,
332 runtime: &Any<A>,
333 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
334 ) -> Result<Any<A>, Any<A>> {
335 match self.0 {
336 Region::Skipped => args().and_then(|a| Any::undefined().call(a)),
337 region => region.call(runtime, args).end(),
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use crate::vm::test::function::{identity, returns, rt};
345 use crate::{
346 naive::Naive,
347 vm::{Any, ToAny, ToArray, ToObject, error},
348 };
349
350 type A = Naive;
351 type Thunk = Box<dyn FnOnce() -> Result<Any<A>, Any<A>>>;
352
353 /// A thunk that must not be forced.
354 fn boom() -> Result<Any<A>, Any<A>> {
355 Err("boom".into())
356 }
357 /// A fresh arguments array, for a chain whose answer is not the array.
358 fn args() -> Result<Any<A>, Any<A>> {
359 Ok([7.0.to_any()].to_array().to_any())
360 }
361 /// Asserts that `chain`, handed a thunk over a fresh arguments array,
362 /// answers that very array: a call of `identity` reached its callee.
363 /// Arrays compare by identity, so the array is made once and cloned.
364 fn answers_arguments(chain: impl FnOnce(Thunk) -> Result<Any<A>, Any<A>>) {
365 let a = args().unwrap();
366 let thunk = a.clone();
367 assert_eq!(chain(Box::new(|| Ok(thunk))), Ok(a));
368 }
369 /// `{ f: identity, n: 1, u: undefined }`
370 fn object() -> Any<A> {
371 [
372 ("f".into(), identity()),
373 ("n".into(), 1.0.to_any()),
374 ("u".into(), Any::undefined()),
375 ]
376 .to_object()
377 .to_any()
378 }
379 fn key(k: &str) -> impl FnOnce() -> Result<Any<A>, Any<A>> {
380 let k: Any<A> = k.into();
381 || Ok(k)
382 }
383
384 // `PropertyLambda`
385
386 /// `a.b` — `end` is the read, throw included.
387 #[test]
388 fn property_end() {
389 assert_eq!(object().dot("n".into()).end(), Ok(1.0.to_any()));
390 assert_eq!(
391 Any::<A>::undefined().dot("n".into()).end(),
392 Err(error::nullish_to_object())
393 );
394 }
395
396 /// `a.f(...c)`, and `a.b(...c)` on a nullish `a`: the throw waited in
397 /// the lambda and `c` was never evaluated.
398 #[test]
399 fn property_end_call() {
400 answers_arguments(|args| object().dot("f".into()).end_call(&rt(), args));
401 assert_eq!(
402 Any::undefined().dot("f".into()).end_call(&rt(), boom),
403 Err(error::nullish_to_object())
404 );
405 }
406
407 /// A live, non-callable callee: the arguments run before the
408 /// callability check, so theirs is the throw.
409 #[test]
410 fn property_end_call_forces_arguments_before_callability() {
411 assert_eq!(
412 object().dot("n".into()).end_call(&rt(), boom),
413 Err("boom".into())
414 );
415 assert_eq!(
416 object().dot("n".into()).end_call(&rt(), args),
417 Err(error::unexpected_type())
418 );
419 }
420
421 /// `a.b?.(...c)`: guarded on the property, then a region.
422 #[test]
423 fn property_option_call() {
424 answers_arguments(|args| object().dot("f".into()).option_call(&rt(), args).end());
425 assert_eq!(
426 object().dot("u".into()).option_call(&rt(), boom).end(),
427 Ok(Any::undefined())
428 );
429 assert_eq!(
430 object().dot("n".into()).option_call(&rt(), boom).end(),
431 Err("boom".into())
432 );
433 assert_eq!(
434 Any::undefined()
435 .dot("f".into())
436 .option_call(&rt(), boom)
437 .end(),
438 Err(error::nullish_to_object())
439 );
440 }
441
442 // `OptionLambda`
443
444 /// `a?.(...c).length` and `a?.().f(...d)`: steps inside the region on
445 /// a live value.
446 #[test]
447 fn option_steps_live() {
448 assert_eq!(
449 identity().option_call(args).dot(key("length")).end(),
450 Ok(1.0.to_any())
451 );
452 let returns_object: Any<A> = returns(object());
453 answers_arguments(|args| {
454 returns_object
455 .option_call(|| Ok([].to_array().to_any()))
456 .dot(key("f"))
457 .call(&rt(), args)
458 .end()
459 });
460 }
461
462 /// A skipped region passes through every step, thunks untouched, and
463 /// ends as `undefined`.
464 #[test]
465 fn option_steps_skipped() {
466 assert_eq!(
467 Any::undefined()
468 .option_call(boom)
469 .dot(boom)
470 .call(&rt(), boom)
471 .dot(boom)
472 .end(),
473 Ok(Any::undefined())
474 );
475 }
476
477 /// A thrown region passes through every step, thunks untouched, and
478 /// ends with its throw.
479 #[test]
480 fn option_steps_thrown() {
481 assert_eq!(
482 identity()
483 .option_call(boom)
484 .dot(key("x"))
485 .call(&rt(), args)
486 .end(),
487 Err("boom".into())
488 );
489 }
490
491 // `OptionPropertyLambda`
492
493 /// `a?.f(...c)`, `a?.n.x`, `a?.u?.(...c)`, `a?.f?.(...c)`: the three
494 /// non-terminal steps on a live chain.
495 #[test]
496 fn option_property_steps_live() {
497 answers_arguments(|args| object().option_dot(key("f")).call(&rt(), args).end());
498 assert_eq!(
499 object().option_dot(key("n")).dot(key("x")).end(),
500 Ok(Any::undefined())
501 );
502 assert_eq!(
503 object().option_dot(key("u")).option_call(&rt(), boom).end(),
504 Ok(Any::undefined())
505 );
506 answers_arguments(|args| object().option_dot(key("f")).option_call(&rt(), args).end());
507 }
508
509 /// `a?.b.c(...d).e?.(...f)` on a nullish `a`: one region, everything
510 /// skipped.
511 #[test]
512 fn option_property_steps_skipped() {
513 assert_eq!(
514 Any::undefined()
515 .option_dot(boom)
516 .dot(boom)
517 .call(&rt(), boom)
518 .dot(boom)
519 .option_call(&rt(), boom)
520 .end(),
521 Ok(Any::undefined())
522 );
523 }
524
525 /// `|()` and `|.` are unguarded: on a live but nullish current value
526 /// the thunk is forced and the operation throws — `a?.b()` with `a.b`
527 /// undefined throws, it does not answer `undefined`. One case per
528 /// unguarded step, so that turning any of them into a guarded step
529 /// fails a test.
530 #[test]
531 fn unguarded_steps_throw_on_nullish_current_value() {
532 let returns_undefined: Any<A> = returns(Any::undefined());
533 assert_eq!(
534 returns_undefined.clone().option_call(args).call(boom).end(),
535 Err("boom".into())
536 );
537 assert_eq!(
538 returns_undefined.option_call(args).dot(boom).end(),
539 Err("boom".into())
540 );
541 assert_eq!(
542 object().option_dot(key("u")).call(&rt(), boom).end(),
543 Err("boom".into())
544 );
545 }
546
547 /// `|.` on a live but nullish current value throws at the read — after
548 /// the key is forced, as JavaScript evaluates `u[k()]`.
549 #[test]
550 fn option_property_dot_forces_key_before_read() {
551 assert_eq!(
552 object().option_dot(key("u")).dot(boom).end(),
553 Err("boom".into())
554 );
555 assert_eq!(
556 object().option_dot(key("u")).dot(key("x")).end(),
557 Err(error::nullish_to_object())
558 );
559 }
560
561 /// `(a?.f)(...c)` live, and `(a?.b)(...c)` on a nullish `a`: the
562 /// parentheses end the region, so the arguments run and `undefined` is
563 /// called — the case the host engines disagree on, and the
564 /// specification's reading.
565 #[test]
566 fn option_property_end_call() {
567 answers_arguments(|args| object().option_dot(key("f")).end_call(&rt(), args));
568 assert_eq!(
569 Any::undefined().option_dot(boom).end_call(&rt(), boom),
570 Err("boom".into())
571 );
572 assert_eq!(
573 Any::undefined().option_dot(boom).end_call(&rt(), args),
574 Err(error::unexpected_type())
575 );
576 }
577
578 /// A thrown chain stays thrown through `end_call`, arguments untouched.
579 #[test]
580 fn option_property_end_call_thrown() {
581 assert_eq!(
582 object().option_dot(boom).end_call(&rt(), boom),
583 Err("boom".into())
584 );
585 }
586}