Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
87adf51
feat: support wasm call_indirect via function tables and dynexec
greenhat Jul 2, 2026
7f4be23
fix: respect module-reserved linear memory in the linker layout
greenhat Jul 2, 2026
f12f9af
docs: state that the call_indirect type check is intentionally omitted
greenhat Jul 2, 2026
328f446
fix(frontend): build function tables from a final slot image
greenhat Jul 2, 2026
1fac37f
fix(hir): preserve layout-critical IR across textual round-trips
greenhat Jul 2, 2026
c2da269
fix(codegen): report layout and table failures as errors, not panics
greenhat Jul 2, 2026
db59cbf
test: execute the runtime failure modes of indirect calls
greenhat Jul 2, 2026
a960341
docs: correct stale dynexec and indirect-call documentation
greenhat Jul 2, 2026
4cc8c4f
fix(frontend): lower all-null function tables and use hygienic table …
greenhat Jul 2, 2026
7763000
fix(codegen): finish checked layout arithmetic and scope entry legality
greenhat Jul 2, 2026
3c36645
feat: check callee signatures before indirect-call dispatch
greenhat Aug 3, 2026
55d3abe
fix(analysis): propagate advice taint through indirect calls
greenhat Aug 3, 2026
a7e00d9
fix(codegen): compute the heap base in the fallible linker path
greenhat Aug 3, 2026
f1101ae
chore: adapt indirect-call emission and tests to the v0.25 assembly API
greenhat Aug 4, 2026
fff0509
fix(codegen): keep component-internal modules out of the package surface
greenhat Aug 4, 2026
0b91cf7
fix(frontend): bump generated table names until they are free
greenhat Aug 4, 2026
c6e7a39
fix(codegen): reject IR the indirect-call lowering would panic on
greenhat Aug 4, 2026
80d77ac
fix(hir): resolve function table entries from their own symbol table
bitwalker Aug 6, 2026
f5577cd
hir: verify hir.exec_indirect arguments against its signature
bitwalker Aug 6, 2026
628daa0
hir: verify indirect-call signature tags against the dispatched table
bitwalker Aug 6, 2026
6787095
hir: print a signature's results rather than its params twice
bitwalker Aug 6, 2026
5ca2995
hir(parser): verify the root, not the anchor it was parsed into
bitwalker Aug 6, 2026
338bc79
hir: cover the exec_indirect rules the verifier enforces, and share i…
bitwalker Aug 6, 2026
14afe76
codegen: check function table entry structure during legalization
bitwalker Aug 6, 2026
dea7ec3
codegen: check data segment layout arithmetic against the address space
bitwalker Aug 6, 2026
d4d8083
hir: name the live function table slot map
bitwalker Aug 6, 2026
76fc649
frontend-wasm: cover a data segment at the end of linear memory
bitwalker Aug 6, 2026
13a21cb
analysis: report advice taint reaching external indirect-call targets
bitwalker Aug 6, 2026
73a5568
analysis: state the root requirement for advice taint analysis
bitwalker Aug 6, 2026
5865467
codegen: discover memory-owning items in nested modules
bitwalker Aug 6, 2026
184f258
codegen: lower modules nested inside other modules
bitwalker Aug 6, 2026
1fca921
codegen: mark the synthetic wrapper component explicitly
bitwalker Aug 6, 2026
74f14ee
codegen: initialize function tables from the modules defining their c…
bitwalker Aug 6, 2026
52f3ce4
frontend-wasm: reject function table entries whose callee signature d…
bitwalker Aug 6, 2026
1fc3e12
frontend-wasm: describe what the compiler actually does with intrinsi…
bitwalker Aug 6, 2026
bf3c74d
style: format the function table additions
bitwalker Aug 6, 2026
185fd94
docs: distinguish the two ways a mis-declared stub fails
bitwalker Aug 6, 2026
3de8450
codegen: write only the table entries that can be dispatched to
bitwalker Aug 7, 2026
2bbb7dd
codegen: report unlowered table callees instead of panicking
bitwalker Aug 7, 2026
bfdfad0
test: pin parse-time verification, the export surface, and the wrappe…
bitwalker Aug 7, 2026
46d2be3
style: compare a module path without allocating a string for it
bitwalker Aug 7, 2026
4b1f522
test: pin the diagnostic for a table callee in an unlowered module
bitwalker Aug 7, 2026
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
42 changes: 25 additions & 17 deletions codegen/masm/src/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use crate::{Event, lower::NativePtr, masm};

pub struct MasmComponent {
pub id: Option<builtin::ComponentId>,
/// True if [`Self::id`] belongs to a component the compiler invented to wrap a bare core
/// module, rather than one an author wrote — see
/// [`builtin::Component::SYNTHETIC_WRAPPER_ATTR`], which is where this comes from.
pub synthetic_wrapper: bool,
/// The path of the root module for this component
///
/// All components must have a canonical root module, even if empty
Expand Down Expand Up @@ -261,10 +265,7 @@ impl MasmComponent {
/// a namespace, not what that namespace is, so a target that names a different one is not
/// contradicting the file the way a component id would be.
fn has_no_authored_identity(&self) -> bool {
match self.id.as_ref() {
Some(id) => id.is_synthetic_wrapper(),
None => true,
}
self.id.is_none() || self.synthetic_wrapper
}

/// Generate an executable module which when run expects the raw data segment data to be
Expand Down Expand Up @@ -598,15 +599,6 @@ mod tests {

use super::*;

/// The identity `frontend/wasm` gives every component it wraps around a core Wasm module.
fn wrapper_id() -> builtin::ComponentId {
builtin::ComponentId {
namespace: Symbol::intern("root_ns"),
name: Symbol::intern("root"),
version: Version::new(1, 0, 0),
}
}

/// The identity a real Wasm *component* carries, which its author chose.
fn authored_id() -> builtin::ComponentId {
builtin::ComponentId {
Expand All @@ -616,7 +608,22 @@ mod tests {
}
}

/// A component of `id`, rooted at the path that id renders to.
/// The component `frontend/wasm` wraps around a core Wasm module: the identity it gives
/// that wrapper, plus the marker saying the compiler invented it — which is what
/// [`MasmComponent::has_no_authored_identity`] reads. The id alone is a name an author
/// may write, and says nothing on its own.
fn wrapper_component() -> MasmComponent {
let id = builtin::ComponentId {
namespace: Symbol::intern("root_ns"),
name: Symbol::intern("root"),
version: Version::new(1, 0, 0),
};
let mut component = component(id);
component.synthetic_wrapper = true;
component
}

/// A component of `id` whose author wrote that id, rooted at the path it renders to.
fn component(id: builtin::ComponentId) -> MasmComponent {
let root_path: Arc<Path> = Arc::from(
id.to_library_path()
Expand Down Expand Up @@ -674,6 +681,7 @@ mod tests {

MasmComponent {
id,
synthetic_wrapper: false,
root: root_path,
init: None,
entrypoint: Some(exec_target(&child_path.join(masm::Path::new("caller")))),
Expand Down Expand Up @@ -859,7 +867,7 @@ mod tests {
fn a_synthetic_wrappers_library_is_rooted_at_the_target_namespace() {
let context = context();
let target = library_target("::example");
let component = component(wrapper_id());
let component = wrapper_component();
let decorated = decorations(&component.modules[1], "caller");

let sources = component.source_inputs(&target, context.session()).unwrap();
Expand Down Expand Up @@ -904,7 +912,7 @@ mod tests {
#[test]
fn a_library_target_named_after_the_wrapper_is_left_alone() {
let context = context();
let component = component(wrapper_id());
let component = wrapper_component();
let expected = paths(&component.modules[1]);
let decorated = decorations(&component.modules[1], "caller");
let allocations = target_allocations(&component.modules[1]);
Expand Down Expand Up @@ -1027,7 +1035,7 @@ mod tests {
/// `.masm` program takes.
#[test]
fn an_executable_target_still_gets_the_generated_main_module() {
for component in [component(wrapper_id()), component_less("::init")] {
for component in [wrapper_component(), component_less("::init")] {
let context = context();
let expected = paths(&component.modules[1]);
let target = Target::executable("main", Uri::new("main.wasm"));
Expand Down
3 changes: 3 additions & 0 deletions codegen/masm/src/emit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ impl<'a> OpEmitter<'a> {
masm::Instruction::SysCall(id) => {
self.invoked.insert(masm::Invoke::new(InvokeKind::SysCall, id.clone()));
}
masm::Instruction::ProcRef(id) => {
self.invoked.insert(masm::Invoke::new(InvokeKind::ProcRef, id.clone()));
}
_ => (),
}
}
Expand Down
193 changes: 193 additions & 0 deletions codegen/masm/src/emit/primop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,110 @@ impl OpEmitter<'_> {
self.emit(masm::Instruction::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()), span);
}

/// Execute the procedure whose MAST root is stored in slot `index` (stack top) of a function
/// table with `num_slots` slots based at `base_elem_addr` (a word-aligned element address).
///
/// Traps with an assertion failure if `index >= num_slots`, or if the slot's signature tag
/// differs from `type_tag` — which also covers null slots, whose tag is the reserved 0. The
/// callee is invoked in the same memory context as the caller (`dynexec`).
///
/// Expects `[index, args...]` on the operand stack, with the index on top. The index is
/// rewritten in place to the slot's element address, which `dynexec` pops before
/// transferring control, so the callee observes `[args...]` in normal argument order.
pub fn exec_indirect(
&mut self,
num_slots: u32,
base_elem_addr: u32,
type_tag: u32,
signature: &Signature,
span: SourceSpan,
) {
// Consume the index operand; all further effects on it are transient
let index = self.stack.pop().expect("operand stack is empty");
assert_eq!(index.ty(), Type::U32, "expected u32 table index for exec_indirect");

// Bounds check: [index, ..] -> [index < num_slots, index, ..] -> [index, ..]
self.emit(masm::Instruction::Dup0, span);
self.emit_push(num_slots, span);
self.emit(masm::Instruction::U32Lt, span);
self.emit(
Self::assert_with_message_inst(
"indirect call: function table index out of bounds",
span,
),
span,
);

// Rewrite the index to the slot's element address: base_elem_addr + index * slot size.
// The felt arithmetic cannot overflow: index < num_slots, and the linker guarantees
// that the whole table fits in the 32-bit address space.
self.emit(
masm::Instruction::MulImm(
Felt::new_unchecked(crate::linker::FunctionTableLayout::SLOT_SIZE_ELEMENTS as u64)
.into(),
),
span,
);
self.emit(
masm::Instruction::AddImm(Felt::new_unchecked(base_elem_addr as u64).into()),
span,
);

// Signature check: the tag stored next to the slot's digest must equal the tag the call
// site expects. A null slot keeps the zero tag that memory is initialized with, so it
// can never match and traps here too.
// [slot_addr, ..] -> [tag_addr, slot_addr, ..] -> [tag, slot_addr, ..] -> [slot_addr, ..]
self.emit(masm::Instruction::Dup0, span);
self.emit(
masm::Instruction::AddImm(
Felt::new_unchecked(
crate::linker::FunctionTableLayout::TYPE_TAG_OFFSET_ELEMENTS as u64,
)
.into(),
),
span,
);
self.emit(masm::Instruction::MemLoad, span);
self.emit_push(type_tag, span);
self.emit(
Self::assert_eq_with_message_inst(
"indirect call: callee signature mismatch or null function reference",
span,
),
span,
);

// Consume the arguments and produce the results on the emulated stack. Signatures for
// indirect calls never carry argument-extension attributes, so argument types must match
// the parameter types exactly. NOTE: this deliberately does not reuse
// `process_call_signature`: its zext/sext paths emit instructions that operate on the
// physical stack top, which at this point holds the transient slot address.
for (i, param) in signature.params.iter().enumerate() {
assert!(
matches!(param.extension(), ArgumentExtension::None),
"invalid exec_indirect: argument extension is not supported for parameter at \
index {i}"
);
let arg = self.stack.pop().expect("operand stack is empty");
assert_eq!(
arg.ty(),
param.ty,
"invalid exec_indirect: invalid argument type for parameter at index {i}"
);
}
for result in signature.results.iter().rev() {
self.push(result.ty.clone());
}

// `dynexec` pops the element address and reads the callee MAST root word at it
self.emit(
masm::Instruction::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
span,
);
self.emit(masm::Instruction::DynExec, span);
self.emit(masm::Instruction::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()), span);
}

/// Execute the given procedure in a new context.
///
/// A function called using this operation is invoked in a new memory context.
Expand Down Expand Up @@ -502,6 +606,95 @@ mod tests {
assert_eq!(&block[0], &Op::Inst(masm::Span::new(span, masm::Instruction::Caller)));
}

/// Pin the exact instruction sequence and stack effect of an indirect call: the bounds
/// check, the in-place index-to-address rewrite, the signature-tag check, and the
/// frame-traced `dynexec`.
#[test]
fn exec_indirect_emits_bounds_check_tag_check_and_dynexec() {
use midenc_hir::{CallConv, Felt};

use crate::linker::FunctionTableLayout;

let mut block = Vec::default();
let context = Rc::new(Context::default());
let mut stack = OperandStack::new(context.clone());
let mut invoked = BTreeSet::default();
let mut emitter = OpEmitter::new(&mut invoked, &mut block, &mut stack);

let signature =
Signature::with_convention(&context, CallConv::C, [Type::I32, Type::I32], [Type::I32]);

// The scheduled operand order is [index, args...], index on top
emitter.push(Type::I32);
emitter.push(Type::I32);
emitter.push(Type::U32);

let span = SourceSpan::default();
let num_slots = 5u32;
let base_elem_addr = 294912u32;
let type_tag = 3u32;
emitter.exec_indirect(num_slots, base_elem_addr, type_tag, &signature, span);

// The emulated stack holds exactly the call result
assert_eq!(emitter.stack_len(), 1);
assert_eq!(emitter.stack()[0], Type::I32);

let insts = block
.iter()
.map(|op| match op {
Op::Inst(inst) => inst.clone().into_inner(),
op => panic!("unexpected non-instruction op: {op:?}"),
})
.collect::<Vec<_>>();
assert_eq!(insts.len(), 14);
// Bounds check: duplicate the index and assert it is in bounds
assert_eq!(insts[0], masm::Instruction::Dup0);
assert!(
matches!(&insts[1], masm::Instruction::Push(masm::Immediate::Value(value)) if *value.inner() == num_slots.into()),
"expected push of the slot count, got {:?}",
&insts[1]
);
assert_eq!(insts[2], masm::Instruction::U32Lt);
assert!(
matches!(&insts[3], masm::Instruction::AssertWithError(masm::Immediate::Value(msg)) if msg.inner().contains("function table index out of bounds")),
"expected bounds-check assertion, got {:?}",
&insts[3]
);
// Rewrite the index to the slot's element address
assert!(
matches!(&insts[4], masm::Instruction::MulImm(masm::Immediate::Value(value)) if *value.inner() == Felt::new_unchecked(FunctionTableLayout::SLOT_SIZE_ELEMENTS as u64)),
"expected multiply by the slot size, got {:?}",
&insts[4]
);
assert!(
matches!(&insts[5], masm::Instruction::AddImm(masm::Immediate::Value(value)) if *value.inner() == Felt::new_unchecked(base_elem_addr as u64)),
"expected add of the table base address, got {:?}",
&insts[5]
);
// Signature check: load the slot's tag and assert it matches the expected tag
assert_eq!(insts[6], masm::Instruction::Dup0);
assert!(
matches!(&insts[7], masm::Instruction::AddImm(masm::Immediate::Value(value)) if *value.inner() == Felt::new_unchecked(FunctionTableLayout::TYPE_TAG_OFFSET_ELEMENTS as u64)),
"expected add of the tag offset, got {:?}",
&insts[7]
);
assert_eq!(insts[8], masm::Instruction::MemLoad);
assert!(
matches!(&insts[9], masm::Instruction::Push(masm::Immediate::Value(value)) if *value.inner() == type_tag.into()),
"expected push of the expected signature tag, got {:?}",
&insts[9]
);
assert!(
matches!(&insts[10], masm::Instruction::AssertEqWithError(masm::Immediate::Value(msg)) if msg.inner().contains("callee signature mismatch")),
"expected signature-check assertion, got {:?}",
&insts[10]
);
// Frame-traced dynexec, which itself pops the slot address
assert!(matches!(&insts[11], masm::Instruction::EmitImm(_)));
assert_eq!(insts[12], masm::Instruction::DynExec);
assert!(matches!(&insts[13], masm::Instruction::EmitImm(_)));
}

#[test]
fn clk_emits_vm_instruction_and_pushes_felt() {
let mut block = Vec::default();
Expand Down
Loading
Loading