Skip to content

Debugger#43

Merged
someone235 merged 48 commits intokaspanet:masterfrom
Manyfestation:debugger
Mar 5, 2026
Merged

Debugger#43
someone235 merged 48 commits intokaspanet:masterfrom
Manyfestation:debugger

Conversation

@Manyfestation
Copy link
Collaborator

@Manyfestation Manyfestation commented Feb 26, 2026

AI usage disclosure

This code was written by a coding agent. I mentored the implementation and reviewed the code.

Source-Level Debugger

Adds source-level stepping debugger infrastructure for SilverScript.

The main challenges:

  • Not every statement emits bytecode (declarations, assignments, tuple destructuring, etc.).
  • Stepping over/into/out of inline calls without a runtime call stack.
  • Showing local vars when locals are purely a compile-time abstraction.

We solve this by adding an optional DebugInfo artifact to compilation, and a DebugSession that consumes it to drive stepping and compute variable values via a shadow Script engine.

The CLI (sil-debug) is a thin demo client on top of DebugInfo + DebugSession.

End-to-End Flow

  1. Parse to AST (builds on the spanned AST from spanned ast, spanned error, domain diagnostics, initial extensions, more granular ast nodes #40).
  2. Compile to bytecode as usual, while a recorder collects debug events.
  3. Merge recordings into a single DebugInfo output (opt-in via CompileOptions::record_debug_infos).
  4. Run/debug via DebugSession (Kaspa's TxScriptEngine + DebugInfo).

Changes Overview

DebugInfo: Optional Compilation Output

When debug recording is enabled, compilation emits a DebugInfo artifact containing:

  • DebugMapping - the ordered source-step timeline
  • DebugVariableUpdate - variable updates attached to that timeline

Each DebugMapping describes one source-level moment:

  • span - the source region to highlight
  • bytecode_start / bytecode_end - where it maps in the emitted script (can be zero-width)
  • kind - what type of mapping:
    • Statement - emitted bytecode
    • Virtual - zero bytecode (declarations, assignments, etc.)
    • InlineCallEnter / InlineCallExit - structural markers around inline calls
    • Synthetic - compiler-only regions with no user source (dispatcher logic, etc.)
  • Step metadata for deterministic stepping and scoping:
    • sequence - global canonical step ordering (the stable step id)
    • call_depth - inline nesting depth (0 = entrypoint, 1+ inside inline calls)
    • frame_id - unique id per inline invocation (isolates locals)

Since multiple source steps can share the same bytecode offset (virtual steps), the debugger steps by sequence, not by offset.

Variable Updates

Getting vars to work at each source step requires bridging compile-time semantics to runtime evaluation.

The approach that stayed clean after a few iterations:

  1. During compilation - record variable updates as expressions.
  2. During debugging - compile those expressions and evaluate them on a shadow TxScriptEngine, seeded with the current call context.

DebugVariableUpdate records: name, type_name, expr (pre-resolved for debugger eval), sequence, and frame_id.

This keeps the debugger honest - real compiler, real engine - and sets us up for future context-aware features (covenant values, TX-aware opcodes, etc.).

Recording During Compilation

All recording lives in compiler/debug_recording.rs, split into two layers:

FunctionDebugRecorder (per-function) - assigns local sequence ids, records a mapping per statement (Statement or Virtual), and attaches variable updates at statement boundaries.

DebugSink (global) - merges per-function recordings into a single DebugInfo, applies bytecode offsets during final script placement, and remaps sequences so they're globally unique and properly ordered.

Inline Calls

Inline calls compile by inlining callee bytecode into the caller. We make stepping work by:

  • Emitting InlineCallEnter / InlineCallExit markers at the call site
  • Compiling the inlined body with incremented call_depth
  • Assigning a fresh frame_id per invocation (so locals don't leak across calls)
  • Merging recorded events back into the caller recorder (with sequence remapping)

DebugSession (debugger/session/)

DebugSession wraps Kaspa's TxScriptEngine + DebugInfo and exposes source-level stepping and inspection.

It builds a step list sorted by sequence. Stepping is depth-aware:

  • step_into() - next mapping regardless of depth
  • step_over() - next mapping where call_depth <= current
  • step_out() - next mapping where call_depth < current

Virtual steps advance the visible source position without consuming opcodes.

Variable Listing

Variable visibility at any step is based on (sequence, frame_id):

  • Entrypoint params - read from the main engine's stack via recorded indices
  • Constructor constants - included and de-duped
  • Locals - pick the latest variable update in the current frame, evaluate via shadow engine

The shadow engine compiles the recorded expression with compile_debug_expr, runs it on a fresh engine seeded with entrypoint params from the main engine, and decodes the result. If anything fails, the variable stays visible but shows Unknown with a reason.

Debugger CLI (debugger/cli/)

The CLI REPL (sil-debug) is a thin wrapper over DebugSession — just enough to test the infrastructure interactively.

Crate Layout

  • silverscript-lang/src/compiler/debug_recording.rs — recording layer (FunctionDebugRecorder + DebugSink)
  • silverscript-lang/src/debug_info.rsDebugInfo data model (DebugMapping, DebugVariableUpdate, etc.)
  • debugger/session/DebugSession runtime (stepping, variable inspection)
  • debugger/cli/sil-debug CLI REPL

Tests

  • debugger/session/tests/debug_session_tests.rs — stepping, virtual steps, inline step into/over/out, breakpoints, expression evaluation
  • debugger/cli/tests/cli_tests.rs — CLI smoke test
cargo run -p cli-debugger -- silverscript-lang/tests/examples/if_statement.sil \
  --function hello \
  --ctor-arg 3 --ctor-arg 10 \
  --arg 5 --arg 5
$ cargo run -p cli-debugger -- silverscript-lang/tests/examples/if_statement.sil \
  --function hello \
  --ctor-arg 3 --ctor-arg 10 \
  --arg 5 --arg 5
    
     Running `target/debug/cli-debugger silverscript-lang/tests/examples/if_statement.sil --function hello --ctor-arg 3 --ctor-arg 10 --arg 5 --arg 5`


     1 | pragma silverscript ^0.1.0;
     2 | 
     3 | contract IfStatement(int x, int y) {
     4 |     entrypoint function hello(int a, int b) {
→    5 |         int d = a + b;
     6 |         d = d - a;
     7 |         if (d == x - 2) {
     8 |             int c = d + b;
     9 |             d = a + c;
    10 |             require(c > d);
    11 |         } else {

(sdb) n #### Enter "n" for "next statement"

     1 | pragma silverscript ^0.1.0;
     2 | 
     3 | contract IfStatement(int x, int y) {
     4 |     entrypoint function hello(int a, int b) {
     5 |         int d = a + b;
→    6 |         d = d - a;
     7 |         if (d == x - 2) {
     8 |             int c = d + b;
     9 |             d = a + c;
    10 |             require(c > d);
    11 |         } else {
    12 |             require(d == a);
a (int) = 5
b (int) = 5
d (int) = 10
x (const) (int) = 3
y (const) (int) = 10

- Rename debug.rs → debug_info.rs in silverscript-lang (types-only module)
- Create debugger-session library crate (session runtime, presentation)
- Create cli-debugger binary crate (renamed from sil-debug)
- Migrate debug session tests and CLI smoke test to new crates
- Update all imports across workspace
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a comprehensive source-level debugger infrastructure for SilverScript, enabling developers to step through contract execution, inspect variables, and set breakpoints.

Changes:

  • Adds DebugInfo artifact system to compilation with source mappings, variable tracking, and inline call metadata
  • Implements DebugSession runtime for stepping (into/over/out), breakpoints, and variable inspection via shadow VM evaluation
  • Introduces CLI debugger (sil-debug) as a REPL-based interface

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated no comments.

Show a summary per file
File Description
silverscript-lang/src/debug_info.rs Core debug metadata structures (mappings, variable updates, constants)
silverscript-lang/src/compiler/debug_recording.rs Debug event recording during compilation with inline call support
silverscript-lang/src/compiler.rs Integration of debug recording into compilation pipeline
debugger/session/src/session.rs Debug session runtime with stepping and variable inspection
debugger/session/src/presentation.rs Value formatting and source context presentation
debugger/cli/src/main.rs CLI REPL interface for interactive debugging
Cargo.toml / Cargo.lock Workspace and dependency updates
Comments suppressed due to low confidence (3)

debugger/session/src/presentation.rs:129

  • The decode_i64 function is duplicated between session.rs and presentation.rs. This code duplication should be eliminated by extracting the function into a shared utility module or making one implementation call the other.
fn decode_i64(bytes: &[u8]) -> Result<i64, String> {
    if bytes.is_empty() {
        return Ok(0);
    }
    if bytes.len() > 8 {
        return Err("numeric value is longer than 8 bytes".to_string());
    }
    let msb = bytes[bytes.len() - 1];
    let sign = 1 - 2 * ((msb >> 7) as i64);
    let first_byte = (msb & 0x7f) as i64;
    let mut value = first_byte;
    for byte in bytes[..bytes.len() - 1].iter().rev() {
        value = (value << 8) + (*byte as i64);
    }
    Ok(value * sign)
}

debugger/session/src/session.rs:890

  • Using Box::leak in tests causes intentional memory leaks. While this is acceptable for tests that run once and exit, consider documenting why this approach is necessary (likely for lifetime constraints with 'static references), or alternatively use an approach that doesn't permanently leak memory, such as scoped threads or once_cell.
        let sig_cache = Box::leak(Box::new(Cache::new(10_000)));
        let reused_values: &'static SigHashReusedValuesUnsync = Box::leak(Box::new(SigHashReusedValuesUnsync::new()));

silverscript-lang/src/compiler.rs:1834

  • The inline call parameter handling creates synthetic __arg_ variables that are inserted into both the callee's environment and the caller's environment. If the function name itself contains an underscore or if parameter names could collide with this naming pattern, there could be unexpected behavior. Consider using a more unique namespace for synthetic variables (e.g., __internal_arg_{name}_{index}_{unique_id}) or verifying that user-defined names cannot start with __arg_.
    for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() {
        let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?;
        let temp_name = format!("__arg_{name}_{index}");
        let param_type_name = type_name_from_ref(&param.type_ref);
        env.insert(temp_name.clone(), resolved.clone());
        types.insert(temp_name.clone(), param_type_name.clone());
        env.insert(param.name.clone(), Expr::new(ExprKind::Identifier(temp_name.clone()), span::Span::default()));
        caller_env.insert(temp_name.clone(), resolved);
        caller_types.insert(temp_name, param_type_name);
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Manyfestation and others added 3 commits February 28, 2026 15:14
Bad merge resolution had reverted PR kaspanet#44 changes:
- Restore serialize_i64 instead of to_le_bytes+OpBin2Num for int fields
- Restore Ok(9) field chunk size (was incorrectly Ok(10))
- Remove spurious OpBin2Num injection in validate_output_state
- Update test expectations to match corrected encoding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove constructor/constant names from env when they collide with
  function param names (prioritizing function parameters).
- Propagate caller's __arg_ bindings and params map into inline calls,
  allowing nested synthetic argument chains to resolve correctly.
- Extract magic string into pub const SYNTHETIC_ARG_PREFIX.
- Add regression tests for both fixes.
Copy link
Contributor

@someone235 someone235 left a comment

Choose a reason for hiding this comment

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

Approved. Waiting for #46 to be merged before merging.

@someone235 someone235 merged commit c36fa26 into kaspanet:master Mar 5, 2026
4 checks passed
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