Skip to content

Python calls run on the machine instead of recursing in C - #16

Merged
soasme merged 5 commits into
mainfrom
flat-calls
Nov 6, 2026
Merged

soasme merged 5 commits into
mainfrom
flat-calls

Conversation

@soasme

@soasme soasme commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

Python-to-Python calls no longer recurse in C. A function body that holds a call and cannot suspend compiles to machine steps (Node.flat). When such a body calls another such function, the call pushes a Call on the context's call stack and the same run_machine loop goes on. A return pops the call and dispatches the caller's next step. Deep recursion is now bounded by the recursion limit alone: tests/api runs a 50,000-deep recursion in a 64 KiB C stack.

Calls that C makes still enter a new run_machine and keep the C stack guard. These come from the walker, special methods, host callbacks and trace functions.

  • Call steps: a call is BC_CALLF when its arguments are operators of literals and names, computed in place, or BC_CALLN when its arguments are on the stack. Keyword calls stay on BC_CALL. All three run out of line, so generators, which share the loop, keep their registers.
  • Statements on the machine: if and return are machine steps inside such bodies. A for whose only call is its iterable stays a walker statement. The statements after a body's last call form one walker step (BC_BLOCK).
  • Frame switch: each call keeps its caller's machine, so a switch waits on one load.
  • Generators: a generator's call steps never enter a flat call.
  • Fixes found on the way:
    • a with body whose __exit__ raised lost its error context;
    • entering and ending a finally reset the frame's line to the try's line;
    • annotation and type-alias value functions stay on the walker.
  • Model: the new tests/tla/SpyCalls models the call stack:
    • C nesting counts host calls only;
    • frames stay within the recursion limit;
    • each environment is released once, even when a trace function reuses a popped slot;
    • storage is trimmed after the outermost call.

Performance

M2, cycles averaged over 8 code layouts, main → this branch:

Program Instructions Cycles
fib -12.9% -2.2%
classes +1.2% +2.0%
generators +1.7% -0.1%
nbody -0.1% +0.3%
loop, sort, strings, dicts, bigint, startup within 0.5% within 1% (wall)

classes is still 2% slower on the M2. On Linux clang, callgrind counts about 1% fewer instructions on classes than main, so the gap comes from Apple clang's code generation. It is still open.

Test plan

  • make test: green, spec 419/419
  • make test-tla in a Linux container: every model passes, SpyCalls included
  • make coverage: every engine line runs (spy.c 25916/25916)
  • tools/run_workflow.py: test job with clang and gcc (sanitize included)
  • tools/run_workflow.py: packages and archive

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj

soasme and others added 5 commits November 6, 2026 16:06
A function body that holds a call, and is not a generator, compiles to
machine steps; its calls of such functions push a frame on the context's
call stack and the same run_machine loop continues. A recursion is bounded
by the recursion limit, not by the C stack: sys.setrecursionlimit(200000)
then a 100000-deep recursion now returns, as on CPython, where it raised
"Stack overflow" before. Calls C makes into Python still nest.

Found on the way: an error raised by __exit__ during a machine unwind got
no __context__, and the machine reported try/except bookkeeping as trace
line events.

make test: all green (spec 419/419, std 176/176 probes, api, oom, threads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
Calls in a function body run as BC_CALLF (operands at hand), BC_CALLN
(arguments on the stack) or BC_CALL, out of line; a call's frame lives on
its machine, and entering, returning and unwinding share one switch. The
statements after a body's last call are one walker step. A generator's
call steps never enter a flat call. SpyCalls models the call stack.

make test: green, spec 419/419. M2, mean over 8 layouts, main -> this:
fib 31.0 -> 33.5 ms (instructions -12%), classes 86.2 -> 88.7 ms,
generators +0.6%, nbody -0.7%, loop +0.7%, others within 1%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
Returning and entering switch machines through the Call at hand rather
than reloading c->calls, so the next step's dispatch waits on one load.

make test: green, spec 419/419. M2, mean over 8 layouts, main -> this:
fib 31.7 -> 31.0 ms (-2.4%, 96.5M cycles against 99M), generators -0.4%,
classes +2.2%, nbody, loop, sort, strings within 0.4%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
call_function keeps its walker path as it was; call_machine holds the
inlined call_enter, run_machine and call_leave.

make test: green, spec 419/419. M2, mean over 8 layouts, main -> this
(cycles): fib 98.0M -> 95.8M (-2.2%), classes 276.2M -> 281.8M (+2.0%),
generators -0.1%, nbody +0.3%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
An annotation or type alias value function's body is one N_ANNOTATIONS
statement, which the machine compiled as a class statement and crashed
on. Entering and ending a finally on the machine no longer reset the
frame's line to the try's. Docs: manual, ROADMAP and optimization.md
describe calls on the machine with their measurements.

make test: green, spec 419/419; make test-tla (Linux container): every
model passes, Annotations, TypeAliases and Tracebacks among them;
make coverage: every engine line runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bq79MgongGBn3NvB5dYBoj
@soasme
soasme merged commit 0dee947 into main Nov 6, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant