Skip to content

feat: enable trace based println#1077

Open
mooori wants to merge 4 commits intonextfrom
mooori/print-via-trace
Open

feat: enable trace based println#1077
mooori wants to merge 4 commits intonextfrom
mooori/print-via-trace

Conversation

@mooori
Copy link
Copy Markdown
Contributor

@mooori mooori commented Apr 13, 2026

Context: #1024

Adds trace-based println support across the compiler pipeline:

  • Introduces a new hir PrintLn op and a MASM TRACE_PRINT_LN event
  • Adds lowering for PrintLn that manages addr, size of the string on the operand stack, such that evaluator and debug executor can pick it up.
  • Adds WASM debug intrinsics and the SDK println(&str) helper to generate that op from Rust.
  • Extends the evaluator/debug path to read bytes from memory, decode UTF-8, and collect printed
    lines.

Companion PR in miden-debug: 54

TODO

  • Remove patched miden-debug GH dependency once changes in miden-debug were released

@mooori mooori force-pushed the mooori/print-via-trace branch from a85899f to a3d8dd1 Compare April 15, 2026 14:15
Comment on lines +89 to +92
// There is no write, but without `MemoryEffect::Write`, the `PrintLn` is not lowered to masm.
// With `Read` only, `PrintLn` probably gets eliminated since it is not producing a result.
#[operand]
#[effects(MemoryEffect(MemoryEffect::Read, MemoryEffect::Write))]
Copy link
Copy Markdown
Contributor Author

@mooori mooori Apr 15, 2026

Choose a reason for hiding this comment

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

Is there another way to prevent PrintLn getting eliminated? Using a fake Write is not ideal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Create a new effect? I mean this operation produces a side effect that is handled in the debug executor (prints/logs/whatever).

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.

I would avoid adding a new effect for just this, and instead remove the effect annotation entirely - this will force analysis to be conservative with respect to this op (i.e. it cannot be presumed to be side-ffect free, and thus removable - and ops with effects cannot be reordered past it).

The effect mechanism is specifically there to opt-in operations to optimization, by declaring that the specific effects those ops have, and thus allowing the optimizer to make decisions about scheduling/reordering/etc. based on the details of those effects. In cases where we explicitly do not want the optimizer to be optimizing a specific op (and especially removing it due to perceived lack of use), it is best to avoid annotating effects at all.

Comment thread eval/src/eval.rs
@mooori mooori force-pushed the mooori/print-via-trace branch from a3d8dd1 to 7965460 Compare April 15, 2026 14:40
@mooori mooori marked this pull request as ready for review April 15, 2026 15:34
@mooori mooori requested review from bitwalker and greenhat April 15, 2026 15:34
Copy link
Copy Markdown
Contributor

@greenhat greenhat left a comment

Choose a reason for hiding this comment

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

Looking good! Please see my notes.

Comment on lines +89 to +92
// There is no write, but without `MemoryEffect::Write`, the `PrintLn` is not lowered to masm.
// With `Read` only, `PrintLn` probably gets eliminated since it is not producing a result.
#[operand]
#[effects(MemoryEffect(MemoryEffect::Read, MemoryEffect::Write))]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Create a new effect? I mean this operation produces a side effect that is handled in the debug executor (prints/logs/whatever).

dialect = HirDialect,
implements(MemoryEffectOpInterface, OpPrinter)
)]
pub struct PrintLn {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be crazy to think that we should consider a new dialect for this operation?

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.

My thought was to place it in the debug dialect that @djolertrk created/was going to create - but since that isn't present in next, I think it is fine to put it in hir initially, and we can move it later.

@mooori mooori requested a review from djolertrk April 20, 2026 16:06

/// Emit a trace marker for `hir.println`, leaving the operands intact until after the trace.
pub fn println(&mut self, span: SourceSpan) {
// Leave them on the stack so debug executor can read them.
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.

I think this comment is misleading - when we emit ops, the input operands are semantically consumed by the operation we emit as MASM - but in cases like this, where there is no actual op that consumes the operands (in fact, trace is not an op at all, but a decorator), it is the drop drop that acts as the "real" operation consuming the operands.

So it isn't that we are leaving them on the stack - we aren't - we are consuming the operands via drop drop below. The reason why we don't pop the operands here like we do elsewhere, is because we want to let the call to dropn(2) handle it.

Comment thread codegen/masm/src/events.rs
Comment thread sdk/sdk/src/debug.rs
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.

I would suggest we also define the println! macro, as follows:

#[macro_export]
macro_rules! println {
    ($message:literal) => {{
        $crate::debug::println($message);
    }};

    ($format:literal, $($arg:tt),+) => {{
        let message = ::alloc::format!($format, $($arg),*);
        $crate::debug::println(&message);
    }}
}

Then in downstream crates of the SDK, we can do stuff like:

use miden_sdk::println;

println!("hello world");

# or

println!("{greeting} {}", greeting = "hello", "world");

Comment thread sdk/sdk/src/lib.rs
unimplemented!("debug intrinsics are only available when targeting the Miden VM")
}

/// Prints the string pointed to by `ptr` in the debug executor.
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.

Functions with multiple cfg-dependent definitions should be defined next to each other when possible.

Copy link
Copy Markdown
Collaborator

@bitwalker bitwalker left a comment

Choose a reason for hiding this comment

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

Looks good overall! I left a few notes on changes I'd like to make, but once this is ready, we'll merge it after miden-debug ships support for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants