Don't execute Python early: split logical calls from physical work
Pysolate’s Prepare–Linearize–Materialize model, and why it is more general than execute-as-you-generate
A program specifies what should happen and at which logical location it should happen; it does not always require each physical piece of work to start only at that location.
What Pysolate wants to exploit is exactly the gap between these two.
In programs where Python is generated by a model, then calls remote tools, datasets, filesystems, or other Host capabilities, the most visible performance problem is usually not local a + b, but high-latency boundary operations: one model call may take dozens of seconds, one search may take several seconds, one data load may take even longer. The most intuitive optimization is “execute as we generate”: the model produces search(...), and the runtime immediately executes it; when the rest of the code is generated, continue from there.
This path is tempting, and dangerous.
An unfinished Python source is not yet a well-formed program. Later it may contain a syntax error, may place earlier calls inside an unclosed control flow, may change parameter sources, may redefine exception behavior inside try/finally, or may cause side effects that should not be published early. More fundamentally, once the Host starts executing any Python prefix, it must maintain partial interpreter state, partial control stack, exception boundaries, object identity, mutable heap, and the future continuation. The optimizer eventually becomes a second Python runtime, or quietly turns “generated code” into a new streaming language.
Pysolate chooses a different path:
- No Python execution during streaming stage.
- After full source seal, still perform exactly one true synchronous, fresh CPython execution.
- The Host can start some external physical work early, based on source facts already seen.
- When Python reaches the original call site, decide whether that early work can represent the current logical call.
- Only when the value is truly needed, synchronously obtain a normal Python value.
This is not “making Python asynchronous”; it is:
This is formalized as a three-phase model:
Abbreviated as PLM: Prepare–Linearize–Materialize.
First, a note: the following is a design model and implementable approach for Pysolate. It does not mean the current implementation already covers all optimizations described. Its value is primarily providing a unified and provable semantic framework for existing source-time pre-dispatch, Host-side pending work, prepared input, caching, and future AST scheduling.
1. Why we do not try “execute one line at a time”
1. Incomplete source is not a safe execution boundary
Assume the model has already emitted:
result = send_email(address, body)
But the rest is not generated yet. It might later be:
if dry_run:
...
or:
raise ValueError("do not send")
or the entire function may eventually contain a syntax error. From the prefix alone, you cannot know whether this call is reachable in the full program, whether it is protected by some exception boundary, or whether it should execute at all.
For read-only calls, this looks slightly lighter:
data = read_dataset("sales.csv")
But even if it does not write to the outside world, it may:
- read a mutable resource;
- consume quota;
- use authority that could be revoked later;
- return data dependent on current working directory or transaction;
- fail, and the original program should observe that failure at a specific location.
So, “no side effect” is not the same as “safe to execute at any time”.
2. Partial execution pushes the Host into an impure position
If the Host really executes a Python prefix, it must answer:
- Where is the control state for a not-yet-completed
if? - What happens when a future result participates in
__add__,__getitem__, descriptor lookups, exceptions, and object identity? - How is continuation restored after more source arrives?
- Prefix already modified Guest heap, then source has a syntax failure later—how is rollback handled?
- A request was sent early, but later code chooses a different branch—how are side effects undone?
- Do multiple logical runs share some interpreter state?
- How do you guarantee final execution is still fresh private state?
These are not small scheduling issues; they redefine Python execution semantics.
3. Better philosophy: do not move “what should happen”; move only the physical work needed to make it happen
Consider:
x = remote_read(key)
That single line contains at least two distinct things:
- Logical event: the program calls
remote_read(key)here and gets a value or raises an exception here; - Physical work: connection setup, authentication, queueing, remote computation, transport, deserialization.
Conventional synchronous execution binds both at the same location. Pysolate’s key judgment is:
The logical call site must stay, but some physical work does not need to wait to start there.
So optimization is not:
execute a piece of Python early
but:
generate a physical candidate result early, which may later be adopted by the logical call
This distinction is the central mental model.
2. Two time axes: logical order vs physical time
There are two fundamentally different kinds of “earlier” in a program.
1. Logical program points
Let:
It denotes a position in source and CPython control flow. For example:
a = A()
x = f(a)
b = B(x)
Logically, we have:
This order defines Python semantics: whether B exists, what its arguments are, and how exceptions propagate are all determined on this logical axis.
2. Physical wall-clock time
Let:
It describes:
- when a request is issued;
- when a provider starts work;
- when the result completes;
- when Python blocks waiting;
- when final return happens.
PLM allows an operation logically at to start physical preparation earlier at time :
where is the physical time when CPython reaches the original logical call site.
The key invariant is:
We can draw it as:
Logical program axis
ℓ0 -------- ℓ1 -------- ℓ2 -------- ℓ3
H_A H_B
│ │
│ original semantic site │
│ │
Physical time axis
p0 -- prepare A -- prepare B -------- linearize A -- materialize A -- ...
This is the simplest mathematical version of “logical ownership / physical work separation.”
3. Hidden world state: why read-only still depends on time
A Host call is usually written as:
where is an explicit argument. But semantically this is incomplete. The real operation also depends on global environment not explicit in source:
Where:
- : the external world—files, databases, Git, markets, remote services, etc.;
- : current authority and capability;
- : cwd, environment variables, transaction, session, configuration;
- : quota, nonce, rate limit, or other resource state.
Therefore a Host operation should be written as:
not just .
1. “Read-only” is only an effect property
stock_price("AAPL") can be read-only because it does not mutate the market:
But it may still satisfy:
So:
This distinction unifies caching, prefetch, version validation, and snapshots.
2. An operation usually depends on only part of the world
Define the semantic footprint of an operation:
For example:
read_git_file(commit_hash, path)
may depend only on the immutable Git object identified by commit_hash; market state, current cwd, and other file changes are irrelevant.
Define two contexts as equivalent for that call:
iff they are indistinguishable on .
If is deterministic, and:
then:
That is the core condition under which early results can be reused directly.
4. Start with the simplest example: move issue earlier, move collect later
Consider:
a = tool_A() # 10s
b = tool_B() # 20s
c = a + b
d = tool_D() # 20s
return c + d
1. Ordinary synchronous execution
If the calls happen in sequence:
A: 10s
B: 20s
D: 20s
The approximate total time is:
2. Minimal split-phase transformation
Use a temporary two-phase form:
_hA = issue(tool_A)
_hB = issue(tool_B)
_hD = issue(tool_D)
a = collect(_hA)
b = collect(_hB)
c = a + b
d = collect(_hD)
return c + d
Here:
issueis non-blocking; the Host starts work and returns an opaque handle;collectis a normal synchronous call: it blocks if not ready and returns a normal Python value once done.
Python has no await, and it never sees Future[int].
The three requests start almost simultaneously, so ideal total time is:
3. Remaining wait
For call , define:
- : physical issue time;
- : physical finish time;
- : materialize/collect time.
The remaining blocking time is:
If latency is , then:
so:
This directly shows:
- moving leftward does not increase wait;
- moving rightward does not increase wait.
But movement is still constrained by semantic dependencies.
5. Why two phases are not enough: add Linearize
If an operation depends on mutable , writing only:
candidate = issue(H, args)
...
value = collect(candidate)
causes a problem:
does this call read the world at issue time, original call-site time, or collect time?
For example:
price = stock_price("AAPL")
pure_local_work()
return price
An early request may read 100 at ; original call site is at ; real collect happens at . These times can differ.
So the cleaner normal form is three phases.
1. Prepare
candidate = prepare(H, frozen_args)
Formally:
It yields a candidate that may include:
pending request
candidate result
version / ETag
snapshot ID
lease
authority epoch
provider session
Prepare may perform physical work, but must be silent in abstract semantics:
That is, it cannot raise to Python early, publish logical write effects, or change Guest-visible state.
2. Linearize
When CPython reaches the original call site:
job = linearize_or_start(candidate, H, actual_args)
Formally:
It decides:
- whether the candidate really represents the current logical call;
- whether params, authority, version, and source site match;
- whether a canonical operation must be started here if invalid;
- for write operations, whether to claim prepared work and then wait for commit.
A typical rule:
Linearize is fixed at the original logical call site.
3. Materialize
value = materialize(job)
Formally:
It:
- blocks synchronously if not complete;
- returns immediately when complete;
- returns a normal Python value;
- or raises an error at this semantic point.
So the complete principle is:
6. Core correctness contract
For a baseline synchronous operation:
the PLM protocol must satisfy:
where:
Intuitively:
No matter how early physical preparation starts, as long as the system validates, adopts, or restarts the right operation at the original logical point and only returns the result at a legal position, Guest-observable behavior should be equivalent to a normal synchronous call at that point.
If is a nondeterministic relation, uniqueness is not required; instead:
meaning the PLM outcome must be one of the results allowed by the original operation in that context.
Candidate validator soundness
Let candidate be:
where is version, lease, or snapshot evidence. The Validator must satisfy:
It may be incomplete: some actually valid candidates can be conservatively rejected, which only loses performance. It must not be unsound: it cannot treat an expired candidate as the current logical result.
7. stock price: full example under hidden temporal variables
Consider:
price = stock_price("AAPL")
Case 1: provider only supports “current price”
Prepare at gets:
At there is no version, snapshot, or verifiable evidence. Then:
The system cannot treat 100 as the result of the current logical call and must re-query at .
But Prepare can still pre-complete:
DNS
TLS
connection pool setup
authentication
provider route
request object allocation
This shows physical phases can be further split:
The less this phase depends on current world state, the farther left it can move.
Case 2: provider returns a version number
Prepare gets:
At , the system performs a linearizable current-version check. If still 42, the candidate can be adopted at this logical point; if it is already 43, it reissues.
The version check is proof that ties candidate result to current world state.
Case 3: call bound to snapshot
price = stock_price("AAPL", snapshot=s)
Then semantics are:
rather than a current read that changes with wall-clock. If snapshot is immutable, the result can be:
- source-time pre-dispatch;
- cached;
- reused across runs on immutable backing;
- materialize deferred.
This also shows:
Cache and early issue are semantically the same thing: both produce “candidate results at another physical time,” and linearize decides whether a candidate can represent the current logical call.
8. Complex control flow: Host does not choose branches; CPython remains the sole semantic executor
Consider:
user = get_user("alice")
if user["premium"]:
score = normalize(user["score"]) # regular Python, 3s
recs = search(user["topic"]) # 15s
if score > 10 and len(recs) > 3:
quote = price(recs[0]["sku"]) # 10s
else:
quote = fallback("premium") # 6s
else:
quote = fallback("basic") # 6s
tax = tax_table("UK") # 20s
return render(quote, tax)
Assume:
get_usertakes 8 seconds;searchtakes 15 seconds;pricetakes 10 seconds;tax_tabletakes 20 seconds;- both
fallbackcalls are safe, cheap, discardable reads.
1. Source streaming stage
The Host reads:
user = get_user("alice")
Since arguments are source-closed:
prepare get_user("alice")
It sees:
search(user["topic"])
At this point it cannot execute user["topic"], because that requires a real Python object and __getitem__ semantics. The Host only records the call site, no request is sent.
It sees:
fallback("premium")
fallback("basic")
If contract allows control speculation, it can prepare both candidates.
It sees:
tax_table("UK")
Arguments are source-closed, so prepare immediately.
The whole streaming stage:
- creates no Python locals;
- does not compute
user["topic"]; - does not decide
premium; - does not run
normalize; - executes no branches.
2. Conceptual AST transform after sealing
_p_user = prepare_or_reuse(GET_USER, "alice")
_p_tax = prepare_or_reuse(TAX, "UK")
_p_fp = prepare_or_reuse(FALLBACK_PREMIUM, "premium")
_p_fb = prepare_or_reuse(FALLBACK_BASIC, "basic")
_j_user = linearize_or_start(
_p_user, GET_USER, "alice"
)
user = materialize(_j_user)
if user["premium"]:
# user is now a real Python dict.
_p_search = prepare(SEARCH, user["topic"])
_j_search = linearize_or_start(
_p_search, SEARCH, user["topic"]
)
# search runs in the Host background; CPython performs local computation normally.
score = normalize(user["score"])
recs = materialize(_j_search)
if score > 10 and len(recs) > 3:
_p_price = prepare(PRICE, recs[0]["sku"])
_j_price = linearize_or_start(
_p_price, PRICE, recs[0]["sku"]
)
quote = materialize(_j_price)
else:
_j_fp = linearize_or_start(
_p_fp, FALLBACK, "premium"
)
quote = materialize(_j_fp)
discard(_p_fb)
else:
_j_fb = linearize_or_start(
_p_fb, FALLBACK, "basic"
)
quote = materialize(_j_fb)
discard(_p_fp)
_j_tax = linearize_or_start(
_p_tax, TAX_TABLE, "UK"
)
tax = materialize(_j_tax)
return render(quote, tax)
3. What exactly happens there
get_userandtax_tablealready started during source generation;useris materialized into a concrete dict only when its original call point is reached;- CPython decides
premium; - after entering the branch,
searcharguments are concrete, so it is emitted immediately; - while search runs, CPython performs
normalize; searchmust be materialized beforelen(recs);- inner branch is still decided by CPython;
pricearguments do not exist untilrecscompletes, so it cannot be sent earlier;- fallback candidates can be prepared early, but only the actual path is linearized;
taxis likely already complete by the time it is used.
The resulting execution naturally forms multiple waves:
source-time prepare wave
↓
materialize user
↓
CPython decides outer branch
↓
runtime prepare search
↓
CPython does independent local work
↓
materialize search
↓
CPython decides inner branch
↓
prepare / adopt price or fallback
↓
materialize
There is no Python scheduler, and no Host-side Python graph evaluator.
9. Data dependencies: why some critical paths cannot be eliminated
Consider:
a = tool_A() # 10s
x = a + 1
b = tool_B(x) # 20s
d = tool_D() # 20s
return b + d
Pysolate can prepare early:
A
D
But B depends on the argument:
x = a + 1
By design, the Host does not execute this Python. Therefore:
- materialize
A; - CPython computes
x; - only then prepare/linearize
B.
The critical path remains:
D can run in parallel with this chain, so total time is near:
This is not an optimizer failure, but an explicit semantic staging barrier:
If in the future we want to automatically compute x and start B the instant A completes, even while main Python thread is doing other work, at least one extra mechanism is needed:
- Python continuation scheduler;
- another Python execution context;
- Host-side expression IR;
- more aggressive local code motion.
These are beyond the minimal and clean PLM model.
10. Side effects: not just whether it can be early, but which phase can be.
Side effects are not a boolean label. A logical operation can often be split into several physical phases, some silent and some not advanceable.
1. Four classes of operation contract
Immutable / snapshot read
Read content hash
Read fixed Git commit
Read fixed dataset version
Result does not depend on a changing current world, so full Prepare is possible.
Validated read
Read object with ETag/version
Read database result with snapshot token
Prepare produces candidate and evidence; Linearize validates, and if not valid it re-executes.
Prepare–commit effect
For example:
send_email(to, body)
Can prepare:
parse recipients
generate MIME
upload temporary attachments
establish connection
complete spam / policy checks
But actual “send” must commit at the original logical point.
Formally:
Here commit generally cannot move right of other logical effects.
Non-stageable
If a call itself:
- immediately causes non-reversible effects;
- consumes a one-shot nonce;
- depends on current state and cannot be validated;
- changes results of other calls;
- cannot be safely canceled or discarded;
then it cannot be advanced and degrades to a normal synchronous call.
2. The real condition for speculation
If a call is hidden in an unknown branch:
if cond:
x = H()
To prepare while cond is unknown, we must preserve:
with the same logical effect as not executing.
If the branch is not taken:
discard(candidate)
must satisfy:
So “read-only” is still not enough. A read can consume quota, hold an unique lease, trigger audit, or affect provider future behavior. Any of these that the program can observe are part of the effect footprint.
3. Authority is also semantic state
Advance work cannot bypass authority.
A candidate should at least bind:
logical run
capability identity
authority epoch
tool identity
normalised argument digest
source seal
dynamic occurrence
For write operations, Linearize or Commit usually re-checks authority. If capability is revoked after Prepare, candidate results must not be automatically published.
This means:
Physical work can be started, reused, or shared early; logical authorization, publishing, and mutable state remain per logical run.
11. Exceptions: Materialize cannot be deferred unconditionally
Consider:
a = H() # may fail
send_email()
return a
If transformed to:
_p = prepare(H)
_j = linearize_or_start(_p, H)
send_email()
a = materialize(_j)
return a
If H fails:
- original code sends no email;
- transformed code has already sent an email.
So they are not equivalent.
1. Materialize sinking commutativity condition
Let:
and is the immediately following statement. We can move across only when:
We write:
If:
we can obtain by adjacent swaps:
2. What a conservative implementation allows to cross
Typical statements that can cross:
proved pure
non-raising
does not read result variable
does not observe local variable binding
does not alter temporal validity of operation
does not alter authority / transaction / cwd
Barrier statements that must stop:
observable I/O or external effects
unknown call that may raise
try / except / finally
with / transaction
return / yield
time and random reads
locals / frame introspection
thread or signal boundaries
first strict use of result
Hence a first implementation can safely keep:
only Prepare moving left, with Materialize kept at the original call site.
Most concurrency gains already exist. Materialize sinking is a stronger, more constrained later optimization.
12. How to relate this to other work
Pysolate does not appear in a vacuum. It is related to futures, dataflow, lazy evaluation, async/await, partial evaluation, speculation, and caching, but places the boundary differently.
1. Futures / Promises: bring “not-yet-available values” into the language
Multilisp future allows an expression to be evaluated in parallel, with an implicit or explicit wait when its concrete value is needed; Liskov and Shrira’s promises separate request issuance from return retrieval for asynchronous remote procedure calls.[1][2]
A typical mental model is:
a: Future[int] = async_A()
b: Future[int] = async_B()
c = touch(a) + touch(b)
The key object is:
Pysolate differs in that:
Future[T]does not appear in user Python;- after materialize, variables remain normal concrete values;
- Host does not require ordinary Python operators to be lifted over Future;
- host work can be prepared before Python has started executing;
- final CPython remains synchronous semantics.
Pysolate borrows “separate invocation and wait,” but keeps Future as an internal compiler/Host handle instead of extending the Guest value domain.
2. Python async / await: explicit coroutines and event loop
Python async model requires a coroutine to suspend at await, yielding control to event loop; the event loop schedules other tasks while waiting on a Future.[3]
This suits applications that intentionally write async code:
a_task = asyncio.create_task(A())
b_task = asyncio.create_task(B())
a, b = await asyncio.gather(a_task, b_task)
Pysolate deliberately does not adopt this user model:
- input is ordinary synchronous Python;
- does not require
async def; - does not insert user-visible
await; - does not require a cooperative event loop inside the final interpreter;
- Host requests advance outside Python thread;
- Python only blocks on the synchronous materialize ABI.
In other words, asyncio makes programmers explicitly manage concurrency; PLM lets the compiler move boundary work physical phases.
3. APPL: future proxy and demand-driven synchronization
APPL is a close cousin. It integrates Python and LLM calls via AST transpilation and uses StringFuture, BooleanFuture, etc. to defer synchronization: string concatenation can continue across futures, while strict contexts like len, str, or boolean control flow trigger materialize. APPL explicitly grounds automatic parallelization in async semantics and Future objects.[4]
Its model can be summarized as:
Call returns Future-like Python object
Future participates in limited Python operations
synchronize on first strict use
Pysolate’s key differences are:
- Streaming phase does not execute Python.
- Future proxy does not leak into normal Python value semantics.
- Complex control flow is still decided by a single post-seal synchronous CPython execution.
- Early work must be adopted at the original logical point with temporal, authority, and effect contracts.
- Scope is not limited to LLM-generation results; it applies to any Host-owned capability.
So APPL is closer to “Python with transparent Futures,” while Pysolate is closer to “a sync Python outer sync plus split-phase physical-work overlay.”
4. Dataflow: making the whole program a dependence graph
Dataflow runtime often models computation as a graph: nodes trigger when input dependencies are ready. Program Dependence Graph makes data and control dependencies explicit and supports optimization and code motion.[5]
If fully dataflow-ized, the example becomes:
A ─┐
├─ add ─┐
B ─┘ ├─ add -> result
D ─────────┘
A scheduler can execute directly along critical path.
Pysolate does not implement full dataflow runtime:
a+bdoes not become a Host node;- Host does not interpret Python operators;
- ordinary heap, exceptions, and dynamic dispatch do not enter Host graph;
- PDG/CFG is used only to prove which Host phase can move;
- final semantics are still executed by CPython.
So a more accurate structure is:
ordinary Python program
+
Host physical-work overlay
rather than:
Python -> full computation graph -> Host execution graph
5. Lazy evaluation: deferred computation, but PLM does early physical work
Call-by-need delays evaluation until needed and uses sharing to avoid repeated computation. Launchbury’s semantics models this sharing and demand-driven values explicitly via heap bindings.[6]
PLM is a mirror image:
Lazy evaluation:
start as late as possible
evaluate when needed
PLM:
start physical work early
deliver/materialize only when needed
You can call Pysolate:
physically eager, logically strict, materially deferred
Physical work is aggressive, logical execution remains original synchronous order, and materialization can be deferred.
6. Partial evaluation and multi-stage programming: executing static parts in advance
Partial evaluation computes parts of a program in advance given known inputs and emits a residual program; multi-stage programming explicitly separates code generation and execution stages.[7]
The distinction from Pysolate is important:
- it does not evaluate ordinary Python during streaming;
- it does not attempt to constant-fold arbitrary
a+b; - it does not generate an alternative evaluator with different Python semantics;
- it only performs physical phases defined by Host contracts in advance.
Prepared dataset folding or replacing validated inputs with constants overlaps with partial evaluation, but those are additional source transformations, not required parts of PLM correctness.
7. Speculative execution and prefetch: doing potentially needed work early
CPU branch speculation, compiler prefetch, and database prefetch all try to start work before demand is certain. PLM shares this:
start early
possibly waste work
discard wrong path
shorten critical path
The difference is PLM’s speculation is bounded by effect and authority contracts: a candidate cannot produce logical effects just because “it is only an optimization.”
8. Linearizability: giving asynchronous physical process a logical activation point
Linearizability requires each concurrent operation to appear to take effect atomically at some instant between invocation and response.[8]
PLM borrows this mental model, but the domain is not identical:
- Prepare can be even earlier than logical invocation;
- therefore Prepare alone cannot be considered the operation occurring;
- at the original call site, Linearize proves the candidate can be legally claimed;
- if it cannot, start a canonical operation from that site.
This intermediate point re-anchors “early physical result” into the original Python semantics.
9. Effect systems: code motion must know what it touches
Effect systems use static information on possible effects to support safe parallelism, reordering, and isolation reasoning.[9]
PLM metadata is essentially a Host-boundary effect/temporal system:
what is read
what is written
whether immutable
whether verifiable
whether speculatable
when authority is checked
whether exception behavior is stable
whether prepare/commit is supported
It does not need a full effect system for all Python; unknowns remain barriers, and conservative correctness is still possible.
10. Leases, versioning, and cache consistency
Distributed-system leases provide bounded guarantees for consistency protocols.[10] In PLM, a lease can become candidate validity evidence:
value
version
valid_until
authority_epoch
This turns “time t” from a vague risk into a verifiable protocol condition.
11. LLM program runtimes
LMQL compiles prompt, constraints, and control flow into efficient inference procedures; SGLang provides a structured LLM-program frontend with runtime parallel control and KV-cache reuse.[11][12]
These systems mainly optimize LLM program semantics or serving substrate. PLM abstraction is lower and broader:
- it does not require Host operations to be model generation;
- it does not require provider-specific KV cache visibility;
- it requires only that operations can be split into safe physical phases;
- the same model can serve search, Git, dataset, object store, remote compute, and prepare/commit effect operations.
13. Why this design is broadly general
PLM applicability is not determined by tool type, but by a factorizable contract:
As long as such a factorization exists, optimization applies.
1. It unifies pre-dispatch and cache
Source-time request:
start now, adopt later
Cache:
prepared earlier, adopt now
Both are candidates:
Linearize does not care whether it has just started, already finished, or came from prior cache; it only cares whether it can represent the current logical operation.
2. It unifies warm preparation and request execution
An operation can be split into:
runtime / interpreter preparation
connection preparation
argument-independent provider setup
argument-dependent request
time-sensitive read
validation
deserialisation
publication
Each layer has different dependency footprint, so each can move to a different location.
3. It does not depend on a full static graph
Even when branch, loop, or params are only known at runtime, CPython can issue the next batch of work as new frontier is reached. No complete DAG must be known upfront.
4. It allows conservative fallback
Any proof failure:
no Prepare
no speculation
no sinking materialize
falls back directly to:
value = synchronous_host_call(...)
So correctness does not depend on optimization success.
5. It fits Host-owned authority boundaries
If Guest cannot directly access network, Git, file, or provider directly and must go through typed Host capability, Host boundary naturally provides:
uniform interception point
tool identity
argument serialization
effect metadata
authority check
trace / replay
cancellation
result ownership
That makes PLM more realistic than guessing arbitrary Python library calls.
14. Formalization: a minimal core language
No need to attempt a full CPython proof. Define a small language and map implementation to an admitted subset.
1. Baseline language
Expressions:
Statements:
where:
- abstracts ordinary local Python computation;
- is an explicit Host operation.
Program state:
includes:
- : Guest local/heap state;
- : logical external context;
- : Host private physical state.
2. PLM extension
Add:
where candidate and job are Guest-unforgeable internal tokens.
3. Labelled transitions
Separate internal events:
and visible events:
Define:
as deleting physical internal events.
4. Trace refinement theorem
For admitted program , if:
- Prepare is silent on logical state;
- validator is sound;
- invalid candidates fallback in Linearize;
- untaken candidates can be silently discarded;
- Materialize returns corresponding job result or exception;
- phase movement respects data/control/effect/exception/temporal dependencies;
- any site that cannot be proved is kept as baseline call;
then:
This says every visible behavior of transformed program is explained by the original synchronous program.
Under stronger deterministic, snapshot-fixed, and no real-time observation assumptions, this can be strengthened to trace equality:
5. Why refinement, not same wall-clock
If program reads real time:
start = time.time()
...
any optimization that makes it faster changes the time it observes.
Likewise, if stock_price() truly reads a live market, after optimization the program reaches the original call site earlier, so it may naturally observe different market state.
So we cannot claim lockstep equivalence for arbitrary wall-clock-observing Python. A more precise theorem is:
For the same external history, after hiding internal Prepare events, PLM logical operations can be legally linearized at the original call site and produce a trace allowed by baseline semantics.
If strict replay is required, then you must:
- virtualize time;
- fix snapshot;
- record/replay external inputs;
- or mark time/random/signal as optimization barriers.
15. Proof structure
Global proof is composed of local lemmas.
Lemma 1: Silent Prepare insertion
If:
only changes Host private state , does not change , and produces no visible label, inserting Prepare is a stuttering step:
Lemma 2: Valid adoption
By validator soundness:
So adopting a candidate does not produce a result not allowed by baseline.
Lemma 3: Invalid fallback
If candidate is invalid, Linearize starts canonical from current logical context, directly restoring baseline semantics.
Lemma 4: Untaken speculation
If Prepare and Discard are both silent, an unentered-branch candidate only changes , and is unobservable after hiding internal events.
Lemma 5: Materialize commutation
If:
then:
By induction, Materialize can be safely moved rightward to the end of any contiguous commutable interval.
Composition
AST transformation decomposes into finitely many steps:
insert Prepare
replace each original call with Linearize + Materialize
move Prepare
move Materialize
insert Discard
Each step preserves trace refinement; transitivity yields whole-program property.
16. Turn a program into a PLM dependence graph
The optimizer does not need a full Python dataflow graph; for each Host call it only creates:
1. Basic edges
param definitions ───────────────> P_h or L_h
P_h ───────────────────> L_h
L_h ───────────────────> M_h
M_h ───────────────────> result consumers
If parameter is source-time known, definitions can target ; if parameter is produced only by CPython, preparation can only occur before .
2. Control edge
A logical call must be controlled by original branch:
If speculation is not allowed:
If silent speculation is allowed, the control edge to can be removed, but the edge to cannot be removed.
This precisely expresses:
physical preparation can cross unknown control flow; logical occurrence cannot.
3. Effect edge
If a statement can change operation world footprint, authority, or validity:
If even Prepare is affected, the edge targets as well.
4. Exception edge
If failure of a previous operation prevents a later logical call:
You can prepare early, but cannot let produce logical effects early.
5. Temporal order edge
Two reads are not always swappable:
a = current_price()
b = current_price()
If they lack a shared snapshot, original program requires logical order:
That is, classic read/read independence is insufficient in temporal worlds.
17. Formalized optimization objective
In an ideal world with infinite resources, with fixed dependency graph latency and total work, the lower bound is set by critical path; work/span models characterize parallel computation by total work and longest dependency chain.[13]
PLM’s simplest scheduling policy is:
That is:
Prepare early, Linearize in-place, Materialize as late as dependency allows.
But real systems also have:
branch hit probability
candidate expiration probability
provider cost
rate limit
memory
concurrent quota
cancel cost
unknown latency
So optimal scheduling is a policy, not a fixed topological order.
You can define:
Global optimum is unnecessary. The following simple optimizations are already effective.
18. Practical optimization 1: Source-closed Prepare
If the streaming analyzer sees:
tax = tax_table("UK")
and proves:
tool identity known
parameters are literal / sealed constant
authority known
Prepare silent
it creates immediately:
candidate = prepare(site, TAX_TABLE, ("UK",))
At final runtime, at the original site:
job = linearize_or_start(
candidate,
TAX_TABLE,
actual_args=("UK",),
)
tax = materialize(job)
It must dynamically check:
source seal
site fingerprint
tool identity
actual args digest
authority epoch
logical run
temporal evidence
Fallback if mismatch.
This is a high-value, low-complexity optimization because runtime guards turn many static proofs into “mistakes only lose performance.”
19. Practical optimization 2: Prepare hoisting within a basic block
Original code:
a = A()
x = pure_local_1()
b = B()
y = pure_local_2()
c = C()
return combine(a, b, c, x, y)
If A/B/C parameters are known, it becomes:
_pA = prepare(A)
_pB = prepare(B)
_pC = prepare(C)
_jA = linearize_or_start(_pA, A)
a = materialize(_jA)
x = pure_local_1()
_jB = linearize_or_start(_pB, B)
b = materialize(_jB)
y = pure_local_2()
_jC = linearize_or_start(_pC, C)
c = materialize(_jC)
return combine(a, b, c, x, y)
Even with no Materialize movement, all three calls can overlap.
The compiler only needs to:
- keep Python A-normalized left-to-right evaluation;
- scan forward inside basic block;
- hoist Prepare across statements known not to affect parameters, authority, or effects;
- stop on unknown calls, exception boundaries, or mutation.
20. Practical optimization 3: Local Materialize sinking
Original code:
a = read_immutable()
x = pure_noexcept()
y = another_pure_noexcept(x)
return use(a, y)
If read_immutable is:
total
noexcept
immutable result
then:
_p = prepare(read_immutable)
_j = linearize_or_start(_p, read_immutable)
x = pure_noexcept()
y = another_pure_noexcept(x)
a = materialize(_j)
return use(a, y)
Materialize can overlap with local computation.
Implementation uses a conservative commutation predicate:
can_sink(M, statement) =
statement.pure
and statement.noexcept
and not reads_result
and not observes_locals
and not changes_context
Unknown means false.
21. Practical optimization 4: Branch speculation
Original code:
if cond:
x = fallback("premium")
else:
x = fallback("basic")
If both calls satisfy:
Prepare silent
result immutable
low cost
cancelable/discardable
they may be prepared while cond is unknown, and only one path is linearized.
Expected benefit can be roughly estimated as:
Only speculate when:
No complex ML scheduler needed. A simple first version can use:
at most N speculative jobs per run
at most M jobs per tool
allow only low-cost immutable reads
22. Practical optimization 5: Just-in-time Prepare
With time validity, “earlier is always better” is no longer universally correct.
Suppose:
- operation latency is ;
- candidate has semantic lease after completion;
- expected logical call happens at .
We want:
so it completes before the call, and:
so it is still valid when called.
So a suitable issue window is:
Examples:
- immutable candidate: , issue immediately;
- latency 20 seconds, lease 5 seconds: issue about 20–25 seconds before expected call;
- no validity guarantee: only pre-transport preparation, no early final current read.
This is a simple but highly discriminative temporal scheduling optimization.
23. Practical optimization 6: Demand promotion and cancellation
Host job priority:
waiting to be materialized
>
entered real control path
>
same-path source-time prepare
>
unknown branch speculation
When Python calls:
materialize(job)
Host promotes the job to highest priority.
After branch is determined, discard unchosen candidates:
discard(candidate)
and cancel quickly to avoid speculative work consuming critical-path provider quota.
24. Practical optimization 7: Singleflight and cache unification
If multiple logical runs request:
same immutable operation
same parameters
same authority-compatible context
Host can share one physical work:
one physical candidate
├── private adoption for run A
├── private adoption for run B
└── cache backing
But must maintain:
logical handle per run
private result ownership
mutable data private copy / COW
cancellation per run
authority checked independently
This is the generalization of “shared physical production, separate sandboxes”:
25. Implementation sketch
1. Compiler pipeline
sealed source
↓
parse AST
↓
mark Host capability calls
↓
A-normalisation
↓
CFG / control dependence / exception region
↓
def-use / mutation / effect facts
↓
create P-L-M nodes
↓
Prepare hoisting
↓
Materialize sinking
↓
AST rewrite
↓
verifier
↓
synchronous CPython
Streaming analyzer uses more conservative prefix facts and handles only source-closed sites.
2. Minimal ABI
candidate = __pysolate_prepare__(
site_token,
tool_id,
frozen_args,
contract_token,
)
job = __pysolate_linearize__(
site_token,
tool_id,
actual_args,
candidate,
)
value = __pysolate_materialize__(job)
__pysolate_discard__(candidate)
Actual fusions are possible:
- no candidate: merge Prepare and Linearize;
- no sinking: merge Linearize and Materialize;
- non-stageable: all three degrade to ordinary synchronous Host call.
3. Host job state
CREATED
↓
PREPARING
├── PENDING
├── CANDIDATE_READY
├── TENTATIVE_FAILED
└── CANCELLED
LINEARIZED
├── ADOPTED
├── VALIDATING
└── CANONICAL_RUNNING
MATERIALIZED
├── RETURNED
└── RAISED
4. Tool contract
temporal:
IMMUTABLE
SNAPSHOT
VERSIONED
LEASED
CURRENT
WALLCLOCK_OBSERVING
effect:
SILENT_READ
PREPARE_COMMIT
NON_STAGEABLE
speculation:
NEVER
SAME_PATH
BUDGETED
failure:
STABLE
RETRY_AT_LINEARIZE
VALIDATE_AT_LINEARIZE
authority:
BIND_AT_PREPARE
RECHECK_AT_LINEARIZE
RECHECK_AT_COMMIT
5. Verifier
At minimum, verify:
each candidate/job created only by internal ABI
Materialize definition dominates all uses
speculation is used only under allowed contract
Prepare parameters and runtime actual args are checkable
does not cross forbidden exception/effect/authority region
dynamic occurrence reuse across loops is not incorrect
untaken candidate has discard/cancellation path
traceback/source mapping preserved
If verifier fails, fallback to original synchronous execution.
26. What PLM explicitly does not cover
A useful abstraction must say what it does not do.
1. Not arbitrary local Python constant folding
For example:
x = expensive_pure_python(a)
PLM does not execute this during streaming, and does not auto partial-evaluate it.
The following belong to traditional compiler / partial evaluation:
constant folding
function specialization
loop unrolling
dead local computation elimination
NumPy graph fusion
local pure function memoization
These can combine with PLM, but are not the same mechanism.
2. Not full future propagation
PLM does not turn:
a + b
a["id"]
obj.method()
into Future graph, nor auto-generate Future[Future[T]].
Therefore, after any local Python computation, any subsequent Host call must wait for CPython to compute the argument.
3. Not full local CPU computation parallel scheduling
If:
a = A() # 1s
cpu_work() # 10s
x = a + 1
b = B(x) # 20s
A finishes in 1 second, but the main Python thread is doing cpu_work() so B starts at 10 seconds.
To automatically start B at the 1-second mark, we would need:
continuation scheduler
second Python context
thread/process parallelism
safe code motion
Host expression IR
This is beyond minimal PLM.
4. Not arbitrary loop-iteration parallelization
for x in xs:
y = tool(x)
Per-iteration splitting is easy; fan-out of all iterations in batch would change:
failure timing
resource peak
variable bindings
partial-effect ordering
break/continue
iterator behavior
It needs separate loop transformations and proofs.
5. Not unsafe current reads
If operation semantics are “true state at call time” and provider does not offer:
snapshot
version
lease
validation
then final value cannot be safely pre-read. At best, one can pre-prepare connection or authentication stages that are time-insensitive.
6. Not arbitrary non-reversible side effects
Real effects:
sending email
a charge
deleting resource
pushing commit
cannot happen early under branch speculation unless the provider supports explicit prepare/commit or compensation.
7. Not all CPython reflective semantics
These capabilities observe AST transforms or variable-binding timing:
locals()
inspect.currentframe()
sys.settrace()
eval()
exec()
along with:
guest threads
signals
real wall-clock
a mutable object with identity sensitivity
arbitrary native extension
They must be blocked, virtualized, or treated as optimization barriers.
8. Not guaranteed globally optimal schedule
Real latency, branch probabilities, provider contention, and temporal invalidation are unknown. Global optimum is an online stochastic scheduling problem.
PLM provides:
normalized valid scheduling space
safe local transformations
a conservative greedy policy
not a universal optimizer oracle.
27. Optimizations not yet feasible, but worth studying
1. Restricted Python continuation
Allow a limited set of proven pure Python operators to run after Host Future completion:
A result
↓
pure projection / arithmetic
↓
B request
This can shorten data-dependent chains but introduces a small continuation runtime. The key research question is: how to bound it without reimplementing Python.
2. Typed projection IR
Support only:
JSON field
tuple index
primitive arithmetic
comparison
formatting
This allows:
B(a["id"])
to be auto-started by Host after A completes. But Python’s __getitem__ is not always equivalent to JSON projection, so it must apply only to typed Host results.
3. Cross-basic-block global scheduling
Using full PDG, dominance, post-dominance, and effect summary, prepare can be hoisted further and materialize sinked further. Yields potentially better performance, but exception and reflection boundaries significantly increase proof complexity.
4. Profile-guided temporal scheduling
Learn from historical traces:
call-site arrival time
branch reach probability
provider latency distribution
candidate invalidation probability
Then choose just-in-time Prepare timing.
5. Speculative branch portfolio
Under a cost budget choose branches worth pre-starting instead of starting everything:
subject to:
6. Automatic derive prepare/commit
For some side-effecting APIs, automatically decompose request into:
construct
upload temporary
validate
commit
This typically requires provider-specific protocol and cannot usually be inferred from Python AST alone.
7. Combine with local partial evaluation
If future work adds pure-function proof, typed arrays, or restricted numerical sublanguage, PLM could in parallel:
- prepare Host work early;
- fold prepared input;
- emit residual Python;
- keep final fresh execution.
That would extend PLM into a fuller staged optimizer, but should be separate layers rather than folded into the minimal semantic core.
Conclusion: not execute earlier, but schedule work more intelligently
Pysolate is often misread as:
“As the model generates one line, we execute that line.”
A more accurate and stronger statement is:
We never execute Python during source streaming. We only use facts already exposed by source to prepare physical work early that may later be adopted by logical calls.
Control flow, object semantics, exceptions, and intermediate state are still decided by one synchronous CPython run after sealing. The Host only owns:
physical candidate
validity evidence
pending job
authority contract
effect protocol
The three-phase normal form compresses this idea into:
Or shorter:
Its generality comes from a simple fact: more of today’s latency happens not in local arithmetic but at controlled system boundaries. As long as that boundary can separate prepare, activate, and deliver, a program need not change its synchronous mental model to gain parallelism.
This may be Pysolate’s most important design philosophy:
References
[1] Barbara Liskov and Liuba Shrira. Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems. PLDI, 1988.
[2] Robert H. Halstead Jr. Multilisp: A Language for Concurrent Symbolic Computation. ACM TOPLAS, 1985.
[3] Yury Selivanov. PEP 492 — Coroutines with async and await syntax; Python asyncio official documentation.
[4] Honghua Dong et al. APPL: A Prompt Programming Language for Harmonious Integration of Programs and Large Language Model Prompts. ACL, 2025; arXiv preprint: 2406.13161.
[5] Jeanne Ferrante, Karl J. Ottenstein, and Joe D. Warren. The Program Dependence Graph and Its Use in Optimization. ACM TOPLAS, 1987.
[6] John Launchbury. A Natural Semantics for Lazy Evaluation. POPL, 1993.
[7] Neil D. Jones, Carsten K. Gomard, and Peter Sestoft. Partial Evaluation and Automatic Program Generation. 1993; Walid Taha and Tim Sheard. Multi-stage Programming with Explicit Annotations. 1997.
[8] Maurice P. Herlihy and Jeannette M. Wing. Linearizability: A Correctness Condition for Concurrent Objects. ACM TOPLAS, 1990.
[9] John M. Lucassen and David K. Gifford. Polymorphic Effect Systems. POPL, 1988.
[10] Cary G. Gray and David R. Cheriton. Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency. SOSP, 1989.
[11] Luca Beurer-Kellner, Marc Fischer, and Martin Vechev. Prompting Is Programming: A Query Language for Large Language Models. PLDI, 2023.
[12] Lianmin Zheng et al. SGLang: Efficient Execution of Structured Language Model Programs. NeurIPS, 2024.
[13] Robert D. Blumofe and Charles E. Leiserson. Scheduling Multithreaded Computations by Work Stealing. JACM, 1999.