Skip to content

Give a rule a terminator it can only know at match time - #23

Merged
liuyingjierun merged 9 commits into
mainfrom
engine/runtime-terminators
Sep 7, 2026
Merged

liuyingjierun merged 9 commits into
mainfrom
engine/runtime-terminators

Conversation

@liuyingjierun

Copy link
Copy Markdown
Contributor

Nine commits: the engine change that lets a rule find its own end, five languages that needed it, and the documentation batch that accumulated ahead of it.

The engine

Heredocs, %w[…], q{…} and Elixir sigils rendered as ordinary code, and not for want of someone writing a pattern: their end is genuinely not knowable when the rule is written. <<<SQL ends at SQL and <<<EOT at EOT — one rule, two terminators, chosen by the text being highlighted.

A rule may now carry close(match, text, from), called after its pattern matches an opening, returning where the whole form ends. Two builders cover every case the grammars needed: one derives a heredoc terminator from the capture holding its name, the other walks to the bracket matching the opener — counting depth where brackets nest, and not counting it where a symmetric delimiter cannot.

This is additive. GrammarRule gains an optional field and every existing rule keeps working untouched, so the public type did not have to break for it. Migrating the hand-written string rules onto one builder — which does break it — stays a round of its own inside the 0.0.x window.

Three decisions worth reviewing

  • An opening with no terminator reports no match rather than running to end of input. A stray << in $(( a << 2 )) is likelier than a real unterminated heredoc, and the failure modes are not comparable: one leaves a shift operator uncoloured, the other paints the rest of the file as a string.
  • Ruby opens a heredoc only on an uppercase word, because items << thing is the append operator and shares the token exactly.
  • The shell opening line is an uncoloured prefix, so the redirect in cat <<EOF > out.txt stays shell rather than being dragged into the literal.

The documentation batch

check:docs-parity compares what cannot legitimately differ between a document and its translation — versions, paths, package specifiers, links, section structure, table row counts — and leaves prose and translated placeholders alone. Run against the tree before this branch, it reports the two omissions it was written for.

It also carries the four translation gaps it was built to catch, the roadmap's claim that jsray-terminal has a release (it has none), and two deadlines rewritten as conditions: "revisit at public beta" named 2026-07-17, and kept being rediscovered as an open question every planning round after that date passed. Minification now carries its answer and a trigger that cannot expire.

Verification

175 tests pass, up from 164. The new cases include the openings that must be declined, and four unterminated-opening storms added to the backtracking guard — these scan forward, so a file of nothing but openings is the shape that would expose a quadratic one.

check:integrity, check:versions and check:docs-parity are green. No version bump: this accumulates under [Unreleased].

Merged in #22 in English only. The rule an integration is meant to follow —
sync Core when you release, not when Core ships — reached half the readers,
and the half it missed are the ones the Chinese doc exists for.
The Chinese table's last column was headed 状态 and said 已公开 for Core — a
status, where the English column says where a user gets the thing today. The
sentence directly underneath already explained the English column, so the
Chinese page contradicted itself before it contradicted the translation.
The English versioning doc says @jsray/core is published from the jsray npm
account; the Chinese one dropped the account and kept only the package name.
Anyone reading it to find out who can publish came away without the answer.
Two things had gone stale in both languages at once, which is the failure a
translation check cannot see.

jsray-terminal is described as having a release carrying an installable
build. It has no releases at all; the only way in is the GitHub install line,
and a tag is already sitting there to cut one from.

"Syncing them to 0.0.2-beta.2 is the next step" was written when Core was on
beta.2 and now names neither the current release nor the rule: an integration
syncs Core as part of its own release, so being behind between releases is
the expected state, not a queued chore.
Every doc here is written twice, and nothing checked that the two agreed. A
change landing in one language breaks no build and reads perfectly well — in
the language you happen to be reading. #22 sat English-only for a day, and
the Chinese repository table had been answering a different question than the
English one for longer than that.

The check compares what cannot legitimately differ between translations:
versions, paths, package specifiers, links, section structure and table row
counts. Prose, sentence counts and translated placeholders are left alone —
<version> against <版本> is not drift. Presence is what it asserts, not
counts, because a translation may merge or split sentences freely.

Run against the two files as they stood before this branch, it reports both
of the omissions above.
The table's last column is the only place in the docs quoting a full npm
specifier, and it is what a reader copies. A parity check catches it going
missing from one language; nothing catches both languages naming the same
stale release, which is the shape every other entry in this file was added
for.
"Revisit at public beta" named 2026-07-17. That date passed, nothing
happened, and the entry became something every planning round rediscovers and
re-argues from scratch. The minify question now carries its answer — a
readable, auditable dist is worth more than the 9 KB brotli saves, measured —
and a condition for reopening it that cannot silently expire: a real size
complaint, or Core growing substantially.

Detection tuning said it "belongs with the 0.0.2 engine work" while we are
already inside 0.0.2. What it actually needs is a beta round of its own,
because retuning the scores reorders all 83 grammars at once and nothing else
in the same round would be separable from it.

The third of these — string rules waiting on "the API pass in 0.0.2" — is
left alone here because the work itself is what rewrites that entry.
Six commits landed without a changelog entry between them: the parity check,
the translations it was built to catch, and the two roadmap deadlines that
were rewritten as conditions. The audits that keep finding unrecorded batches
would have found this one.
Heredocs, %w[…], q{…} and sigils rendered as ordinary code, and not for want
of a pattern: their end is genuinely not knowable when the rule is written.
`<<<SQL` ends at SQL and `<<<EOT` at EOT — the same rule, two different
terminators, chosen by the text being highlighted. A RegExp cannot express
that, which is why five languages had a hole in the same shape.

A rule may now carry `close(match, text, from)`, called after its pattern
matches an opening, returning where the form ends. Two builders cover every
case the grammars needed: one derives a heredoc terminator from the capture
holding its name, the other walks to the bracket matching the opener,
counting depth where brackets nest and not counting it where a symmetric
delimiter cannot.

This is additive. `GrammarRule` gains an optional field and every existing
rule keeps working untouched, so the public type did not have to break for
it — the migration of the hand-written string rules onto one builder, which
does break it, stays a round of its own.

Three decisions worth keeping:

An opening with no terminator reports no match rather than running to the end
of input. A stray `<<` in `$(( a << 2 ))` is likelier than a real
unterminated heredoc, and the failure modes are not comparable: one leaves a
shift operator uncoloured, the other paints the rest of the file as a string.

Ruby opens a heredoc only on an uppercase word, because `items << thing` is
the append operator and shares the token exactly.

The shell opening line is consumed as an uncoloured prefix, so the redirect
in `cat <<EOF > out.txt` stays shell instead of being dragged into the
literal.

Eleven cases in tests/constructs.test.mjs, including the openings that must
be declined and four unterminated-opening storms in the backtracking guard —
these scan forward, so a file of nothing but openings is the shape that would
expose a quadratic one.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
jsray 3b6fd0f Commit Preview URL

Branch Preview URL
Sep 07 2026, 10:12 PM

@liuyingjierun
liuyingjierun merged commit bb31e96 into main Sep 7, 2026
6 checks passed
@liuyingjierun
liuyingjierun deleted the engine/runtime-terminators branch September 7, 2026 22:13
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