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 self.0.and_then(|m| {
248 m.resolve(runtime)
249 .and_then(|()| args())
250 .and_then(|a| m.call(runtime, a))
251 })
252 }
253 /// `|()` of an intrinsic's helper, `slice(s, a, b)`: the member call
254 /// that asks no runtime, the own property else the native else the
255 /// `TypeError`, since no body defines the name
256 /// ([`Member::call_native`]).
257 pub fn end_call_native(
258 self,
259 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
260 ) -> Result<Any<A>, Any<A>> {
261 self.0.and_then(|m| args().and_then(|a| m.call_native(a)))
262 }
263 /// `|?.()`: `a.b?.(...args)`, opening a region.
264 pub fn option_call(
265 self,
266 runtime: &Any<A>,
267 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
268 ) -> OptionLambda<A> {
269 OptionLambda(Region::from(self.0).option_call(runtime, args))
270 }
271}
272
273/// A region is open and no receiver is live: the state after a `?.()` node,
274/// `a?.(...b)`, or a `|()` or `|?.()` step inside a region. Its steps are
275/// `|()` (`call`) and `|.` (`dot`); no `|?.()`, and no `|!()` — there is no
276/// receiver for either to justify.
277#[must_use]
278pub struct OptionLambda<A: IVm>(pub(crate) Region<A, Any<A>>);
279
280impl<A: IVm> OptionLambda<A> {
281 /// No continuation: the region closes, a skipped chain is `undefined`.
282 pub fn end(self) -> Result<Any<A>, Any<A>> {
283 self.0.end()
284 }
285 /// `|()`: call the current value, inside the region.
286 pub fn call(self, args: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionLambda<A> {
287 OptionLambda(self.0.call_value(args))
288 }
289 /// `|.`: a property of the current value, inside the region; the value
290 /// becomes the receiver.
291 pub fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionPropertyLambda<A> {
292 OptionPropertyLambda(self.0.dot(key))
293 }
294}
295
296/// Both bits live: the state after a `?.` node, `a?.b`, or a `|.` step
297/// inside a region. Every step is legal here: `|()` (`call`), `|.` (`dot`),
298/// `|?.()` (`option_call`) and `|!()` (`end_call`).
299#[must_use]
300pub struct OptionPropertyLambda<A: IVm>(pub(crate) Region<A, Member<A>>);
301
302impl<A: IVm> OptionPropertyLambda<A> {
303 /// No continuation: `(a?.b)`, the region closed with nothing after it.
304 pub fn end(self) -> Result<Any<A>, Any<A>> {
305 self.0.end()
306 }
307 /// `|()`: `a?.b(...args)`, the property called on its receiver, skipped
308 /// with the region.
309 pub fn call(
310 self,
311 runtime: &Any<A>,
312 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
313 ) -> OptionLambda<A> {
314 OptionLambda(self.0.call(runtime, args))
315 }
316 /// `|.`: `a?.b.c`, skipped with the region.
317 pub fn dot(self, key: impl FnOnce() -> Result<Any<A>, Any<A>>) -> OptionPropertyLambda<A> {
318 OptionPropertyLambda(self.0.dot(key))
319 }
320 /// `|?.()`: `a?.b?.(...args)`, its own guard on the callee.
321 pub fn option_call(
322 self,
323 runtime: &Any<A>,
324 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
325 ) -> OptionLambda<A> {
326 OptionLambda(self.0.option_call(runtime, args))
327 }
328 /// `|!()`, terminal: `(a?.b)(...args)`. The parentheses close the
329 /// region first, so a skipped chain is `undefined` here and the call
330 /// happens regardless — the arguments are evaluated, then `undefined`
331 /// is called and throws. A live chain calls the property on its
332 /// receiver; a thrown chain stays thrown, arguments untouched.
333 pub fn end_call(
334 self,
335 runtime: &Any<A>,
336 args: impl FnOnce() -> Result<Any<A>, Any<A>>,
337 ) -> Result<Any<A>, Any<A>> {
338 match self.0 {
339 Region::Live(m) => m
340 .resolve(runtime)
341 .and_then(|()| args())
342 .and_then(|a| m.call(runtime, a)),
343 Region::Skipped => args().and_then(|a| Any::undefined().call(a)),
344 Region::Thrown(e) => Err(e),
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use crate::vm::test::function::{identity, returns, rt};
352 use crate::{
353 naive::Naive,
354 vm::{Any, ToAny, ToArray, ToObject, error},
355 };
356
357 type A = Naive;
358 type Thunk = Box<dyn FnOnce() -> Result<Any<A>, Any<A>>>;
359
360 /// A thunk that must not be forced.
361 fn boom() -> Result<Any<A>, Any<A>> {
362 Err("boom".into())
363 }
364 /// A fresh arguments array, for a chain whose answer is not the array.
365 fn args() -> Result<Any<A>, Any<A>> {
366 Ok([7.0.to_any()].to_array().to_any())
367 }
368 /// Asserts that `chain`, handed a thunk over a fresh arguments array,
369 /// answers that very array: a call of `identity` reached its callee.
370 /// Arrays compare by identity, so the array is made once and cloned.
371 fn answers_arguments(chain: impl FnOnce(Thunk) -> Result<Any<A>, Any<A>>) {
372 let a = args().unwrap();
373 let thunk = a.clone();
374 assert_eq!(chain(Box::new(|| Ok(thunk))), Ok(a));
375 }
376 /// `{ f: identity, n: 1, u: undefined }`
377 fn object() -> Any<A> {
378 [
379 ("f".into(), identity()),
380 ("n".into(), 1.0.to_any()),
381 ("u".into(), Any::undefined()),
382 ]
383 .to_object()
384 .to_any()
385 }
386 fn key(k: &str) -> impl FnOnce() -> Result<Any<A>, Any<A>> {
387 let k: Any<A> = k.into();
388 || Ok(k)
389 }
390
391 // `PropertyLambda`
392
393 /// `a.b` — `end` is the read, throw included.
394 #[test]
395 fn property_end() {
396 assert_eq!(object().dot("n".into()).end(), Ok(1.0.to_any()));
397 assert_eq!(
398 Any::<A>::undefined().dot("n".into()).end(),
399 Err(error::nullish_to_object())
400 );
401 }
402
403 /// `a.f(...c)`, and `a.b(...c)` on a nullish `a`: the throw waited in
404 /// the lambda and `c` was never evaluated.
405 #[test]
406 fn property_end_call() {
407 answers_arguments(|args| object().dot("f".into()).end_call(&rt(), args));
408 assert_eq!(
409 Any::undefined().dot("f".into()).end_call(&rt(), boom),
410 Err(error::nullish_to_object())
411 );
412 }
413
414 /// A live, non-callable callee: the arguments run before the
415 /// callability check, so theirs is the throw.
416 #[test]
417 fn property_end_call_forces_arguments_before_callability() {
418 assert_eq!(
419 object().dot("n".into()).end_call(&rt(), boom),
420 Err("boom".into())
421 );
422 assert_eq!(
423 object().dot("n".into()).end_call(&rt(), args),
424 Err(error::unexpected_type())
425 );
426 }
427
428 /// `a.b?.(...c)`: guarded on the property, then a region.
429 #[test]
430 fn property_option_call() {
431 answers_arguments(|args| object().dot("f".into()).option_call(&rt(), args).end());
432 assert_eq!(
433 object().dot("u".into()).option_call(&rt(), boom).end(),
434 Ok(Any::undefined())
435 );
436 assert_eq!(
437 object().dot("n".into()).option_call(&rt(), boom).end(),
438 Err("boom".into())
439 );
440 assert_eq!(
441 Any::undefined()
442 .dot("f".into())
443 .option_call(&rt(), boom)
444 .end(),
445 Err(error::nullish_to_object())
446 );
447 }
448
449 // `OptionLambda`
450
451 /// `a?.(...c).length` and `a?.().f(...d)`: steps inside the region on
452 /// a live value.
453 #[test]
454 fn option_steps_live() {
455 assert_eq!(
456 identity().option_call(args).dot(key("length")).end(),
457 Ok(1.0.to_any())
458 );
459 let returns_object: Any<A> = returns(object());
460 answers_arguments(|args| {
461 returns_object
462 .option_call(|| Ok([].to_array().to_any()))
463 .dot(key("f"))
464 .call(&rt(), args)
465 .end()
466 });
467 }
468
469 /// A skipped region passes through every step, thunks untouched, and
470 /// ends as `undefined`.
471 #[test]
472 fn option_steps_skipped() {
473 assert_eq!(
474 Any::undefined()
475 .option_call(boom)
476 .dot(boom)
477 .call(&rt(), boom)
478 .dot(boom)
479 .end(),
480 Ok(Any::undefined())
481 );
482 }
483
484 /// A thrown region passes through every step, thunks untouched, and
485 /// ends with its throw.
486 #[test]
487 fn option_steps_thrown() {
488 assert_eq!(
489 identity()
490 .option_call(boom)
491 .dot(key("x"))
492 .call(&rt(), args)
493 .end(),
494 Err("boom".into())
495 );
496 }
497
498 // `OptionPropertyLambda`
499
500 /// `a?.f(...c)`, `a?.n.x`, `a?.u?.(...c)`, `a?.f?.(...c)`: the three
501 /// non-terminal steps on a live chain.
502 #[test]
503 fn option_property_steps_live() {
504 answers_arguments(|args| object().option_dot(key("f")).call(&rt(), args).end());
505 assert_eq!(
506 object().option_dot(key("n")).dot(key("x")).end(),
507 Ok(Any::undefined())
508 );
509 assert_eq!(
510 object().option_dot(key("u")).option_call(&rt(), boom).end(),
511 Ok(Any::undefined())
512 );
513 answers_arguments(|args| object().option_dot(key("f")).option_call(&rt(), args).end());
514 }
515
516 /// `a?.b.c(...d).e?.(...f)` on a nullish `a`: one region, everything
517 /// skipped.
518 #[test]
519 fn option_property_steps_skipped() {
520 assert_eq!(
521 Any::undefined()
522 .option_dot(boom)
523 .dot(boom)
524 .call(&rt(), boom)
525 .dot(boom)
526 .option_call(&rt(), boom)
527 .end(),
528 Ok(Any::undefined())
529 );
530 }
531
532 /// `|()` and `|.` are unguarded: on a live but nullish current value
533 /// the thunk is forced and the operation throws — `a?.b()` with `a.b`
534 /// undefined throws, it does not answer `undefined`. One case per
535 /// unguarded step, so that turning any of them into a guarded step
536 /// fails a test.
537 #[test]
538 fn unguarded_steps_throw_on_nullish_current_value() {
539 let returns_undefined: Any<A> = returns(Any::undefined());
540 assert_eq!(
541 returns_undefined.clone().option_call(args).call(boom).end(),
542 Err("boom".into())
543 );
544 assert_eq!(
545 returns_undefined.option_call(args).dot(boom).end(),
546 Err("boom".into())
547 );
548 assert_eq!(
549 object().option_dot(key("u")).call(&rt(), boom).end(),
550 Err("boom".into())
551 );
552 }
553
554 /// `|.` on a live but nullish current value throws at the read — after
555 /// the key is forced, as JavaScript evaluates `u[k()]`.
556 #[test]
557 fn option_property_dot_forces_key_before_read() {
558 assert_eq!(
559 object().option_dot(key("u")).dot(boom).end(),
560 Err("boom".into())
561 );
562 assert_eq!(
563 object().option_dot(key("u")).dot(key("x")).end(),
564 Err(error::nullish_to_object())
565 );
566 }
567
568 /// `(a?.f)(...c)` live, and `(a?.b)(...c)` on a nullish `a`: the
569 /// parentheses end the region, so the arguments run and `undefined` is
570 /// called — the case the host engines disagree on, and the
571 /// specification's reading.
572 #[test]
573 fn option_property_end_call() {
574 answers_arguments(|args| object().option_dot(key("f")).end_call(&rt(), args));
575 assert_eq!(
576 Any::undefined().option_dot(boom).end_call(&rt(), boom),
577 Err("boom".into())
578 );
579 assert_eq!(
580 Any::undefined().option_dot(boom).end_call(&rt(), args),
581 Err(error::unexpected_type())
582 );
583 }
584
585 /// A thrown chain stays thrown through `end_call`, arguments untouched.
586 #[test]
587 fn option_property_end_call_thrown() {
588 assert_eq!(
589 object().option_dot(boom).end_call(&rt(), boom),
590 Err("boom".into())
591 );
592 }
593}