?
u
/
p
1-9
for
loop (This specification consists of the following parts:
is
expressionmatch
Expressionnew
semantics changes:
for
-in
, for
-of
, and for
-await
-of
Statementstry
StatementTrivia built-in matchers are folded. Click to not show the trivia sections.
The pattern-matching champion group designed this proposal with a layering approach. It does not mean the proposal is an MVP. The champion group wishes to ship the proposal as a whole when possible, but we can drop some features if there is strong pushback from the committee.
This approach allows the champion group to consider how all features combine and also how the proposal should behave if any of the features are missing.
A feature will have a note if
Clause
Specification Name | [[Description]] | Value and Purpose |
---|---|---|
%Symbol.customMatcher% |
"Symbol.customMatcher"
|
A method that performs custom pattern matching semantics. Called by the semantics of the pattern-matching features. |
All objects have an internal slot named [[ConstructedBy]], which is a
The abstract operation InitializeInstanceElements takes arguments O (an Object) and constructor (an ECMAScript
The
The
The
The
The
The
The
The
The
The
The
The
The
At any time, if a set of objects and/or symbols S is not
The following are the additions of the restricted productions in the grammar:
See match
Expression
const isOk = response is { ok: true, status: > 200 and < 400 };
match
Expressionconst isOk = match (response) {
{ ok: true, status: > 200 and < 400 }: true,
default: false
};
~=
instead of a is
. See waldemarhorwat/syntax@main/contextual-keywords.md
and Syntax effects on rest of the language.
for
-in
, for
-of
, and for
-await
-of
Statementsfor
iteration statements. It might look like this:
for (const response of responses) {
if (item is { ok: true, let body }) {
}
}
// can be written as
for (const response is { ok: true, let body } of responses) {
}
// or
for (const response of responses matches { ok: true, let body }) {
}
try
Statementtry
statement. It might look like this:
try { }
catch (error) {
if (error is { message: /JSON/ }) { return null; }
throw error;
}
// can be written as
try { }
catch (error is { message: /JSON/ }) { return null; }
// unmatched error will be re-thrown.
The
The
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
// For non-class functions.
[] is Array.isArray; // true, by Array.isArray(expr)
// For objects created by `new`, it uses private-field-like semantics.
class MyError extends Error {}
const myError = new MyError();
myError is MyError; // true
myError is Error; // true
Object.create(MyError.prototype) is MyError; // false
// Also works for normal functions
function ES5StyleClass() {}
new ES5StyleClass() is ES5StyleClass; // true
Object.create(ES5StyleClass.prototype) is ES5StyleClass; // false
This does not work with ES5 style class inherit.
function MyError() {
Error.call(this);
}
MyError.prototype = Object.create(Error.prototype);
var error = new MyError();
error is MyError; // true
error is Error; // false
Not everyone in the champion group agrees with private-field-like brand check semantics.
There are performance concerns, "hackable" concerns, and interaction with %Symbol.hasInstance% concerns.
Another approach is to use the instanceof
semantics.
This function performs the following steps when called:
The initial value of Symbol.customMatcher
is the well-known symbol
This property has the attributes { [[Writable]]:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
if (expr is Error(let message, { let cause })) {}
Error instances are a String. The only specified uses of
[[ErrorData]] is to identify Error, AggregateError, and NativeError instances as Error objects within
Object.prototype.toString
and their
Each NativeError function performs the following steps when called:
"%NativeError.prototype%"
, « [[ErrorData]] »).NativeError instances are a String. The only specified use
of [[ErrorData]] is by Object.prototype.toString
(
This function performs the following steps when called:
AggregateError instances are a String. The only specified use
of [[ErrorData]] is by Object.prototype.toString
(
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
let regex = /(?<id>\d+)-?/g
'012-345' is regex(["012-", "012"], { groups: { id: "345" } });
// true, match with %Symbol.matchAll%
let regex2 = /(?<id>\d+)-?/
'012-345' is regex({ groups: { id: "012" } });
// true, match with %Symbol.match%
This function performs the following steps when called:
if (expr is Array(1, 2, 3, 4)) {}
This function performs the following steps when called:
const isPNG = binary is Uint8Array(
0x89, 0x50, 0x4E, 0x47,
0x0D, 0x0A, 0x1A, 0x0A, ...
); // the ... is necessary otherwise it will only match a length-8 binary.
This function performs the following steps when called:
expr is Map([[1, 2], [3, 4]]);
// matches new Map([[1, 2], [3, 4]])
// but not new Map([[3, 4], [1, 2]])
This function performs the following steps when called:
expr is Set([1, 2, 3]);
// matches new Set([1, 2, 3])
// but not new Set([3, 2, 1])
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
if (expr is WeakRef(let object)) {}
This function performs the following steps when called:
This function performs the following steps when called:
This function performs the following steps when called:
All built-in
This is not a consensus in the champion group.
This production will be added by discard bindings proposal.
Some of the committe members prefer _
as the discard binding identifier.
if (expr is [let x, void, void]) {}
// can be written as:
if (expr is [let x,,,]) {}
// or
if (expr is [let x, let _y, let _z,]) {}
if (value is null) {};
if (value is true) {};
if (value is 1_000_000) {};
if (value is "string") {};
if (value is `line 1
line 2`) {};
Preserve the interpolation syntax for (1) concerns about arbitrary expression interpolation and (2) intuitive string matching.
For example, match "com.example:method()"
by
if (qualifiedName is `${isPackageName and let pkg}:${isIdentifier and let method}()`) {}
if (expr is { kind: let kind }) {}
return match (expr) {
[let left, let op, let right]: evaluate(op, left, right);
};
if (expr is { x: undefined, y: undefined }) {}
if (expr is { angle: Math.PI, let length }) {}
if (expr is { ok: true, value: this.#lastValue }) {}
if (expr === import.meta?.hot?.data && expr is { let previousData }) {}
if (expr is WeakRef(let object)) {}
if (value is { version: 2, data: this.#data }) {};
// can be written as:
const data = this.#data;
if (expr is { version: 2, data: data }) {}
It is similar to the DecoratorMemberExpression.
Should we allow MemberExpressionPattern[expr]
to match with computed property names?
It does not clear if this should match
Object matchers
if (expr is { version: 2, type: String, value?: void }) {}
// test if subject[Symbol.iterator] is present.
if (expr is { [Symbol.iterator]?: Function }) {}
if (expr is { 0: String }) {}
Array matchers
if (expr is ["request", let method, ...let params]) {}
// match an iterable that first 2 values match a and b and no more values.
if (expr is [a, b]) {}
// match an iterable that first 2 values match a and b.
if (expr is [a, b, ...]) {}
class MyClass {
#field;
test(data) {
if (data is { value: { #field: let field } }) {}
// can be written as
if (data is { let value: MyClass }) {
let field = value.#field;
}
}
}
if (expr is { type: "report", let key }) { reportKey(key); }
else { reportKey(Symbol.for("missing")) };
// can be written as
if (expr is { type: "report", let key = Symbol.for("missing") }) { reportKey(key); }
The ?
in the production
The feature is not a consensus in the champion group yet.
if (expr is { let x, let y? }) {}
// can be written as
if (expr is { let x }) {}
else if (expr is { let x, let y }) {}
if (expr is [x, y?]) {}
// can be written as
if (expr is [x]) {}
else if (expr is [x, y]) {}
These productions can be removed.
This feature is not a consensus in the champion group yet.
if (expr is { version: 2, let y: String }) {}
// can be written as
if (expr is { version: 2, y: String and let y }) {}
Computed properties can be removed from this production, but it will be harder to match computed properties.
if (expr is { let length, [Symbol.iterator]: Function }) {}
// can be written as
function isIterable(value) { return typeof value[Symbol.iterator] === "function"; }
if (expr is { let length } and isIterable) {}
if (expr is -0) {} // not matching `+0` and vice versa
if (expr is { x: -Infinity, y: -Infinity }) {}
if (expr is { angle: -Math.PI, let length }) {}
Only literal +0
or -0
will be matched with +
x and -
x where x is 0 will be matched with
+0
and -0
.
The code example above can be written as:
const negPi = -Math.PI;
if (Object.is(expr, -0)) {} // not matching `+0` and vice versa
if (expr is { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY }) {}
if (expr is { angle: negPi, let length }) {}
if (expr is > -10 and < 10) {}
if (expr is { value: instanceof Error and { let message } }) {}
if (expr is [=== Array, === Object]) {}
It is possible to add the following production.
The recommended way to match a String is to use the typeof
test.
if (expr is { version: 2 or 3, value: String }) {}
// can be written as this if built-ins might be replaced:
const isString = {
[Symbol.customMatcher](val) {
return typeof val === "string";
}
};
if (expr is { version: 2 or 3, value: isString }) {}
It is possible to add the following production.
function hasPrivateField(val) { return #field in val; }
if (expr is { version: 2 or 3, data: hasPrivateField }) {}
// can be written as:
if (expr is { version: 2 or 3, data: has #field }) {}
This feature can be a proposal on its own, to be the in
operator reversed.
if (expr has #field) { }
if (expr hasOwn "prototype") { }
The instanceof
production can be removed, but it will be harder to match by instanceof
semantics.
This feature is not a consensus in the champion group.
The class matchers match by private-field semantics,
the instanceof
match is an escape hatch to match with the instanceof
semantics.
if (expr is { elements: [instanceof Map, instanceof Map] }) {}
// can be written as:
function isInstanceOfMap(val) { return val instanceof Map; }
if (expr is { elements: [isInstanceOfMap, isInstanceOfMap] }) {}
The ==
, !=
, ===
, and !==
production can be removed,
but it will be harder to match by ===
semantics when the
This feature is not a consensus in the champion group.
if (expr is { type: "create", initializers: [createX] }) {}
The code above will try to call createX as a custom matcher.
This feature is an opt-out of the custom matcher, to always do the ===
check.
if (expr is { type: "create", initializers: [=== createX] }) {}
// can be written as:
function isCreateX(val) { return val === createX; }
if (expr is { type: "create", initializers: [isCreateX] }) {}
The in
production can be removed, but it will be harder to match by in
semantics.
This feature is not a consensus in the champion group.
if (expr is { let key: in object } and inCache) {}
// can be written as:
function isInObj(key) { return key in object; }
if (expr is { let key: isInObj } and inCache) {}
The >
, <
, >=
and <=
production can be removed, but it will be harder to match numbers.
This feature is not a consensus in the champion group.
if (expr is [> -Math.PI and < Math.PI, > 0]) {}
// can be written as:
if (expr[0] > -Math.PI && expr[0] < Math.PI && expr[1] > 0) {}
return match (expr) {
{ let x, let y, let z } and if (norm(x, y, z) < maxLength): [x, y];
{ let x, let y } and if (norm(x, y) < maxLength): [x, y];
Number and < maxLength let length: [length];
}
if (expr is { version: 2 or 3 }) {}
if (expr is { version: Number and not 1 }) {}
if (expr is { __proto__: null, property?: void }) {} // Syntax Error
if (expr is { "__proto__": null, property?: void }) {} // Syntax Error
if (expr is { ["__proto__"]: null, property?: void }) {} // no Syntax Error
if (expr is { x: 0, y: 0, ...rest }) {} // Syntax Error
if (expr is { x: 0, y: 0, ...let rest }) {} // no Syntax Error, bind rest properties to _rest_
if (expr is { x: 0, y: 0, ...(isEmpty) }) {} // no Syntax Error, call isEmpty with the rest object
if (expr is { x, y, z }) {} // Syntax Error
if (expr is { x: void, y: void, z: void }) {} // no Syntax Error
if (expr is { let x, let y, let z }) {} // no Syntax Error
if (expr is { if }) {} // no Syntax Error
value is [1, 2?, 3]; // Syntax Error
value is [1, 2?, 3?]; // no Syntax Error
value is [1, 2?, , ]; // Syntax Error (Elision)
value is [1, 2?, void?, ]; // no Syntax Error
value is [1, 2?, ...]; // no Syntax Error
value is a and b and c; // no Syntax Error
value is a or b or c; // no Syntax Error
value is a and b or c; // Syntax Error
value is (a and b) or c; // no Syntax Error
value is a and (b or c); // no Syntax Error
value is not not a; // Syntax Error
value is not (not a); // no Syntax Error
value is not a or b; // Syntax Error
value is not (a or b); // no Syntax Error
The
?
is present, return The
The
The
var
, let
or const
based on The
The
The
?
is present, return « ».?
is present, return « ».?
is present, return « ».var
, let
or const
based on ?
is present, return « ».var
, let
or const
based on The
The
The
?
is present, return 1.The
+
-
Only literal +0
or -0
will be matched with +
x and -
x where x is 0 will be matched with
The
The
The
match
Expression
When processing an instance of the production
the interpretation of
const result = match (value) {
{ op: "add", let lhs, let rhs }: lhs + rhs,
{ op: "sub", let lhs, let rhs }: lhs - rhs,
{ op: "mul", let lhs, let rhs }: lhs * rhs,
{ op: "div", let lhs, let rhs }: lhs / rhs,
}
If the do
expression proposal will never happen,
we will need to add a statement version of
match (value) {
{ let x, let y, let z }: do {
const w = average(x, y, z);
return w * w;
},
// ...
}
is
expressionconst result =
value is { op: "add", var lhs, var rhs } ? lhs + rhs :
value is { op: "sub", var lhs, var rhs } ? lhs - rhs :
value is { op: "mul", var lhs, var rhs } ? lhs * rhs :
value is { op: "div", var lhs, var rhs } ? lhs / rhs :
(() => { throw new TypeError() })
default
clauses.default
clause is not the final clause.The
The
The abstract operation InvokeCustomMatcher takes arguments matcher (an
this
value when calling the custom matchers. Not everyone in the champion group agrees we need to keep the this
value.
const zero = new BigNumber(0);
match (expr) {
zero.equal: console.log('zero point matched.');
BigNumber: console.log(expr.toString() + ' left.');
}
function f() { return document.all; }
if (null is f) {}
// not match, by ToBoolean
if (null is f(let html, ...)) {}
// match, because document.all is an object and has [Symbol.iterator].
The abstract operation ValidateCustomMatcherHint takes argument hint (an
The abstract operation CreateMatchCache takes no arguments and returns a
The abstract operation GetMatchCache takes arguments subject (an
The abstract operation HasPropertyCached takes arguments subject (an
The abstract operation GetCached takes arguments subject (an
The abstract operation GetIteratorCached takes arguments subject (an
The abstract operation IteratorStepCached takes arguments iterator (an
The abstract operation GetIteratorNthValueCached takes arguments iterator (an
The abstract operation FinishListMatch takes arguments iterator (an
This abstract operation is called the final step of the evaluation of
For example, when matching with []
, the expectedLength is 0.
This abstract operation checks if cachedLength is 0.
If the iterator is done, the match succeeds.
If the iterator is not done, it will try to get the 0th value from the iterator.
If there is a 0th value, the match fails.
Another example is when matching with [1, 2, 3]
, the expectedLength is 3.
This abstract operation checks if cachedLength is 3.
If the iterator is done, the match succeeds.
If the iterator is not done, it will try to get the 3th (remember we start from the 0th) value from the iterator.
If there is a 3rd (which means the cachedLength is now 4) value, the match fails.
The abstract operation FinishMatch takes arguments matchCompletion (a
If any error occurs when closing iterators, this abstract operation ignores the matchCompletion and returns an
for (const iterator of iterators) {
try {
match(iterator) {
[String, ...] and [...let list]: callback(list);
[...]: continue;
}
} catch {
}
}
In the code example above,
if the second branch matches (and returns a continue
will be ignored,
and the catch
block will be evaluated.
Object in pattern-matching:
Array in pattern-matching:
Limited
© 2024 Daniel Rosenwasser,Jack Works,Jordan Harband,Mark Cohen,Ross Kirsling,Tab Atkins
All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.