Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
## v0.30.0 (Unreleased)

#### Features

- Added `trace`, `trace.CONST`, and `trace.event("...")` assembly as syntactic sugar for emitting optional read-only trace events ([#3478](https://github.com/0xMiden/miden-vm/pull/3478)).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also adds Trace and TraceImm to the public exhaustive miden_assembly_syntax::ast::Instruction enum, so downstream exhaustive matches stop compiling. cargo semver-checks check-release -p miden-assembly-syntax --baseline-version 0.29.0 --release-type minor reports enum_variant_added for both variants.

Could we mark this entry [BREAKING]?


## v0.29.0 (2026-08-04)

#### Changes
Expand Down
15 changes: 8 additions & 7 deletions core/src/events/sys_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,13 +413,14 @@ pub enum SystemEvent {
// --------------------------------------------------------------------------------------------
/// Signals an optional, read-only trace event to the host.
///
/// When `emit` observes this system event ID at stack position 0, the VM forwards the user
/// trace event ID at stack position 1 to the host's trace handler. This is typically emitted
/// as `push.<user_trace_id> push.<sys::trace_event> emit`. Trace handlers can observe
/// the processor state, but cannot mutate VM state or the advice provider. If no handler is
/// registered for the user trace event ID, the event is a no-op.
///
/// Hosts are expected to not raise an error if they encounter a `user_trace_id` for which no
/// Assembly programs emit trace events with `trace`, `trace.CONST`, or `trace.event("...")`.
/// When the underlying `emit` observes this system event ID at stack position 0, the VM
/// forwards the user trace event ID at stack position 1 to the host's trace handler. The
/// immediate form lowers to `push.<user_trace_id> push.<sys::trace_event> emit drop drop`.
/// Trace handlers can observe the processor state, but cannot mutate VM state or the advice
/// provider. If no handler is registered for the user trace event ID, the event is a no-op.
///
/// Hosts are expected not to raise an error if they encounter a `user_trace_id` for which no
/// trace handler is registered.
///
/// Inputs:
Expand Down
3 changes: 2 additions & 1 deletion core/src/operations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ pub enum Operation {
/// assembly (`emit.event("...")` or `emit.CONST` where `CONST=event("...")`).
/// - System events are identified by reserved [`SystemEvent`](crate::events::SystemEvent) IDs.
/// Most are handled by the VM; `SystemEvent::TraceEvent` triggers the host's optional
/// read-only trace handler for the trace event id at stack position 1.
/// read-only trace handler for the trace event id at stack position 1. Assembly exposes this
/// through `trace`, `trace.CONST`, and `trace.event("...")`.
/// - Any non system event ID is forwarded to the host's regular event handler.
///
/// This operation does not change the state of the user stack aside from reading the value.
Expand Down
4 changes: 4 additions & 0 deletions crates/assembly-syntax/src/ast/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ pub enum Instruction {
// ----- event decorators --------------------------------------------------------------------
Emit,
EmitImm(ImmFelt),

// ----- traces (read-only events) -----------------------------------------------------------
Trace,
TraceImm(ImmFelt),
}

impl Instruction {
Expand Down
6 changes: 6 additions & 0 deletions crates/assembly-syntax/src/ast/instruction/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ impl PrettyPrint for Instruction {
Self::Emit => const_text("emit"),
Self::EmitImm(value) => inst_with_felt_imm("emit", value),

// ----- traces (read-only events) ----------------------------------------------------
Self::Trace => const_text("trace"),
// Printing `TraceImm` such that it is consistent with `EmitImm`, even though this
// does not round-trip. `trace.<FELT_IMM>` is invalid.
Self::TraceImm(value) => inst_with_felt_imm("trace", value),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting a parsed trace.event(...) now writes trace.<felt>, but this PR also rejects numeric trace immediates.

I reproduced this with trace.event("test::roundtrip"): the formatted module fails to parse with invalid instruction trace or malformed operands.

Could this print a valid equivalent such as push.<felt> trace drop, and cover it with a parse-format-parse test?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of printing as trace.<felt> was to be in line with emit.<felt>, see the comment displayed in the code snippet above. In 23fb57b I've changed it to print as push.<felt> trace drop and added a test to verify that this can be parsed back.

Should we open an issue to change the printing of EmitImm to push.<felt> emit drop so that it can be parsed back too? Though I assume that would be a breaking change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Handled by the early return for !has_textual_representation()
Self::DebugVar(_) => unreachable!(),
}
Expand Down
8 changes: 4 additions & 4 deletions crates/assembly-syntax/src/ast/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ where
| MemStoreImm(imm)
| MemStoreWBeImm(imm)
| MemStoreWLeImm(imm) => visitor.visit_immediate_u32(imm),
EmitImm(imm) => visitor.visit_immediate_felt(imm),
EmitImm(imm) | TraceImm(imm) => visitor.visit_immediate_felt(imm),
SysEvent(sys_event) => visitor.visit_system_event(Span::new(span, sys_event)),
Exec(target) => visitor.visit_exec(target),
Call(target) => visitor.visit_call(target),
Expand Down Expand Up @@ -505,7 +505,7 @@ where
| MemStoreWLe | MemStream | AdvPipe | AdvPush | AdvPushW | AdvLoadW | Hash | HMerge
| HPerm | MTreeGet | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec
| DynCall | DebugVar(_) | HornerBase | HornerExt | CryptoStream | EvalCircuit
| LogDeferred | Emit => ControlFlow::Continue(()),
| LogDeferred | Emit | Trace => ControlFlow::Continue(()),
}
}

Expand Down Expand Up @@ -1040,7 +1040,7 @@ where
| MemStoreImm(imm)
| MemStoreWBeImm(imm)
| MemStoreWLeImm(imm) => visitor.visit_mut_immediate_u32(imm),
EmitImm(imm) => visitor.visit_mut_immediate_felt(imm),
EmitImm(imm) | TraceImm(imm) => visitor.visit_mut_immediate_felt(imm),
SysEvent(sys_event) => visitor.visit_mut_system_event(Span::new(span, sys_event)),
Exec(target) => visitor.visit_mut_exec(target),
Call(target) => visitor.visit_mut_call(target),
Expand Down Expand Up @@ -1069,7 +1069,7 @@ where
| MemStoreWLe | MemStream | AdvPipe | AdvPush | AdvPushW | AdvLoadW | Hash | HMerge
| HPerm | MTreeGet | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec
| DynCall | DebugVar(_) | HornerBase | HornerExt | EvalCircuit | CryptoStream
| LogDeferred | Emit => ControlFlow::Continue(()),
| LogDeferred | Emit | Trace => ControlFlow::Continue(()),
}
}

Expand Down
50 changes: 50 additions & 0 deletions crates/assembly-syntax/src/parser/cst/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ static PRIMITIVE_SPECS: &[PrimitiveSpec] = &[
spelling: "emit",
build: || Instruction::Emit,
},
PrimitiveSpec {
spelling: "trace",
build: || Instruction::Trace,
},
PrimitiveSpec {
spelling: "eval_circuit",
build: || Instruction::EvalCircuit,
Expand Down Expand Up @@ -1208,6 +1212,7 @@ fn lower_extended_instruction(
},

ExtendedInstructionKind::Emit => lower_emit_instruction(context, span, &tokens),
ExtendedInstructionKind::Trace => lower_trace_instruction(context, span, &tokens),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trace duplicates the full emit extended parser below, including constant lookup and event-name hashing.

THe instruction spec could carry the immediate constructor and use one lowering helper keyed by spec.keyword instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THe instruction spec could carry the immediate constructor and use one lowering helper keyed by spec.keyword instead.

Not sure if I understand correctly, but Wouldn't that require changing ExtendedInstructionSpec?

For now I removed the duplication by lowering both through a new shared function: lower_event_imm_instruction.

ExtendedInstructionKind::ErrorCode(build) => {
lower_error_code_instruction(context, span, &tokens, spec.keyword, build)
},
Expand All @@ -1224,6 +1229,7 @@ enum ExtendedInstructionKind {
Push,
Invocation(fn(ast::InvocationTarget) -> Instruction),
Emit,
Trace,
ErrorCode(fn(ast::ErrorMsg) -> Instruction),
}

Expand Down Expand Up @@ -1252,6 +1258,10 @@ static EXTENDED_INSTRUCTION_SPECS: &[ExtendedInstructionSpec] = &[
keyword: "emit",
kind: ExtendedInstructionKind::Emit,
},
ExtendedInstructionSpec {
keyword: "trace",
kind: ExtendedInstructionKind::Trace,
},
ExtendedInstructionSpec {
keyword: "assert",
kind: ExtendedInstructionKind::ErrorCode(Instruction::AssertWithError),
Expand Down Expand Up @@ -1383,6 +1393,46 @@ fn lower_emit_instruction(
}
}

/// Lowers `trace.<const>` and `trace.event("name")`.
fn lower_trace_instruction(
context: &mut LoweringContext<'_>,
instruction_span: SourceSpan,
tokens: &[SyntaxToken],
) -> Result<Option<Vec<ast::Op>>, ParsingError> {
if tokens.len() < 3
|| tokens[0].kind() != SyntaxKind::Ident
|| tokens[0].text() != "trace"
|| tokens[1].kind() != SyntaxKind::Dot
{
return Ok(None);
}

match &tokens[2..] {
[name] if name.kind() == SyntaxKind::Ident && name.text() != "event" => {
let name = context.lower_constant_ident_token(name)?;
Ok(Some(vec![inst_op(
instruction_span,
Instruction::TraceImm(Immediate::Constant(name)),
)]))
},
[event, lparen, string, rparen]
if event.kind() == SyntaxKind::Ident
&& event.text() == "event"
&& lparen.kind() == SyntaxKind::LParen
&& matches!(string.kind(), SyntaxKind::QuotedString | SyntaxKind::QuotedIdent)
&& rparen.kind() == SyntaxKind::RParen =>
{
let value = unquote_string_token(string, context.parse().span_for_token(string))?;
let event_id = EventId::from_name(value.as_ref()).as_felt();
Ok(Some(vec![inst_op(
instruction_span,
Instruction::TraceImm(Immediate::Value(Span::new(instruction_span, event_id))),
)]))
},
_ => Ok(None),
}
}

/// Lowers `.err=` forms for assertion-like instructions.
fn lower_error_code_instruction(
context: &mut LoweringContext<'_>,
Expand Down
3 changes: 3 additions & 0 deletions crates/assembly-syntax/src/parser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ begin
adv.insert_hdword
adv.push_mapvaln
emit
trace
mem_load
u32div
add.1
Expand Down Expand Up @@ -751,6 +752,8 @@ begin
procref.foo::bar
emit.EVENT_ID
emit.event(\"abc\")
trace.EVENT_ID
trace.event(\"abc\")
assert.err=\"oops\"
u32assert.err=ERR_CODE
end
Expand Down
12 changes: 9 additions & 3 deletions crates/assembly-syntax/src/sema/passes/const_eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,20 +143,23 @@ where
}
fn visit_mut_inst(&mut self, inst: &mut Span<Instruction>) -> ControlFlow<()> {
use crate::ast::Instruction;
if let Instruction::EmitImm(Immediate::Constant(name)) = &**inst {
if let Instruction::EmitImm(Immediate::Constant(name))
| Instruction::TraceImm(Immediate::Constant(name)) = &**inst
{
let span = name.span();
match self.env.get(name) {
Ok(Some(
CachedConstantValue::Miss(ConstantExpr::Hash(HashKind::Event, _))
| CachedConstantValue::Hit(ConstantValue::Hash(HashKind::Event, _)),
)) => {
// CHANGE: allow `emit.EVENT` when `EVENT` was defined via
// CHANGE: allow `emit.EVENT` / `trace.EVENT` when `EVENT` was defined via
// const.EVENT = event("...")
// NOTE: This function only validates the kind; the actual resolution to a Felt
// happens below in `visit_mut_immediate_felt` just like other Felt immediates.
// Enabled syntax:
// const.EVT = event("...")
// emit.EVT
// trace.EVT
},
Ok(Some(CachedConstantValue::Miss(expr @ ConstantExpr::Var(_)))) => {
// A reference to another constant was used, try to evaluate the expression
Expand All @@ -181,12 +184,15 @@ where
}
},
Ok(Some(_)) => {
// CHANGE: disallow `emit.CONST` unless CONST is defined via `event("...")`.
// CHANGE: disallow `emit.CONST` / `trace.CONST` unless CONST is defined via
// `event("...")`.
// Examples which now error:
// const.BAD = 42
// emit.BAD
// trace.BAD
// const.W = word("foo")
// emit.W
// trace.W
self.errors.push(
ConstEvalError::InvalidConstant {
span,
Expand Down
23 changes: 23 additions & 0 deletions crates/assembly/src/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use miden_assembly_syntax::{
};
use miden_core::{
Felt, WORD_SIZE, ZERO,
events::SystemEvent,
operations::{AssemblyOp, Operation},
};

Expand Down Expand Up @@ -601,6 +602,28 @@ impl Assembler {
let event_id_value = event_id.expect_value();
block_builder.push_ops([Push(event_id_value), Emit, Drop]);
},

// trace: reads the trace ID from the top of the stack and expands to
// `push.<sys::trace_event>, emit, drop`, leaving the stack unchanged.
Instruction::Trace => {
// The trace ID is already on the stack. In addition we need the system event which
// triggers traces.
let sys_event_id = SystemEvent::TraceEvent.event_id().as_felt();
block_builder.push_ops([Push(sys_event_id), Emit, Drop]);
},
// trace.<id>: expands to
// `push.<id>, push.<sys::trace_event>, emit, drop, drop`, leaving the stack unchanged.
Instruction::TraceImm(trace_id) => {
let trace_id_value = trace_id.expect_value();
let sys_event_id = SystemEvent::TraceEvent.event_id().as_felt();
block_builder.push_ops([
Push(trace_id_value),
Push(sys_event_id),
Emit,
Drop,
Drop,
]);
},
}

Ok(None)
Expand Down
Loading
Loading