Skip to content

Commit 66a8d05

Browse files
committed
resource-effects Stage 7: the effect model as a cross-frontend seam
EXPLORATORY (research/resource-effects-v1; pre-registered in Own.NET-paperwork stage7-seam-prereg-v1.json). The OwnTS twin emitter reads the same key file the C# extractor reads (--effects): a listed ambient factory mints an acquire, a listed receiver method on a tracked handle is a release, under the same provenance gate (--trust-all is the M6 twin). Two twins, four arms each, pinned by tests/test_re_ownts_seam.py with Python == Rust; the core has 0 changed lines. A seam proof for one shape, never TypeScript support. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am9eQwzNfbugH72eVKetC2
1 parent a8004c5 commit 66a8d05

7 files changed

Lines changed: 213 additions & 4 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"schema": "own.net/re-oracle/v1",
3+
"label": "Stage 7 gate twin: the same rows as SUGGESTED (shadow)",
4+
"entries": [
5+
{
6+
"callable": "createKey",
7+
"effect": "return_fresh_owned",
8+
"provenance": "SUGGESTED"
9+
},
10+
{
11+
"callable": "Key.destroy",
12+
"effect": "receiver_terminal_release",
13+
"provenance": "SUGGESTED"
14+
}
15+
]
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"schema": "own.net/re-oracle/v1",
3+
"label": "Stage 7 seam key for the OwnTS twins (MODELLED)",
4+
"entries": [
5+
{
6+
"callable": "createKey",
7+
"effect": "return_fresh_owned",
8+
"provenance": "MODELLED"
9+
},
10+
{
11+
"callable": "Key.destroy",
12+
"effect": "receiver_terminal_release",
13+
"provenance": "MODELLED"
14+
}
15+
]
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// resource-effects Stage 7 seam twin (pre-registered in Own.NET-paperwork stage7-seam-prereg-v1.json):
2+
// Key is NOT a resource type by the T7 rules (no close()/dispose()); only the effect model makes
3+
// createKey an acquire and destroy() a release. Truth: k is never destroyed (a leak).
4+
interface Key {
5+
destroy(): void;
6+
export(): string;
7+
}
8+
declare function createKey(): Key;
9+
10+
export function bug(): void {
11+
const k = createKey();
12+
k.export();
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// resource-effects Stage 7 seam twin: the same shape, destroyed. Truth: no leak.
2+
interface Key {
3+
destroy(): void;
4+
export(): string;
5+
}
6+
declare function createKey(): Key;
7+
8+
export function safe(): void {
9+
const k = createKey();
10+
k.destroy();
11+
}

‎docs/notes/re-stage7-seam.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# resource-effects Stage 7 — the effect model as a cross-frontend seam (EXPLORATORY)
2+
3+
Research branch `research/resource-effects-v1`. Pre-registered in Own.NET-paperwork
4+
`paper-eval/resource-effects/stage7-seam-prereg-v1.json`; evidence in `stage7-seam-v1.json`. A seam
5+
proof for one shape; never TypeScript support. #304 frozen and unchanged; #382 untouched.
6+
7+
The OwnTS twin emitter (`frontend/ownts/ownts_p037x.py`, +49/-4 lines) reads the SAME key file the C#
8+
extractor reads (`--effects <key.json>`): an ambient factory listed `return_fresh_owned` mints an
9+
`acquire` even when its declared type carries no `close()`/`dispose()`, and `x.m()` where `<Type>.m` is
10+
listed `receiver_terminal_release` on a tracked handle is a `release`; the same provenance rule applies
11+
(DECLARED / BODY_PROVED / MODELLED applied, MINED / SUGGESTED shadow, `--trust-all` = the M6 twin).
12+
The ops are the ordinary acquire/release; the core (Rust and Python) has 0 changed lines.
13+
14+
Measured on the two twins (`corpus/re-controls/ownts/e7-effects-*.ts`; `tests/test_re_ownts_seam.py`
15+
pins the 8 rows): without effects both are silent (untracked); with the MODELLED key the bug twin reports
16+
OWN001 and the safe twin nothing; with the same rows as SUGGESTED both are silent and the emitter counts
17+
two shadow rows; with `--trust-all` the SUGGESTED rows act. Python == Rust on every row; the eight
18+
P-037-X twins still pass.
19+
20+
What this shows: the effect vocabulary of this track (two effects), its carrier (the key file) and its
21+
trust policy (the provenance gate) live outside any language; a frontend contributes only an identity
22+
matcher (~40 lines here). What it does not show: anything about TypeScript beyond these two twins.

‎frontend/ownts/ownts_p037x.py‎

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
_IF = re.compile(r"^\s*if\s*\((.*)\)\s*\{\s*$")
3333
_RET = re.compile(r"^\s*return\b(.*);\s*$")
3434
_ELSE = re.compile(r"^\s*\}?\s*else\s*\{\s*$")
35+
_EFFECT_CALL = re.compile(r"^\s*(\w+)\s*\.\s*(\w+)\s*\(\s*\)\s*;\s*$")
3536

3637

3738
def _line_of(text: str, pos: int) -> int:
@@ -75,12 +76,41 @@ def _block_end(lines: list[str], start: int) -> int:
7576
return len(lines) - 1
7677

7778

79+
# resource-effects Stage 7 (research/resource-effects-v1, EXPLORATORY; pre-registered in
80+
# Own.NET-paperwork paper-eval/resource-effects/stage7-seam-prereg-v1.json): the EFFECT MODEL seam.
81+
# `--effects <key.json>` reads the same key file the C# extractor reads (entries {callable, effect,
82+
# provenance}); only DECLARED / BODY_PROVED / MODELLED entries apply, MINED / SUGGESTED are shadow
83+
# (counted, never applied) unless `--trust-all` (the M6 twin). The ops emitted are the ordinary
84+
# `acquire` / `release`; the core is untouched. A seam proof for one shape, never TypeScript
85+
# support.
86+
_APPLIED = {"DECLARED", "BODY_PROVED", "MODELLED"}
87+
88+
89+
def _load_effects(path: str | None, trust_all: bool) -> tuple[dict[str, str], dict[str, int]]:
90+
applied: dict[str, str] = {}
91+
shadow: dict[str, int] = {}
92+
if not path:
93+
return applied, shadow
94+
with open(path, encoding="utf-8") as fh:
95+
doc = json.load(fh)
96+
for e in doc.get("entries", []):
97+
prov = e.get("provenance", "MODELLED")
98+
if prov in _APPLIED or trust_all:
99+
applied[e["callable"]] = e["effect"]
100+
else:
101+
shadow[prov] = shadow.get(prov, 0) + 1
102+
return applied, shadow
103+
104+
78105
class Emitter:
79-
def __init__(self, text: str, module: str, file: str, ordinals: bool) -> None:
106+
def __init__(self, text: str, module: str, file: str, ordinals: bool,
107+
effects: dict[str, str] | None = None) -> None:
80108
self.text = text
81109
self.module = module
82110
self.file = file
83111
self.ordinals = ordinals
112+
self.effects = effects or {}
113+
self.minted: dict[str, set[str]] = {}
84114
self.resource_types = {m.group(1) for m in _IFACE.finditer(text)
85115
if re.search(r"\b(close|dispose)\s*\(\s*\)\s*:\s*void", m.group(2))}
86116
self.ambient = {m.group(1): m.group(3) for m in _AMBIENT.finditer(text)}
@@ -91,6 +121,15 @@ def __init__(self, text: str, module: str, file: str, ordinals: bool) -> None:
91121
"start": m.end(), "line": _line_of(text, m.start())}
92122

93123
# --- one function -----------------------------------------------------------------------
124+
def _handle_types(self, var: str, fn: dict) -> set[str]:
125+
"""The declared type(s) a tracked handle may have: the ambient factory's return type that
126+
minted it (recorded at the acquire), or the parameter's type."""
127+
types = set(self.minted.get(var, ()))
128+
for n, ty in fn["params"]:
129+
if n == var:
130+
types.add(ty)
131+
return types
132+
94133
def _sig(self, name: str) -> str:
95134
return ",".join(ty for _, ty in self.funcs[name]["params"])
96135

@@ -153,8 +192,10 @@ def _lower(self, lines: list[str], base_line: int, fn: dict, tracked: set[str],
153192
continue
154193
m = _ACQ_STMT.match(ln)
155194
if (m and m.group(2) in self.ambient
156-
and self.ambient[m.group(2)] in self.resource_types):
195+
and (self.ambient[m.group(2)] in self.resource_types
196+
or self.effects.get(m.group(2)) == "return_fresh_owned")):
157197
tracked.add(m.group(1))
198+
self.minted.setdefault(m.group(1), set()).add(self.ambient[m.group(2)])
158199
nodes.append({"op": "acquire", "var": m.group(1), "line": line_no,
159200
"column": ln.index(m.group(1)) + 1, "kind": "disposable"})
160201
i += 1
@@ -164,6 +205,13 @@ def _lower(self, lines: list[str], base_line: int, fn: dict, tracked: set[str],
164205
nodes.append({"op": "release", "var": m.group(1), "line": line_no})
165206
i += 1
166207
continue
208+
m = _EFFECT_CALL.match(ln)
209+
if (m and m.group(1) in tracked and self.effects
210+
and any(self.effects.get(f"{ty}.{m.group(2)}") == "receiver_terminal_release"
211+
for ty in self._handle_types(m.group(1), fn))):
212+
nodes.append({"op": "release", "var": m.group(1), "line": line_no})
213+
i += 1
214+
continue
167215
m = _CALL_STMT.match(ln)
168216
if m and m.group(1) in self.funcs:
169217
callee = self.funcs[m.group(1)]
@@ -251,14 +299,23 @@ def main(argv: list[str]) -> int:
251299
out = argv[argv.index("-o") + 1] if "-o" in argv else None
252300
if out in args:
253301
args.remove(out)
302+
effects_path = argv[argv.index("--effects") + 1] if "--effects" in argv else None
303+
if effects_path in args:
304+
args.remove(effects_path)
254305
if not args:
255-
print("usage: ownts_p037x.py FILE.ts [-o facts.json] [--no-ordinal]", file=sys.stderr)
306+
print("usage: ownts_p037x.py FILE.ts [-o facts.json] [--no-ordinal] "
307+
"[--effects key.json] [--trust-all]", file=sys.stderr)
256308
return 2
309+
effects, shadow = _load_effects(effects_path, "--trust-all" in argv)
310+
if shadow:
311+
print("re-effects shadow: " + " ".join(f"{k}={v}" for k, v in sorted(shadow.items())),
312+
file=sys.stderr)
257313
path = args[0]
258314
text = open(path, encoding="utf-8").read()
259315
stripped = re.sub(r"//[^\n]*", "", text)
260316
module = re.sub(r"\.[jt]sx?$", "", path.rsplit("/", 1)[-1]).replace("-", "_")
261-
facts = Emitter(stripped, module, path, ordinals="--no-ordinal" not in argv).facts()
317+
facts = Emitter(stripped, module, path, ordinals="--no-ordinal" not in argv,
318+
effects=effects).facts()
262319
payload = json.dumps(facts, indent=2)
263320
if out:
264321
open(out, "w", encoding="utf-8").write(payload + "\n")

‎tests/test_re_ownts_seam.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
"""resource-effects Stage 7 (research/resource-effects-v1, EXPLORATORY): the effect-model seam on the
3+
OwnTS twin emitter. corpus/re-controls/ownts/e7-*.ts are the twins frozen in Own.NET-paperwork
4+
`paper-eval/resource-effects/stage7-seam-prereg-v1.json`; each is emitted under four arms (no effects,
5+
the MODELLED key, the same rows as SUGGESTED, SUGGESTED with --trust-all) and run through the Python
6+
reference and, when `OWEN_RUST_CORE` names an own-cli, through Rust (parity). The table is the RECORD
7+
of the Stage 7 measurement; a row that moves is a change to classify, never one to repair here.
8+
A seam proof for one shape, never TypeScript support. Always runs (no external toolchain)."""
9+
10+
from __future__ import annotations
11+
12+
import os
13+
import re
14+
import subprocess
15+
import sys
16+
import tempfile
17+
18+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19+
_EMIT = os.path.join(_REPO, "frontend", "ownts", "ownts_p037x.py")
20+
_TWINS = os.path.join(_REPO, "corpus", "re-controls", "ownts")
21+
_KEYS = os.path.join(_REPO, "corpus", "re-controls", "keys")
22+
_CODE = re.compile(r"\[(OWN\d{3})\]")
23+
24+
# arm -> emitter options
25+
_ARMS: dict[str, list[str]] = {
26+
"none": [],
27+
"model": ["--effects", os.path.join(_KEYS, "effects-ts.json")],
28+
"suggested": ["--effects", os.path.join(_KEYS, "effects-ts-suggested.json")],
29+
"trustall": ["--effects", os.path.join(_KEYS, "effects-ts-suggested.json"), "--trust-all"],
30+
}
31+
# twin -> arm -> codes
32+
_RECORD: dict[str, dict[str, list[str]]] = {
33+
"e7-effects-bug": {"none": [], "model": ["OWN001"], "suggested": [], "trustall": ["OWN001"]},
34+
"e7-effects-safe": {"none": [], "model": [], "suggested": [], "trustall": []},
35+
}
36+
37+
38+
def _run(argv: list[str], env: dict[str, str]) -> str:
39+
p = subprocess.run(argv, cwd=_REPO, capture_output=True, text=True, env=env)
40+
return p.stdout + p.stderr
41+
42+
43+
def run() -> int:
44+
env = {**os.environ, "PYTHONPATH": _REPO}
45+
rust = os.environ.get("OWEN_RUST_CORE")
46+
if rust and not os.path.isfile(rust):
47+
rust = None
48+
fails: list[str] = []
49+
with tempfile.TemporaryDirectory() as tmp:
50+
for twin, arms in sorted(_RECORD.items()):
51+
for arm, want in arms.items():
52+
facts = os.path.join(tmp, f"{twin}.{arm}.json")
53+
subprocess.run([sys.executable, _EMIT, os.path.join(_TWINS, f"{twin}.ts"),
54+
"-o", facts, *_ARMS[arm]], cwd=_REPO, check=True,
55+
capture_output=True, text=True)
56+
py = sorted(_CODE.findall(_run([sys.executable, "-m", "ownlang", "ownir", facts,
57+
"--severity", "warning"], env)))
58+
if py != sorted(want):
59+
fails.append(f"{twin} [{arm}]: python {py} != recorded {want}")
60+
if rust:
61+
rs = sorted(_CODE.findall(_run([rust, "ownir", facts, "--severity", "warning"],
62+
{**env, "OWEN_P037X_GUARDED": "0"})))
63+
if rs != py:
64+
fails.append(f"{twin} [{arm}]: rust {rs} != python {py} (parity)")
65+
for f in fails:
66+
print(f"FAIL: {f}")
67+
n = sum(len(a) for a in _RECORD.values())
68+
print(f"re ownts seam: {n} row(s), {len(fails)} failed"
69+
+ ("" if rust else " (Rust parity not checked: OWEN_RUST_CORE unset)"))
70+
return 1 if fails else 0
71+
72+
73+
if __name__ == "__main__":
74+
raise SystemExit(run())

0 commit comments

Comments
 (0)