Skip to content

Commit

Permalink
feat: Add debug expression
Browse files Browse the repository at this point in the history
dbg!(x) behaves in Simfony like in Rust: It is a NOP that prints its
input value as a side effect. The Simfony compiler adds a debug symbol
for this purpose.

I split FallibleCall from DebugValue in the debug symbol API because
FallibleCall is supposed to be used in error messages, while DebugValue
is used for logging on the side. I don't want to handle dbg! as a case
in an error message, even though dbg! is infallible.
  • Loading branch information
uncomputable committed Sep 16, 2024
1 parent bfb6a54 commit 0a67719
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 9 deletions.
15 changes: 13 additions & 2 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,12 @@ pub enum CallName {
IsNone(ResolvedType),
/// [`Option::unwrap`].
Unwrap,
/// [`assert`].
/// [`assert!`].
Assert,
/// [`panic`] without error message.
/// [`panic!`] without error message.
Panic,
/// [`dbg!`].
Debug,
/// Cast from the given source type.
TypeCast(ResolvedType),
/// A custom function that was defined previously.
Expand Down Expand Up @@ -1070,6 +1072,14 @@ impl AbstractSyntaxTree for Call {
scope.track_call(from, TrackedCallName::Panic);
analyze_arguments(from.args(), &args_tys, scope)?
}
CallName::Debug => {
let args_tys = [ty.clone()];
check_argument_types(from.args(), &args_tys).with_span(from)?;
let args = analyze_arguments(from.args(), &args_tys, scope)?;
let [arg_ty] = args_tys;
scope.track_call(from.args().first().unwrap(), TrackedCallName::Debug(arg_ty));
args
}
CallName::TypeCast(source) => {
if StructuralType::from(&source) != StructuralType::from(ty) {
return Err(Error::InvalidCast(source, ty.clone())).with_span(from);
Expand Down Expand Up @@ -1176,6 +1186,7 @@ impl AbstractSyntaxTree for CallName {
parse::CallName::Unwrap => Ok(Self::Unwrap),
parse::CallName::Assert => Ok(Self::Assert),
parse::CallName::Panic => Ok(Self::Panic),
parse::CallName::Debug => Ok(Self::Debug),
parse::CallName::TypeCast(target) => {
scope.resolve(target).map(Self::TypeCast).with_span(from)
}
Expand Down
5 changes: 5 additions & 0 deletions src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ impl Call {
let fail = ProgNode::fail(scope.ctx(), FailEntropy::ZERO);
with_debug_symbol(args, &fail, scope, self)
}
CallName::Debug => {
// dbg! computes the identity function
let iden = ProgNode::iden(scope.ctx());
with_debug_symbol(args, &iden, scope, self.args().first().unwrap())
}
CallName::TypeCast(..) => {
// A cast converts between two structurally equal types.
// Structural equality of Simfony types A and B means
Expand Down
37 changes: 34 additions & 3 deletions src/debug.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;

use either::Either;
use simplicity::Cmr;

use crate::error::Span;
Expand Down Expand Up @@ -29,6 +30,7 @@ pub enum TrackedCallName {
UnwrapLeft(ResolvedType),
UnwrapRight(ResolvedType),
Unwrap,
Debug(ResolvedType),
}

/// Fallible call expression with runtime input value.
Expand All @@ -49,6 +51,13 @@ pub enum FallibleCallName {
Unwrap,
}

/// Debug expression with runtime input value.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct DebugValue {
text: Arc<str>,
value: Value,
}

impl DebugSymbols {
/// Insert a tracked call expression.
/// Use the Simfony source `file` to extract the Simfony text of the expression.
Expand Down Expand Up @@ -100,10 +109,12 @@ impl TrackedCall {
}

/// Supply the Simplicity input value of the call expression at runtime.
/// Convert the debug call into a fallible call or into a debug value,
/// depending on the kind of debug symbol.
///
/// Return `None` if the Simplicity input value is of the wrong type,
/// according to the debug symbol.
pub fn map_value(&self, value: &StructuralValue) -> Option<FallibleCall> {
pub fn map_value(&self, value: &StructuralValue) -> Option<Either<FallibleCall, DebugValue>> {
let name = match self.name() {
TrackedCallName::Assert => FallibleCallName::Assert,
TrackedCallName::Panic => FallibleCallName::Panic,
Expand All @@ -115,11 +126,19 @@ impl TrackedCall {
Value::reconstruct(value, ty).map(FallibleCallName::UnwrapRight)?
}
TrackedCallName::Unwrap => FallibleCallName::Unwrap,
TrackedCallName::Debug(ty) => {
return Value::reconstruct(value, ty)
.map(|value| DebugValue {
text: Arc::clone(&self.text),
value,
})
.map(Either::Right)
}
};
Some(FallibleCall {
Some(Either::Left(FallibleCall {
text: Arc::clone(&self.text),
name,
})
}))
}
}

Expand All @@ -134,3 +153,15 @@ impl FallibleCall {
&self.name
}
}

impl DebugValue {
/// Access the Simfony text of the debug expression.
pub fn text(&self) -> &str {
&self.text
}

/// Access the runtime input value.
pub fn value(&self) -> &Value {
&self.value
}
}
5 changes: 3 additions & 2 deletions src/minimal.pest
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jet = @{ "jet::" ~ (ASCII_ALPHANUMERIC | "_")+ }
witness_name = @{ (ASCII_ALPHANUMERIC | "_")+ }
builtin_type = @{ "Either" | "Option" | "bool" | "List" | unsigned_type }

builtin_function = @{ jet | "unwrap_left" | "unwrap_right" | "for_while" | "is_none" | "unwrap" | "assert" | "panic" | "match" | "into" | "fold" }
builtin_function = @{ jet | "unwrap_left" | "unwrap_right" | "for_while" | "is_none" | "unwrap" | "assert" | "panic" | "match" | "into" | "fold" | "dbg" }
function_name = { !builtin_function ~ identifier }
typed_identifier = { identifier ~ ":" ~ ty }
function_params = { "(" ~ (typed_identifier ~ ("," ~ typed_identifier)*)? ~ ")" }
Expand Down Expand Up @@ -64,9 +64,10 @@ unwrap = @{ "unwrap" }
assert = @{ "assert!" }
panic = @{ "panic!" }
type_cast = { "<" ~ ty ~ ">::into" }
debug = @{ "dbg!" }
fold = { "fold::<" ~ function_name ~ "," ~ list_bound ~ ">" }
for_while = { "for_while::<" ~ function_name ~ ">" }
call_name = { jet | unwrap_left | unwrap_right | is_none | unwrap | assert | panic | type_cast | fold | for_while | function_name }
call_name = { jet | unwrap_left | unwrap_right | is_none | unwrap | assert | panic | type_cast | debug | fold | for_while | function_name }
call_args = { "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" }
call_expr = { call_name ~ call_args }
dec_literal = @{ (ASCII_DIGIT | "_")+ }
Expand Down
8 changes: 6 additions & 2 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,12 @@ pub enum CallName {
Unwrap,
/// [`Option::is_none`].
IsNone(AliasedType),
/// [`assert`].
/// [`assert!`].
Assert,
/// [`panic`] without error message.
/// [`panic!`] without error message.
Panic,
/// [`dbg!`].
Debug,
/// Cast from the given source type.
TypeCast(AliasedType),
/// Name of a custom function.
Expand Down Expand Up @@ -671,6 +673,7 @@ impl fmt::Display for CallName {
CallName::IsNone(ty) => write!(f, "is_none::<{ty}>"),
CallName::Assert => write!(f, "assert!"),
CallName::Panic => write!(f, "panic!"),
CallName::Debug => write!(f, "dbg!"),
CallName::TypeCast(ty) => write!(f, "<{ty}>::into"),
CallName::Custom(name) => write!(f, "{name}"),
CallName::Fold(name, bound) => write!(f, "fold::<{name}, {bound}>"),
Expand Down Expand Up @@ -943,6 +946,7 @@ impl PestParse for CallName {
Rule::unwrap => Ok(Self::Unwrap),
Rule::assert => Ok(Self::Assert),
Rule::panic => Ok(Self::Panic),
Rule::debug => Ok(Self::Debug),
Rule::type_cast => {
let inner = pair.into_inner().next().unwrap();
AliasedType::parse(inner).map(Self::TypeCast)
Expand Down

0 comments on commit 0a67719

Please sign in to comment.