Proposal: regular expressions (Regex.*) — PCRE/PECL-compatible syntax, linear-time & non-expert-safe #18
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A regular-expression library with PCRE/PECL-compatible syntax, so text
patterns learned anywhere (PHP, JavaScript, most editors) just work in Ludic.
Why it matters for game devs
{player_name}orBBCode-ish
[color=red]…[/color]tags./whisper …), validatea level code.
Proposed API (illustrative)
compile(returns an error value on bad patterns — never crashes),matches,find,find_all,replace, capture groups (numbered + named).common escapes. Document exactly which features are supported.
Considerations
"catastrophic backtracking." Strongly prefer a linear-time NFA/Thompson
engine (RE2/Rust-regex style) and/or a match-step budget so a bad pattern from
a modder can't freeze the game.
\w, classes).Regexvalue) — don't recompile per frame.(backreferences, lookbehind) is explicitly out of scope for a linear engine —
call out what's supported.
Scope / acceptance
compile/matches/find/find_all/replace.Related: Unicode, error handling, #2 (Text).
Shipped in
b798e30— aRegex.*namespace backed by a linear-time engine.Why linear time matters (the acceptance's headline): the engine is a Thompson NFA / Pike VM, not a backtracker, so a modder's pattern can never freeze the game.
(a+)+$on 40 non-matching chars,(a*)*b,(.*a){20}b— all run in microseconds; a 50 KB input scans in ~7 ms. No catastrophic backtracking, no step-budget hack needed.Engine (
runtime/native/regex.ludic+regex_vm.ludic, ~700 lines of Ludic, C-free): a pattern compiles to a small bytecode program (an unanchored lazy.*?prefix makes a plain search match anywhere); the VM advances every alive thread in lockstep per input byte, deduped by program counter, carrying capture slots with leftmost-greedy priority.Supported syntax: literals,
., classes[...](ranges, negation,\d \w \s+\D \W \S), anchors^ $, alternation|, capturing(...)and non-capturing(?:...)groups, quantifiers* + ? {n} {n,} {n,m}in greedy or lazy (?-suffixed) form, and the common escapes. Numbered capture groups. Errors are values —Regex.compilereturnsnullon a bad pattern, never a crash.Out of scope (documented, inherent to a linear engine): backreferences and look-around. And on the degenerate case of a nullable subpattern under an unbounded quantifier (e.g.
(a*)*), match/capture positions may differ from Python's backtracker — the price of the O(n·m) guarantee.Surface (
Regex.*):compilevalidmatchestestfindexecnextreplacegroupgroup_countstartendok.matches/find/replacetake a pattern string (compile-on-call);compile+test/exec/nextreuse a compiledRegexin a hot loop.next+endgive the find-all loop;replaceexpands\0..\9group refs (dialogue/localization tokens).On-demand: the parser sets a flag when it sees
Regex.and splices the engine only then — zero cost when unused, and it works in a plain tool, not just an ECS game.Verification: a 20 000-case grammar fuzzer against Python's
re— 100% agreement on realistic patterns (0 / 15 000 including capture groups) and 99.8% on whole-match spans across the full pathological grammar, the residual being exactly the documented nullable-quantifier case.examples/library/regex.ludicasserts the behaviour and is wired intox test(now 60 passed, 0 failed); a Regex docs section + 13 per-symbol pages; coverage/vocabulary/fence checks green; seed reseeded and the C-free bootstrap fixpoint holds.Acceptance
next)/replace\w \d \sshorthands)re)Named-group access by name (parsing of
(?P<name>…)is already accepted, captured by number) and a list-returningfind_allare the natural follow-ups.