Skip to content

Implement trace-based printing infrastructure #1024

@bitwalker

Description

@bitwalker

What should be done?

The Miden VM does not directly provide any stdio functionality, but it does provide certain escape hatches for hosts to provide custom behavior for MASM programs being executed. The primary ones are emit (for events) and trace (for discardable instrumentation/events). The Host callbacks for these can access the processor state, and in the case of emit, also produce side effects via the advice provider.

The downside to emit, is that it is an actual operation in MAST, which means that when stripping debug info/instrumentation, emit would remain in the program. The trace "instruction" however, is actually a decorator, not an operation, and so it is stripped in those scenarios. This makes it particularly ideal for debug and instrumentation use cases where it is desirable to strip the instrumentation in release builds.

We want to implement the ability to print Rust strings during execution using trace events.

How should it be done?

The implementation I've sketched out consists of implementing a new compiler intrinsic (and corresponding primop in the hir dialect) that prints a Rust string reference (i.e. &str, a fat pointer), by emitting trace.PRINT_TRACE_ID while the string reference is on top of the operand stack.

The intrinsic signature in Rust would be unsafe extern "C" fn __println(ptr: *const u8, len: usize), which would translate to Wasm as func $println (param i32 i32), where the first parameter is the pointer to the start of the string, the second is the length. This would be converted to the primop, i.e. hir.println, by the frontend.

We'd surface the low-level plumbing for this in the SDK via:

pub fn println(s:  &str) {
    unsafe {
        let bytes = s.as_bytes();
        __println(bytes.as_ptr(), bytes.len());
    }
}

A related task to this one is providing our own println! macro that formats and prints a string using the println intrinsic.

The host side of all this would live in miden-debug, by handling PRINT_TRACE_ID via the on_trace callback as follows:

  1. Extract the string reference components from the top of the operand stack, accessing it via ProcessState
  2. Validate that the string reference meets certain criteria (i.e. the memory address is in the addressable range, the size of the string is no larger than some soft limit)
  3. Read the raw bytes from linear memory via ProcessState
  4. Convert the bytes to a Rust string using core::str::from_utf8, and then print them to stdout

When is this task done?

With the plumbing above implemented, there are two objectives here:

  1. Calling println from Rust prints the string correctly to stdout when executed under the debug executor
  2. We want to support printing panic messages using println as follows, and validate it with a test:
#[panic_handler]
fn my_panic(info: &core::panic::PanicInfo) -> ! {
    if let Some(message) = info.message().as_str() {
        println(message);
    }
    core::arch::wasm32::unreachable()
}

In the future we may expand on this to format PanicInfo with a non-static message, but we'll punt on that initially.

Additional context

No response

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions