Symex: typed step-result wire; suspend on unresolved globals; drop kind tags#589
Merged
Conversation
…nd tags
Strips three variants of "stuff a kind tag in a value when the type system
already had it" from the symex policy boundary, plus replaces a silent
default-value fallback with failure-atomic suspension.
* ("ptr", N) wrapping removed. Pointers travel as bare PyLongs. The kind
tag was already inconsistent — extract_address had a bare-int fallback
because pointers loaded out of memory came through that way; only fresh
mints (make_literal_ptr, resume_addr, libc models) wrapped. has_address
now matches concrete_has_address (non-bool, non-zero PyLong). Consumer-
side defensive decoders dropped across dispatch.py, lens.py, events.py,
and the python_to_value* helpers.
* CallAction enum dropped. (SKIP, v) and (MODEL, v) fell through to
identical C++ behavior, so the wrapper was speculative ceremony. Handlers
now return their replacement value directly; None / NotImplemented falls
through; Skip(value) is the typed disambiguator for the rare "skip with
None" case (e.g. ctx.default()). CallAction::MODEL removed from
Suspension.h.
* Step results are typed dataclasses. SymbolicStep and the concrete step
no longer emit ("completed", v)-style string-tagged tuples; they
construct Completed / Errored / Budget / Branch / Switch /
MemAddrSuspension / GlobalSuspension / Suspended instances. Forks use
typed BranchFork / SwitchFork / MemAddrFork / GlobalFork. Engine
dispatches via isinstance. Drops StepResultKind and the latent
forks[0]["sub_kind"] reader bug (the fork dict's "kind" field was never
the right discriminator). The previous "call-addr" / "load-addr" /
"store-addr" sub-kinds collapse into one MemAddrSuspension with is_write
and is_call_target booleans.
* Failure-atomic unresolved globals. compute_global_ptr no longer drops a
valid address_hint because the IR-derived size is 0 (place_at substitutes
8 internally). When resolution genuinely fails, the policy hands off via
a new Policy::on_unresolved_global hook: the concrete default errors the
path with a new ErrorKind::UNRESOLVED_GLOBAL; the symbolic policy
snapshots, re-pushes the suspended work item, and emits a
GlobalContinuation (which already existed but was never wired). New
_interp.resume_global(state, entity_id, address) writes the resolved
address into the snapshot's global_addresses cache so the re-stepped
COMPUTE_GLOBAL_PTR cache-hits. engine.py dispatches global suspensions
through the existing intercept.address_for(kind="global") chain,
terminating with Terminal.UNRESOLVED_GLOBAL when no handler claims it.
kumarak
approved these changes
May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Strips three variants of "stuff a kind tag in a value when the type system already had it" from the symex policy boundary, plus replaces a silent default-value fallback with failure-atomic suspension.
("ptr", N)wrapping removed. Pointers travel as barePyLongs. The kind tag was already inconsistent —extract_addresshad a bare-int fallback because pointers loaded out of memory came through that way; only fresh mints (make_literal_ptr,resume_addr, libc models) wrapped.has_addressnow matchesconcrete_has_address(non-bool, non-zeroPyLong). Consumer-side defensive decoders dropped acrossdispatch.py,lens.py,events.py, and thepython_to_value*helpers.CallActionenum dropped.(SKIP, v)and(MODEL, v)fell through to identical C++ behavior, so the wrapper was speculative ceremony. Handlers now return their replacement value directly;None/NotImplementedfalls through;Skip(value)is the typed disambiguator for the rare "skip withNone" case (e.g.ctx.default()).CallAction::MODELremoved fromSuspension.h.Step results are typed dataclasses.
SymbolicStepand the concretestepno longer emit("completed", v)-style string-tagged tuples; they constructCompleted/Errored/Budget/Branch/Switch/MemAddrSuspension/GlobalSuspension/Suspendedinstances. Forks use typedBranchFork/SwitchFork/MemAddrFork/GlobalFork. Engine dispatches viaisinstance. DropsStepResultKindand the latentforks[0]["sub_kind"]reader bug (the fork dict's"kind"field was never the right discriminator). The previous"call-addr"/"load-addr"/"store-addr"sub-kinds collapse into oneMemAddrSuspensionwithis_writeandis_call_targetbooleans.Failure-atomic unresolved globals.
compute_global_ptrno longer drops a validaddress_hintbecause the IR-derived size is 0 (place_atsubstitutes 8 internally). When resolution genuinely fails, the policy hands off via a newPolicy::on_unresolved_globalhook: the concrete default errors the path with a newErrorKind::UNRESOLVED_GLOBAL; the symbolic policy snapshots, re-pushes the suspended work item, and emits aGlobalContinuation(which already existed but was never wired). New_interp.resume_global(state, entity_id, address)writes the resolved address into the snapshot'sglobal_addressescache so the re-steppedCOMPUTE_GLOBAL_PTRcache-hits.engine.pydispatches global suspensions through the existingintercept.address_for(kind="global")chain, terminating withTerminal.UNRESOLVED_GLOBALwhen no handler claims it.