re: three ISL built-in names were wrong, including the most-used one

All re-read twice — the handler, and the thing it calls — because each had
been named from its shape rather than its effect.

* id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ;
  stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from
  the active list and returns it to the free list. It destroys the thread.
  2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was.
* id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the
  thread parked at the target pc, or itself if the target is its own pc.
  It waits for nothing.
* id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger
  container and then frees every thread whose pc differs from the caller's
  — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage
  02 sites all sit in the phase terminator, next to timer_stop,
  clear_flag(-1) and MARK_LAST_PHASE.

One name recovered from the game's own text: opcode 992 prints
"RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message
(2683 sites).

Return codes documented properly: 1 = restart the coroutine from its entry
(previously not recorded at all), 3 = terminate. And the blocking set was
wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block;
its handler ends `b 0x822724F8`, so it always returns 0.

Unit-operand resolution settled from DATA over all 28 stages rather than by
reading 147 handlers: a slot qualifies only if every value is a valid
symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most
of the table — that last clause is what discriminates, since every small
integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one
at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the
table, and the resolver now declines rather than inventing a name.

New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and
128 take type 8 at slot 12 in 100% of their sites.

A downstream inference is withdrawn with it: the note reading the live
trigger counter attributed it to "the script arming watches as it goes" via
built-in 100. The measurement stands; the attribution does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-25 22:09:05 +00:00
parent 723fc9b890
commit 1ba5d0a4a3
5 changed files with 261 additions and 56 deletions

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Regenerate the committed ISL artefacts under `docs/re/data/`.
isl_report.py <StageNN.ssb> calls -> the call-site census + a listing
The artefact was produced by an uncommitted one-off, so it drifted out of date
twice: once when operand staging was fixed (calls printed with too few
arguments) and once when three built-in names were corrected. Keeping the
generator in the tree is the point of this file.
`data/isl-stage02-conditions.txt` still has no generator here. A first attempt
is not committed because it printed most sites as bare `builtinN`: neither
`isl.resync` (it gives up far from a valid start) nor a naive linear decode from
the phase base reaches every call site, so producing that listing faithfully
needs the coroutine entry points, which `start_coroutine`'s operand carries and
this tool does not yet follow.
"""
import collections
import sys
import isl
def census(b):
cs = isl.call_sites(b)
return cs, collections.Counter(bid for _, bid, _ in cs)
def emit_calls(b, path):
cs, h = census(b)
s2 = isl.symbols(b, 2)
print('# %s — ISL disassembly artefacts' % path)
print()
print('Generated by `tools/re-capture/isl_report.py calls`.')
print()
print('%d call sites, %d distinct built-ins' % (len(cs), len(h)))
for bid, n in h.most_common():
print(' builtin %-4d %5d site(s)' % (bid, n))
print()
print('## phase-control sites')
for bid, nm in ((6, 'END_PHASE'), (62, 'FORCE_END_PHASE'),
(39, 'MARK_LAST_PHASE'), (40, 'mark_not_last')):
offs = [off for off, b2, _ in cs if b2 == bid]
print('%-3d %-18s %2d: %s'
% (bid, nm, len(offs), ' '.join('0x%x' % o for o in offs)))
print()
print('## named built-ins used, by traffic')
for bid, n in h.most_common():
nm = isl.BUILTIN.get(bid)
if nm:
print(' %-3d %-24s %4d' % (bid, nm, n))
print()
print('## phase code bases')
print('Each phase has its OWN base; the file header offset is not it.')
print(' ' + ' '.join('0x%x' % x for x in isl.phase_bases(b)))
print()
print('## disassembly into the first END_PHASE')
target = [off for off, bid, _ in cs if bid == 6][0]
start = isl.resync(b, target)
print('resync from 0x%X' % start)
for line in isl.dis(b, start, 64, code_base=0x24, sym2=s2):
print(line)
def main():
path = sys.argv[1]
b = isl.load(path)
name = path.replace('\\', '/').split('/')[-1]
{'calls': emit_calls}[sys.argv[2]](b, name)
if __name__ == '__main__':
main()