From 0674cb828f07cb303f791a24830dcff9a20e90eb Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 17 Aug 2026 19:30:53 +0800 Subject: [PATCH 1/2] feat(opy): clarify semantic ownership boundary (Fixes #30) --- compatibility/support-matrix.json | 72 +++++++++++++++---- compatibility/tests/test_support_matrix.py | 16 +++++ .../src/manifest/data/manifest.json | 10 ++- crates/opy-frontend/src/manifest/mod.rs | 57 +++++++++++++++ crates/opy-frontend/src/support.rs | 6 +- docs/opy/architecture.md | 41 +++++++++-- docs/opy/compat-manifest-spec.md | 51 +++++++++++-- docs/opy/compatibility-baseline.md | 19 +++-- docs/opy/support-matrix.md | 20 +++--- 9 files changed, 246 insertions(+), 46 deletions(-) diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 6681d10..64bb22e 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -9,7 +9,7 @@ }, "snapshot": { "date": "2026-08-16", - "note": "Merged-main baseline (PRs #9-#14; issues #2-#6 delivered, #7 partially delivered). Frontend (PR #9), evidence base (PR #10), JS runtime (PR #11), tooling API/CLI (PR #12), differential suite (PR #13), and cross-platform runtime CI (PR #14) are merged on main. Frontend-supported rows: declared syntax surface, settings blocks, structured diagnostics, preprocessing (include/define/undef), macro statements, rule directives/model, JavaScript macros, and runtime hooks. Semantic-supported rows: declaration resolution, for-loop binders, modules, keyword arguments, and the declared alias surface. Remaining planned rows: full builtin/enum/receiver surfaces beyond the manifest-declared evidence, advanced directives, translations, and optimization controls. Workshop-dependent features are lowering-dependent and inventory-only until the workshop-rs integration stage (#8). #!postCompileHook is parsed/validated/recorded by the frontend; execution against the final Workshop text is lowering-dependent (#8).", + "note": "Merged-main baseline (PRs #9-#14; issues #2-#6 delivered, #7 partially delivered). Frontend (PR #9), evidence base (PR #10), JS runtime (PR #11), tooling API/CLI (PR #12), differential suite (PR #13), and cross-platform runtime CI (PR #14) are merged on main. Frontend-supported rows: declared syntax surface, settings blocks, structured diagnostics, preprocessing (include/define/undef), macro statements, rule directives/model, JavaScript macros, and runtime hooks. Semantic-supported rows: declaration resolution, for-loop binders, modules, keyword arguments, the declared alias surface, and the OPY-owned manifest overlay for builtin/member/enum semantics. After #30, canonical Workshop builtin/member/enum breadth is represented by separate lowering-dependent rows; remaining planned rows are the pure syntax and directive/preprocessing work tracked by #28 and #29 plus the explicitly recorded bare playervar receiver residual. Workshop-dependent features are lowering-dependent and inventory-only until the workshop-rs integration stage (#8). #!postCompileHook is parsed/validated/recorded by the frontend; execution against the final Workshop text is lowering-dependent (#8).", "asOfCommit": "cbb7242f4be36a803ebc0a4a3147a6574840f734" }, "states": { @@ -157,9 +157,9 @@ }, { "id": "semantics/builtin-actions-values", - "name": "Builtin actions and values (manifest-declared surface; full 225 action / 267 value Workshop surface)", + "name": "OPY builtin actions and values: manifest identities, signatures, aliases, and call semantics", "category": "semantics", - "state": "planned", + "state": "semantic-supported", "evidence": [ "probes:generic-builtins", "probes:chase-over-time", @@ -173,13 +173,24 @@ "fixtures:synthetic/basic-rule", "fixtures:synthetic/chase-condition-agentlab" ], - "notes": "Issue #4. Resolves through the opy-rs semantic compatibility manifest (crates/opy-frontend/src/manifest/, spec: docs/opy/compat-manifest-spec.md); every entry probe-validated against the pinned oracle. Full surface stays baseline-planned." + "notes": "Issue #30. The manifest-declared OPY overlay is semantic-supported: source identities, action/value position, signatures, keyword binding, aliases, and canonical catalogId links are probe-validated against the pinned oracle. Canonical Workshop existence/content/emission breadth is tracked separately as lowering-dependent." + }, + { + "id": "semantics/workshop-builtin-catalog", + "name": "Canonical Workshop builtin action/value catalog existence, content, and emission", + "category": "semantics", + "state": "lowering-dependent", + "evidence": [ + "probes:catalog-only-names", + "upstream:src/data/opy/functions.ts" + ], + "notes": "Issue #30/#8. Canonical Workshop action/value catalog breadth, catalog existence, content domains, localized spellings, and emission are owned by workshop-rs. The integration adapter cross-checks manifest catalogId links; opy-rs does not copy the catalog or claim full 225/267 coverage." }, { "id": "semantics/receiver-members", - "name": "Receiver/member functions: receiver categories, explicit-argument signatures, variable receivers", + "name": "OPY receiver/member semantics: receiver categories and explicit-argument signatures", "category": "semantics", - "state": "planned", + "state": "semantic-supported", "evidence": [ "probes:receiver-calls", "probes:enum-gated-members", @@ -191,13 +202,35 @@ "fixtures:synthetic/receiver-calls", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #4. Includes the bare-playervar-receiver residual item (A = B.C, baseline-planned). Workshop emission of member calls through the catalog is lowering-dependent." + "notes": "Issue #30. The manifest-declared OPY member overlay is semantic-supported: receiver syntax/category semantics, explicit-argument signatures, variable receiver rules, aliases, and catalogId links are probe-validated. Canonical member existence/content and Workshop emission remain lowering-dependent." }, { - "id": "semantics/enum-domains", - "name": "Builtin enum/constant domains and members (declared domains; 46 upstream domains incl. Hero/Map/Gamemode)", + "id": "semantics/workshop-receiver-catalog", + "name": "Canonical Workshop receiver/member catalog existence, content, and emission", + "category": "semantics", + "state": "lowering-dependent", + "evidence": [ + "probes:receiver-calls", + "upstream:src/data/opy/memberFunctions.ts" + ], + "notes": "Issue #30/#8. Canonical receiver member lists, member existence, content-specific receiver/domain validation, localized spellings, and emission are Workshop-owned. The frontend preserves unknown/member diagnostics that can be decided from OPY metadata and defers catalog checks to integration." + }, + { + "id": "semantics/receiver-playervar", + "name": "Bare playervar receiver member access (`A = B.C`)", "category": "semantics", "state": "planned", + "evidence": [ + "upstream:src/tests/variables.opy", + "upstream:src/data/opy/memberFunctions.ts" + ], + "notes": "Issue #30 audit. The pinned oracle accepts this OPY-owned receiver form, but the current frontend still rejects it. It remains an explicit, source-language follow-up rather than being hidden inside canonical Workshop member breadth; #8 must not be used to claim it complete." + }, + { + "id": "semantics/enum-domains", + "name": "OPY enum/domain semantics: declared domain identities and contextual dispatch", + "category": "semantics", + "state": "semantic-supported", "evidence": [ "probes:unknown-enum", "fixtures:synthetic/chase-enums", @@ -205,7 +238,18 @@ "upstream:src/tests/builtinenums.opy", "upstream:src/data/opy/constants.ts" ], - "notes": "Issue #4. Workshop enum member/domain validation (member existence, domain mismatch) was removed from the core and is lowering-dependent (#8): member accesses on declared domain identities resolve as opaque identities, and the checks are never approximated. Full domain surface stays baseline-planned; emission spellings are lowering-dependent." + "notes": "Issue #30. Manifest parameter domains and contextual dispatch are semantic-supported OPY identity links with oracle/corpus evidence. Domain member lists, member existence, domain membership, localized spellings, and emission remain Workshop-owned and are never approximated in opy-rs." + }, + { + "id": "semantics/workshop-enum-domains", + "name": "Canonical Workshop enum/domain member lists, membership, and emission", + "category": "semantics", + "state": "lowering-dependent", + "evidence": [ + "probes:unknown-enum", + "upstream:src/data/opy/constants.ts" + ], + "notes": "Issue #30/#8. Canonical enum domains and members (including Hero, Map, Gamemode, Team, and settings/content domains) are Workshop catalog data. opy-rs carries identity links only; workshop-rs performs member existence and domain compatibility checks at lowering." }, { "id": "semantics/aliases", @@ -515,15 +559,15 @@ ], "summary": { "byState": { - "planned": 8, + "planned": 6, "frontend-supported": 14, - "semantic-supported": 5, - "lowering-dependent": 8, + "semantic-supported": 8, + "lowering-dependent": 11, "end-to-end-supported": 0 }, "byCategory": { "syntax": 7, - "semantics": 10, + "semantics": 14, "preprocessing": 3, "macros": 3, "directives": 2, diff --git a/compatibility/tests/test_support_matrix.py b/compatibility/tests/test_support_matrix.py index 1fbfa02..2e93c58 100644 --- a/compatibility/tests/test_support_matrix.py +++ b/compatibility/tests/test_support_matrix.py @@ -100,6 +100,22 @@ def test_summary_matches_features(self): len(self.features), ) + def test_semantic_ownership_split_is_explicit(self): + by_id = {feature["id"]: feature for feature in self.features} + for overlay in ( + "semantics/builtin-actions-values", + "semantics/receiver-members", + "semantics/enum-domains", + ): + self.assertEqual(by_id[overlay]["state"], "semantic-supported") + for catalog in ( + "semantics/workshop-builtin-catalog", + "semantics/workshop-receiver-catalog", + "semantics/workshop-enum-domains", + ): + self.assertEqual(by_id[catalog]["state"], "lowering-dependent") + self.assertEqual(by_id["semantics/receiver-playervar"]["state"], "planned") + if __name__ == "__main__": unittest.main() diff --git a/crates/opy-frontend/src/manifest/data/manifest.json b/crates/opy-frontend/src/manifest/data/manifest.json index b3980a8..90ad10d 100644 --- a/crates/opy-frontend/src/manifest/data/manifest.json +++ b/crates/opy-frontend/src/manifest/data/manifest.json @@ -55,6 +55,7 @@ "params": [] }, { + "catalogLink": "special-lowering", "evidence": [ "generic-builtins" ], @@ -67,6 +68,7 @@ ] }, { + "catalogLink": "special-lowering", "evidence": [ "generic-builtins" ], @@ -138,6 +140,7 @@ ] }, { + "catalogLink": "special-lowering", "contextualDomain": { "by": "rate", "domain": "ChaseReeval", @@ -208,6 +211,7 @@ ] }, { + "catalogLink": "legacy-alias", "evidence": [ "aliases" ], @@ -299,6 +303,7 @@ ] }, { + "catalogLink": "special-lowering", "context": "forIterable", "evidence": [ "range-for-header" @@ -395,6 +400,7 @@ ] }, { + "catalogLink": "special-lowering", "evidence": [ "receiver-calls" ], @@ -620,6 +626,7 @@ "receiver": "Player" }, { + "catalogLink": "catalog-gap", "evidence": [ "member-aliases" ], @@ -629,6 +636,7 @@ "receiver": "Player" }, { + "catalogLink": "catalog-gap", "evidence": [ "member-aliases" ], @@ -661,7 +669,7 @@ "reviewed": true }, "reference": { - "contentCommit": "889d974", + "contentCommit": "889d9749d1def17f146548cbddb94ea1ab015847", "integrity": "sha512-oX17nauJcPTaKIrRFY/rD0Rl8atqFUVv9Hg2TKH+A68/fC8+ZO344Mkd1A/Y0oOVp1hr5tktMBjzMEDDnMEYUw==", "name": "overpy", "version": "9.7.10" diff --git a/crates/opy-frontend/src/manifest/mod.rs b/crates/opy-frontend/src/manifest/mod.rs index c471ee9..42971e1 100644 --- a/crates/opy-frontend/src/manifest/mod.rs +++ b/crates/opy-frontend/src/manifest/mod.rs @@ -78,6 +78,20 @@ pub enum FunctionKind { MemberValue, } +/// How the frontend-owned function identity connects to Workshop lowering. +/// +/// `canonical` entries carry a `catalogId`; the other variants are explicit +/// reasons why a source-level function does not have a direct catalog entry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub enum CatalogLink { + #[default] + Canonical, + SpecialLowering, + LegacyAlias, + CatalogGap, +} + impl FunctionKind { /// Whether this kind is an action (statement-position builtin). pub fn is_action(self) -> bool { @@ -241,6 +255,9 @@ pub struct Function { #[serde(default)] #[serde(rename = "catalogId")] pub catalog_id: Option, + /// The explicit reason a source-level function has no direct catalog id. + #[serde(default)] + pub catalog_link: CatalogLink, /// The probe ids that validate this entry against the pinned oracle. #[serde(default)] pub evidence: Vec, @@ -474,6 +491,24 @@ impl Manifest { } } } + match (&function.catalog_id, function.catalog_link) { + (Some(_), CatalogLink::Canonical) + | (None, CatalogLink::SpecialLowering) + | (None, CatalogLink::LegacyAlias) + | (None, CatalogLink::CatalogGap) => {} + (Some(id), link) => { + return Err(ManifestError(format!( + "function '{}' has catalogId '{id}' but catalogLink is {:?}", + function.id, link + ))); + } + (None, CatalogLink::Canonical) => { + return Err(ManifestError(format!( + "function '{}' has no catalogId or explicit catalogLink reason", + function.id + ))); + } + } if let Some(contextual) = &function.contextual_domain { let by_param = function .params @@ -673,6 +708,10 @@ mod tests { assert_eq!(manifest.schema_version, 1); assert_eq!(manifest.reference.name, "overpy"); assert_eq!(manifest.reference.version, "9.7.10"); + assert_eq!( + manifest.reference.content_commit, + "889d9749d1def17f146548cbddb94ea1ab015847" + ); assert!(!manifest.functions.is_empty()); assert!(!manifest.aliases.is_empty()); // Enum-domain *identities* come from the function signatures @@ -682,6 +721,20 @@ mod tests { for domain in ["Invis", "ChaseTimeReeval", "Team", "LosCheck", "Color"] { assert!(manifest.domain_identity(domain), "{domain}"); } + assert_eq!( + manifest + .function("chase") + .expect("chase entry") + .catalog_link, + CatalogLink::SpecialLowering + ); + assert_eq!( + manifest + .member("getHero") + .expect("getHero entry") + .catalog_link, + CatalogLink::CatalogGap + ); assert!( !manifest.domain_identity("ChaseReeval"), "contextual domains are not standalone identities" @@ -718,6 +771,10 @@ mod tests { let error = mutate(|file| file.functions.push(file.functions[0].clone())) .expect_err("duplicate function id must fail"); assert!(error.0.contains("duplicate function id")); + // A direct catalog link must be explicit about being canonical. + let error = mutate(|file| file.functions[0].catalog_link = CatalogLink::CatalogGap) + .expect_err("canonical catalog id must not carry a gap reason"); + assert!(error.0.contains("catalogLink")); // entry without evidence let error = mutate(|file| file.functions[0].evidence.clear()) .expect_err("missing evidence must fail"); diff --git a/crates/opy-frontend/src/support.rs b/crates/opy-frontend/src/support.rs index b2a270f..9e1c74f 100644 --- a/crates/opy-frontend/src/support.rs +++ b/crates/opy-frontend/src/support.rs @@ -218,14 +218,14 @@ mod tests { assert_eq!(syntax.len(), 7); assert!(syntax.iter().all(|feature| feature.category == "syntax")); let lowering = matrix.features_by_state("lowering-dependent"); - assert_eq!(lowering.len(), 8); + assert_eq!(lowering.len(), 11); assert!( lowering .iter() .all(|feature| feature.state == "lowering-dependent") ); - assert_eq!(matrix.summary().by_state["planned"], 8); - assert_eq!(matrix.summary().by_category["semantics"], 10); + assert_eq!(matrix.summary().by_state["planned"], 6); + assert_eq!(matrix.summary().by_category["semantics"], 14); // Every feature id is unique. let mut ids: Vec<&str> = matrix .features diff --git a/docs/opy/architecture.md b/docs/opy/architecture.md index 34de074..32e83ac 100644 --- a/docs/opy/architecture.md +++ b/docs/opy/architecture.md @@ -58,6 +58,34 @@ stage (issue #8). or a temporary Workshop IR; those checks are deferred (`lowering-dependent`, #8) rather than approximated. +## Integration input contract (#8) + +The frontend-to-Workshop boundary has two inputs, both already available +without a `workshop-rs` dependency in this repository: + +* the resolved Opy HIR v1 program, whose `call` and `receiverCall` nodes retain + the OPY source identity and source span; and +* the validated OPY semantic manifest (`Manifest::builtin()`), whose function + entries provide the call kind, receiver category, parameter binding/defaults, + aliases, contextual dispatch, and optional canonical `catalogId`. + +The #8 adapter combines those inputs by looking up a call's resolved OPY +identity in the manifest. It does not require parser or HIR redesign and it +does not infer Workshop content from an absent entry. For an entry with a +`catalogId`, `workshop-rs` must cross-check that the canonical id exists in its +catalog and has the compatible action/value/member kind. It must also +cross-check manifest alias targets and contextual dispatch targets against the +same canonical-id namespace. Parameter `domain` values and contextual option +domains are identity links; member lists, domain membership, localized +spellings, and settings/content data remain `workshop-rs` inputs. + +Entries without a `catalogId` are explicit frontend special forms or +integration gaps (`debug`, `print`, `chase`, `stopChasing`, `range`, `append`, +`getHero`, and `hasStatus` in the current manifest). The adapter must handle +those cases by their documented source semantics or report a structured +lowering-dependent gap; it must not synthesize a catalog id or copy Workshop +data into `opy-rs`. + ## Stable contracts The following contracts are stable and preserved verbatim across the @@ -115,12 +143,13 @@ partially delivered): native differential suite (`cargo test -p opy-frontend --test differential`). -Readiness: issue #7 is the active Workshop-independent compatibility gate -(the eight remaining `planned` features and the full builtin/enum/receiver -surface beyond the manifest-declared evidence); issue #8 (Workshop lowering, -catalog, emission, and post-compile-hook execution against Workshop text) is -blocked on `#7` and the `wrightkit/workshop-rs#2` contracts and is not -started here. +Readiness: issue #7 is the active Workshop-independent compatibility gate. +Issue #30 has separated the manifest-declared OPY semantic overlay from the +canonical Workshop catalog; the remaining `planned` rows are the pure syntax +and directive work tracked by #28/#29 plus the explicitly recorded bare +playervar receiver residual. Issue #8 (Workshop lowering, catalog, emission, +and post-compile-hook execution against Workshop text) is blocked on #7 and +the `wrightkit/workshop-rs#2` contracts and is not started here. ## Validation diff --git a/docs/opy/compat-manifest-spec.md b/docs/opy/compat-manifest-spec.md index 85541b2..3d5a47e 100644 --- a/docs/opy/compat-manifest-spec.md +++ b/docs/opy/compat-manifest-spec.md @@ -66,6 +66,7 @@ It is **language-compatibility metadata**, distinct from: "default": "DESTINATION_AND_DURATION" } ], "catalogId": "chaseOverTime", // link to the Workshop emission catalog id + "catalogLink": "canonical", // canonical | special-lowering | legacy-alias | catalog-gap "evidence": ["chase-over-time"] // oracle probes validating this entry }, { @@ -153,10 +154,15 @@ Entry semantics: rejected like the reference rejects it. * `context`: a call-context restriction; `"forIterable"` (`range`) is only valid as a `for ... in` iterable. -* `catalogId`: the canonical Workshop emission catalog id; absent for - special emission forms (`debug`/`print`, `append` via Modify) or emission - surfaces not yet catalog-covered (documented gaps). Catalog linkage is a - `workshop-rs` integration concern. +* `catalogId`: the canonical Workshop emission catalog id. A direct catalog + entry uses `catalogLink: "canonical"` (the default); a missing `catalogId` + must carry an explicit `catalogLink` reason: `special-lowering` for a + frontend form with custom lowering (`debug`, `print`, `chase`, `range`, or + `append`), `legacy-alias` for a source identity whose compatibility is + represented by an alias path (`stopChasing`), or `catalog-gap` for a + probe-evidenced source member without a current canonical catalog entry + (`getHero`, `hasStatus`). Catalog linkage is a `workshop-rs` integration + concern; opy-rs must not guess an id for a non-canonical entry. * `evidence`: every entry must reference at least one probe recording oracle acceptance (deterministic `check` failure otherwise). @@ -188,7 +194,9 @@ hash, and, for rejections, the diagnostic category fragment). integrity (the contextual domain is not a standalone declared identity, the selector parameter exists and its keyword spellings cover the options, every option domain is declared), and entries lacking oracle evidence all - fail deterministically; a canonical-rewrite test pins the data file to its + fail deterministically. A function with `catalogId` must use + `catalogLink: "canonical"`; a function without one must state a non-canonical + reason. A canonical-rewrite test pins the data file to its byte-canonical form, and a cross-check test pins every `catalogId` (and contextual option target) to the Workshop emission catalog once the `workshop-rs` catalog contract exists. @@ -225,6 +233,39 @@ hash, and, for rejections, the diagnostic category fragment). * documentation, agents, and future release metadata can consume the same declared boundary. +## Integration cross-check contract (#30, consumed by #8) + +The manifest is the frontend-owned side of the integration contract. The +consumer receives the resolved Opy HIR plus this validated manifest; it does +not need to import OverPy data or add a parser/frontend dependency. HIR +`call`/`receiverCall` names are resolved against the manifest before lowering, +so source spans and OPY diagnostics remain owned by `opy-rs`. + +The adapter pins the `workshop-rs` `CatalogIdentity` (implementation version, +catalog version, content digest, target, and locale coverage) in its own +integration evidence. For every manifest function entry with `catalogId`, the +adapter must verify all of the following against that canonical catalog: + +1. the id exists; +2. `action`/`memberAction` map to Workshop `Action`, while + `value`/`memberValue` map to Workshop `Value`; +3. a member entry's receiver category is compatible with the canonical + signature; and +4. each `Param.domain` is checked at its explicit-argument index (the member + receiver is not counted as a parameter) against the catalog's expected + domain. + +The adapter performs the same canonical-id check for alias targets and +contextual-domain option targets. A contextual merged domain such as +`ChaseReeval` is not passed to `Catalog::enum_domain`; its concrete option +domains are. A `domain` is only an identity link in this manifest: the +canonical member list, domain membership, localized spelling, settings keys, +and content values are validated by `workshop-rs` at lowering. Entries without +`catalogId` follow their explicit `catalogLink` reason and must not be assigned +guessed ids. A failed cross-check is a structured integration error, never a +silent success. This keeps Workshop catalog/member/enum/settings/locale data +in its owning repository. + ## Non-goals * A runtime-downloadable or hot-updating content registry. diff --git a/docs/opy/compatibility-baseline.md b/docs/opy/compatibility-baseline.md index 4c3012b..7fcf4f6 100644 --- a/docs/opy/compatibility-baseline.md +++ b/docs/opy/compatibility-baseline.md @@ -68,9 +68,13 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and | 4a | `@Team`/`@Slot` with arguments, `@Name`, `@Hero`, `@Disabled`, `@Delimiter`, `@NewPage`, `@SuppressWarnings` | `evidence-prioritized` | ❌ | ❌ | ❌ | ❌ | ✅ oracle probes | | 5 | **Preprocessing/include/macro**: `#!include`, `#!define` (object- and function-like), `#!undef`, include cycle detection | `baseline-supported` | ✅ | ✅ | ✅ (integration) | ✅ | ✅ | | 5a | `#!mainFile`, `#!allowMacroRedeclaration`, `#!optimize*`/`#!replace0By*` family, `#!translations`, `#!rulePrefix*`, `__script__` JS hooks | `legacy-quirk/demand-driven` | ❌ | ❌ | ❌ | ❌ | partial | -| 6 | **Builtin actions & values (generic)**: the 225 action / 267 value Workshop surface | `baseline-supported` for the manifest-declared evidence surface (chaseOverTime, isGameInProgress, getPlayersInRadius, worldVector, the corpus call surface); the full surface stays **`baseline-planned`** | ✅ | ✅ | ✅ (catalog subset, integration) | ✅ | ✅ probes | -| 7 | **Receiver/member functions**: `eventPlayer.setMoveSpeed(100)`, `eventPlayer.isAlive()`, variable receivers | `baseline-supported` for the manifest-declared member surface (receiver categories, explicit-arg signatures); **`baseline-planned`** for the full member surface | ✅ | ✅ | ✅ (catalog subset, integration) | ✅ | ✅ | -| 8 | **Builtin enum/constant domains**: 46 upstream domains (incl. `Hero`/`Map`/`Gamemode` literals) | `baseline-supported` for the manifest-declared domain **identities** (opaque member resolution; Workshop member lists are catalog content and **not** carried here; see `compat-manifest-spec.md`); **`baseline-planned`** (systematic) for the full surface | ✅ (declared domains) | ✅ | partial | partial | ✅ probes | +| 6 | **OPY builtin actions & values (generic)**: manifest identities, signatures, aliases, and call semantics | `baseline-supported` for the probe-validated manifest overlay; canonical Workshop existence/content/emission is `lowering-dependent` | ✅ | ✅ | partial (integration) | ✅ | ✅ probes | +| 6a | **Canonical Workshop builtin action/value catalog**: full catalog existence, content, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | +| 7 | **OPY receiver/member semantics**: receiver categories, explicit-argument signatures, variable receivers | `baseline-supported` for the manifest-declared OPY overlay; canonical member existence/content/emission is `lowering-dependent` | ✅ | ✅ | partial (integration) | ✅ | ✅ probes | +| 7a | **Canonical Workshop receiver/member catalog**: member existence, content, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | +| 7b | Bare playervar receiver member access (`A = B.C`) | `baseline-planned` (explicit OPY-owned residual; not a Workshop catalog reclassification) | ❌ | ❌ | ❌ | ❌ | ✅ oracle evidence | +| 8 | **OPY enum/domain semantics**: declared domain identities and contextual dispatch | `baseline-supported` for manifest identity links; canonical member lists/membership/emission are `lowering-dependent` | ✅ (identities) | ✅ | partial (integration) | partial | ✅ probes | +| 8a | **Canonical Workshop enum/domain catalog**: member lists, membership, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | | 9 | **Aliases**: old function names (`stopChasingVariable`→`stopChasing`, `getCurrentHero`→`getHero`, `hasStatusEffect`→`hasStatus`, …), hero renames (`MCCREE`→`CASSIDY`), `ChaseReeval` contextual alias | `baseline-supported` for the three manifest-declared non-contextual aliases and the `ChaseReeval` call-context resolution; the remaining alias surface stays `legacy-quirk/demand-driven` | ✅ (declared) | ✅ | ✅ (chase forms catalog-covered at integration) | ✅ | ✅ | | 10 | **Modules**: `random.{randint,uniform,choice,shuffle}` | `baseline-supported` (corpus: `random.uniform`, `random.choice`) | ✅ | ✅ | ✅ (integration) | ✅ | ✅ | | 11 | **Named/keyword arguments**: `chase(A, B, rate=30, …)`, generic `name = expr` binding against manifest signatures | `baseline-supported` for the evidence surface (generic keyword binding plus the `chase`/`ChaseReeval` call-context forms); `raycast` `include=`/`exclude=` forms and macro keyword arguments stay `evidence-prioritized` (no corpus/reference evidence in the declared surface) | ✅ | ✅ | ✅ (integration) | ✅ | ✅ probes | @@ -80,12 +84,13 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and ## Current `planned` entries -The eight Workshop-independent features still `planned` in +The six explicitly tracked gaps still `planned` in `compatibility/support-matrix.json`, the mechanically checked state source, are: `syntax/switch`, `syntax/string-modifiers`, -`semantics/builtin-actions-values`, `semantics/receiver-members`, -`semantics/enum-domains`, `preprocessing/advanced-directives`, -`translations/directive`, and `optimization/controls`. Their tiers above +`semantics/receiver-playervar`, `preprocessing/advanced-directives`, +`translations/directive`, and `optimization/controls`. The three semantic +overlay rows are supported at the OPY manifest boundary; their canonical +Workshop catalog rows are explicitly `lowering-dependent`. Their tiers above distinguish **evidence-prioritized** work (broad or high-fan-out surface with clear tooling value, ordered by corpus/consumer evidence) from **legacy-quirk/demand-driven** compatibility (rare historical quirks and diff --git a/docs/opy/support-matrix.md b/docs/opy/support-matrix.md index 680830d..ecb87d2 100644 --- a/docs/opy/support-matrix.md +++ b/docs/opy/support-matrix.md @@ -171,7 +171,7 @@ The pinned reference ABI (`src/compiler/tokenizer.ts`, `src/quickjs.ts`, `random.choice` → `random.` calls; `eventPlayer.member` → `PlayerVar`/receiver call on `EventPlayer`; variable receivers (`points.append`, `candlePos[i2]`) → `ReceiverCall`/`Index`. -- Builtin action/value/member identity, signatures, receiver categories, +- OPY-owned builtin action/value/member identity, signatures, receiver categories, parameter enum domains, and non-contextual aliases resolve through the OPY semantic compatibility manifest (`crates/opy-frontend/src/manifest/`, schema v1; spec in [`compat-manifest-spec.md`](compat-manifest-spec.md)), the @@ -197,7 +197,7 @@ The pinned reference ABI (`src/compiler/tokenizer.ts`, `src/quickjs.ts`, `eventPlayer.teleport(eventPlayer.getPosition())`, `target.setMoveSpeed(50)` on a player-valued global) lower to `ReceiverCall`; their Workshop emission resolves through the `workshop-rs` - catalog, **lowering-dependent** (inventory-only until integration). The + catalog, **lowering-dependent** (inventory-only until integration); the corpus-evidenced receiver methods are the `synthetic/receiver-calls` fixture methods plus the enum-gated members (en-US spellings per the oracle-transcribed evidence). @@ -228,8 +228,9 @@ The pinned reference ABI (`src/compiler/tokenizer.ts`, `src/quickjs.ts`, removed from the frontend core and is **lowering-dependent** (#8). The checks are never approximated (custom, user-declared enum members are OPY-level source semantics and stay frontend-validated). Enum - domains/members beyond the declared baseline remain `baseline-planned`; - emission coverage stays corpus-scoped and is **lowering-dependent**. + complete Workshop domain/member catalog remains `lowering-dependent` and is + owned by `workshop-rs`; the manifest carries identity links only and never a + copied member allowlist. - `wait()` / `wait(time)` default-argument filling: the reference appends `Wait.IGNORE_CONDITION` (and `0.016` for the no-argument form). - **Named/keyword arguments** (`name = expr` call arguments) bind against the @@ -312,12 +313,11 @@ The pinned reference ABI (`src/compiler/tokenizer.ts`, `src/quickjs.ts`, post-compile hooks" above); the remaining runtime surface is `lowering-dependent`: hook output into Workshop emission and catalog constant population (`Map`/`Hero`/… objects stay empty). -- OverPy enum domains/members beyond the manifest's declared baseline (a - data change, `baseline-planned` in 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 once integration lands, never silently. +- Canonical Workshop enum domains/members and emission spellings are + `lowering-dependent` and owned by `workshop-rs`; they are not OPY frontend + gaps. Manifest entries without a direct catalog id carry an explicit + `catalogLink` reason (`special-lowering`, `legacy-alias`, or `catalog-gap`) + and remain visible to the integration adapter. - Rule `disabled` markers (no corpus evidence for the source annotation). - Expression-level `in`/`not in` membership operators: rejected at parsing (`for ... in` headers are supported). From 28e9dfd48274cb6685cdde45827dac96d54cd79a Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 18 Aug 2026 04:33:25 +0800 Subject: [PATCH 2/2] feat(opy): preserve variable member expressions (Refs #30) --- compatibility/differential-expectations.json | 1 + .../synthetic/receiver-playervar/fixture.json | 20 ++++++++ .../synthetic/receiver-playervar/oracle.json | 27 +++++++++++ .../synthetic/receiver-playervar/source.opy | 5 ++ compatibility/support-matrix.json | 9 ++-- compatibility/tests/test_runner.py | 2 +- compatibility/tests/test_support_matrix.py | 4 +- crates/opy-frontend/src/cst.rs | 2 + crates/opy-frontend/src/hir/dump.rs | 7 +++ crates/opy-frontend/src/hir/types.rs | 13 +++++ crates/opy-frontend/src/hir/validate.rs | 2 + crates/opy-frontend/src/lower.rs | 48 ++++++++++++++++++- crates/opy-frontend/src/parser.rs | 6 ++- crates/opy-frontend/src/support.rs | 2 +- crates/opy-frontend/src/tooling.rs | 1 + crates/opy-frontend/tests/differential.rs | 6 +++ docs/opy/compatibility-baseline.md | 8 ++-- 17 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 compatibility/fixtures/synthetic/receiver-playervar/fixture.json create mode 100644 compatibility/fixtures/synthetic/receiver-playervar/oracle.json create mode 100644 compatibility/fixtures/synthetic/receiver-playervar/source.opy diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index c97a44b..075f51a 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -15,6 +15,7 @@ {"fixture": "synthetic/preprocessing", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:synthetic/preprocessing/oracle.json", "implementation-invariant:preprocessor-include-define-undef"], "note": "Include, object-like and function-like defines, and undef are preserved through preprocessing."}, {"fixture": "synthetic/settings", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/settings/oracle.json", "implementation-invariant:settings-structure-only"], "note": "Settings are structurally represented; Workshop key and leaf validation remains lowering-dependent."}, {"fixture": "synthetic/receiver-calls", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/receiver-calls/oracle.json", "implementation-invariant:receiver-call-resolution"], "note": "The exercised receiver/member forms resolve through the OPY semantic model."}, + {"fixture": "synthetic/receiver-playervar", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/receiver-playervar/oracle.json", "implementation-invariant:receiver-playervar-member-expression"], "note": "A bare variable member expression is retained as an OPY HIR member node; canonical Workshop member validation remains lowering-dependent."}, {"fixture": "synthetic/chase-enums", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-enums/oracle.json", "implementation-invariant:opaque-workshop-enum-identities"], "note": "Declared chase enum identities resolve without duplicating Workshop catalog validation."}, {"fixture": "synthetic/chase-condition-agentlab", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-condition-agentlab/oracle.json", "regression:agent-lab-chase-condition-shape"], "note": "The agent-lab chase condition shape is retained as an OPY frontend regression."}, {"fixture": "synthetic/chase-keywords", "nativeStatus": "success", "classification": "match", "ruleNames": true, "evidence": ["oracle:synthetic/chase-keywords/oracle.json", "implementation-invariant:keyword-binding"], "note": "Generic keyword binding and the chase contextual form resolve."}, diff --git a/compatibility/fixtures/synthetic/receiver-playervar/fixture.json b/compatibility/fixtures/synthetic/receiver-playervar/fixture.json new file mode 100644 index 0000000..d99face --- /dev/null +++ b/compatibility/fixtures/synthetic/receiver-playervar/fixture.json @@ -0,0 +1,20 @@ +{ + "category": "synthetic", + "expectedStatus": "success", + "features": [ + "semantics/receiver-playervar" + ], + "files": { + "source.opy": "c8040ee474759c5b27541b2b9a514d350f605f471bc23246d1baeadc37a5eadc" + }, + "id": "synthetic/receiver-playervar", + "provenance": { + "kind": "original", + "license": "AGPL-3.0-or-later", + "origin": "Wright compatibility corpus; minimized source-language regression for the pinned OverPy receiver-member behavior.", + "redistributable": true + }, + "provenanceNote": "Synthetic-original fixture authored from the pinned oracle observation that a bare variable member expression is accepted. It does not reproduce Workshop catalog data; member existence remains an integration concern.", + "schemaVersion": 1, + "source": "source.opy" +} diff --git a/compatibility/fixtures/synthetic/receiver-playervar/oracle.json b/compatibility/fixtures/synthetic/receiver-playervar/oracle.json new file mode 100644 index 0000000..c2284e0 --- /dev/null +++ b/compatibility/fixtures/synthetic/receiver-playervar/oracle.json @@ -0,0 +1,27 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "\u2713 Lockfile passes supply-chain policies (verified 1d ago)\nLockfile is up to date, resolution step is skipped\nProgress: resolved 1, reused 0, downloaded 0, added 0\nPackages: +1\n+\nProgress: resolved 1, reused 1, downloaded 0, added 1, done\n\ndependencies:\n+ overpy 9.7.10\n\nDone in 334ms using pnpm v11.10.0\n", + "workshop": "variables {\n global:\n 0: A\n 1: B\n player:\n 0: B\n 2: C\n}\n\nrule (\"receiver\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(A, (Global.B).C);\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: A\n 1: B\n player:\n 0: B\n 2: C\n}\n\nrule (\"receiver\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(A, (Global.B).C);\n }\n}\n\n", + "workshopSha256": "8273c1fcb0e1e6c2f2bece1cc5f4730d1f93e62d65a629cfb8c6f0f06382fb6c" + }, + "fixture": "synthetic/receiver-playervar", + "input": { + "sha256": "c8040ee474759c5b27541b2b9a514d350f605f471bc23246d1baeadc37a5eadc", + "source": "source.opy" + }, + "oracle": { + "gitHead": "1e2688954302a402d076944b46db07efb14d7b61", + "integrity": "sha512-oX17nauJcPTaKIrRFY/rD0Rl8atqFUVv9Hg2TKH+A68/fC8+ZO344Mkd1A/Y0oOVp1hr5tktMBjzMEDDnMEYUw==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.10.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.10" + }, + "schemaVersion": 1 +} diff --git a/compatibility/fixtures/synthetic/receiver-playervar/source.opy b/compatibility/fixtures/synthetic/receiver-playervar/source.opy new file mode 100644 index 0000000..dcb5f19 --- /dev/null +++ b/compatibility/fixtures/synthetic/receiver-playervar/source.opy @@ -0,0 +1,5 @@ +globalvar A +playervar B +rule "receiver": + @Event eachPlayer + A = B.C diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 64bb22e..11f3fee 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -219,12 +219,13 @@ "id": "semantics/receiver-playervar", "name": "Bare playervar receiver member access (`A = B.C`)", "category": "semantics", - "state": "planned", + "state": "semantic-supported", "evidence": [ + "fixtures:synthetic/receiver-playervar", "upstream:src/tests/variables.opy", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #30 audit. The pinned oracle accepts this OPY-owned receiver form, but the current frontend still rejects it. It remains an explicit, source-language follow-up rather than being hidden inside canonical Workshop member breadth; #8 must not be used to claim it complete." + "notes": "Issue #30 repair. The pinned oracle accepts this OPY-owned receiver form and the frontend preserves its variable receiver and member identity in HIR with source provenance. Canonical member existence and emission remain Workshop-owned and lowering-dependent; #8 must not be used to claim the OPY expression incomplete." }, { "id": "semantics/enum-domains", @@ -559,9 +560,9 @@ ], "summary": { "byState": { - "planned": 6, + "planned": 5, "frontend-supported": 14, - "semantic-supported": 8, + "semantic-supported": 9, "lowering-dependent": 11, "end-to-end-supported": 0 }, diff --git a/compatibility/tests/test_runner.py b/compatibility/tests/test_runner.py index a24edc0..f724ce9 100644 --- a/compatibility/tests/test_runner.py +++ b/compatibility/tests/test_runner.py @@ -36,7 +36,7 @@ def test_repository_fixture_metadata_and_snapshots_are_valid(self): fixtures = run_oracle.discover_fixtures( COMPATIBILITY_DIR / "fixtures" ) - self.assertEqual(len(fixtures), 27) + self.assertEqual(len(fixtures), 28) for fixture_path, fixture in fixtures: snapshot = fixture_path.parent / "oracle.json" self.assertTrue(snapshot.is_file(), fixture["id"]) diff --git a/compatibility/tests/test_support_matrix.py b/compatibility/tests/test_support_matrix.py index 2e93c58..715d74e 100644 --- a/compatibility/tests/test_support_matrix.py +++ b/compatibility/tests/test_support_matrix.py @@ -114,7 +114,9 @@ def test_semantic_ownership_split_is_explicit(self): "semantics/workshop-enum-domains", ): self.assertEqual(by_id[catalog]["state"], "lowering-dependent") - self.assertEqual(by_id["semantics/receiver-playervar"]["state"], "planned") + self.assertEqual( + by_id["semantics/receiver-playervar"]["state"], "semantic-supported" + ) if __name__ == "__main__": diff --git a/crates/opy-frontend/src/cst.rs b/crates/opy-frontend/src/cst.rs index 15db6ff..bf397da 100644 --- a/crates/opy-frontend/src/cst.rs +++ b/crates/opy-frontend/src/cst.rs @@ -234,6 +234,8 @@ pub enum Expr { Member { receiver: Box, member: String, + /// The exact span of the member identifier after `.`. + member_span: Span, span: Span, }, Index { diff --git a/crates/opy-frontend/src/hir/dump.rs b/crates/opy-frontend/src/hir/dump.rs index 4e074d2..46cf5ee 100644 --- a/crates/opy-frontend/src/hir/dump.rs +++ b/crates/opy-frontend/src/hir/dump.rs @@ -386,6 +386,13 @@ fn render_expr(expr: &Expr, out: &mut String) { out.push('.'); out.push_str(name); } + Expr::Member { + receiver, member, .. + } => { + render_expr(receiver, out); + out.push('.'); + out.push_str(member); + } Expr::EventPlayer { .. } => out.push_str("eventPlayer"), Expr::Constant { name, .. } => out.push_str(name), Expr::Call { name, args, .. } => { diff --git a/crates/opy-frontend/src/hir/types.rs b/crates/opy-frontend/src/hir/types.rs index 12ccc46..ca9aeac 100644 --- a/crates/opy-frontend/src/hir/types.rs +++ b/crates/opy-frontend/src/hir/types.rs @@ -409,6 +409,17 @@ pub enum Expr { #[serde(skip_serializing_if = "Option::is_none")] span: Option, }, + /// An OPY member expression whose canonical Workshop meaning is deferred + /// to the integration catalog. The receiver and source member identity + /// remain available to tooling and lowering. + Member { + receiver: Box, + member: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + member_span: Option, + #[serde(skip_serializing_if = "Option::is_none")] + span: Option, + }, EventPlayer { #[serde(skip_serializing_if = "Option::is_none")] span: Option, @@ -486,6 +497,7 @@ impl Expr { | Expr::Enum { span, .. } | Expr::GlobalVar { span, .. } | Expr::PlayerVar { span, .. } + | Expr::Member { span, .. } | Expr::EventPlayer { span } | Expr::Constant { span, .. } | Expr::Call { span, .. } @@ -511,6 +523,7 @@ impl Expr { Expr::Enum { .. } => "enum", Expr::GlobalVar { .. } => "globalVar", Expr::PlayerVar { .. } => "playerVar", + Expr::Member { .. } => "member", Expr::EventPlayer { .. } => "eventPlayer", Expr::Constant { .. } => "constant", Expr::Call { .. } => "call", diff --git a/crates/opy-frontend/src/hir/validate.rs b/crates/opy-frontend/src/hir/validate.rs index eeb09d8..5596dc5 100644 --- a/crates/opy-frontend/src/hir/validate.rs +++ b/crates/opy-frontend/src/hir/validate.rs @@ -53,6 +53,7 @@ const EXPR_KINDS: &[&str] = &[ "enum", "globalVar", "playerVar", + "member", "eventPlayer", "constant", "call", @@ -600,6 +601,7 @@ fn for_each_expr<'a>(expr: &'a Expr, f: &mut impl FnMut(&'a Expr)) { for_each_expr(z, f); } Expr::PlayerVar { player, .. } => for_each_expr(player, f), + Expr::Member { receiver, .. } => for_each_expr(receiver, f), Expr::Call { args, .. } | Expr::MacroCall { args, .. } | Expr::Format { args, .. } => { for arg in args { for_each_expr(arg, f); diff --git a/crates/opy-frontend/src/lower.rs b/crates/opy-frontend/src/lower.rs index 6c3ba1f..084b764 100644 --- a/crates/opy-frontend/src/lower.rs +++ b/crates/opy-frontend/src/lower.rs @@ -462,8 +462,9 @@ impl Lowerer { Expr::Member { receiver, member, + member_span, span, - } => self.lower_member(receiver, member, *span, macro_params), + } => self.lower_member(receiver, member, *member_span, *span, macro_params), Expr::Index { array, index, span } => HirExpr::Index { array: Box::new(self.lower_expr(array, macro_params, CallPosition::Value)), index: Box::new(self.lower_expr(index, macro_params, CallPosition::Value)), @@ -550,6 +551,7 @@ impl Lowerer { &mut self, receiver: &Expr, member: &str, + member_span: Span, span: Span, _macro_params: &[String], ) -> HirExpr { @@ -601,6 +603,21 @@ impl Lowerer { ); return HirExpr::Null { span: None }; } + // A bare variable receiver member is valid OPY source syntax even + // when canonical member existence is deferred to Workshop. Keep + // both the resolved variable receiver and the source member + // identity in HIR instead of treating it as an unknown member. + if self.globals.contains(name) + || self.players.contains(name) + || default_var_index(name).is_some() + { + return HirExpr::Member { + receiver: Box::new(self.lower_name(name, receiver.span(), &[])), + member: member.to_string(), + member_span: Some(member_span.into()), + span: Some(span.into()), + }; + } } self.error_at( "unsupported-member", @@ -974,6 +991,7 @@ impl Lowerer { receiver, member, span, + .. } = &arg.value { if let Expr::Name { name, .. } = receiver.as_ref() { @@ -1365,6 +1383,34 @@ mod tests { ); } + #[test] + fn bare_variable_member_expression_preserves_receiver_and_member() { + let hir = lower_ok( + "globalvar A\nplayervar B\nrule \"receiver\":\n @Event eachPlayer\n A = B.C\n", + ); + let HirStmt::Assign { value, .. } = &hir + .rules + .iter() + .find_map(|entry| { + let RuleEntry::Rule(rule) = entry else { + return None; + }; + rule.actions.first() + }) + .expect("assignment") + else { + panic!("expected assignment"); + }; + let HirExpr::Member { + receiver, member, .. + } = value.as_ref() + else { + panic!("expected opaque member expression, got {value:?}"); + }; + assert_eq!(member, "C"); + assert!(matches!(receiver.as_ref(), HirExpr::PlayerVar { name, .. } if name == "B")); + } + #[test] fn receiver_call_values_lower_in_conditions() { // `@Condition eventPlayer.isAlive()` lowers to a ReceiverCall value; diff --git a/crates/opy-frontend/src/parser.rs b/crates/opy-frontend/src/parser.rs index f96e133..a8f9e5a 100644 --- a/crates/opy-frontend/src/parser.rs +++ b/crates/opy-frontend/src/parser.rs @@ -917,6 +917,7 @@ impl Parser<'_> { receiver, member, span, + .. } => Expr::ReceiverCall { receiver, name: member, @@ -949,15 +950,18 @@ impl Parser<'_> { } TokenKind::Dot => { self.advance(); + let member_token = self.peek().clone(); let member = match self.expect_ident("a member name after '.'") { Ok(member) => member, Err(()) => return Err(()), }; - let end = self.tokens[self.pos.saturating_sub(1)].span.end; + let member_span = member_token.span; + let end = member_span.end; let span = Span::new(base.span().file, base.span().start, end); base = Expr::Member { receiver: Box::new(base), member, + member_span, span, }; } diff --git a/crates/opy-frontend/src/support.rs b/crates/opy-frontend/src/support.rs index 9e1c74f..beacd48 100644 --- a/crates/opy-frontend/src/support.rs +++ b/crates/opy-frontend/src/support.rs @@ -224,7 +224,7 @@ mod tests { .iter() .all(|feature| feature.state == "lowering-dependent") ); - assert_eq!(matrix.summary().by_state["planned"], 6); + assert_eq!(matrix.summary().by_state["planned"], 5); assert_eq!(matrix.summary().by_category["semantics"], 14); // Every feature id is unique. let mut ids: Vec<&str> = matrix diff --git a/crates/opy-frontend/src/tooling.rs b/crates/opy-frontend/src/tooling.rs index 1cd08bb..3dcc946 100644 --- a/crates/opy-frontend/src/tooling.rs +++ b/crates/opy-frontend/src/tooling.rs @@ -507,6 +507,7 @@ impl SemanticModel { sites.push((SymbolKind::Player, name.clone(), to_frontend_span(*span))); } } + HirExpr::Member { receiver, .. } => Self::collect_expr(receiver, sites), HirExpr::Array { elements, .. } => { for element in elements { Self::collect_expr(element, sites); diff --git a/crates/opy-frontend/tests/differential.rs b/crates/opy-frontend/tests/differential.rs index 130f055..321633c 100644 --- a/crates/opy-frontend/tests/differential.rs +++ b/crates/opy-frontend/tests/differential.rs @@ -199,6 +199,12 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { true, "receiver/member call forms; oracle status success.", ); + resolve( + &mut cases, + "synthetic/receiver-playervar", + true, + "bare variable member expression is preserved as an OPY HIR member node; canonical Workshop member validation remains lowering-dependent.", + ); resolve( &mut cases, "synthetic/chase-enums", diff --git a/docs/opy/compatibility-baseline.md b/docs/opy/compatibility-baseline.md index 7fcf4f6..0e3f9b0 100644 --- a/docs/opy/compatibility-baseline.md +++ b/docs/opy/compatibility-baseline.md @@ -72,7 +72,7 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and | 6a | **Canonical Workshop builtin action/value catalog**: full catalog existence, content, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | | 7 | **OPY receiver/member semantics**: receiver categories, explicit-argument signatures, variable receivers | `baseline-supported` for the manifest-declared OPY overlay; canonical member existence/content/emission is `lowering-dependent` | ✅ | ✅ | partial (integration) | ✅ | ✅ probes | | 7a | **Canonical Workshop receiver/member catalog**: member existence, content, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | -| 7b | Bare playervar receiver member access (`A = B.C`) | `baseline-planned` (explicit OPY-owned residual; not a Workshop catalog reclassification) | ❌ | ❌ | ❌ | ❌ | ✅ oracle evidence | +| 7b | Bare playervar receiver member access (`A = B.C`) | `baseline-supported` for the OPY member-expression representation; canonical member existence remains lowering-dependent | ✅ | ✅ | partial (integration) | ✅ | ✅ oracle evidence | | 8 | **OPY enum/domain semantics**: declared domain identities and contextual dispatch | `baseline-supported` for manifest identity links; canonical member lists/membership/emission are `lowering-dependent` | ✅ (identities) | ✅ | partial (integration) | partial | ✅ probes | | 8a | **Canonical Workshop enum/domain catalog**: member lists, membership, and emission | `lowering-dependent` (`workshop-rs`, #8) | — | — | ❌ (integration) | — | ✅ inventory/oracle evidence | | 9 | **Aliases**: old function names (`stopChasingVariable`→`stopChasing`, `getCurrentHero`→`getHero`, `hasStatusEffect`→`hasStatus`, …), hero renames (`MCCREE`→`CASSIDY`), `ChaseReeval` contextual alias | `baseline-supported` for the three manifest-declared non-contextual aliases and the `ChaseReeval` call-context resolution; the remaining alias surface stays `legacy-quirk/demand-driven` | ✅ (declared) | ✅ | ✅ (chase forms catalog-covered at integration) | ✅ | ✅ | @@ -84,10 +84,10 @@ rejected/documented-absent dimension, `—` an inapplicable dimension, and ## Current `planned` entries -The six explicitly tracked gaps still `planned` in +The five explicitly tracked gaps still `planned` in `compatibility/support-matrix.json`, the mechanically checked state source, are: `syntax/switch`, `syntax/string-modifiers`, -`semantics/receiver-playervar`, `preprocessing/advanced-directives`, +`preprocessing/advanced-directives`, `translations/directive`, and `optimization/controls`. The three semantic overlay rows are supported at the OPY manifest boundary; their canonical Workshop catalog rows are explicitly `lowering-dependent`. Their tiers above @@ -108,7 +108,7 @@ and remaining gaps stay classified rather than being filed per-symbol. | Evidence | Oracle 9.7.10 | opy-rs status | Classification | | --- | --- | --- | --- | -| **Bare playervar receiver**: `A = B.C` (declared playervar member on a player-valued receiver) | accept (`__playerVar__`) | not implemented: currently rejects with `unsupported-member`; must accept per the manifest contract | `baseline-planned` (receiver/member semantics + playervar member resolution, category 7) | +| **Bare playervar receiver**: `A = B.C` (declared playervar member on a player-valued receiver) | accept (`__playerVar__`) | implemented as a provenance-preserving OPY member expression; canonical member existence remains lowering-dependent | `baseline-supported` (receiver/member semantics; category 7) | | **Value member as statement**: `B.isAlive()` on its own line | **reject** ("Expected an action, but got … a value") | implemented: rejects with `value-in-action-position` | `baseline-supported` (reviewed difference; recorded in the probe set) | | **Generic action gap**: `chaseOverTime(A, 0, 30, ChaseTimeReeval.NONE)` | accept (warning recorded) | implemented: manifest-declared; differential fixture: `synthetic/chase-condition-agentlab`, probe `chase-over-time` | `baseline-supported` (manifest-covered); emission via catalog spelling is lowering-dependent | | **Generic value gap**: `@Condition isGameInProgress() == true` | accept | implemented: manifest entry; probe `is-game-in-progress` | `baseline-supported` (manifest-covered) |