Skip to content

compiler: add destructuring declarations and assignments - #500

Open
jow- wants to merge 2 commits into
feat/stcall-typeof-throwfrom
feat/syntax-destruct-assignment
Open

jow- wants to merge 2 commits into
feat/stcall-typeof-throwfrom
feat/syntax-destruct-assignment

Conversation

@jow-

@jow- jow- commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Support array and object destructuring in let/const declarations and in assignment expressions, including nested patterns, default values, holes, computed keys, member targets and rest elements:

let [a, {b, c: [d = 1]}, ...rest] = src;
const {x, y: z, w = 5, ...others} = obj;
[arr[i], o.k, ...o.tail] = src;
({a, [key]: b = 3, ...others} = obj);

Assignments evaluate to the assigned value and may appear wherever an assignment expression can. The initializer is evaluated first, then the targets and the loaded values of each item in turn; declared variables are inaccessible while the initializer and defaults are compiled, yielding the usual temporal dead zone errors.

An array/object literal that starts an assignment expression is a pattern if the matching closing bracket is followed by =. This is determined by the new uc_lexer_skip_balanced(), which skips raw text up to the closing bracket (recognizing only strings, template literals, regexps and comments), without compiling anything or affecting lexer/parser state — so the nature of the expression is known ahead of compiling it, without backtracking. Sources gain uc_source_read_at() (read-ahead peek at arbitrary offsets) and uc_source_resync() (resume sequential reading).

To generate code in source order despite the initializer-evaluates-first semantics, pattern code is emitted ahead of initializer code with jumps to run them in the right order; nested pattern defaults use the same scheme.

Rest elements reuse existing machinery instead of adding instructions: the container is copied with I_NARR/I_NOBJ, merged with I_MARR/I_MOBJ exactly like a literal spread (so an unsuitable container is reported the same way), and the already-bound items are taken off the copy with internal splice()/rawdelete() calls emitted as I_STCALL. Both work on plain copies, so no metamethods are dispatched. No new VM instruction is introduced.

Stacked on #509 (vm: invoke standard library functions internally, add typeof and throw), which provides I_STCALL; merge that first.

@jow-
jow- force-pushed the feat/syntax-destruct-assignment branch from 5883414 to 24273c2 Compare October 19, 2026 16:03
@jow-
jow- force-pushed the feat/build-flags branch from 7aa19e5 to 0dc287e Compare October 19, 2026 16:27
Base automatically changed from feat/build-flags to master October 19, 2026 17:44
@jow-
jow- force-pushed the feat/syntax-destruct-assignment branch 2 times, most recently from 2619713 to 76e6408 Compare October 23, 2026 21:09
@jow-
jow- force-pushed the feat/syntax-destruct-assignment branch from 76e6408 to 5dd1957 Compare October 23, 2026 22:19
@jow-
jow- changed the base branch from master to feat/stcall-typeof-throw October 23, 2026 22:19
@jow-
jow- force-pushed the feat/stcall-typeof-throw branch from 1980f8c to 3b87b29 Compare October 23, 2026 22:35
@jow-
jow- force-pushed the feat/syntax-destruct-assignment branch from 5dd1957 to 693e679 Compare October 23, 2026 22:35
@jow-
jow- force-pushed the feat/stcall-typeof-throw branch from 3b87b29 to a357bc0 Compare October 23, 2026 22:37
@jow-
jow- force-pushed the feat/syntax-destruct-assignment branch from 693e679 to 6b77e1f Compare October 23, 2026 22:37
jow- added 2 commits October 24, 2026 00:52
Add uc_lexer_skip_balanced() which skips over the text up to the bracket
closing the one that ends at a given source offset, and tells whether `=`
or `=>` follows it. It also reports which of the enclosed brackets are
followed by one of them, so that callers can avoid skipping the same
text repeatedly for nested expressions.

The procedure looks at the raw text and only recognizes what can hide
brackets: strings, template literals including their placeholders,
regular expressions and comments. Whether a slash starts a regular
expression or is a division is guessed from the preceding character.
Lexer and parser state are not affected, which allows deciding about the
nature of an expression like a destructuring pattern or the parameter
list of an arrow function ahead of compiling it, without having to
backtrack.

To support this, sources gain uc_source_read_at() to look at text at
arbitrary offsets using a small read-ahead cache, and uc_source_resync()
to continue sequential reading afterwards.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Support array and object destructuring in `let` and `const`
declarations and in assignment expressions, including nested patterns,
default values, holes, computed keys, member targets and rest elements:

  let [a, {b, c: [d = 1]}, ...rest] = src;
  const {x, y: z, w = 5, ...others} = obj;
  [arr[i], o.k, ...o.tail] = src;
  ({a, [key]: b = 3, ...others} = obj);

Assignments evaluate to the assigned value and may appear wherever an
assignment expression can, e.g. `x = [a, b] = y` or as call argument.
Defaults apply for null values. The initializer is evaluated first,
followed by the targets and the loaded values of each item in turn.

An array or object literal that starts an assignment expression is a
pattern if the matching closing bracket is followed by `=`, which is
determined using uc_lexer_skip_balanced() without compiling anything
(literals enclosed in already examined ones reuse that result). Patterns
of declarations are known by the keyword and do not need this.

The initializer of a pattern follows the pattern in the source text but
has to be evaluated first. To generate code in source order nevertheless,
the code of the pattern is emitted ahead of the code of the initializer
and jumps are used to run them in the right order:

      JMP init
  pat:  <pattern>
      JMP end
  init: <initializer>
      JMP pat
  end:

Nested patterns emit their default value behind their code in the same
fashion. As a consequence, declared variables are inaccessible while
the initializer and the defaults of nested patterns are compiled, which
yields the usual temporal dead zone errors, source positions stay
monotonic and runtime errors point into the pattern. Syntax errors in
patterns are reported at the offending target.

Rest elements need no instruction of their own: they copy the container
with a literal spread and take the items already bound off the copy, with
splice() for an array and rawdelete() for an object, both called
internally, see I_STCALL. The copy is created below the container so that
the merge consumes the loaded value and the stack stays as balanced as it
does for an ordinary item, and the merge is what reports an unsuitable
container, the way a literal spread does.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
@jow-
jow- force-pushed the feat/syntax-destruct-assignment branch from 6b77e1f to e3e6656 Compare October 23, 2026 22:52

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant