Status: accepted baseline — living .opy frontend support matrix
Scope: the .opy source-language surface Wright's native frontend supports,
with production/corpus evidence for each feature and explicitly deferred
constructs
This matrix records the corpus-evidenced current surface. The
forward-looking, tiered baseline (what is planned, evidence-prioritized, or
demand-driven) lives in
compatibility-baseline.md, and the pinned
reference identity behind both is recorded centrally in
docs/compatibility/upstream-references.md.
Every claimed feature is backed by the compatibility corpus
(compatibility/fixtures/**/source.opy and pinned adapter HIR fixtures) or
marked as investigation. The architecture is lexer → preprocess → CST/parser → resolve/lower → Opy HIR (see docs/architecture.md and
crates/wright-opy).
| Source | Use |
|---|---|
compatibility/fixtures/{basic-rule,control-flow,declarations-rules,expressions-values,preprocessing,diagnostics}/source.opy |
Synthetic corpus surface |
compatibility/fixtures/real-world/overpy-cake/source.opy |
Real-world surface (arrays, macros, effects) |
adapter/fixtures/**/*.json |
Pinned OverPy 9.7.10 HIR reference for each source |
crates/wright-opy/tests/differential.rs |
Native-vs-reference parity suite (machine-readable report at target/wright-differential-report.json) |
- Identifiers, integer and decimal number literals (source text preserved),
double-quoted strings with
\n/\t/\\escapes,true/false/None. - Line comments (
#), block comments (/* */),#!directives. - Operators:
+ - * / // % ** == != < <= > >= = += -= *= /= //= %= and or not, plus./,/:/(/)/[/]/@. (inis only thefor ... inheader keyword; expression-levelin/not inmembership operators are not supported — see the deferred list.)
globalvar name/globalvar name = expr/globalvar name <index>(the bare-integer form is an explicit Workshop variable index, matching the reference; integer-0literal initializers are dropped from HIR (matching the reference adapter); non-zero and non-integer numeric initializers are preserved, e.g.j = 5andk = 0.0keep the source spelling through emission). Initializer semantics are profile-independent: the Initialize rules are synthesized by the HIR → WIR lowering, sooff,compat, andaggressiveall preserve them (#112).playervar name(same forms).subroutine name.def name():subroutine bodies (parameters are outside the declared surface; rejected explicitly).enum Name: MEMBER, ...— members fold to numeric constants (Phase.FINISHED→1), matching the reference.macro name(params):statement bodies withMacroParamreferences.
#!include "file.opy"— root-relative include resolution, cycle detection (include-cycle), missing-file diagnostics (include-not-found), included files registered in the HIR file registry (reference behavior).#!define NAME value— object-like macros; recursive expansion at use sites (a define may reference earlier defines); recursion guard (macro-recursion).#!define name(args) value— function-like macros with argument substitution (cakeBeam(start, end, yPos) → createBeam(...)).#!undef NAME.- Unsupported directives fail explicitly (
unsupported-directive).
rule "name":with@Event global/@Event eachPlayer/@Condition <expr>.@Team/@Slotare accepted only without arguments (corpus events use OverPy defaults); other@directives fail explicitly.- Statements: expression statements,
=and augmented assignment,if/elif/else,for x in range(...),while,pass. for-loop binder resolution (#114): the loop variable must resolve to a global variable — either a declaredglobalvar, or an OverPy default variable name (A–Z,AA–AZ, …,DA–DX), which the pinned reference accepts as an implicit global at its fixed Workshop slot (e.g.for I in range(0, 10):with no declaration, the agent-lab regression). Nested same-name loops reuse the same implicit variable (no separate binding), matching the reference. An undeclared lowercase binder is rejected exactly like the reference rejects it (unknown-identifier, reference: "Unknown function name").range(stop)/range(start, stop)/range(start, stop, step)are all supported.
- Literals, arrays
[...], parenthesized expressions. - Calls (
range,len,abs,sqrt,debug,print,wait,createBeam,playEffect,getAllPlayers,disableInspector, …). vect(x, y, z)→ HIRVector(3 arguments required; other arities are an explicit error)."text".format(args)→ HIRFormat; bare calls of declared subroutines →CallSubroutinestatements; dotted module callsrandom.uniform/random.choice→random.<name>calls;eventPlayer.member→PlayerVar/receiver call onEventPlayer; variable receivers (points.append,candlePos[i2]) →ReceiverCall/Index.- Builtin action/value/member identity, signatures, receiver categories,
parameter enum domains, and non-contextual aliases resolve through the OPY
semantic compatibility manifest
(
crates/wright-opy/src/manifest/data/manifest.json, schema v1; spec incompat-manifest-spec.md, issue #109) — the single authoritative semantic table, replacing the formerKNOWN_ENUMShardcoded subset. Every manifest entry is probe-validated against the pinned OverPy 9.7.10 oracle (crates/wright-opy/src/manifest/probes/). Unknown or misplaced builtins fail at semantic resolution with structured, source-located diagnostics (unknown-action,unknown-value,unknown-member,invalid-arity,invalid-receiver,enum-domain-mismatch,action-in-value-position,value-in-action-position,invalid-call-context,invalid-iterable, plus the argument-binding codesunknown-keyword,duplicate-argument,missing-argument,positional-after-keyword,keyword-required,keyword-unsupported,invalid-argumentfor #110), never as emitter catalog misses. - Reference-validated evidence surface:
chaseOverTime(...)(action; 3–4 arguments, reevaluation defaults toDESTINATION_AND_DURATION),isGameInProgress()(value),getPlayersInRadius(...)(value; teamTeam.ALLandLosCheck.OFFdefaults fill),worldVector(...)(value,Transformargument), and the enum-gated memberseventPlayer.setInvisibility(Invis.X),eventPlayer.setStatusEffect(..., Status.X, ...),eventPlayer.getThrottle(). - Receiver/member calls (
eventPlayer.setMoveSpeed(100),eventPlayer.teleport(eventPlayer.getPosition()),target.setMoveSpeed(50)on a player-valued global) lower toReceiverCalland resolve at emission through the canonicalworkshop-rscatalog; the corpus-evidenced receiver methods are thesynthetic/receiver-callsfixture methods plus the #106 enum-gated members (en-US spellings perdocs/workshop/support-matrix.md, oracle-transcribed with provenance in the catalog). - Non-contextual source aliases resolve to their canonical names
(
stopChasingVariable→stopChasing; member aliasesgetCurrentHero→getHero,hasStatusEffect→hasStatus); their emission spellings are not yet catalog-covered (documented emission gap). TheChaseReevalcontextual alias resolves only through thechasekeyword call context (#110) and stays out of the alias table. - Builtin Workshop enums from the manifest's reference-validated domains:
Beam.{GOOD,GRAPPLE},Color.{YELLOW,WHITE,RED,ORANGE,GREEN,BLUE,BLACK, PURPLE,AQUA,VIOLET,ROSE},DynamicEffect.{BAD_EXPLOSION,GOOD_EXPLOSION, RING_EXPLOSION,GOOD_PICKUP_EFFECT,BAD_PICKUP_EFFECT,BUFF_IMPACT_SOUND, DEBUFF_IMPACT_SOUND},EffectReeval.{VISIBILITY,COLOR,VISIBILITY_AND_COLOR},Wait.IGNORE_CONDITION,ChaseTimeReeval.{NONE,DESTINATION_AND_DURATION}(reference-validated against the pinned OverPy 9.7.10 enum block and emission, #105),ChaseRateReeval.{NONE,DESTINATION_AND_RATE}(NONEadditionally corpus-evidenced by the real-world overpy-meipocalypseChaseReeval.NONErate-chase calls, which the reference resolves to theChaseRateReevaldomain), plus the evidence domainsInvis.{ALL,ENEMIES,NONE},Transform.{ROTATION,ROTATION_AND_TRANSLATION},Status.{ASLEEP,BURNING,FROZEN,HACKED,INVINCIBLE,KNOCKED_DOWN,PHASED_OUT, ROOTED,STUNNED,UNKILLABLE},LosCheck.{OFF,SURFACES, SURFACES_AND_ALL_BARRIERS,SURFACES_AND_ENEMY_BARRIERS},Team.ALL. Members outside the declared domains (including spellings the pinned reference rejects, such asColor.CYANorDynamicEffect.SPARKLES) fail explicitly (unknown-enum-member). Enum domains/members beyond the declared baseline remainbaseline-planned; emission coverage stays corpus-scoped (a manifest-valid member can still hit a catalog miss at emission when no spelling is catalogged). wait()/wait(time)default-argument filling: the reference appendsWait.IGNORE_CONDITION(and0.016for the no-argument form); native matches.- Named/keyword arguments (
name = exprcall arguments, #110) bind against the manifest's canonical parameter names — the pinned reference's declared names (wait(time=1),wait(waitBehavior=Wait.IGNORE_CONDITION, time=2),chaseOverTime(g, 10, duration=3),chaseOverTime(g, 10, 3, reevaluation=ChaseTimeReeval.NONE),vect(x=1, y=2, z=3),getPlayersInRadius(center=…, radius=…, team=Team.ALL),eventPlayer.setStatusEffect(assister=…, status=…, duration=…),print(text="x"),len(array=…),debug(value=…),stopChasing(variable=g), member forms likeeventPlayer.setMaxHealth(healthPercent=100)). Keyword arguments may appear in any order before the first positional argument; the reference rejects positional arguments after keyword arguments (positional-after-keyword), unknown keyword names (unknown-keyword), duplicate bindings (duplicate-argument), and missing required arguments (missing-argument) — all structured, source-located diagnostics. The reference's generic binder is routed around forrange,random.*, and.format(keyword arguments on those fail withkeyword-unsupported), and formacroinvocations. - The
chasekeyword form (#110, reference special form):chase(variable, destination, rate=…, ChaseReeval.MEMBER)andchase(variable, destination, duration=…, ChaseReeval.MEMBER)— exactly four arguments, the 3rd passed as therate/durationkeyword and the 4th as a bareChaseReeval.MEMBERaccess.ChaseReevalresolves only in this call context:rate=selects theChaseRateReevaldomain and lowers the call tochaseAtRate;duration=selectsChaseTimeReevaland lowers tochaseOverTime. Members are checked against the selected domain (chase(g, 10, rate=2, ChaseReeval.DESTINATION_AND_DURATION)is rejected withenum-domain-mismatch, matching the reference's "Unknown chaseratereeval"). Outside the chase signatureChaseReevalnever resolves (a bareg = ChaseReeval.NONEis rejected like the reference). The first argument must be a variable (invalid-argumentotherwise); emission dispatches on its kind: a global variable emitsChase Global Variable At Rate/Over Time, a player variable emitsChase Player Variable At Rate/Over Time(player, name, …)(catalog spellingschaseAtRate,chasePlayerVariableAtRate,chasePlayerVariableOverTime, oracle-transcribed, #110). The parenthesized member formchase(…, (ChaseReeval.NONE))is accepted by the native frontend though the reference's raw-token check rejects it (documented presentation-level difference; the parenthesized form is not pinned by probes). chaseOverTime(...)requires a variable first argument like the reference (invalid-argumentforchaseOverTime(10, …)), which also selects the global/player emission form.- Undeclared identifiers, enum types without members, and unsupported member
accesses are structured, source-located semantic errors
(
unknown-identifier,enum-type-without-member,unsupported-member).
- Malformed input produces structured
FrontendErrors (stable codes likeparse-error,lex-error) with 1-based source spans; the parser recovers at statement boundaries to report multiple useful errors. - Native diagnostics map into the shared
wright-result/v1contract (stagefrontend, severityerror).
- Top-of-file
settings { ... }custom-game-settings blocks (JSONC: quoted keys,"/'strings with escapes, numbers,true/false, string lists, nested groups, trailing commas) — recognized and consumed before lexing (scoped lexing: the block never enters the token stream and the lexer gains no global braces), parsed into the typed HIRsettingspayload, and emitted as the Workshopsettingssection beforevariables. - Corpus-evidenced keys render per the emission table (fixture-evidenced
data; see
crates/wright-ir/src/settings/table.rs); keys, enum values, and map/hero list elements outside the table fail explicitly (settings-unknown-key/settings-unknown-value). - Placement rules: the block must be the first construct in the main file
(
settings-placementotherwise); a second block is rejected; asettings "file"form is rejected (settings-invalid); settings blocks in included files are rejected (settings-placementat the included file's keyword span). - The emitted
settingssection is deliberately not reparseable by the Workshop parser (a.wsdecompiler is a non-goal); the settings-free round-trip guarantee is unchanged.
- A full decompiler architecture (comment/formatting-preserving reconstruction of arbitrary Workshop text); the declared reconstruction surface above is semantic reconstruction, not original-source recovery.
- Macro/
#!definevalues that require runtime evaluation (no scripting). - OverPy enum domains/members beyond the manifest's declared baseline (a
data change,
baseline-plannedin the compatibility baseline). - Emission spellings for manifest-valid entries not yet catalog-covered
(alias targets
stopChasing/getHero/hasStatus, and enum members without a catalogged spelling); these fail at emission with catalog diagnostics, never silently. - Rule
disabledmarkers (no corpus evidence for the source annotation). - Expression-level
in/not inmembership operators — rejected at parsing (for ... inheaders are supported). - Backslash line continuation (
\at end of line inside string concatenations / macro bodies) — rejected at lexing. - Postfix increment/decrement (
++/--) — rejected at parsing. - Dict literals (
{...}) — rejected at lexing. - Triple-quoted strings / docstrings (
""") — rejected at lexing. - Subroutine parameters, default
@Team/@Slotoverrides,raycastinclude=/exclude=named-argument forms (no reference/corpus evidence in the declared surface; the reference'sraycastspecial form is not manifest-declared), and macro keyword arguments (the reference's macro substitution treats them as raw text; rejected explicitly). - Full OverPy formatting semantics:
debug()/print()emission (Create HUD Textetc.) follows the simplified semantic formatting documented inv1-matrix.md. - Emission presentation: variable references emit as
Global.<name>(the native Workshop parser's canonical spelling) where the reference emits the bare variable name; observable semantics and round-trip validity are unchanged.
wright_opy::reconstruct consumes a validated Workshop IR program and emits
deterministic, byte-stable canonical OPY that the native frontend accepts and
that re-lowers to a structurally equivalent WIR program under
workshop_rs::roundtrip::equivalent. The machine-readable support
boundary (supported vs explicitly rejected constructs, with a consistency
test) lives in
crates/wright-opy/tests/fixtures/reconstruct/boundary.json; the round-trip
suite is crates/wright-opy/tests/reconstruct.rs, which runs
Workshop → WIR → reconstructed OPY → native frontend → HIR → WIR per fixture
and writes a per-fixture report to target/wright-reconstruction-report.json
with one reconstructed OPY per fixture under target/wright-reconstruction/.
- Variable and player-variable declarations with explicit Workshop indices
(
globalvar name <index>,playervar name <index>), plus declaration initializers reconstructed from the leadingInitialize global variables/Initialize player variablesrules (globalvar name = value). Zero-valued initializers are spelled0.0because the frontend drops integer-0initializers (matching the reference adapter). - Subroutine declarations and
def name():subroutine bodies. rule "name":with@Event global/@Event eachPlayerand@Conditionlines.- Scalar, string, bool,
None, array, vector, and enum values; global andeventPlayer.membervariable access;eventPlayeritself. - Binary and unary operator calls in their OPY source spellings
(
(a + b),(a == b),(a and b),(not a),(-a)),formatvalues ("text".format(...)), manifest value calls (isGameInProgress,getPlayersInRadius,worldVector, …) and manifest member-value calls (eventPlayer.getPosition(), …). - Set/Modify global and player variable actions (modify ops
Add…Raise To Powerasx = x <op> v,Append To Arrayasx.append(v)), subroutine calls,if/elif/else,while,for x in range(start, stop, step), the manifest action calls (waitwith full arity,disableInspector,playEffect,chaseOverTime, …) and manifest member actions (eventPlayer.setMoveSpeed(100), …), and the dedicateddebug(x)/print(x)nodes.
Every WIR construct the OPY frontend cannot recompile identically fails with
a structured diagnostic naming the construct (never partial or misleading
OPY): the per-player loop form (For Player Variable), disabled rules,
variable targets on arbitrary (non-eventPlayer) player expressions, names
that are not valid OPY identifiers or collide with OPY keywords/literals,
negative and non-finite number literals (the lexer has no negative-literal
token), enums outside the manifest's declared domains, Remove From Array
modifies, calls the frontend lowers to dedicated nodes (debug, print,
append, vect, range, chase), Workshop-spelled call names with no
manifest source form (add, countOf, createBeamEffect, …), calls whose
arity/domains the frontend would reject or default-fill, Set actions whose
value is a binary over the same variable (they re-lower to Modify), and
rule layouts the deterministic re-lowering cannot reproduce (non-leading or
mixed initializer rules, subroutine-body rules after normal rules or out of
table order, unsorted global slots, non-canonical subroutine indices,
initializer-bearing globals whose slot differs from the lowest free slot).
Reconstructed OPY is simple low-level valid OPY: comments, macros, functions, settings blocks, and source abstractions are not recovered.
The reconstructor is exposed end-to-end through one shared driver/session
conversion operation: wright convert --target opy <workshop-input> (CLI) and
CompilerSession::convert(ConvertTarget::Opy) (library) load validated
Workshop input through the driver's own load() path and call
wright_opy::reconstruct::reconstruct unchanged. The reconstructed source is
the result.text of the wright-result/v1 envelope; a construct outside the
declared surface fails with the reconstructor's stable diagnostics (stage
reconstruction, exit code 3) and no partial source. The operation is
Workshop → OPY only: non-Workshop inputs are rejected explicitly, and there is
no direct OPY ↔ OSTW path. The cross-format suite
(crates/wright-driver/tests/convert.rs) proves the full loop
Workshop → convert(opy) → native frontend → HIR → WIR → Workshop for the
fixtures above and writes target/wright-convert-report.json.
The native frontend produces wright_core::hir::Program (Opy HIR v1) with the
same protocol envelope, file registry, declarations, and rules as the
reference adapter — verified by the differential suite at the HIR boundary
(spans and the producer identity normalized away). It never requires Node or
OverPy; the adapter remains available as an explicit WRIGHT_ADAPTER_PATH
fallback and as the pinned compatibility oracle.