Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions bin/Index/IRGen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ std::optional<FunctionIR> IRGenerator::Generate(
uint32_t func_scope = PushStructure(
mx::ir::StructureKind::FUNCTION_SCOPE, EntityIdOf(*body));
func_.body_scope_index = func_scope;
AssociateBlockWithStructure(frame); // FRAME was created before scope; wire it up now.
AssociateBlockWithStructure(entry);

// ENTER_SCOPE for the function body.
Expand Down Expand Up @@ -375,6 +376,17 @@ std::optional<FunctionIR> IRGenerator::Generate(
// Insert compensation blocks for gotos that cross scope boundaries.
InsertGotoCompensationBlocks();

// Safety net: any block still without a parent structure (e.g. a
// LABEL block forward-referenced by a goto whose label was never
// defined, or any future synthetic block that escapes the per-site
// associate calls) gets attached to the function body scope so
// `IRBlock::parent_function()` always resolves.
for (uint32_t bi = 0; bi < func_.blocks.size(); ++bi) {
if (func_.blocks[bi].parent_structure_index == UINT32_MAX) {
AssociateBlockWithStructure(bi, func_.body_scope_index);
}
}

// Patch empty blocks before computing dominators.
// Empty blocks arise when all paths into a merge/exit block already
// terminated (e.g., both if-branches return), or from empty switch cases.
Expand Down Expand Up @@ -464,6 +476,7 @@ std::optional<FunctionIR> IRGenerator::GenerateGlobalInit(
uint32_t func_scope = PushStructure(
mx::ir::StructureKind::FUNCTION_SCOPE, EntityIdOf(var));
func_.body_scope_index = func_scope;
AssociateBlockWithStructure(frame); // FRAME was created before scope; wire it up now.
AssociateBlockWithStructure(entry);

// ENTER_SCOPE for the function body.
Expand Down Expand Up @@ -504,6 +517,14 @@ std::optional<FunctionIR> IRGenerator::GenerateGlobalInit(
// Pop FUNCTION_SCOPE.
PopStructure();

// Safety net: associate any block missing a parent structure with the
// function body scope. Mirrors the pass in `Generate()`.
for (uint32_t bi = 0; bi < func_.blocks.size(); ++bi) {
if (func_.blocks[bi].parent_structure_index == UINT32_MAX) {
AssociateBlockWithStructure(bi, func_.body_scope_index);
}
}

// Patch empty blocks.
for (uint32_t bi = 0; bi < func_.blocks.size(); ++bi) {
auto &block = func_.blocks[bi];
Expand Down Expand Up @@ -579,12 +600,17 @@ void IRGenerator::PopStructure() {
}

void IRGenerator::AssociateBlockWithStructure(uint32_t block_idx) {
if (current_structure_index_ == UINT32_MAX) return;
func_.blocks[block_idx].parent_structure_index = current_structure_index_;
AssociateBlockWithStructure(block_idx, current_structure_index_);
}

void IRGenerator::AssociateBlockWithStructure(uint32_t block_idx,
uint32_t struct_idx) {
if (struct_idx == UINT32_MAX) return;
func_.blocks[block_idx].parent_structure_index = struct_idx;
StructureIR::ChildRef ref;
ref.index = block_idx;
ref.is_structure = false;
func_.structures[current_structure_index_].children.push_back(ref);
func_.structures[struct_idx].children.push_back(ref);
}

void IRGenerator::AssociateObjectWithScope(uint32_t obj_idx) {
Expand Down Expand Up @@ -631,7 +657,12 @@ uint32_t IRGenerator::NewBlock(mx::ir::BlockKind kind) {
// The dead block gets an IMPLICIT_UNREACHABLE terminator immediately so that
// CurrentBlockTerminated() returns true and dead-code skipping works.
void IRGenerator::SwitchToDeadBlock() {
SwitchToBlock(NewBlock(mx::ir::BlockKind::UNREACHABLE));
uint32_t dead = NewBlock(mx::ir::BlockKind::UNREACHABLE);
// Anchor the dead block to the current scope so its parent_function()
// resolves; otherwise downstream consumers walking up the structure
// chain hit a nullopt parent.
AssociateBlockWithStructure(dead);
SwitchToBlock(dead);
InstructionIR term;
term.opcode = mx::ir::OpCode::IMPLICIT_UNREACHABLE;
EmitTopLevel(std::move(term));
Expand Down Expand Up @@ -1495,22 +1526,33 @@ void IRGenerator::EmitSwitchStmt(const pasta::Stmt &s) {
}

// If no explicit default, add an implicit default that branches to the
// switch exit block. Without this, the interpreter errors when no case
// matches (e.g., a switch with gaps in its case values).
// switch exit block. The default needs its own block (not exit_block
// directly) because IRSwitchCaseStructure::target_block() looks up the
// structure's first child block; if we used exit_block, it would belong
// to the outer SWITCH structure (line ~1703 below) and the default's
// SWITCH_CASE structure would have no children, making target_block()
// return {} and the interpreter's `decide_switch` skip past the default
// entirely on a non-matching selector.
if (!has_default) {
// Create a structure for the implicit default so serialization succeeds.
uint32_t default_block = NewBlock(mx::ir::BlockKind::SWITCH_DEFAULT);

uint32_t impl_struct = PushStructure(
mx::ir::StructureKind::SWITCH_CASE, EntityIdOf(s));
auto &sc_struct = func_.structures[impl_struct];
sc_struct.is_default = true;
AssociateBlockWithStructure(default_block);
PopStructure();

InstructionIR::SwitchCaseIR implicit_default;
implicit_default.is_default = true;
implicit_default.block_index = exit_block;
implicit_default.block_index = default_block;
implicit_default.structure_index = impl_struct;
term.switch_cases.push_back(implicit_default);
AddEdge(current_block_index_, exit_block);
AddEdge(current_block_index_, default_block);
// The empty default_block falls through to the switch exit; the
// post-processing pass at the top of Generate() gives empty blocks
// an IMPLICIT_GOTO terminator to their first successor.
AddEdge(default_block, exit_block);
}

uint32_t term_idx = EmitTopLevel(std::move(term));
Expand Down Expand Up @@ -1634,6 +1676,7 @@ void IRGenerator::EmitSwitchStmt(const pasta::Stmt &s) {
// Emit the loop condition and back-edge.
// After the last case in the body, branch to the condition block.
uint32_t cond_block = NewBlock(mx::ir::BlockKind::LOOP_CONDITION);
AssociateBlockWithStructure(cond_block);
EmitBranch(cond_block);
SwitchToBlock(cond_block);

Expand All @@ -1646,6 +1689,7 @@ void IRGenerator::EmitSwitchStmt(const pasta::Stmt &s) {
? cases[loop_top_ci].block_index
: exit_block;
uint32_t loop_exit = NewBlock(mx::ir::BlockKind::LOOP_EXIT);
AssociateBlockWithStructure(loop_exit);
EmitCondBranch(cond_val, loop_body_block, loop_exit, EntityIdOf(stmt));
SwitchToBlock(loop_exit);
} else {
Expand Down Expand Up @@ -4595,8 +4639,16 @@ void IRGenerator::InsertGotoCompensationBlocks() {

// Compensation block needed.

// Create a compensation block.
// Create a compensation block. This pass runs *after* all PushStructure
// / PopStructure activity, so `current_structure_index_` is no longer
// meaningful — anchor the new block explicitly to the common-ancestor
// scope (the structure the compensation logically lives at), or to the
// function body scope as a fallback. Without a parent, the block has no
// path back to its function via `IRBlock::parent_function()`.
uint32_t comp_block = NewBlock(mx::ir::BlockKind::COMPENSATION);
uint32_t comp_parent = (common_ancestor != UINT32_MAX) ? common_ancestor
: func_.body_scope_index;
AssociateBlockWithStructure(comp_block, comp_parent);

// Redirect the source instruction → comp_block instead of → target.
auto &goto_inst = func_.instructions[pg.goto_inst_idx];
Expand Down
5 changes: 5 additions & 0 deletions bin/Index/IRGen.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ class IRGenerator {
mx::RawEntityId source_eid = mx::kInvalidEntityId);
void PopStructure();
void AssociateBlockWithStructure(uint32_t block_idx);
// Explicit-target overload: used by post-emission passes (e.g. goto
// compensation) where `current_structure_index_` is no longer
// meaningful but the synthetic block still needs a parent so
// `IRBlock::parent_function()` resolves.
void AssociateBlockWithStructure(uint32_t block_idx, uint32_t struct_idx);
void AssociateObjectWithScope(uint32_t obj_idx);

// Emit EXIT_SCOPE for all enclosing scopes up to (but not including)
Expand Down
60 changes: 56 additions & 4 deletions bindings/Python/Interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,11 @@ static PyObject *py_init_state(PyObject *, PyObject *args) {
PyObject *global_resolver = (nargs >= 7) ? PyTuple_GetItem(args, 6) : Py_None;
PyObject *func_addr_resolver =
(nargs >= 8) ? PyTuple_GetItem(args, 7) : Py_None;
PyObject *entity_by_addr_resolver =
(nargs >= 9) ? PyTuple_GetItem(args, 8) : Py_None;
return SymbolicInitState(state_obj, second, py_policy, func_obj,
args_list, func_resolver, global_resolver,
func_addr_resolver);
func_addr_resolver, entity_by_addr_resolver);
}

PyErr_SetString(PyExc_TypeError,
Expand Down Expand Up @@ -657,9 +659,11 @@ static PyObject *py_step(PyObject *, PyObject *args) {
PyObject *global_resolver = (nargs >= 6) ? PyTuple_GetItem(args, 5) : Py_None;
PyObject *func_addr_resolver =
(nargs >= 7) ? PyTuple_GetItem(args, 6) : Py_None;
PyObject *entity_by_addr_resolver =
(nargs >= 8) ? PyTuple_GetItem(args, 7) : Py_None;
return SymbolicStep(state_obj, second, py_policy, max_steps,
func_resolver, global_resolver,
func_addr_resolver);
func_addr_resolver, entity_by_addr_resolver);
}

PyErr_SetString(PyExc_TypeError,
Expand Down Expand Up @@ -770,6 +774,45 @@ static PyObject *py_get_value_at(PyObject *, PyObject *args) {
return obj;
}

// Resume a state suspended on a symbolic SWITCH selector by entering
// the chosen target block. The driver picks one case (or default) per
// fork and calls this with the cloned snapshot + the IRBlock to enter.
//
// Path-condition constraints (selector ∈ [low, high], etc.) are added on
// the Python side; this entry-point only manipulates the substrate state.
//
// resume_switch_case(state, target_block_obj)
static PyObject *py_resume_switch_case(PyObject *, PyObject *args) {
PyObject *state_obj;
PyObject *block_obj;
if (!PyArg_ParseTuple(args, "OO", &state_obj, &block_obj)) {
return nullptr;
}
if (Py_TYPE(state_obj) != &InterpreterStateType) {
PyErr_SetString(PyExc_TypeError, "Expected InterpreterState");
return nullptr;
}
auto block = from_python<IRBlock>(block_obj);
if (!block) {
PyErr_SetString(PyExc_TypeError,
"resume_switch_case: second argument must be IRBlock");
return nullptr;
}
auto *sw = reinterpret_cast<InterpreterStateWrapper *>(state_obj);
auto *symbolic = sw->symbolic_state
? reinterpret_cast<ir::interpret::PyWrapperFor<SymbolicState> *>(
sw->symbolic_state)->data
: nullptr;
if (!symbolic || symbolic->call_stack.empty()) {
PyErr_SetString(PyExc_RuntimeError,
"resume_switch_case: symbolic state has no live call frame");
return nullptr;
}
symbolic->work_stack.push_back(
{ir::interpret::WorkKind::ENTER_BLOCK, IRInstruction{}, *block});
Py_RETURN_NONE;
}

static PyObject *py_clone_state(PyObject *, PyObject *args) {
PyObject *state_obj;
if (!PyArg_ParseTuple(args, "O", &state_obj)) return nullptr;
Expand Down Expand Up @@ -832,6 +875,8 @@ static PyObject *py_init_state_frame(PyObject *, PyObject *args) {
(nargs >= 8) ? PyTuple_GetItem(args, 7) : Py_None;
PyObject *func_addr_resolver =
(nargs >= 9) ? PyTuple_GetItem(args, 8) : Py_None;
PyObject *entity_by_addr_resolver =
(nargs >= 10) ? PyTuple_GetItem(args, 9) : Py_None;

if (Py_TYPE(memory_obj) != &ConcreteMemoryType) {
PyErr_SetString(PyExc_TypeError,
Expand All @@ -842,7 +887,7 @@ static PyObject *py_init_state_frame(PyObject *, PyObject *args) {
return SymbolicInitStateFrame(state_obj, memory_obj, py_policy, func_obj,
param_addrs, return_addr,
func_resolver, global_resolver,
func_addr_resolver);
func_addr_resolver, entity_by_addr_resolver);
}

// init_state_at: mid-block entry for under-constrained symbolic execution.
Expand Down Expand Up @@ -878,6 +923,8 @@ static PyObject *py_init_state_at(PyObject *, PyObject *args) {
(nargs >= 10) ? PyTuple_GetItem(args, 9) : Py_None;
PyObject *func_addr_resolver =
(nargs >= 11) ? PyTuple_GetItem(args, 10) : Py_None;
PyObject *entity_by_addr_resolver =
(nargs >= 12) ? PyTuple_GetItem(args, 11) : Py_None;

if (Py_TYPE(memory_obj) != &ConcreteMemoryType) {
PyErr_SetString(PyExc_TypeError,
Expand All @@ -888,7 +935,7 @@ static PyObject *py_init_state_at(PyObject *, PyObject *args) {
return SymbolicInitStateAt(state_obj, memory_obj, py_policy, func_obj,
block_obj, param_addrs, return_addr,
value_seed, func_resolver, global_resolver,
func_addr_resolver);
func_addr_resolver, entity_by_addr_resolver);
}

// Module methods.
Expand Down Expand Up @@ -925,6 +972,11 @@ static PyMethodDef InterpreterMethods[] = {
" Symbolic-address sibling of resume_addr: writes an arbitrary "
"Python value (typically a z3 expression) into the suspended op's "
"address-operand cache slot."},
{"resume_switch_case", py_resume_switch_case, METH_VARARGS,
"resume_switch_case(state, target_block)\n"
" Resume a state suspended on a symbolic SWITCH selector by "
"entering the chosen target IRBlock. Path-condition constraints are "
"the driver's responsibility."},
{"get_value_at", py_get_value_at, METH_VARARGS,
"get_value_at(state, eid) -> Python value\n"
" Read the cached value at an operand entity-id from the live "
Expand Down
Loading
Loading