Optional Chaining

Status: implemented; the language-design approval record is still open

The behavior below was added to the compiler and specification in #2660. Its required explicit language-design approval is not recorded; the approval-process follow-up remains open.

const o = { a: { b: 1 }, f: (...x) => x };
const n = null;
export default [o?.a.b, n?.a.b, o.f?.(2), n?.(3), (o?.a).b];

An optional chain is JavaScript's: v?.k and v?.[k] read the property v.k and v[k] read, with the key the same constant either takes (property access), and v?.(…) calls v as v(…) does — unless v is null or undefined, when the whole chain is undefined and nothing after the ?. is evaluated: not the key, not the arguments, and not the steps written after it. n?.a.b is undefined under a nullish n, where n.a.b throws, and n?.f(g()) never calls g. The steps after a ?. are the chain's own until a parenthesis ends it, so (n?.a).b reads b of undefined and throws — the one thing a group is observable through (grouping) — and the value is JavaScript's in every spelling: a?.b.c and (a?.b).c are two programs, a?.b?.(c) and (a?.b)?.(c) one, since a region closed before a guard is unobservable.

A call keeps its receiver through a ?. as it keeps it through a .: a?.b(c) and a.b?.(c) call b with a as this, so a built-in member function is called through either, and (a?.b)(c) keeps it too, as (a.b)(c) does. The key is judged as an access's is — a prototype name refused as a read, a?.at, and allowed as a call, a?.at(0); a member function a module may not call refused at its key, a?.push(1); length read from any value — and a key computed at run time is the conversion, a?.[Number(i)], as it is after . (property access), established only where the guard lets the chain go on.

The graph is fjs/edag/README.md's Chains, where every spelling has one shape and the host engine agrees with it: a?.b is ['?.', a, 'b'], the steps after it the node's continuation, ['?.', a, 'b', ['|.', 'c']] for a?.b.c, and a group an access over the node, ['.', ['?.', a, 'b'], 'c']. a.b?.(c) is the access's own step, ['.', a, 'b', ['|?.()', [c]]], and a?.(c) the call node ['?.()', a, [c]]. The graph outputs write them: the FunctionalScript one as above, a group where the region closed; the EDAG one as the nodes; the Rust one as the method chain nanvm-lib runs. Value outputs evaluate the chain and serialize its selected result. A ?. directly before a decimal digit is the token ?. here, as the tokenizer reads it (?. before a digit): a?.5:1 is refused at the 5, and will be the conditional it is in JavaScript when .5 is a number.

Examples

The compiler demo offers the programs optionalChaining in module.f.js, each with its real output.

The demo on this page runs sample.f.js, this page's example as a function of one number — optional property reads and calls on an object, and on null — with every output fjs compile makes of it: the expression graph (EDAG), the Rust and the WebAssembly that Rust builds to. Its Run button calls the default export with 0 and 1, as JavaScript and as WebAssembly, and shows what each call returned or threw.

const o = { a: { b: 1 }, f: (...x) => x };
export default (...a) => {
    const v = [o, null][Number(a[0])];
    return [v?.a.b, v?.f(2), v?.f?.(3)];
};

Reference

Demo

In FunctionalScript

fjs compile accepts this module, and it runs as JavaScript and as WebAssembly alike.

A FunctionalScript module and what fjs compile makes of it: the expression graph it lowers to, the Rust it compiles to on the NaNVM runtime, and the WebAssembly that Rust builds to. Run calls its default export with 0, then 1, first as the JavaScript it already is and then as WebAssembly, and shows what each call returned or threw and how long one call takes, timed over 1000 calls.

FunctionalScript source:

/**
 * The example of [`README.md`](./README.md) (Optional Chaining) as a
 * function of one number, for the page's WASM demo: optional property reads
 * and calls on an object, and on `null`.
 *
 * @module
 */

const o = { a: { b: 1 }, f: (/** @type {readonly number[]} */ ...x) => x };
/** @type {(...a: readonly number[]) => unknown} */
export default (...a) => {
    const v = [o, null][Number(a[0])];
    return [v?.a.b, v?.f(2), v?.f?.(3)];
};

Expression graph (EDAG) that fjs compile lowers it to:

=>lengthframe 0body0{}[]{}af[]012{}b1=>lengthbody0rest?..objkeykey."a""b"?.()objkey...."f"[]?.?.()objkey...."f"[].objidx[]02[]03[]01frame 0nullNumberoperand.objidxrest0

The same graph, as fjs compile writes it to a .edag.data.js file:

const $0=[".",["[]",[["frame",0],null]],["Number",[".",["rest"],0]]];export default ["{}",[[":","default",["=>",0,[["{}",[[":","a",["{}",[[":","b",1]]]],[":","f",["=>",0,[],["rest"]]]]]],["[]",[["?.",$0,"a",["|.","b"]],["?.",$0,"f",["|()",[2]]],["?.",$0,"f",["|?.()",[3]]]]]]]]];

Rust that fjs compile writes from it, on the NaNVM runtime:

// @generated by `fjs compile`. Do not edit: recompile the source module instead.

use nanvm_lib::vm::unstable::{f64_any, string_any, string_key};
use nanvm_lib::vm::{Any, IStaticFunction, Nullish, ToAny, ToArray, ToObject};

#[rustfmt::skip]
#[allow(clippy::if_same_then_else)]
pub fn module<A: IStaticFunction>(runtime: Any<A>) -> Result<Any<A>, Any<A>> {
    Ok([(string_key("default"), {
        let _b3: Any<A> = [(string_key("a"), [(string_key("b"), f64_any(0x3ff0000000000000))].to_object().to_any()), (string_key("f"), A::static_function(|_self, _args| {
            let _b128: Any<A> = _args.rest(0).to_any();
            Ok(_b128.clone())
        }, 0, [runtime.clone()].to_array(), Some("(...$0)=>$0")).to_any())].to_object().to_any();
        A::static_function(|_self, _args| {
            let _b704: Any<A> = _args.rest(0).to_any();
            let _b9: Any<A> = (Any::dot([A::frame(_self)[0].clone(), Nullish::Null.to_any()].to_array().to_any(), (Any::number((Any::dot(_b704.clone(), f64_any(0x0000000000000000)).end())?))?).end())?;
            Ok([(Any::option_dot(_b9.clone(), || Ok(string_any("a"))).dot(|| Ok(string_any("b"))).end())?, (Any::option_dot(_b9.clone(), || Ok(string_any("f"))).call(&A::frame(_self)[1], || Ok([f64_any(0x4000000000000000)].to_array().to_any())).end())?, (Any::option_dot(_b9.clone(), || Ok(string_any("f"))).option_call(&A::frame(_self)[1], || Ok([f64_any(0x4008000000000000)].to_array().to_any())).end())?].to_array().to_any())
        }, 0, [_b3.clone(), runtime.clone()].to_array(), Some("(...$1)=>{const $2=[$0,null][Number($1[0])];return [$2?.a.b,$2?.f(2),$2?.f?.(3)];}")).to_any()
    })].to_object().to_any())
}

WebAssembly text (WAT) of the functions compiled from this source, without the NaNVM runtime:

Refused:

File size 191031 exceeds maximum allowed size of 131072 bytes: 'nanvm/demos/wasm/optional_chaining.wat.marked'

Emergent Testing

Idle. Press Run to start the suite.

  • ./proof.f.mjs