This is a strawman for an advanced generator-comprehension syntax for ECMAScript based in the LINQ syntax. The syntax allows for custom semantics for evaluation, as each clause in a query is translated into regular function calls for special symbol-named methods that are evaluated if found on the sequence (see Abstract Operations for more information).
const q = from user of users
where user.role === "guest"
select user;
for (const user of q) {
...
}You can see a proof-of-concept of this syntax using tagged templates at https://github.com/rbuckton/iterable-query-linq.
- Query – One or more Ranges defined using integrated query syntax that produce a resulting Sequence.
- Sequence – A value that is either an Iterable (i.e. having an
@@iteratormethod), or an AsyncIterable (i.e. having an@@asyncIteratormethod). - Outer sequence – The sequence produced by the Range for any preceeding query body clauses.
- Inner sequence – A related sequence produced by the current query body clause.
- Range – The sequence and any related range variables bound to the sequence.
- A Range begins with a
from,join,join into, orintoclause and ends at the nextgrouporselectclause. - The first Range in a QueryExpression must begin with a
fromclause. - A Range is the lexical scope for a range variable.
- A Range is has the following internal slots:
[[Source]]– The underlying Sequence for the Range.[[Bindings]]– The bound names for any Range Variables in scope within the Range.[[Async]]– Abooleanvalue indicating whether the Range is asynchronous (i.e. contains afromorjoinclause with anawaitmodifier).
- A Range begins with a
- Range Variable – A variable binding that is lexically scoped to the current Range.
- The value for a Range Variable will be based on the per-iteration value of the Range.
- A Range Variable may be a BindingIdentifier, ObjectBindingPattern, or ArrayBindingPattern.
- Grouping – An Object that has an
@@groupingmethod (see CreateGrouping).- When called, the
@@groupingmethod should return an object with the following properties:key– The key for the current group of results.values– An Iterable containing the values in the group.
- For convenience, a runtime-generated Grouping will define the following properties:
key– The key for the current group of results.values– An Iterable containing the values in the group.@@grouping– A method that returnsthis.@@iterator– A method that returnsthis.values[@@iterator]().
- When called, the
A Query starts with a from clause, consists of zero or more query body clauses, and ends with either a select or group clause.
The from clause begins a QueryExpression, binding a range variable that refers to each element of a sequence:
from x of y ...
- Introduces a range variable (
x) over an Iterable (y). - When evaluated, the runtime will execute the QuerySelect abstract operation with the following
arguments:
outer– The sequence (y).selector– A runtime-generated function that evaluates and introduces the new range variable (i.e.(x) => ({ x })).async– A value indicating whether theawaitmodifier was present (see Asynchronous Ranges, below).
A from clause may also bind a range variable over an AsyncIterable by applying the await modifier, similar to
for await of. Unlike for await of, the await modifier supported outside of an async function.
from await x of y ...
If any from clause in a Range includes the await keyword, the entire Range is converted into an AsyncIterable.
Subsequent from clauses result in a cartesian-join:
... from x of y ...
- Introduces a new range variable (
x) over an inner sequence (y). - The resulting Range is a cartesian-join of the outer sequence and the inner sequence.
- When evaluated, the runtime will execute the QuerySelectMany abstract operation with the following arguments:
outer– The outer sequence.projection– A runtime-generated function that evaluates and returns the inner sequence (y) for each element (a) of the outer sequence (i.e.({ a }) => y).resultSelector– A runtime-generated function that binds the range variables of both the outer sequence (a) and inner sequence (x) (i.e.({ a }, x) => ({ a, x })).async– A value indicating whether theawaitmodifier was present in this Range or any outer Range (see Asynchronous Ranges, below).
- Cartesian Joins may also use the
awaitmodifier.
The join clause is used to define a one-to-one relationship between the elements of two Iterables.
All joins are performed using an "equijoin", comparing the the keys selected from the outer sequence
and inner sequence using the SameValueZero abstract operation:
... join x of y on a.key equals x.key ...
- Introduces a new range variable
xover an inner sequencey. - The resulting Range is an inner-join of the outer sequence and the inner sequence.
- When evaluated, the runtime will execute the QueryJoin abstract operation with the following arugments:
outer– The outer sequence.inner– The inner sequence (i.e.y)outerKeySelector– A runtime-generated function that selects the key for the outer sequence (i.e.({ a }) => a.key)innerKeySelector– A runtime-generated function that selects the key for the inner sequence (i.e.(x) => x.key)resultSelector– A runtime-generated function that binds the range variables (a) of the preceeding Range and the new range variable (x) of thejoinclause (i.e.({ a }, x) => ({ a, x })).async– A value indicating whether theawaitmodifier was present in this Range or any outer Range (see Asynchronous Ranges, below).
- The
joinclause also supports theawaitmodifier:... join await x of y ...
The join into clause is used to define a one-to-many relationship between the elements of two
Iterables. All joins are performed using an "equijoin", comparing the the keys selected
from the outer sequence and inner sequence using the SameValueZero abstract operation:
... join x of y on a.key equals x.key into z ...
- Introduces a new range variable (
x) over an inner sequence (y) that is scoped only to thejoinclause itself. - Introduces a new range variable (
z) containing elements of the inner sequence related to each element of the outer sequence. - The resulting Range is a left-join of the outer sequence and the inner sequence.
- When evaluated, the runtime will execute the QueryGroupJoin abstract operation with the following arugments:
outer– The outer sequence.inner– The inner sequence (i.e.y)outerKeySelector- A runtime-generated function that selects the key for the outer sequence (i.e.({ a }) => a.key)innerKeySelector- A runtime-generated function that selects the key for the inner sequence (i.e.(x) => x.key)resultSelector- A runtime-generated function that binds the range variables (a) of the preceeding Range and the new range variable (z) of thejoin intoclause (i.e.({ a }, z) => ({ a, z })).async– A value indicating whether theawaitmodifier was present in this Range or any outer Range (see Asynchronous Ranges, below).
- The
join intoclause also supports theawaitmodifier:... join await x of y ... into z ...
The let clause introduces a new range variable into the current Range:
... let x = a.y ...
- Introduces a new range variable (
x) over the outer sequence. - The resulting Range combines the range variables of the preceeding range with the new range variable.
- When evaluated, the runtime will execute the QuerySelect abstract operation with the following arguments:
outer– The outer sequence.selector– A runtime-generated function that binds the range variables (a) of the preceeding Range and the new range variable (x) of theletclause (i.e.({ a }) => ({ a, x: a.y })).async– A value indicating whether theawaitmodifier was present in the outer Range (see Asynchronous Ranges, below).
The where clause filters the current Range, skipping items that do not match the supplied criteria:
... where a.id === 1 ...
- When evaluated, the runtime will execute the QueryWhere abstract operation with the following arguments:
outer– The outer sequence.predicate– A runtime-generated function that evaluates the filter expression against the range variables of the current range (i.e.({ a }) => a.id === 1).async– A value indicating whether theawaitmodifier was present in the outer Range (see Asynchronous Ranges, below).
The orderby clause is used to sort the preceeding Range:
... orderby a.lastName, a.age descending ...
- Composed of one or more comma-separated comparators.
- A comparator is a selector expression followed by either the
ascending(optional) ordescendingkeyword. - When the first comparator is evaluated, the runtime will execute the QueryOrderBy abstract operation with the
following arguments:
outer– The outer sequence.keySelector– A runtime-generated function that selects the expression to use as the sort key (i.e.({ a }) => a.lastName).descending–trueif thedescendingkeyword was present.async– A value indicating whether theawaitmodifier was present in the outer Range (see Asynchronous Ranges, below).
- When each remaining comparator is evaluated, the runtime will execute the QueryThenBy abstract operation with
the following arguments:
outer– The sequence produced by the previous comparator.keySelector– A runtime-generated function that selects the expression to use as the sort key (i.e.({ a }) => a.lastName).descending–trueif thedescendingkeyword was present.async– A value indicating whether theawaitmodifier was present in the outer Range (see Asynchronous Ranges, below).
The group clause is used to group elements of the preceeding Range by a specific key:
... group a.firstName by a.lastName
- Groups the elements of the outer sequence by the provided key selector (
x.lastName). - Each element in the group is mapped using the provided element selector (
x.firstName). - Keys are compared using the SameValueZero abstract operation.
- The final result is a sequence of Groupings.
- A
groupclause can end a Query. - A
groupclause may start a new Query by appending anintoclause (see below). - When evaluated, the runtime will execute the QueryGroupBy abstract operation with the
following arguments:
outer– The outer sequence.keySelector– A runtime-generated function that selects the key used to group the results (i.e.({ a }) => a.lastName).elementSelector– A runtime-generated function that selects the value for each element in the group (i.e.({ a }) => a.firstName).async– A value indicating whether theawaitmodifier was present in the outer Range (see Asynchronous Ranges, below).
The select clause is used to select the results for each element of the preceeding Range:
... select a.fullName
- Selects the elements of the outer sequence by the provided selector (
x.fullName). - The final result is a sequence of elements produced by the selector.
- A
selectclause can end a Query. - A
selectclause may start a new Query by appending anintoclause (see below). - When evaluated, the runtime will execute the QuerySelect abstract operation with the
following arguments:
outer– The outer sequence.selector– A runtime-generated function that selects the value for each element in the outer sequence (i.e.({ a }) => a.fullName).async– A value indicating whether theawaitmodifier was present in the outer Range (see Asynchronous Ranges, below).
An into clause may follow a select or group clause to start a new top-level Range
using the elements of the outer sequence bound to a fresh range variable:
... group x by x.role into y ...
... select x.fullName into y ...
- Introduces a new range variable (
y) over the outer sequence.
The following are rough outlines of the abstract operations mentioned above.
TODO: Detailed algorithm steps.
// approximate algorithm
function QuerySelect(outer, selector, async) {
if (async) {
const selectAsync = outer[@@selectAsync];
if (selectAsync) return selectAsync.call(outer, selector);
return {
async * [@@asyncIterator]() {
for await (const outerElement of outer) {
yield selector(outerElement);
}
}
};
}
else {
const select = outer[@@select];
if (select) return select.call(outer, selector);
return {
* [@@iterator]() {
for (const outerElement of outer) {
yield selector(outerElement);
}
}
};
}
}TODO: Detailed algorithm steps.
// approximate algorithm
function QuerySelectMany(outer, projection, resultSelector, async) {
if (async) {
const selectManyAsync = outer[@@selectManyAsync];
if (selectManyAsync) return selectManyAsync.call(outer, projection, resultSelector);
return {
async * [@@asyncIterator]() {
for await (const outerElement of outer) {
const inner = projection(outerElement);
for (await const innerElement of inner) {
yield resultSelector(outerElement, innerElement);
}
}
}
};
}
else {
const selectMany = outer[@@selectMany];
if (selectMany) return selectMany.call(outer, projection);
return {
* [@@iterator]() {
for (const outerElement of outer) {
const inner = projection(outerElement);
for (const innerElement of inner) {
yield resultSelector(outerElement, innerElement);
}
}
}
};
}
}TODO: Detailed algorithm steps.
// approximate algorithm
function QueryJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, async) {
if (async) {
const joinAsync = outer[@@joinAsync];
if (joinAsync) return joinAsync.call(outer, inner, outerKeySelector, innerKeySelector, resultSelector);
return {
async * [@@asyncIterator]() {
const groupings = await CreateGroupingsAsync(inner, innerKeySelector, _ => _);
for await (const outerElement of outer) {
const outerKey = outerKeySelector(outerElement);
if (groupings.has(outerKey)) {
const innerElements = groupings.get(outerKey);
for (const innerElement of innerElements) {
yield resultSelector(outerElement, innerElement);
}
}
}
}
};
}
else {
const join = outer[@@join];
if (join) return join.call(outer, inner, outerKeySelector, innerKeySelector, resultSelector);
return {
* [@@iterator]() {
const groupings = CreateGroupings(inner, innerKeySelector, _ => _);
for (const outerElement of outer) {
const outerKey = outerKeySelector(outerElement);
if (groupings.has(outerKey)) {
const innerElements = groupings.get(outerKey);
for (const innerElement of innerElements) {
yield resultSelector(outerElement, innerElement);
}
}
}
}
};
}
}TODO: Detailed algorithm steps.
// approximate algorithm
function QueryGroupJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, async) {
if (async) {
const joinAsync = outer[@@joinAsync];
if (joinAsync) return joinAsync.call(outer, inner, outerKeySelector, innerKeySelector, resultSelector);
return {
async * [@@asyncIterator]() {
const groupings = await CreateGroupingsAsync(inner, innerKeySelector, _ => _);
for await (const outerElement of outer) {
const outerKey = outerKeySelector(outerElement);
if (groupings.has(outerKey)) {
const innerElements = groupings.get(outerKey);
yield resultSelector(outerElement, innerElements);
}
}
}
};
}
else {
const join = outer[@@join];
if (join) return join.call(outer, inner, outerKeySelector, innerKeySelector, resultSelector);
return {
* [@@iterator]() {
const groupings = CreateGroupings(inner, innerKeySelector, _ => _);
for (const outerElement of outer) {
const outerKey = outerKeySelector(outerElement);
if (groupings.has(outerKey)) {
const innerElements = groupings.get(outerKey);
yield resultSelector(outerElement, innerElement);
}
}
}
};
}
}TODO: Detailed algorithm steps.
// approximate algorithm
function QueryWhere(outer, predicate, async) {
if (async) {
const whereAsync = outer[@@whereAsync];
if (whereAsync) return whereAsync.call(outer, predicate);
return {
async * [@@asyncIterator]() {
for await (const outerElement of outer) {
if (predicate(outerElement)) {
yield outerElement;
}
}
}
};
}
else {
const where = outer[@@where];
if (where) return where.call(outer, predicate);
return {
* [@@iterator]() {
for (const outerElement of outer) {
if (predicate(outerElement)) {
yield outerElement;
}
}
}
};
}
}TODO: Detailed algorithm steps.
// approximate algorithm
function QueryOrderBy(outer, keySelector, descending, async) {
if (async) {
const orderByAsync = outer[@@orderByAsync];
if (orderByAsync) return orderByAsync.call(outer, keySelector, descending);
return CreateAsyncOrderByIterable(outer, keySelector, descending, undefined);
}
else {
const orderBy = outer[@@orderBy];
if (orderBy) return orderBy.call(outer, keySelector, descending);
return CreateOrderByIterable(outer, keySelector, descending, undefined);
}
}
function CreateAsyncOrderByIterable(outer, keySelector, descending, parent) {
const orderByInfo = { keySelector, descending, parent };
return {
[@@thenByAsync](keySelector, descending) {
return CreateAsyncOrderByIterable(outer, keySelector, descending, orderByInfo);
},
async * [@@asyncIterator]() {
let outerElements;
for (await outerElement of outer) {
if (outerElements === undefined) {
outerElements = [outerElement];
}
else {
outerElements.push(outerElement);
}
}
const sorter = CreateSorter(outerElements, orderByInfo);
const length = outerElements.length;
const indices = Array(length);
for (let i = 0; i < length; i++) {
indices[i] = i;
}
indices.sort(sorter);
for (const index of indices) {
yield outerElements[index];
}
}
};
}
function CreateOrderByIterable(outer, keySelector, descending, parent) {
const orderByInfo = { keySelector, descending, parent };
return {
[@@thenBy](keySelector, descending) {
return CreateOrderByIterable(outer, keySelector, descending, orderByInfo);
},
* [@@iterator]() {
const outerElements = Array.from(outer);
const sorter = CreateSorter(outerElements, orderByInfo);
const length = outerElements.length;
const indices = Array(length);
for (let i = 0; i < length; i++) {
indices[i] = i;
}
indices.sort(sorter);
for (const index of indices) {
yield outerElements[index];
}
}
};
}
function CreateSorter(elements, { keySelector, descending, parent }, nextSorter) {
const keys = elements.map(keySelector);
const sorter = (a, b) => {
const result = CompareValues(keys[a], keys[b]);
if (result === 0) {
if (nextSorter) return nextSorter(a, b);
return a - b;
}
if (descending) return -result;
return result;
};
if (parent) return CreateSorter(elements, parent, sorter);
return sorter;
}TODO: Detailed algorithm steps.
// approximate algorithm
function QueryThenBy(outer, keySelector, descending, async) {
if (async) {
const thenByAsync = outer[@@thenByAsync];
if (thenByAsync) return thenByAsync.call(outer, keySelector, descending);
}
else {
const thenBy = outer[@@thenBy];
if (thenBy) return thenBy.call(outer, keySelector, descending);
}
throw new TypeError();
}TODO: Detailed algorithm steps.
// approximate algorithm
function QueryGroupBy(outer, keySelector, elementSelector, async) {
if (async) {
const groupByAsync = outer[@@groupByAsync];
if (groupByAsync) return groupByAsync.call(outer, keySelector, elementSelector);
return {
async * [@@asyncIterator]() {
const groupings = await CreateGroupingsAsync(outer, keySelector, elementSelector);
for (const [key, values] of groupings) {
yield CreateGrouping(key, values);
}
}
};
}
else {
const groupBy = outer[@@groupBy];
if (groupBy) return groupBy.call(outer, keySelector, elementSelector);
return {
* [@@iterator]() {
const groupings = CreateGroupings(outer, keySelector, elementSelector);
for (const [key, values] of groupings) {
yield CreateGrouping(key, values);
}
}
};
}
}
function CreateGrouping(key, values) {
return {
key,
values,
[@@grouping]() {
return this;
},
* [@@iterator]() {
yield* values;
}
};
}TODO: Detailed algorithm steps.
// approximate algorithm
function CreateGroupings(sequence, keySelector, elementSelector) {
const groupings = new Map();
for (const item of sequence) {
const key = keySelector(item);
const element = elementSelector(item);
if (groupings.has(key)) {
groupings.get(key).push(element);
}
else {
groupings.set(key, [element]);
}
}
return groupings;
}TODO: Detailed algorithm steps.
// approximate algorithm
async function CreateGroupingsAsync(sequence, keySelector, elementSelector) {
const groupings = new Map();
for await (const item of sequence) {
const key = keySelector(item);
const element = elementSelector(item);
if (groupings.has(key)) {
groupings.get(key).push(element);
}
else {
groupings.set(key, [element]);
}
}
return groupings;
}QueryExpression :
FromClause QueryBody
QueryBody :
QueryBodyClauses? SelectOrGroupClause QueryContinuation?
QueryBodyClauses :
QueryBodyClause
QueryBodyClauses QueryBodyClause
QueryBodyClause :
FromClause
JoinClause
LetClause
WhereClause
OrderbyClause
SelectOrGroupClause :
SelectClause
GroupClause
RangeBinding :
BindingIdentifier[~Yield, ~Await]
BindingPattern[~Yield, ~Await]
QueryContinuation :
`into` RangeBinding QueryBody
FromClause :
`from` `await`? RangeBinding `of` AssignmentExpression[+In, ~Yield, ~Await]
CoverElementAccessExpressionAndQueryExpressionHead[~Yield, ~Await] `of` AssignmentExpression[+In, ~Yield, +Await]
JoinClause :
`join` `await`? RangeBinding `of` AssignmentExpression[+In, ~Yield, ~Await] `on` AssignmentExpression[+In, ~Yield, ~Await] `equals` AssignmentExpression[+In, ~Yield, ~Await]
`join` `await`? RangeBinding `of` AssignmentExpression[+In, ~Yield, ~Await] `on` AssignmentExpression[+In, ~Yield, ~Await] `equals` AssignmentExpression[+In, ~Yield, ~Await] `into` RangeBinding
LetClause :
`let` RangeBinding `=` AssignmentExpression[+In, ~Yield, ~Await]
WhereClause :
`where` AssignmentExpression[+In, ~Yield, ~Await]
OrderbyClause :
`orderby` OrderbyComparatorList
OrderbyComparatorList :
OrderbyComparator
OrderbyComparatorList `,` OrderbyComparator
OrderbyComparator :
AssignmentExpression[+In, ~Yield, ~Await] `ascending`?
AssignmentExpression[+In, ~Yield, ~Await] `descending`
GroupClause :
`group` AssignmentExpression[+In, ~Yield, ~Await] `by` AssignmentExpression[+In, ~Yield, ~Await]
SelectClause :
`select` AssignmentExpression[+In, ~Yield, ~Await]
CoverElementAccessExpressionAndQueryExpressionHead[Yield, Await] :
MemberExpression[?Yield, ?Await] ArrayLiteral[?Yield, ?Await]
MemberExpression[?Yield, ?Await] [no LineTerminator here] ObjectBindingPattern[~Yield, ~Await]
MemberExpression[?Yield, ?Await] [no LineTerminator here] BindingIdentifier[~Yield, ~Await]
MemberExpression[?Yield, ?Await] [no LineTerminator here] `await` RangeBinding
MemberExpression[Yield, Await] :
<del>MemberExpression[?Yield, ?Await] `[` Expression[+In, ?Yield, ?Await] `]`</del>
<ins>CoverElementAccessExpressionAndQueryExpressionHead[?Yield, ?Await]</ins>
AssignmentExpression[In, Yield, Await] :
<ins>QueryExpression</ins>
When processing an instance of the production MemberExpression : CoverElementAccessExpressionAndQueryExpressionHead, the
interpretation of CoverElementAccessExpressionAndQueryExpressionHead is refined using the following grammar:
MemberExpression[Yield, Await] :
MemberExpression[?Yield, ?Await] `[` Expression[+In, ?Yield, ?Await] `]`
When processing an instance of the production FromClause : CoverElementAccessExpressionAndQueryExpressionHead `of` AssignmentExpression,
the interpretation of CoverElementAccessExpressionAndQueryExpressionHead is refined using the following grammar:
QueryExpressionHead :
`from` `await`? RangeBinding