Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ jobs:
with:
merge-multiple: true

- name: Generate checksums
run: sha256sum simc-* > SHA256SUMS

- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
Expand All @@ -106,3 +109,4 @@ jobs:
simc-macos-x86_64.tar.gz
simc-macos-aarch64.tar.gz
simc-windows-x86_64.zip
SHA256SUMS
147 changes: 147 additions & 0 deletions src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,150 @@ impl DebugValue {
&self.value
}
}

#[cfg(feature = "serde")]
mod serde_impl {
//! JSON form of [`DebugSymbols`]: a map from CMR (hex) to `{text, name}`, with
//! types carried as their string form (like the ABI), so external tooling can
//! re-attach the symbols to a program whose nodes it resolves by CMR.

use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use std::sync::Arc;

use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use simplicity::Cmr;

use super::{DebugSymbols, TrackedCall, TrackedCallName};
use crate::parse::ParseFromStr;
use crate::types::ResolvedType;

/// Wire form of [`TrackedCallName`]: unit variants as strings, type-carrying
/// variants as single-key objects holding the type's string form.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum NameRepr {
Assert,
Panic,
Jet,
UnwrapLeft(String),
UnwrapRight(String),
Unwrap,
Debug(String),
}

#[derive(Serialize, Deserialize)]
struct CallRepr {
text: String,
name: NameRepr,
}

impl From<&TrackedCallName> for NameRepr {
fn from(name: &TrackedCallName) -> Self {
match name {
TrackedCallName::Assert => Self::Assert,
TrackedCallName::Panic => Self::Panic,
TrackedCallName::Jet => Self::Jet,
TrackedCallName::UnwrapLeft(ty) => Self::UnwrapLeft(ty.to_string()),
TrackedCallName::UnwrapRight(ty) => Self::UnwrapRight(ty.to_string()),
TrackedCallName::Unwrap => Self::Unwrap,
TrackedCallName::Debug(ty) => Self::Debug(ty.to_string()),
}
}
}

impl TryFrom<NameRepr> for TrackedCallName {
type Error = String;

fn try_from(name: NameRepr) -> Result<Self, Self::Error> {
let ty = |s: String| {
ResolvedType::parse_from_str(&s).map_err(|error| format!("type `{s}`: {error}"))
};
Ok(match name {
NameRepr::Assert => Self::Assert,
NameRepr::Panic => Self::Panic,
NameRepr::Jet => Self::Jet,
NameRepr::UnwrapLeft(s) => Self::UnwrapLeft(ty(s)?),
NameRepr::UnwrapRight(s) => Self::UnwrapRight(ty(s)?),
NameRepr::Unwrap => Self::Unwrap,
NameRepr::Debug(s) => Self::Debug(ty(s)?),
})
}
}

impl Serialize for TrackedCall {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
CallRepr {
text: self.text.to_string(),
name: NameRepr::from(&self.name),
}
.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for TrackedCall {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let repr = CallRepr::deserialize(deserializer)?;
Ok(TrackedCall {
text: Arc::from(repr.text.as_str()),
name: TrackedCallName::try_from(repr.name).map_err(D::Error::custom)?,
})
}
}

impl Serialize for DebugSymbols {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// BTreeMap for deterministic field order in the emitted JSON.
let map: BTreeMap<String, &TrackedCall> = self
.0
.iter()
.map(|(cmr, call)| (cmr.to_string(), call))
.collect();
map.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for DebugSymbols {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let map = BTreeMap::<String, TrackedCall>::deserialize(deserializer)?;
let inner = map
.into_iter()
.map(|(cmr, call)| {
Cmr::from_str(&cmr)
.map(|cmr| (cmr, call))
.map_err(|error| D::Error::custom(format!("CMR `{cmr}`: {error}")))
})
.collect::<Result<HashMap<Cmr, TrackedCall>, D::Error>>()?;
Ok(DebugSymbols(inner))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::error::Span;

#[test]
fn debug_symbols_roundtrip() {
let file = "assert!(jet::is_zero_32(x)); dbg!(y)";
let mut symbols = DebugSymbols::default();
symbols.insert(
Span::new_in_default_file(0..28),
Cmr::from_byte_array([1; 32]),
TrackedCallName::Assert,
file,
);
symbols.insert(
Span::new_in_default_file(29..36),
Cmr::from_byte_array([2; 32]),
TrackedCallName::Debug(ResolvedType::from(crate::types::UIntType::U32)),
file,
);

let json = serde_json::to_string(&symbols).expect("serialize");
let back: DebugSymbols = serde_json::from_str(&json).expect("deserialize");
assert_eq!(symbols, back);
}
}
}
174 changes: 173 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ pub struct TemplateProgram {

impl TemplateProgram {
/// Parses and flattens a multi-file program into a single enriched [`parse::Program`]
/// with all imports resolved and each file wrapped in a `unit_N` module.
/// with all imports resolved: the entry file's items stay at the top level and
/// each dependency file is wrapped in a `unit_N` module, so the flattened
/// output is itself a valid entry file.
///
/// ## Errors
///
Expand Down Expand Up @@ -357,6 +359,33 @@ impl CompiledProgram {
version::SimcDirective::current_version()
}

/// The witness layout of the program: the name and Simplicity value type of
/// every witness node, in the post order in which the nodes occur in the
/// Simplicity target code.
///
/// This is the contract for external tooling that satisfies a program without
/// this compiler's frontend: witness nodes are never shared at commitment
/// time, and [`Self::commit`] preserves node order, so the `k`-th witness
/// node encountered in post order takes a value of the type described by the
/// `k`-th entry of this layout. A name can appear in several entries when the
/// program references the same witness more than once; every occurrence takes
/// the same value, exactly as [`Self::satisfy`] assigns it.
pub fn witness_layout(&self) -> Vec<(crate::str::WitnessName, Arc<simplicity::types::Final>)> {
use simplicity::dag::DagLike as _;

self.simplicity
.as_ref()
.post_order_iter::<simplicity::dag::InternalSharing>()
.filter_map(|item| match item.node.inner() {
simplicity::node::Inner::Witness(name) => Some((
name.shallow_clone(),
Arc::clone(&item.node.cached_data().arrow().target),
)),
_ => None,
})
.collect()
}

/// Satisfy the SimplicityHL program with the given `witness_values`.
///
/// ## Errors
Expand Down Expand Up @@ -1579,6 +1608,149 @@ fn main() {
err
);
}

/// A program with two witnesses of distinct types, for the layout tests.
fn two_witness_program() -> CompiledProgram {
let src = "fn main() {
let a: u16 = witness::A;
let b: u32 = witness::B;
assert!(jet::eq_16(a, 7));
assert!(jet::eq_32(b, 9));
}";
CompiledProgram::new(
src,
Arguments::default(),
false,
Box::new(crate::ast::ElementsJetHinter::new()),
)
.unwrap()
}

/// The witness values matching [`two_witness_program`].
fn two_witness_values() -> WitnessValues {
let mut values = std::collections::HashMap::new();
values.insert(
crate::str::WitnessName::from_str_unchecked("A"),
Value::from(crate::value::UIntValue::U16(7)),
);
values.insert(
crate::str::WitnessName::from_str_unchecked("B"),
Value::from(crate::value::UIntValue::U32(9)),
);
WitnessValues::from(values)
}

/// The layout lists every witness node with its name and Simplicity type,
/// in the order the nodes occur in the target code.
#[test]
fn witness_layout_names_and_types() {
let layout = two_witness_program().witness_layout();
let entries: Vec<(&str, String)> = layout
.iter()
.map(|(name, ty)| (name.as_inner(), ty.to_string()))
.collect();
assert_eq!(
entries,
[("A", "2^16".to_string()), ("B", "2^32".to_string())]
);
}

/// The layout order is the satisfaction order: assigning values to witness
/// nodes *by layout index* — on the name-free commit program, through the
/// consensus encode/decode round-trip — yields the same redeem program as
/// [`CompiledProgram::satisfy`] assigning them by name. This is the contract
/// external tooling relies on to satisfy a program without the compiler's
/// frontend.
#[test]
fn witness_layout_populates_by_index() {
use simplicity::dag::PostOrderIterItem;
use simplicity::node::{self, Converter, Inner, NoWitness};

/// Assigns the `k`-th witness node the `k`-th of the given values.
struct IndexPopulator {
values: Vec<simplicity::Value>,
next: usize,
}

impl Converter<node::Commit, node::Redeem> for IndexPopulator {
type Error = String;

fn convert_witness(
&mut self,
_: &PostOrderIterItem<&CommitNode>,
_: &NoWitness,
) -> Result<simplicity::Value, Self::Error> {
let value = self
.values
.get(self.next)
.cloned()
.ok_or("more witness nodes than layout entries")?;
self.next += 1;
Ok(value)
}

fn convert_disconnect(
&mut self,
_: &PostOrderIterItem<&CommitNode>,
_: Option<&Arc<RedeemNode>>,
_: &node::NoDisconnect,
) -> Result<Arc<RedeemNode>, Self::Error> {
unreachable!("SimplicityHL does not use disconnect right now")
}

fn convert_data(
&mut self,
data: &PostOrderIterItem<&CommitNode>,
inner: Inner<&Arc<RedeemNode>, &Arc<RedeemNode>, &simplicity::Value>,
) -> Result<Arc<node::RedeemData>, Self::Error> {
let inner = inner
.map(|node| node.cached_data())
.map_disconnect(|node| node.cached_data())
.map_witness(simplicity::Value::shallow_clone);
Ok(Arc::new(node::RedeemData::new(
data.node.cached_data().arrow().shallow_clone(),
inner,
)))
}
}

let compiled = two_witness_program();
let witness_values = two_witness_values();
let expected = compiled
.satisfy(witness_values.shallow_clone())
.unwrap()
.redeem()
.to_vec_with_witness();

// Order the values per the layout, converted exactly as satisfaction does.
let ordered: Vec<simplicity::Value> = compiled
.witness_layout()
.iter()
.map(|(name, _)| {
simplicity::Value::from(crate::value::StructuralValue::from(
witness_values.get(name).unwrap(),
))
})
.collect();

// Round-trip the name-free program through the consensus serialization,
// as tooling that holds only the program bytes would.
let bytes = compiled.commit().to_vec_without_witness();
let decoded = CommitNode::decode::<_, simplicity::jet::Elements>(
simplicity::BitIter::from(&bytes[..]),
)
.unwrap();

let mut populator = IndexPopulator {
values: ordered,
next: 0,
};
let redeem = decoded
.convert::<simplicity::dag::InternalSharing, _, _>(&mut populator)
.unwrap();
assert_eq!(populator.next, populator.values.len());
assert_eq!(redeem.to_vec_with_witness(), expected);
}
}

#[cfg(test)]
Expand Down
Loading
Loading