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
3738def _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+
78105class 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 " )
0 commit comments