From eca34e57d47594d371be0294365a4b28ed7ce670 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Thu, 24 Sep 2026 17:11:12 +0200 Subject: [PATCH 1/6] feat(variables): Introduce new schema Introduce a new schema for sending extracted variables. The new schema is compatible with the old one, but provides ways to send more data about variables, including type and kind information. In the future, we can expand the schema to allow multiple representations for variables, e.g. some values may be represented as both arrays and pointers. --- .../src/{interface.rs => interface/mod.rs} | 8 +- .../src/interface/variables.rs | 100 ++++++++++++++++++ .../src/symbolication/native.rs | 61 +++++------ crates/symbolicli/src/event.rs | 8 +- 4 files changed, 136 insertions(+), 41 deletions(-) rename crates/symbolicator-native/src/{interface.rs => interface/mod.rs} (99%) create mode 100644 crates/symbolicator-native/src/interface/variables.rs diff --git a/crates/symbolicator-native/src/interface.rs b/crates/symbolicator-native/src/interface/mod.rs similarity index 99% rename from crates/symbolicator-native/src/interface.rs rename to crates/symbolicator-native/src/interface/mod.rs index 62efe6e4f..7b5585712 100644 --- a/crates/symbolicator-native/src/interface.rs +++ b/crates/symbolicator-native/src/interface/mod.rs @@ -18,9 +18,13 @@ use symbolicator_sources::SourceConfig; use thiserror::Error; use url::Url; +pub(crate) use self::variables::Value as VariableValue; +pub use self::variables::{Variable, VariableKind, Variables}; use crate::memory::MemoryAccess; pub use crate::metrics::StacktraceOrigin; +mod variables; + #[derive(Debug, Clone)] /// A request for symbolication of multiple stack traces. pub struct SymbolicateStacktraces { @@ -356,8 +360,8 @@ pub struct RawFrame { /// Mapping of local variables and expression names that were available in this frame. /// /// Current format is heavily work in progress. - #[serde(skip_serializing_if = "Option::is_none")] - pub vars: Option>, + #[serde(default, skip_serializing_if = "Variables::is_empty")] + pub vars: Variables, /// Information about how the raw frame was created. #[serde(default, skip_serializing_if = "is_default_value")] diff --git a/crates/symbolicator-native/src/interface/variables.rs b/crates/symbolicator-native/src/interface/variables.rs new file mode 100644 index 000000000..dc43cb3b0 --- /dev/null +++ b/crates/symbolicator-native/src/interface/variables.rs @@ -0,0 +1,100 @@ +//! Contains types for representing variable values. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use symbolic::debuginfo::VariableKind as SymbolicVariableKind; + +/// Variables keyed by their names. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Variables(HashMap); + +impl Variables { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl FromIterator<(String, Variable)> for Variables { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// A variable extracted from a memory dump. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Variable { + kind: VariableKind, + #[serde(flatten)] + value: TypedValue, +} + +/// The variable's kind (e.g. local or parameter). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum VariableKind { + Local, + Parameter, +} + +/// A [`Value`] with a type. +/// +/// Constructed via [`Value::ty`]. +// TODO: I am not sure we need this as a separate type. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TypedValue { + #[serde(rename = "type")] + ty: String, + #[serde(flatten)] + value: Value, +} + +impl TypedValue { + /// Convert this [`TypedValue`] into a [`Variable`] with the given kind. + pub fn kind(self, kind: VariableKind) -> Variable { + Variable { kind, value: self } + } +} + +/// A value of a variable or field within a variable (e.g. list item, pointee, struct field). +/// +/// This struct allows for multiple possible representations. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Value { + /// A fully-formatted, source-specific representation of a variable value. + /// + /// This should typically be used for primitive types like integers, booleans, chars. + formatted: Option, +} + +impl Value { + /// Create a new empty [`Value`]. + pub fn new() -> Self { + Self::default() + } + + /// Provide a formatted value on the [`Value`]. + pub fn formatted(self, formatted: Option) -> Self { + #[expect(clippy::needless_update, reason = "we will add other representations")] + Self { formatted, ..self } + } + + /// Convert this [`Value`] to a [`TypedValue`] with the given type. + pub fn ty(self, ty: T) -> TypedValue + where + T: Into, + { + let ty = ty.into(); + TypedValue { ty, value: self } + } +} + +impl From for VariableKind { + fn from(value: SymbolicVariableKind) -> Self { + match value { + SymbolicVariableKind::Parameter => VariableKind::Parameter, + SymbolicVariableKind::Local => VariableKind::Local, + } + } +} diff --git a/crates/symbolicator-native/src/symbolication/native.rs b/crates/symbolicator-native/src/symbolication/native.rs index d180dabb0..7d41dde63 100644 --- a/crates/symbolicator-native/src/symbolication/native.rs +++ b/crates/symbolicator-native/src/symbolication/native.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use minidump::CpuContext; use symbolic::common::{CpuFamily, InstructionInfo, Language, split_path}; use symbolic::symcache::{ @@ -10,6 +8,7 @@ use symbolicator_service::utils::hex::HexValue; use crate::interface::{ AdjustInstructionAddr, FrameStatus, RawFrame, Registers, Signal, SymbolicatedFrame, + VariableValue, Variables, }; use crate::memory::{MemoryAccess, MemoryAccessExt}; @@ -69,10 +68,9 @@ pub fn symbolicate_native_frame( frame.filename.clone() }; - let mut vars = None; - if let Some(memory) = memory { - vars = do_extract_variables(&source_location, symcache, &frame.registers, memory); - } + let vars = memory.map_or_default(|memory| { + do_extract_variables(&source_location, symcache, &frame.registers, memory) + }); rv.push(SymbolicatedFrame { status: FrameStatus::Symbolicated, @@ -185,35 +183,28 @@ fn do_extract_variables<'data, 'cache>( cache: &SymCache<'cache>, registers: &Registers, memory: &dyn MemoryAccess, -) -> Option> { - let mut result = BTreeMap::new(); - - for variable in source_location.variables() { - let Some(name) = variable.name() else { - continue; - }; - - let mut ty = String::new(); - resolve_type_name(&mut ty, cache, variable.ty(), 0); - - let value = variable - .locations() - .find_map(|loc| resolve_variable_value(cache, registers, memory, loc, variable.ty())); - - // This doesn't handle name collisions currently. - result.insert( - name.to_owned(), - match value { - Some(value) => format!("{value} ({ty})").into(), - None => ty.into(), - }, - ); - } - - match result.is_empty() { - false => Some(result), - true => None, - } +) -> Variables { + source_location + .variables() + .flat_map(|variable| { + let name = variable.name()?; + + let mut ty = String::new(); + resolve_type_name(&mut ty, cache, variable.ty(), 0); + + let value = variable.locations().find_map(|loc| { + resolve_variable_value(cache, registers, memory, loc, variable.ty()) + }); + + Some(( + name.to_owned(), + VariableValue::new() + .formatted(value) + .ty(ty) + .kind(variable.kind().into()), + )) + }) + .collect() } fn resolve_variable_value( diff --git a/crates/symbolicli/src/event.rs b/crates/symbolicli/src/event.rs index 5f4cd00c6..cd4969081 100644 --- a/crates/symbolicli/src/event.rs +++ b/crates/symbolicli/src/event.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use anyhow::bail; @@ -9,7 +8,7 @@ use symbolicator_js::interface::{ }; use symbolicator_native::interface::{ AddrMode, CompleteObjectInfo, FrameTrust, RawFrame, RawStacktrace, Signal, StacktraceOrigin, - SymbolicateStacktraces, + SymbolicateStacktraces, Variables, }; use symbolicator_service::types::{FrameOrder, Platform, RawObjectInfo, Scope, ScrapingConfig}; use symbolicator_service::utils::hex::HexValue; @@ -268,7 +267,8 @@ struct Frame { post_context: Vec, module: Option, source_link: Option, - vars: Option>, + #[serde(default)] + variables: Variables, #[serde(default)] trust: FrameTrust, #[serde(default)] @@ -294,7 +294,7 @@ fn to_raw_frame(value: Frame) -> Option { context_line: value.context_line, post_context: value.post_context, source_link: value.source_link, - vars: value.vars, + vars: value.variables, trust: value.trust, registers: Default::default(), file_revision: None, From 9ba3d8310b81f87a5c272ba5355e8933cf6b9fb6 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 25 Sep 2026 10:23:23 +0200 Subject: [PATCH 2/6] a few simplifications --- .../src/interface/variables.rs | 18 ++++++++++++++---- .../src/symbolication/native.rs | 13 ++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/symbolicator-native/src/interface/variables.rs b/crates/symbolicator-native/src/interface/variables.rs index dc43cb3b0..1dd7f20dd 100644 --- a/crates/symbolicator-native/src/interface/variables.rs +++ b/crates/symbolicator-native/src/interface/variables.rs @@ -16,9 +16,15 @@ impl Variables { } } -impl FromIterator<(String, Variable)> for Variables { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) +impl FromIterator<(S, Variable)> for Variables +where + S: Into, +{ + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Self(iter.into_iter().map(|(k, v)| (k.into(), v)).collect()) } } @@ -52,7 +58,11 @@ pub(crate) struct TypedValue { impl TypedValue { /// Convert this [`TypedValue`] into a [`Variable`] with the given kind. - pub fn kind(self, kind: VariableKind) -> Variable { + pub fn kind(self, kind: K) -> Variable + where + K: Into, + { + let kind = kind.into(); Variable { kind, value: self } } } diff --git a/crates/symbolicator-native/src/symbolication/native.rs b/crates/symbolicator-native/src/symbolication/native.rs index 7d41dde63..92ef3be9d 100644 --- a/crates/symbolicator-native/src/symbolication/native.rs +++ b/crates/symbolicator-native/src/symbolication/native.rs @@ -196,13 +196,12 @@ fn do_extract_variables<'data, 'cache>( resolve_variable_value(cache, registers, memory, loc, variable.ty()) }); - Some(( - name.to_owned(), - VariableValue::new() - .formatted(value) - .ty(ty) - .kind(variable.kind().into()), - )) + let interface_variable = VariableValue::new() + .formatted(value) + .ty(ty) + .kind(variable.kind()); + + Some((name, interface_variable)) }) .collect() } From 120552808ed8bff7b2189e8e53ef2bcfd12e4f29 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 25 Sep 2026 12:05:04 +0200 Subject: [PATCH 3/6] code review --- crates/symbolicator-native/src/interface/variables.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/symbolicator-native/src/interface/variables.rs b/crates/symbolicator-native/src/interface/variables.rs index 1dd7f20dd..598ed3a44 100644 --- a/crates/symbolicator-native/src/interface/variables.rs +++ b/crates/symbolicator-native/src/interface/variables.rs @@ -47,7 +47,6 @@ pub enum VariableKind { /// A [`Value`] with a type. /// /// Constructed via [`Value::ty`]. -// TODO: I am not sure we need this as a separate type. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct TypedValue { #[serde(rename = "type")] @@ -69,7 +68,7 @@ impl TypedValue { /// A value of a variable or field within a variable (e.g. list item, pointee, struct field). /// -/// This struct allows for multiple possible representations. +/// This struct allows for multiple co-existing representations. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct Value { /// A fully-formatted, source-specific representation of a variable value. From c57996443d45fe777aa16a27875015fd51bd5410 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Thu, 8 Oct 2026 11:25:49 +0200 Subject: [PATCH 4/6] formatted -> value --- crates/symbolicator-native/src/interface/variables.rs | 10 ++++++---- crates/symbolicator-native/src/symbolication/native.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/symbolicator-native/src/interface/variables.rs b/crates/symbolicator-native/src/interface/variables.rs index 598ed3a44..ccd47bad6 100644 --- a/crates/symbolicator-native/src/interface/variables.rs +++ b/crates/symbolicator-native/src/interface/variables.rs @@ -74,7 +74,7 @@ pub(crate) struct Value { /// A fully-formatted, source-specific representation of a variable value. /// /// This should typically be used for primitive types like integers, booleans, chars. - formatted: Option, + value: Option, } impl Value { @@ -83,10 +83,12 @@ impl Value { Self::default() } - /// Provide a formatted value on the [`Value`]. - pub fn formatted(self, formatted: Option) -> Self { + /// Provide a fully-formatted, source-specific representation of the [`Value`]. + /// + /// This should typically be used for primitive types like integers, booleans, and chars. + pub fn value(self, value: Option) -> Self { #[expect(clippy::needless_update, reason = "we will add other representations")] - Self { formatted, ..self } + Self { value, ..self } } /// Convert this [`Value`] to a [`TypedValue`] with the given type. diff --git a/crates/symbolicator-native/src/symbolication/native.rs b/crates/symbolicator-native/src/symbolication/native.rs index 92ef3be9d..74e5b7871 100644 --- a/crates/symbolicator-native/src/symbolication/native.rs +++ b/crates/symbolicator-native/src/symbolication/native.rs @@ -197,7 +197,7 @@ fn do_extract_variables<'data, 'cache>( }); let interface_variable = VariableValue::new() - .formatted(value) + .value(value) .ty(ty) .kind(variable.kind()); From 1beb1fe39e3e8f75f08a977520c5f33c1c4e948a Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 13 Oct 2026 15:03:15 +0200 Subject: [PATCH 5/6] variables -> vars --- crates/symbolicli/src/event.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/symbolicli/src/event.rs b/crates/symbolicli/src/event.rs index cd4969081..84479c865 100644 --- a/crates/symbolicli/src/event.rs +++ b/crates/symbolicli/src/event.rs @@ -268,7 +268,7 @@ struct Frame { module: Option, source_link: Option, #[serde(default)] - variables: Variables, + vars: Variables, #[serde(default)] trust: FrameTrust, #[serde(default)] @@ -294,7 +294,7 @@ fn to_raw_frame(value: Frame) -> Option { context_line: value.context_line, post_context: value.post_context, source_link: value.source_link, - vars: value.variables, + vars: value.vars, trust: value.trust, registers: Default::default(), file_revision: None, From b33ea59966e367a57a5c35934c7ca8f71c3e565b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:41:35 +0000 Subject: [PATCH 6/6] code review: inline TypedValue into Variable, use BTreeMap Co-Authored-By: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- .../src/interface/variables.rs | 47 ++++++------------- .../src/symbolication/native.rs | 3 +- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/crates/symbolicator-native/src/interface/variables.rs b/crates/symbolicator-native/src/interface/variables.rs index ccd47bad6..f3e7ca698 100644 --- a/crates/symbolicator-native/src/interface/variables.rs +++ b/crates/symbolicator-native/src/interface/variables.rs @@ -1,6 +1,6 @@ //! Contains types for representing variable values. -use std::collections::HashMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use symbolic::debuginfo::VariableKind as SymbolicVariableKind; @@ -8,7 +8,7 @@ use symbolic::debuginfo::VariableKind as SymbolicVariableKind; /// Variables keyed by their names. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct Variables(HashMap); +pub struct Variables(BTreeMap); impl Variables { pub fn is_empty(&self) -> bool { @@ -32,8 +32,10 @@ where #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Variable { kind: VariableKind, + #[serde(rename = "type")] + ty: String, #[serde(flatten)] - value: TypedValue, + value: Value, } /// The variable's kind (e.g. local or parameter). @@ -44,33 +46,11 @@ pub enum VariableKind { Parameter, } -/// A [`Value`] with a type. -/// -/// Constructed via [`Value::ty`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct TypedValue { - #[serde(rename = "type")] - ty: String, - #[serde(flatten)] - value: Value, -} - -impl TypedValue { - /// Convert this [`TypedValue`] into a [`Variable`] with the given kind. - pub fn kind(self, kind: K) -> Variable - where - K: Into, - { - let kind = kind.into(); - Variable { kind, value: self } - } -} - /// A value of a variable or field within a variable (e.g. list item, pointee, struct field). /// /// This struct allows for multiple co-existing representations. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct Value { +pub struct Value { /// A fully-formatted, source-specific representation of a variable value. /// /// This should typically be used for primitive types like integers, booleans, chars. @@ -87,17 +67,20 @@ impl Value { /// /// This should typically be used for primitive types like integers, booleans, and chars. pub fn value(self, value: Option) -> Self { - #[expect(clippy::needless_update, reason = "we will add other representations")] - Self { value, ..self } + Self { value } } - /// Convert this [`Value`] to a [`TypedValue`] with the given type. - pub fn ty(self, ty: T) -> TypedValue + /// Convert this [`Value`] into a [`Variable`] with the given type and kind. + pub fn into_variable(self, ty: T, kind: K) -> Variable where T: Into, + K: Into, { - let ty = ty.into(); - TypedValue { ty, value: self } + Variable { + kind: kind.into(), + ty: ty.into(), + value: self, + } } } diff --git a/crates/symbolicator-native/src/symbolication/native.rs b/crates/symbolicator-native/src/symbolication/native.rs index 74e5b7871..4d180b172 100644 --- a/crates/symbolicator-native/src/symbolication/native.rs +++ b/crates/symbolicator-native/src/symbolication/native.rs @@ -198,8 +198,7 @@ fn do_extract_variables<'data, 'cache>( let interface_variable = VariableValue::new() .value(value) - .ty(ty) - .kind(variable.kind()); + .into_variable(ty, variable.kind()); Some((name, interface_variable)) })