From 503019001949568e710aff216ed6f4235e8d6f55 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Tue, 20 Jan 2026 17:41:16 +0200 Subject: [PATCH 1/6] add lexer The lexer parses incoming code into tokens, which makes it simpler to process using `chumsky`. --- Cargo.lock | 175 +++++++++++++++++++++-------- Cargo.toml | 1 + src/lexer.rs | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 440 insertions(+), 46 deletions(-) create mode 100644 src/lexer.rs diff --git a/Cargo.lock b/Cargo.lock index 986aa39..4900c4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.18" @@ -61,6 +67,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + [[package]] name = "arbitrary" version = "1.1.3" @@ -181,12 +196,14 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.83" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ + "find-msvc-tools", "jobserver", "libc", + "shlex", ] [[package]] @@ -195,6 +212,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chumsky" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" +dependencies = [ + "hashbrown", + "regex-automata", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + [[package]] name = "clap" version = "4.5.37" @@ -237,18 +268,18 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -292,6 +323,24 @@ dependencies = [ "secp256k1-zkp", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "generic-array" version = "0.14.7" @@ -321,6 +370,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hex-conservative" version = "0.2.1" @@ -377,9 +437,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" @@ -413,6 +473,15 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -421,20 +490,19 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pest" -version = "2.7.3" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", - "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.3" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ "pest", "pest_generator", @@ -442,9 +510,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.3" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ "pest", "pest_meta", @@ -455,11 +523,10 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.3" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -479,6 +546,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.33" @@ -637,15 +714,21 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simplicity-lang" version = "0.7.0" @@ -680,6 +763,7 @@ version = "0.4.1" dependencies = [ "arbitrary", "base64 0.21.3", + "chumsky", "clap", "either", "getrandom", @@ -704,6 +788,19 @@ dependencies = [ "simplicityhl", ] +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + [[package]] name = "strsim" version = "0.11.1" @@ -732,37 +829,17 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thiserror" -version = "1.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.31", -] - [[package]] name = "typenum" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" @@ -770,6 +847,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "utf8parse" version = "0.2.2" @@ -778,9 +861,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" diff --git a/Cargo.toml b/Cargo.toml index 20372f7..c0cbcf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ either = "1.12.0" itertools = "0.13.0" arbitrary = { version = "1", optional = true, features = ["derive"] } clap = "4.5.37" +chumsky = "0.11.2" [target.wasm32-unknown-unknown.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..d7df297 --- /dev/null +++ b/src/lexer.rs @@ -0,0 +1,309 @@ +use chumsky::prelude::*; +use std::fmt; + +use crate::str::{Binary, Decimal, Hexadecimal}; + +pub type Spanned = (T, SimpleSpan); +pub type Tokens<'src> = Vec<(Token<'src>, crate::error::Span)>; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Token<'src> { + // Keywords + Fn, + Let, + Type, + Mod, + Const, + Match, + + // Control symbols + Arrow, + Colon, + Semi, + Comma, + Eq, + FatArrow, + LParen, + RParen, + LBracket, + RBracket, + LBrace, + RBrace, + LAngle, + RAngle, + + // Number literals + DecLiteral(Decimal), + HexLiteral(Hexadecimal), + BinLiteral(Binary), + + // Boolean literal + Bool(bool), + + // Identifier + Ident(&'src str), + + // Jets, witnesses, and params + Jet(&'src str), + Witness(&'src str), + Param(&'src str), + + // Built-in functions + Macro(&'src str), + + // Comments and block comments + // + // We would discard them for the compiler, but they are needed, for example, for the formatter. + Comment, + BlockComment, +} + +impl<'src> fmt::Display for Token<'src> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Token::Fn => write!(f, "fn"), + Token::Let => write!(f, "let"), + Token::Type => write!(f, "type"), + Token::Mod => write!(f, "mod"), + Token::Const => write!(f, "const"), + Token::Match => write!(f, "match"), + + Token::Arrow => write!(f, "->"), + Token::Colon => write!(f, ":"), + Token::Semi => write!(f, ";"), + Token::Comma => write!(f, ","), + Token::Eq => write!(f, "="), + Token::FatArrow => write!(f, "=>"), + Token::LParen => write!(f, "("), + Token::RParen => write!(f, ")"), + Token::LBracket => write!(f, "["), + Token::RBracket => write!(f, "]"), + Token::LBrace => write!(f, "{{"), + Token::RBrace => write!(f, "}}"), + Token::LAngle => write!(f, "<"), + Token::RAngle => write!(f, ">"), + + Token::DecLiteral(s) => write!(f, "{}", s), + Token::HexLiteral(s) => write!(f, "0x{}", s), + Token::BinLiteral(s) => write!(f, "0b{}", s), + + Token::Ident(s) => write!(f, "{}", s), + + Token::Macro(s) => write!(f, "{}", s), + + Token::Jet(s) => write!(f, "jet::{}", s), + Token::Witness(s) => write!(f, "witness::{}", s), + Token::Param(s) => write!(f, "param::{}", s), + + Token::Bool(b) => write!(f, "{}", b), + + Token::Comment => write!(f, "comment"), + Token::BlockComment => write!(f, "block_comment"), + } + } +} + +pub fn lexer<'src>( +) -> impl Parser<'src, &'src str, Vec>>, extra::Err>> +{ + let num = text::digits(10) + .to_slice() + .map(|s: &str| Token::DecLiteral(Decimal::from_str_unchecked(s.replace('_', "").as_str()))); + let hex = just("0x") + .ignore_then(text::digits(16).to_slice()) + .map(|s: &str| { + Token::HexLiteral(Hexadecimal::from_str_unchecked(s.replace('_', "").as_str())) + }); + let bin = just("0b") + .ignore_then(text::digits(2).to_slice()) + .map(|s: &str| Token::BinLiteral(Binary::from_str_unchecked(s.replace('_', "").as_str()))); + + let macros = + choice((just("assert!"), just("panic!"), just("dbg!"), just("list!"))).map(Token::Macro); + + let keyword = text::ident().map(|s| match s { + "fn" => Token::Fn, + "let" => Token::Let, + "type" => Token::Type, + "mod" => Token::Mod, + "const" => Token::Const, + "match" => Token::Match, + "true" => Token::Bool(true), + "false" => Token::Bool(false), + _ => Token::Ident(s), + }); + + let jet = just("jet::") + .labelled("jet") + .ignore_then(text::ident()) + .map(Token::Jet); + let witness = just("witness::") + .labelled("witness") + .ignore_then(text::ident()) + .map(Token::Witness); + let param = just("param::") + .labelled("param") + .ignore_then(text::ident()) + .map(Token::Param); + + let op = choice(( + just("->").to(Token::Arrow), + just("=>").to(Token::FatArrow), + just("=").to(Token::Eq), + just(":").to(Token::Colon), + just(";").to(Token::Semi), + just(",").to(Token::Comma), + just("(").to(Token::LParen), + just(")").to(Token::RParen), + just("[").to(Token::LBracket), + just("]").to(Token::RBracket), + just("{").to(Token::LBrace), + just("}").to(Token::RBrace), + just("<").to(Token::LAngle), + just(">").to(Token::RAngle), + )); + + let comment = just("//") + .ignore_then(any().and_is(just('\n').not()).repeated()) + .to(Token::Comment); + + let block_comment = recursive(|block| { + just("/*") + .map_with(|_, e| e.span()) + .then(choice((block.ignored(), any().and_is(just("*/").not()).ignored())).repeated()) + .then(just("*/").or_not()) + .validate(|((open_span, _content), close), _span, emit| { + if close.is_none() { + emit.emit(Rich::custom(open_span, "Unclosed block comment")); + } + Token::BlockComment + }) + }); + let token = choice(( + comment, + block_comment, + jet, + witness, + param, + macros, + keyword, + hex, + bin, + num, + op, + )); + + token + .map_with(|tok, e| (tok, e.span())) + .padded() + .recover_with(skip_then_retry_until(any().ignored(), end())) + .repeated() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lex<'src>( + input: &'src str, + ) -> (Option>>, Vec>) { + let (tokens, errors) = lexer().parse(input).into_output_errors(); + let tokens = tokens.map(|vec| vec.iter().map(|(tok, _)| tok.clone()).collect::>()); + (tokens, errors) + } + + #[test] + fn test_block_comment_simple() { + let input = "/* hello world */"; + let (tokens, errors) = lex(input); + + assert!(errors.is_empty(), "Expected no errors, found: {:?}", errors); + assert_eq!( + tokens, + Some(vec![Token::BlockComment]), + "Should produce a single block comment token" + ); + } + + #[test] + fn test_block_comment_nested() { + let input = "/* outer /* inner */ outer */"; + let (tokens, errors) = lex(input); + + assert!(errors.is_empty()); + assert_eq!(tokens, Some(vec![Token::BlockComment])); + } + + #[test] + fn test_block_comment_deeply_nested() { + let input = "/* 1 /* 2 /* 3 */ 2 */ 1 */"; + let (tokens, errors) = lex(input); + + assert!(errors.is_empty()); + assert_eq!(tokens, Some(vec![Token::BlockComment])); + } + + #[test] + fn test_block_comment_multiline() { + let input = "/* \n line 1 \n /* inner \n line */ \n */"; + let (tokens, errors) = lex(input); + + assert!(errors.is_empty()); + assert_eq!(tokens, Some(vec![Token::BlockComment])); + } + + #[test] + fn test_block_comment_unclosed() { + let input = "/* unclosed comment start"; + let (tokens, errors) = lex(input); + + assert_eq!(errors.len(), 1, "Expected exactly 1 error"); + + let err = &errors[0]; + assert_eq!(err.span().start, 0); + assert_eq!(err.span().end, 2); + assert_eq!(err.to_string(), "Unclosed block comment"); + + assert_eq!(tokens, Some(vec![Token::BlockComment])); + } + + #[test] + fn test_block_comment_partial_nesting_unclosed() { + let input = "/* outer /* inner */"; + let (tokens, errors) = lex(input); + + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].span().start, 0); + assert_eq!(tokens, Some(vec![Token::BlockComment])); + } + + #[test] + fn test_block_comment_double_unclosed() { + let input = "/* outer /* inner"; + let (tokens, errors) = lex(input); + + assert_eq!(errors.len(), 2); + + assert_eq!(errors[0].span().start, 9); + assert_eq!(errors[0].to_string(), "Unclosed block comment"); + + assert_eq!(errors[1].span().start, 0); + assert_eq!(errors[1].to_string(), "Unclosed block comment"); + + assert_eq!(tokens, Some(vec![Token::BlockComment])); + } + + #[test] + fn lexer_test() { + use chumsky::prelude::*; + + // Check if the lexer parses the example file without errors. + let src = include_str!("../examples/last_will.simf"); + + let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); + let _ = tokens.unwrap(); + + assert!(lex_errs.is_empty()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 702089a..a815d20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod debug; pub mod dummy_env; pub mod error; pub mod jet; +pub mod lexer; pub mod named; pub mod num; pub mod parse; From 6c4d82f1b328201adb2231880c48b31bdb67d1b6 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Tue, 20 Jan 2026 18:01:06 +0200 Subject: [PATCH 2/6] add `chumsky` parsing This commit introduce multiple changes, because it full rewrite of parsing and error Changes in `error.rs`: - Change `Span` to use byte offsets in place of old `Position` - Add `line-index` crate to calculate line and column of byte offset - Change `RichError` implementation to use new `Span` structure - Implement `chumsky` error traits, so it can be used in error reporting of parsers - add `expected..found` error Changes in `parse.rs`: - Fully rewrite `pest` parsers to `chumsky` parsers. - Change `ParseFromStr` trait to use this change. --- Cargo.lock | 139 +---- Cargo.toml | 3 +- src/error.rs | 356 +++++++----- src/lexer.rs | 24 + src/parse.rs | 1569 ++++++++++++++++++++++++++++---------------------- 5 files changed, 1118 insertions(+), 973 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4900c4a..5b82ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,15 +173,6 @@ dependencies = [ "hex-conservative", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -266,25 +257,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "derive_arbitrary" version = "1.1.3" @@ -296,16 +268,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "either" version = "1.13.0" @@ -341,16 +303,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.10" @@ -451,6 +403,16 @@ dependencies = [ "cc", ] +[[package]] +name = "line-index" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e27e0ed5a392a7f5ba0b3808a2afccff16c64933312c84b57618b49d1209bd2" +dependencies = [ + "nohash-hasher", + "text-size", +] + [[package]] name = "log" version = "0.4.22" @@ -473,6 +435,12 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "object" version = "0.32.2" @@ -488,49 +456,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "pest" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.31", -] - -[[package]] -name = "pest_meta" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" -dependencies = [ - "pest", - "sha2", -] - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -712,17 +637,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -768,9 +682,8 @@ dependencies = [ "either", "getrandom", "itertools", + "line-index", "miniscript", - "pest", - "pest_derive", "serde", "serde_json", "simplicity-lang", @@ -830,16 +743,10 @@ dependencies = [ ] [[package]] -name = "typenum" -version = "1.19.0" +name = "text-size" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "unicode-ident" @@ -859,12 +766,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index c0cbcf7..11c81a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,6 @@ serde = ["dep:serde", "dep:serde_json"] [dependencies] base64 = "0.21.2" -pest = "2.1.3" -pest_derive = "2.7.1" serde = { version = "1.0.188", features = ["derive"], optional = true } serde_json = { version = "1.0.105", optional = true } simplicity-lang = { version = "0.7.0" } @@ -34,6 +32,7 @@ itertools = "0.13.0" arbitrary = { version = "1", optional = true, features = ["derive"] } clap = "4.5.37" chumsky = "0.11.2" +line-index = "0.1.2" [target.wasm32-unknown-unknown.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/src/error.rs b/src/error.rs index c14ed78..d30eeed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,155 +1,100 @@ use std::fmt; -use std::num::NonZeroUsize; +use std::ops::Range; use std::sync::Arc; -use simplicity::hashes::{sha256, Hash, HashEngine}; -use simplicity::{elements, Cmr}; +use chumsky::error::Error as ChumskyError; +use chumsky::input::ValueInput; +use chumsky::label::LabelError; +use chumsky::util::MaybeRef; +use chumsky::DefaultExpected; -use crate::parse::{MatchPattern, Rule}; +use itertools::Itertools; +use line_index::{LineCol, LineIndex, TextSize}; +use simplicity::elements; + +use crate::lexer::Token; +use crate::parse::MatchPattern; use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName}; use crate::types::{ResolvedType, UIntType}; -/// Position of an object inside a file. -/// -/// [`pest::Position<'i>`] forces us to track lifetimes, so we introduce our own struct. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct Position { - /// Line where the object is located. - /// - /// Starts at 1. - pub line: NonZeroUsize, - /// Column where the object is located. - /// - /// Starts at 1. - pub col: NonZeroUsize, -} - -impl Position { - /// A dummy position. - #[cfg(feature = "arbitrary")] - pub(crate) const DUMMY: Self = Self::new(1, 1); - - /// Create a new position. - /// - /// ## Panics - /// - /// Line or column are zero. - pub const fn new(line: usize, col: usize) -> Self { - // assert_ne not available in constfn - assert!(line != 0, "line must not be zero",); - // Safety: Checked above - let line = unsafe { NonZeroUsize::new_unchecked(line) }; - assert!(col != 0, "column must not be zero",); - // Safety: Checked above - let col = unsafe { NonZeroUsize::new_unchecked(col) }; - Self { line, col } - } -} - /// Area that an object spans inside a file. -/// -/// The area cannot be empty. -/// -/// [`pest::Span<'i>`] forces us to track lifetimes, so we introduce our own struct. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct Span { /// Position where the object starts, inclusively. - pub start: Position, - /// Position where the object ends, inclusively. - pub end: Position, + pub start: usize, + /// Position where the object ends, exclusively. + pub end: usize, } impl Span { /// A dummy span. #[cfg(feature = "arbitrary")] - pub(crate) const DUMMY: Self = Self::new(Position::DUMMY, Position::DUMMY); + pub(crate) const DUMMY: Self = Self::new(0, 0); /// Create a new span. /// /// ## Panics /// /// Start comes after end. - pub const fn new(start: Position, end: Position) -> Self { - // NonZeroUsize does not implement const comparisons (yet) - // So we call NonZeroUsize:get() to compare usize in const - assert!( - start.line.get() <= end.line.get(), - "Start cannot come after end" - ); - assert!( - start.line.get() < end.line.get() || start.col.get() <= end.col.get(), - "Start cannot come after end" - ); + pub const fn new(start: usize, end: usize) -> Self { + assert!(start <= end, "Start cannot come after end"); Self { start, end } } - /// Check if the span covers more than one line. - pub const fn is_multiline(&self) -> bool { - self.start.line.get() < self.end.line.get() + /// Return a slice from the given `file` that corresponds to the span. + pub fn to_slice<'a>(&self, file: &'a str) -> Option<&'a str> { + file.get(self.start..self.end) } +} - /// Return the CMR of the span. - pub fn cmr(&self) -> Cmr { - let mut hasher = sha256::HashEngine::default(); - hasher.input(&self.start.line.get().to_be_bytes()); - hasher.input(&self.start.col.get().to_be_bytes()); - hasher.input(&self.end.line.get().to_be_bytes()); - hasher.input(&self.end.col.get().to_be_bytes()); - let hash = sha256::Hash::from_engine(hasher); - Cmr::from_byte_array(hash.to_byte_array()) - } +impl chumsky::span::Span for Span { + type Context = (); - /// Return a slice from the given `file` that corresponds to the span. - /// - /// Return `None` if the span runs out of bounds. - pub fn to_slice<'a>(&self, file: &'a str) -> Option<&'a str> { - let mut current_line = 1; - let mut current_col = 1; - let mut start_index = None; + type Offset = usize; - for (i, c) in file.char_indices() { - if current_line == self.start.line.get() && current_col == self.start.col.get() { - start_index = Some(i); - } - if current_line == self.end.line.get() && current_col == self.end.col.get() { - let start_index = start_index.expect("start comes before end"); - let end_index = i; - return Some(&file[start_index..end_index]); - } - if c == '\n' { - current_line += 1; - current_col = 1; - } else { - current_col += 1; - } + fn new((): Self::Context, range: Range) -> Self { + Self { + start: range.start, + end: range.end, } + } + + fn context(&self) -> Self::Context {} - None + fn start(&self) -> Self::Offset { + self.start + } + + fn end(&self) -> Self::Offset { + self.end } } -impl<'a> From<&'a pest::iterators::Pair<'_, Rule>> for Span { - fn from(pair: &'a pest::iterators::Pair) -> Self { - let (line, col) = pair.line_col(); - let start = Position::new(line, col); - // end_pos().line_col() is O(n) in file length - // https://github.com/pest-parser/pest/issues/560 - // We should generate `Span`s only on error paths - let (line, col) = pair.as_span().end_pos().line_col(); - let end = Position::new(line, col); - Self::new(start, end) +impl fmt::Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}..{}", self.start, self.end)?; + Ok(()) + } +} + +impl From for Span { + fn from(span: chumsky::span::SimpleSpan) -> Self { + Self { + start: span.start, + end: span.end, + } + } +} + +impl From> for Span { + fn from(range: Range) -> Self { + Self::new(range.start, range.end) } } impl From<&str> for Span { fn from(s: &str) -> Self { - let start = Position::new(1, 1); - let end_line = std::cmp::max(1, s.lines().count()); - let end_col = std::cmp::max(1, s.lines().next_back().unwrap_or("").len()); - let end = Position::new(end_line, end_col); - debug_assert!(start.line <= end.line); - debug_assert!(start.line < end.line || start.col <= end.col); - Span::new(start, end) + Span::new(0, s.len()) } } @@ -222,6 +167,10 @@ impl RichError { } } + pub fn file(&self) -> &Option> { + &self.file + } + pub fn error(&self) -> &Error { &self.error } @@ -235,9 +184,17 @@ impl fmt::Display for RichError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.file { Some(ref file) if !file.is_empty() => { - let start_line_index = self.span.start.line.get() - 1; - let n_spanned_lines = self.span.end.line.get() - start_line_index; - let line_num_width = self.span.end.line.get().to_string().len(); + let index = LineIndex::new(file); + + let start_pos = index.line_col(TextSize::from(self.span.start as u32)); + let end_pos = index.line_col(TextSize::from(self.span.end as u32)); + + let start_line_index = start_pos.line as usize; + let end_line_index = end_pos.line as usize; + + let n_spanned_lines = end_line_index.saturating_sub(start_line_index) + 1; + let line_num_width = (end_line_index + 1).to_string().len(); + writeln!(f, "{:width$} |", " ", width = line_num_width)?; let mut lines = file.lines().skip(start_line_index).peekable(); @@ -248,21 +205,28 @@ impl fmt::Display for RichError { writeln!(f, "{line_num:line_num_width$} | {line_str}")?; } - let (underline_start, underline_length) = match self.span.is_multiline() { - true => (0, start_line_len), - false => ( - self.span.start.col.get(), - self.span.end.col.get() - self.span.start.col.get(), - ), + let line_start_byte = index + .offset(LineCol { + line: start_pos.line, + col: 0, + }) + .map_or(0, |ts| u32::from(ts) as usize); + + let start_col = file[line_start_byte..self.span.start].chars().count() + 1; + + let (underline_start, underline_length) = if start_line_index == end_line_index { + let end_col = file[line_start_byte..self.span.end].chars().count() + 1; + (start_col, end_col.saturating_sub(start_col)) + } else { + (0, start_line_len) }; + write!(f, "{:width$} |", " ", width = line_num_width)?; write!(f, "{:width$}", " ", width = underline_start)?; write!(f, "{:^ { - write!(f, "{}", self.error) - } + _ => write!(f, "{}", self.error), } } } @@ -281,19 +245,92 @@ impl From for String { } } -impl From> for RichError { - fn from(error: pest::error::Error) -> Self { - let description = error.variant.message().to_string(); - let (start, end) = match error.line_col { - pest::error::LineColLocation::Pos((line, col)) => { - (Position::new(line, col), Position::new(line, col + 1)) - } - pest::error::LineColLocation::Span((line, col), (line_end, col_end)) => { - (Position::new(line, col), Position::new(line_end, col_end)) - } - }; - let span = Span::new(start, end); - Self::new(Error::Grammar(description), span) +/// Implementation of traits for using inside `chumsky` parsers. +impl<'tokens, 'src: 'tokens, I> ChumskyError<'tokens, I> for RichError +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + fn merge(self, other: Self) -> Self { + match (&self.error, &other.error) { + (Error::Grammar(_), Error::Grammar(_)) => other, + (Error::Grammar(_), _) => other, + (_, Error::Grammar(_)) => self, + _ => other, + } + } +} + +impl<'tokens, 'src: 'tokens, I> LabelError<'tokens, I, DefaultExpected<'tokens, Token<'src>>> + for RichError +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + fn expected_found( + expected: E, + found: Option>>, + span: Span, + ) -> Self + where + E: IntoIterator>>, + { + let expected_tokens: Vec = expected + .into_iter() + .map(|t| match t { + DefaultExpected::Token(maybe) => maybe.to_string(), + DefaultExpected::Any => "anything".to_string(), + DefaultExpected::SomethingElse => "something else".to_string(), + DefaultExpected::EndOfInput => "end of input".to_string(), + _ => "UNEXPECTED_TOKEN".to_string(), + }) + .collect(); + + let found_string = found.map(|t| t.to_string()); + + Self { + error: Error::Syntax { + expected: expected_tokens, + label: None, + found: found_string, + }, + span, + file: None, + } + } +} + +impl<'tokens, 'src: 'tokens, I> LabelError<'tokens, I, &'tokens str> for RichError +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + fn expected_found( + expected: E, + found: Option>>, + span: Span, + ) -> Self + where + E: IntoIterator, + { + let expected_strings: Vec = expected.into_iter().map(|s| s.to_string()).collect(); + let found_string = found.map(|t| t.to_string()); + + Self { + error: Error::Syntax { + expected: expected_strings, + label: None, + found: found_string, + }, + span, + file: None, + } + } + + fn label_with(&mut self, label: &'tokens str) { + if let Error::Syntax { + label: ref mut l, .. + } = &mut self.error + { + *l = Some(label.to_string()); + } } } @@ -305,10 +342,13 @@ pub enum Error { ArraySizeNonZero(usize), ListBoundPow2(usize), BitStringPow2(usize), - HexStringLen(usize), - ForWhileWidthPow2(usize), CannotParse(String), Grammar(String), + Syntax { + expected: Vec, + label: Option, + found: Option, + }, IncompatibleMatchArms(MatchPattern, MatchPattern), // TODO: Remove CompileError once SimplicityHL has a type system // The SimplicityHL compiler should never produce ill-typed Simplicity code @@ -356,14 +396,6 @@ impl fmt::Display for Error { f, "Expected a valid bit string length (1, 2, 4, 8, 16, 32, 64, 128, 256), found {len}" ), - Error::HexStringLen(len) => write!( - f, - "Expected an even hex string length (0, 2, 4, 6, 8, ...), found {len}" - ), - Error::ForWhileWidthPow2(bit_width) => write!( - f, - "Expected a power of two (1, 2, 4, 8, 16, ...) as for-while bit width, found {bit_width}" - ), Error::CannotParse(description) => write!( f, "Cannot parse: {description}" @@ -372,6 +404,21 @@ impl fmt::Display for Error { f, "Grammar error: {description}" ), + Error::Syntax { expected, label, found } => { + let found_text = found.clone().unwrap_or("end of input".to_string()); + match (label, expected.len()) { + (Some(l), _) => write!(f, "Expected {}, found {}", l, found_text), + (None, 1) => { + let exp_text = expected.first().unwrap(); + write!(f, "Expected '{}', found '{}'", exp_text, found_text) + } + (None, 0) => write!(f, "Unexpected {}", found_text), + (None, _) => { + let exp_text = expected.iter().map(|s| format!("'{}'", s)).join(", "); + write!(f, "Expected one of {}, found '{}'", exp_text, found_text) + } + } + } Error::IncompatibleMatchArms(pattern1, pattern2) => write!( f, "Match arm `{pattern1}` is incompatible with arm `{pattern2}`" @@ -526,7 +573,7 @@ let x: u32 = Left( #[test] fn display_single_line() { let error = Error::ListBoundPow2(5) - .with_span(Span::new(Position::new(1, 14), Position::new(1, 20))) + .with_span(Span::new(13, 19)) .with_file(Arc::from(FILE)); let expected = r#" | @@ -540,7 +587,7 @@ let x: u32 = Left( let error = Error::CannotParse( "Expected value of type `u32`, got `Either, _>`".to_string(), ) - .with_span(Span::new(Position::new(2, 21), Position::new(4, 2))) + .with_span(Span::new(41, FILE.len())) .with_file(Arc::from(FILE)); let expected = r#" | @@ -573,8 +620,8 @@ let x: u32 = Left( let expected = "Cannot parse: This error has no file"; assert_eq!(&expected, &error.to_string()); - let error = Error::CannotParse("This error has no file".to_string()) - .with_span(Span::new(Position::new(1, 1), Position::new(2, 2))); + let error = + Error::CannotParse("This error has no file".to_string()).with_span(Span::new(5, 10)); assert_eq!(&expected, &error.to_string()); } @@ -585,10 +632,5 @@ let x: u32 = Left( .with_file(Arc::from(EMPTY_FILE)); let expected = "Cannot parse: This error has an empty file"; assert_eq!(&expected, &error.to_string()); - - let error = Error::CannotParse("This error has an empty file".to_string()) - .with_span(Span::new(Position::new(1, 1), Position::new(2, 2))) - .with_file(Arc::from(EMPTY_FILE)); - assert_eq!(&expected, &error.to_string()); } } diff --git a/src/lexer.rs b/src/lexer.rs index d7df297..bae88f4 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -201,6 +201,30 @@ pub fn lexer<'src>( .collect() } +/// Lexes an input string into a stream of tokens with spans. +/// +/// All comments in the input code are discarded. +pub fn lex<'src>(input: &'src str) -> (Option>, Vec) { + let (tokens, errors) = lexer().parse(input).into_output_errors(); + ( + tokens.map(|vec| { + vec.into_iter() + .map(|(tok, span)| (tok, crate::error::Span::from(span))) + .filter(|(tok, _)| !matches!(tok, Token::Comment | Token::BlockComment)) + .collect::>() + }), + errors + .iter() + .map(|err| { + crate::error::RichError::new( + crate::error::Error::CannotParse(err.reason().to_string()), + (*err.span()).into(), + ) + }) + .collect::>(), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parse.rs b/src/parse.rs index cd98faa..d8b5d3d 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,25 +6,21 @@ use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::Arc; +use chumsky::input::ValueInput; +use chumsky::prelude::*; use either::Either; -use itertools::Itertools; use miniscript::iter::{Tree, TreeLike}; -use pest::Parser; -use pest_derive::Parser; -use crate::error::{Error, RichError, Span, WithFile, WithSpan}; +use crate::error::{Error, RichError, Span}; use crate::impl_eq_hash; +use crate::lexer::Token; use crate::num::NonZeroPow2Usize; use crate::pattern::Pattern; use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, WitnessName, }; -use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; - -#[derive(Parser)] -#[grammar = "minimal.pest"] -struct IdentParser; +use crate::types::{AliasedType, BuiltinAlias, TypeConstructible}; /// A program is a sequence of items. #[derive(Clone, Debug)] @@ -276,6 +272,16 @@ impl Expression { _ => self, } } + + pub fn empty(span: Span) -> Self { + Self { + inner: ExpressionInner::Single(SingleExpression { + inner: SingleExpressionInner::Tuple(Arc::new([])), + span, + }), + span, + } + } } impl_eq_hash!(Expression; inner); @@ -830,794 +836,967 @@ impl fmt::Display for MatchPattern { } } -/// Trait for types that can be parsed from a PEST pair. -trait PestParse: Sized { - /// Expected rule for parsing the type. - const RULE: Rule; - - /// Parse a value of the type from a PEST pair. - /// - /// # Panics - /// - /// The rule of the pair is not the expected rule ([`Self::RULE`]). - fn parse(pair: pest::iterators::Pair) -> Result; -} - macro_rules! impl_parse_wrapped_string { - ($wrapper: ident, $rule: ident) => { - impl PestParse for $wrapper { - const RULE: Rule = Rule::$rule; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - Ok(Self::from_str_unchecked(pair.as_str())) + ($wrapper: ident, $label: literal) => { + impl ChumskyParse for $wrapper { + fn parser<'tokens, 'src: 'tokens, I>( + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + select! { + Token::Ident(ident) => Self::from_str_unchecked(ident) + } + .labelled($label) } } }; } -impl_parse_wrapped_string!(FunctionName, function_name); -impl_parse_wrapped_string!(Identifier, identifier); -impl_parse_wrapped_string!(WitnessName, witness_name); -impl_parse_wrapped_string!(AliasName, alias_name); -impl_parse_wrapped_string!(ModuleName, module_name); +impl_parse_wrapped_string!(FunctionName, "function name"); +impl_parse_wrapped_string!(Identifier, "identifier"); +impl_parse_wrapped_string!(WitnessName, "witness name"); +impl_parse_wrapped_string!(AliasName, "alias name"); +impl_parse_wrapped_string!(ModuleName, "module name"); -/// Copy of [`FromStr`] that internally uses the PEST parser. +/// Copy of [`FromStr`] that internally uses the `chumsky` parser. pub trait ParseFromStr: Sized { /// Parse a value from the string `s`. fn parse_from_str(s: &str) -> Result; } -impl ParseFromStr for A { - fn parse_from_str(s: &str) -> Result { - let mut pairs = IdentParser::parse(A::RULE, s) - .map_err(RichError::from) - .with_file(s)?; - let pair = pairs.next().unwrap(); - A::parse(pair).with_file(s) - } +/// Trait for generating parsers of themselves. +/// +/// Replacement for previous `PestParse` trait. +pub trait ChumskyParse: Sized { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>; } -impl PestParse for Program { - const RULE: Rule = Rule::program; +type ParseError<'src> = extra::Err; - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let items = pair - .into_inner() - .filter_map(|pair| match pair.as_rule() { - Rule::item => Some(Item::parse(pair)), - _ => None, - }) - .collect::, RichError>>()?; - Ok(Program { items, span }) - } -} +/// This implementation only returns first encountered error. +impl ParseFromStr for A { + fn parse_from_str(s: &str) -> Result { + let (tokens, lex_errs) = crate::lexer::lex(s); -impl PestParse for Item { - const RULE: Rule = Rule::item; + let Some(tokens) = tokens else { + return Err(lex_errs.first().cloned().unwrap_or(RichError::new( + Error::CannotParse("Unknown reason".to_string()), + Span::new(0, 0), + ))); + }; - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let pair = pair.into_inner().next().unwrap(); - match pair.as_rule() { - Rule::type_alias => TypeAlias::parse(pair).map(Item::TypeAlias), - Rule::function => Function::parse(pair).map(Item::Function), - _ => Ok(Self::Module), + let (ast, parse_errs) = A::parser() + .map_with(|parsed, _| parsed) + .parse( + tokens + .as_slice() + .map((s.len()..s.len()).into(), |(t, s)| (t, s)), + ) + .into_output_errors(); + + if parse_errs.is_empty() { + Ok(ast.ok_or(RichError::new( + Error::CannotParse("Empty AST without an error.".to_string()), + Span::new(0, 0), + ))?) + } else { + let err = parse_errs.first().unwrap().clone(); + Err(err) } } } -impl PestParse for Function { - const RULE: Rule = Rule::function; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let _fn_keyword = it.next().unwrap(); - let name = FunctionName::parse(it.next().unwrap())?; - let params = { - let pair = it.next().unwrap(); - debug_assert!(matches!(pair.as_rule(), Rule::function_params)); - pair.into_inner() - .map(FunctionParam::parse) - .collect::, RichError>>()? - }; - let ret = match it.peek().unwrap().as_rule() { - Rule::function_return => { - let pair = it.next().unwrap(); - debug_assert!(matches!(pair.as_rule(), Rule::function_return)); - let pair = pair.into_inner().next().unwrap(); - let ty = AliasedType::parse(pair)?; - Some(ty) +/// Parse a token, and, if not found, place itself in place of missing one. +/// +/// Should be only used when we know that this token should be there. For example, type of +/// `List` would require comma inside angle brackets. +fn parse_token_with_recovery<'tokens, 'src: 'tokens, I>( + tok: Token<'src>, +) -> impl Parser<'tokens, I, Token<'src>, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + just(tok.clone()).recover_with(via_parser(empty().to(tok))) +} + +/// Parser with error recovery for expressions, which would always contains given delimiters. +/// +/// Can track span of open delimiter (if any). +fn delimited_with_recovery<'tokens, 'src: 'tokens, I, P, T>( + parser: P, + open: Token<'src>, + close: Token<'src>, + fallback: T, +) -> impl Parser<'tokens, I, T, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + P: Parser<'tokens, I, T, ParseError<'src>> + Clone, + T: Clone + 'tokens, +{ + just(open.clone()) + .map_with(|_, e| e.span()) + .then(parser.recover_with(via_parser(nested_delimiters( + open.clone(), + close.clone(), + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + (Token::LBrace, Token::RBrace), + (Token::LAngle, Token::RAngle), + ], + move |_| fallback.clone(), + )))) + .then(just(close).or_not()) + // TODO: we should use information about open delimiter + .validate(move |((open_span, content), close_token), _, emit| { + if close_token.is_none() { + emit.emit(Error::Grammar(format!("Unclosed delimiter {open}")).with_span(open_span)) } - _ => None, - }; - let body = Expression::parse(it.next().unwrap())?; - - Ok(Self { - name, - params, - ret, - body, - span, + content }) - } -} - -impl PestParse for FunctionParam { - const RULE: Rule = Rule::typed_identifier; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let mut it = pair.into_inner(); - let identifier = Identifier::parse(it.next().unwrap())?; - let ty = AliasedType::parse(it.next().unwrap())?; - Ok(Self { identifier, ty }) - } -} - -impl PestParse for Statement { - const RULE: Rule = Rule::statement; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let inner_pair = pair.into_inner().next().unwrap(); - match inner_pair.as_rule() { - Rule::assignment => Assignment::parse(inner_pair).map(Statement::Assignment), - Rule::expression => Expression::parse(inner_pair).map(Statement::Expression), - _ => unreachable!("Corrupt grammar"), - } - } } -impl PestParse for Pattern { - const RULE: Rule = Rule::pattern; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let pair = PatternPair(pair); - let mut output = vec![]; - - for data in pair.post_order_iter() { - match data.node.0.as_rule() { - Rule::pattern => {} - Rule::variable_pattern => { - let identifier = Identifier::parse(data.node.0.into_inner().next().unwrap())?; - output.push(Pattern::Identifier(identifier)); - } - Rule::ignore_pattern => { - output.push(Pattern::Ignore); +impl ChumskyParse for AliasedType { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let atom = select! { + Token::Ident(ident) => { + match ident + { + "u1" => AliasedType::u1(), + "u2" => AliasedType::u2(), + "u4" => AliasedType::u4(), + "u8" => AliasedType::u8(), + "u16" => AliasedType::u16(), + "u32" => AliasedType::u32(), + "u64" => AliasedType::u64(), + "u128" => AliasedType::u128(), + "u256" => AliasedType::u256(), + "Ctx8" | "Pubkey" | "Message64" | "Message" | "Signature" | "Scalar" | "Fe" | "Gej" + | "Ge" | "Point" | "Height" | "Time" | "Distance" | "Duration" | "Lock" | "Outpoint" + | "Confidential1" | "ExplicitAsset" | "Asset1" | "ExplicitAmount" | "Amount1" + | "ExplicitNonce" | "Nonce" | "TokenAmount1" => AliasedType::builtin(BuiltinAlias::from_str(ident).unwrap()), + "bool" => AliasedType::boolean(), + _ => AliasedType::alias(AliasName::from_str_unchecked(ident)), } - Rule::tuple_pattern => { - let size = data.node.n_children(); - let elements = output.split_off(output.len() - size); - debug_assert_eq!(elements.len(), size); - output.push(Pattern::tuple(elements)); - } - Rule::array_pattern => { - let size = data.node.n_children(); - let elements = output.split_off(output.len() - size); - debug_assert_eq!(elements.len(), size); - output.push(Pattern::array(elements)); - } - _ => unreachable!("Corrupt grammar"), - } - } - - debug_assert!(output.len() == 1); - Ok(output.pop().unwrap()) - } -} - -impl PestParse for Assignment { - const RULE: Rule = Rule::assignment; + }, + }; - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let _let_keyword = it.next().unwrap(); - let pattern = Pattern::parse(it.next().unwrap())?; - let ty = AliasedType::parse(it.next().unwrap())?; - let expression = Expression::parse(it.next().unwrap())?; - Ok(Assignment { - pattern, - ty, - expression, - span, + let num = select! { + Token::DecLiteral(i) => i.clone() + } + .labelled("decimal number") + .recover_with(via_parser( + none_of([Token::RAngle, Token::RBracket]) + .ignored() + .or(empty()) + .to(Decimal::from_str_unchecked("0")), + )); + + let error_type = AliasedType::alias(AliasName::from_str_unchecked("error")); + + recursive(|ty| { + let args = delimited_with_recovery( + ty.clone() + .then_ignore(parse_token_with_recovery(Token::Comma)) + .then(ty.clone()), + Token::LAngle, + Token::RAngle, + (error_type.clone(), error_type.clone()), + ); + + let sum_type = just(Token::Ident("Either")) + .ignore_then(args) + .map(|(left, right)| AliasedType::either(left, right)) + .labelled("Either"); + + let option_type = just(Token::Ident("Option")) + .ignore_then(delimited_with_recovery( + ty.clone(), + Token::LAngle, + Token::RAngle, + error_type.clone(), + )) + .map(AliasedType::option) + .labelled("Option"); + + let tuple = delimited_with_recovery( + ty.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect() + .map(|s: Vec| AliasedType::tuple(s)), + Token::LParen, + Token::RParen, + AliasedType::tuple(Vec::new()), + ) + .labelled("tuple"); + + let array = delimited_with_recovery( + ty.clone() + .then_ignore(parse_token_with_recovery(Token::Semi)) + .then(num.clone()) + .map(|(ty, size)| { + AliasedType::array(ty, usize::from_str(size.as_inner()).unwrap_or_default()) + }), + Token::LBracket, + Token::RBracket, + AliasedType::array(error_type.clone(), 0), + ) + .labelled("array"); + + let list = just(Token::Ident("List")) + .ignore_then(delimited_with_recovery( + ty.then_ignore(parse_token_with_recovery(Token::Comma)) + .then(num.clone().validate(|num, e, emit| { + match NonZeroPow2Usize::from_str(num.as_inner()) { + Ok(number) => number, + Err(err) => { + emit.emit( + Error::Grammar(format!("Cannot parse list bound: {err}")) + .with_span(e.span()), + ); + // fallback to default value + NonZeroPow2Usize::TWO + } + } + })), + Token::LAngle, + Token::RAngle, + ( + AliasedType::alias(AliasName::from_str_unchecked("error")), + NonZeroPow2Usize::TWO, + ), + )) + .map(|(ty, size)| AliasedType::list(ty, size)) + .labelled("List"); + + choice((sum_type, option_type, tuple, array, list, atom)) + .map_with(|inner, _| inner) + .labelled("type") }) } } -impl PestParse for Call { - const RULE: Rule = Rule::call_expr; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let name = CallName::parse(it.next().unwrap())?; - let args = { - let pair = it.next().unwrap(); - debug_assert!(matches!(pair.as_rule(), Rule::call_args)); - pair.into_inner() - .map(Expression::parse) - .collect::, RichError>>()? - }; - - Ok(Self { name, args, span }) +impl ChumskyParse for Program { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let skip_until_next_item = any() + .then( + any() + .filter(|t| !matches!(t, Token::Fn | Token::Type | Token::Mod)) + .repeated(), + ) + // map to empty module + .map_with(|_, _| Item::Module); + + Item::parser() + .recover_with(via_parser(skip_until_next_item)) + .repeated() + .collect::>() + .map_with(|items, e| Program { + items: Arc::from(items), + span: e.span(), + }) } } -impl PestParse for CallName { - const RULE: Rule = Rule::call_name; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let pair = pair.into_inner().next().unwrap(); - match pair.as_rule() { - Rule::jet => JetName::parse(pair).map(Self::Jet), - Rule::unwrap_left => { - let inner = pair.into_inner().next().unwrap(); - AliasedType::parse(inner).map(Self::UnwrapLeft) - } - Rule::unwrap_right => { - let inner = pair.into_inner().next().unwrap(); - AliasedType::parse(inner).map(Self::UnwrapRight) - } - Rule::is_none => { - let inner = pair.into_inner().next().unwrap(); - AliasedType::parse(inner).map(Self::IsNone) - } - 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) - } - Rule::fold => { - let mut it = pair.into_inner(); - let name = FunctionName::parse(it.next().unwrap())?; - let bound = NonZeroPow2Usize::parse(it.next().unwrap())?; - Ok(Self::Fold(name, bound)) - } - Rule::array_fold => { - let mut it = pair.into_inner(); - let name = FunctionName::parse(it.next().unwrap())?; - let non_zero_usize_parse = - |pair: pest::iterators::Pair| -> Result { - let size = pair.as_str().parse::().with_span(&pair)?; - NonZeroUsize::new(size) - .ok_or(Error::ArraySizeNonZero(size)) - .with_span(&pair) - }; - let size = non_zero_usize_parse(it.next().unwrap())?; - Ok(Self::ArrayFold(name, size)) - } - Rule::for_while => { - let mut it = pair.into_inner(); - let name = FunctionName::parse(it.next().unwrap())?; - Ok(Self::ForWhile(name)) - } - Rule::function_name => FunctionName::parse(pair).map(Self::Custom), - _ => panic!("Corrupt grammar"), - } +impl ChumskyParse for Item { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let func_parser = Function::parser().map(Item::Function); + let type_parser = TypeAlias::parser().map(Item::TypeAlias); + let mod_parser = Module::parser().map(|_| Item::Module); + + choice((func_parser, type_parser, mod_parser)) + } +} + +impl ChumskyParse for Function { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let params = delimited_with_recovery( + FunctionParam::parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LParen, + Token::RParen, + Vec::new(), + ) + .map(Arc::from) + .labelled("function parameters"); + + let ret = just(Token::Arrow) + .ignore_then(AliasedType::parser()) + .or_not() + .labelled("return type"); + + let body = just(Token::LBrace) + .rewind() + .ignore_then(Expression::parser()) + .recover_with(via_parser(nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + ], + Expression::empty, + ))) + .labelled("function body"); + + just(Token::Fn) + .ignore_then(FunctionName::parser()) + .then(params) + .then(ret) + .then(body) + .map_with(|(((name, params), ret), body), e| Self { + name, + params, + ret, + body, + span: e.span(), + }) } } -impl PestParse for JetName { - const RULE: Rule = Rule::jet; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let jet_name = pair.as_str().strip_prefix("jet::").unwrap(); - Ok(Self::from_str_unchecked(jet_name)) - } -} +impl ChumskyParse for FunctionParam { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let identifier = Identifier::parser(); -impl PestParse for TypeAlias { - const RULE: Rule = Rule::type_alias; + let ty = AliasedType::parser(); - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let _type_keyword = it.next().unwrap(); - let name = AliasName::parse(it.next().unwrap())?; - let ty = AliasedType::parse(it.next().unwrap())?; - Ok(Self { name, ty, span }) + identifier + .then_ignore(just(Token::Colon)) + .then(ty) + .map(|(identifier, ty)| Self { identifier, ty }) } } -impl PestParse for Expression { - const RULE: Rule = Rule::expression; +impl Statement { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let assignment = Assignment::parser(expr.clone()).map(Statement::Assignment); - fn parse(pair: pest::iterators::Pair) -> Result { - let span = Span::from(&pair); - let pair = match pair.as_rule() { - Rule::expression => pair.into_inner().next().unwrap(), - Rule::block_expression | Rule::single_expression => pair, - _ => unreachable!("Corrupt grammar"), - }; + let expression = expr.map(Statement::Expression); - let inner = match pair.as_rule() { - Rule::block_expression => { - let mut it = pair.into_inner().peekable(); - let statements = it - .peeking_take_while(|pair| matches!(pair.as_rule(), Rule::statement)) - .map(Statement::parse) - .collect::, RichError>>()?; - let expression = it - .next() - .map(|pair| Expression::parse(pair).map(Arc::new)) - .transpose()?; - ExpressionInner::Block(statements, expression) - } - Rule::single_expression => ExpressionInner::Single(SingleExpression::parse(pair)?), - _ => unreachable!("Corrupt grammar"), - }; - - Ok(Expression { inner, span }) + choice((assignment, expression)) } } -impl PestParse for SingleExpression { - const RULE: Rule = Rule::single_expression; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - - let span = Span::from(&pair); - let inner_pair = pair.into_inner().next().unwrap(); - - let inner = match inner_pair.as_rule() { - Rule::left_expr => { - let l = inner_pair.into_inner().next().unwrap(); - Expression::parse(l) - .map(Arc::new) - .map(Either::Left) - .map(SingleExpressionInner::Either)? - } - Rule::right_expr => { - let r = inner_pair.into_inner().next().unwrap(); - Expression::parse(r) - .map(Arc::new) - .map(Either::Right) - .map(SingleExpressionInner::Either)? - } - Rule::none_expr => SingleExpressionInner::Option(None), - Rule::some_expr => { - let r = inner_pair.into_inner().next().unwrap(); - Expression::parse(r) - .map(Arc::new) - .map(Some) - .map(SingleExpressionInner::Option)? - } - Rule::false_expr => SingleExpressionInner::Boolean(false), - Rule::true_expr => SingleExpressionInner::Boolean(true), - Rule::call_expr => SingleExpressionInner::Call(Call::parse(inner_pair)?), - Rule::bin_literal => Binary::parse(inner_pair).map(SingleExpressionInner::Binary)?, - Rule::hex_literal => { - Hexadecimal::parse(inner_pair).map(SingleExpressionInner::Hexadecimal)? - } - Rule::dec_literal => Decimal::parse(inner_pair).map(SingleExpressionInner::Decimal)?, - Rule::witness_expr => SingleExpressionInner::Witness(WitnessName::parse( - inner_pair.into_inner().next().unwrap(), - )?), - Rule::param_expr => SingleExpressionInner::Parameter(WitnessName::parse( - inner_pair.into_inner().next().unwrap(), - )?), - Rule::variable_expr => { - let identifier_pair = inner_pair.into_inner().next().unwrap(); - SingleExpressionInner::Variable(Identifier::parse(identifier_pair)?) - } - Rule::expression => { - SingleExpressionInner::Expression(Expression::parse(inner_pair).map(Arc::new)?) - } - Rule::match_expr => Match::parse(inner_pair).map(SingleExpressionInner::Match)?, - Rule::tuple_expr => inner_pair - .clone() - .into_inner() - .map(Expression::parse) - .collect::, _>>() - .map(SingleExpressionInner::Tuple)?, - Rule::array_expr => inner_pair - .clone() - .into_inner() - .map(Expression::parse) - .collect::, _>>() - .map(SingleExpressionInner::Array)?, - Rule::list_expr => { - let elements = inner_pair - .into_inner() - .map(|inner| Expression::parse(inner)) - .collect::, _>>()?; - SingleExpressionInner::List(elements) - } - _ => unreachable!("Corrupt grammar"), - }; - - Ok(SingleExpression { inner, span }) +impl Assignment { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + just(Token::Let) + .ignore_then(Pattern::parser()) + .then_ignore(parse_token_with_recovery(Token::Colon)) + .then(AliasedType::parser()) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(expr) + .map_with(|((pattern, ty), expression), e| Self { + pattern, + ty, + expression, + span: e.span(), + }) } } -impl PestParse for Decimal { - const RULE: Rule = Rule::dec_literal; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let decimal = pair.as_str().replace('_', ""); - Ok(Self::from_str_unchecked(decimal.as_str())) +impl ChumskyParse for Pattern { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + recursive(|pat| { + let variable = Identifier::parser().map(Pattern::Identifier); + + let ignore = select! { + Token::Ident("_") => Pattern::Ignore, + }; + + let tuple = delimited_with_recovery( + pat.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LParen, + Token::RParen, + Vec::new(), + ) + .map(Pattern::tuple); + + let array = delimited_with_recovery( + pat.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LBracket, + Token::RBracket, + Vec::new(), + ) + .map(Pattern::array); + + choice((ignore, variable, tuple, array)).labelled("pattern") + }) } } -impl PestParse for Binary { - const RULE: Rule = Rule::bin_literal; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let binary = pair.as_str().strip_prefix("0b").unwrap().replace('_', ""); - Ok(Self::from_str_unchecked(binary.as_str())) +impl Call { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let args = delimited_with_recovery( + expr.separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LParen, + Token::RParen, + Vec::new(), + ) + .map(Arc::from) + .labelled("call arguments"); + + CallName::parser() + .then(args) + .map_with(|(name, args), e| Self { + name, + args, + span: e.span(), + }) } } -impl PestParse for Hexadecimal { - const RULE: Rule = Rule::hex_literal; +impl ChumskyParse for CallName { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let double_colon = just(Token::Colon).then(just(Token::Colon)).labelled("::"); - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let hexadecimal = pair.as_str().strip_prefix("0x").unwrap().replace('_', ""); - Ok(Self::from_str_unchecked(hexadecimal.as_str())) - } -} + let turbofish_start = double_colon.clone().then(just(Token::LAngle)).ignored(); -impl PestParse for Match { - const RULE: Rule = Rule::match_expr; + let generics_close = just(Token::RAngle); - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let _match_keyword = it.next().unwrap(); - let scrutinee_pair = it.next().unwrap(); - let scrutinee = Expression::parse(scrutinee_pair.clone()).map(Arc::new)?; - let first = MatchArm::parse(it.next().unwrap())?; - let second = MatchArm::parse(it.next().unwrap())?; + let type_cast = just(Token::LAngle) + .ignore_then(AliasedType::parser()) + .then_ignore(generics_close.clone()) + .then_ignore(just(Token::Colon).then(just(Token::Colon))) + .then_ignore(just(Token::Ident("into"))) + .map(CallName::TypeCast); - let (left, right) = match (&first.pattern, &second.pattern) { - (MatchPattern::Left(..), MatchPattern::Right(..)) => (first, second), - (MatchPattern::Right(..), MatchPattern::Left(..)) => (second, first), - (MatchPattern::None, MatchPattern::Some(..)) => (first, second), - (MatchPattern::False, MatchPattern::True) => (first, second), - (MatchPattern::Some(..), MatchPattern::None) => (second, first), - (MatchPattern::True, MatchPattern::False) => (second, first), - (p1, p2) => { - return Err(Error::IncompatibleMatchArms(p1.clone(), p2.clone())).with_span(span) - } + let builtin_generic_ty = |name: &'static str, ctor: fn(AliasedType) -> Self| { + just(Token::Ident(name)) + .ignore_then(turbofish_start.clone()) + .ignore_then(AliasedType::parser()) + .then_ignore(generics_close.clone()) + .map(ctor) }; - Ok(Self { - scrutinee, - left, - right, - span, - }) + let unwrap_left = builtin_generic_ty("unwrap_left", CallName::UnwrapLeft); + let unwrap_right = builtin_generic_ty("unwrap_right", CallName::UnwrapRight); + let is_none = builtin_generic_ty("is_none", CallName::IsNone); + + let fold = just(Token::Ident("fold")) + .ignore_then(turbofish_start.clone()) + .ignore_then(FunctionName::parser()) + .then_ignore(parse_token_with_recovery(Token::Comma)) + .then(select! { Token::DecLiteral(s) => s }.labelled("list size")) + .then_ignore(generics_close.clone()) + .validate(|(func, bound_str), e, emit| { + let bound = match bound_str.as_inner().parse::() { + Ok(num) => match NonZeroPow2Usize::new(num) { + Some(val) => val, + None => { + emit.emit(Error::ListBoundPow2(num).with_span(e.span())); + NonZeroPow2Usize::TWO + } + }, + Err(_) => { + emit.emit( + Error::CannotParse(format!("Invalid number: {}", bound_str)) + .with_span(e.span()), + ); + NonZeroPow2Usize::TWO + } + }; + + CallName::Fold(func, bound) + }); + + let array_fold = just(Token::Ident("array_fold")) + .ignore_then(turbofish_start.clone()) + .ignore_then(FunctionName::parser()) + .then_ignore(parse_token_with_recovery(Token::Comma)) + .then(select! { Token::DecLiteral(s) => s }.labelled("array size")) + .then_ignore(generics_close.clone()) + .validate(|(func, size_str), e, emit| { + let size = match size_str.as_inner().parse::() { + Ok(0) => { + emit.emit(Error::ArraySizeNonZero(0).with_span(e.span())); + NonZeroUsize::new(1).unwrap() + } + Ok(n) => NonZeroUsize::new(n).unwrap(), + Err(_) => { + emit.emit( + Error::CannotParse(format!("Invalid number: {}", size_str)) + .with_span(e.span()), + ); + NonZeroUsize::new(1).unwrap() + } + }; + + CallName::ArrayFold(func, size) + }); + + let for_while = just(Token::Ident("for_while")) + .ignore_then(turbofish_start.clone()) + .ignore_then(FunctionName::parser()) + .then_ignore(generics_close.clone()) + .map(CallName::ForWhile); + + let simple_builtins = select! { + Token::Ident("unwrap") => CallName::Unwrap, + Token::Macro("assert!") => CallName::Assert, + Token::Macro("panic!") => CallName::Panic, + Token::Macro("dbg!") => CallName::Debug, + }; + + let jet = select! { Token::Jet(s) => JetName::from_str_unchecked(s) }.map(CallName::Jet); + + let custom_func = FunctionName::parser().map(CallName::Custom); + + choice(( + type_cast, + unwrap_left, + unwrap_right, + is_none, + fold, + array_fold, + for_while, + simple_builtins, + jet, + custom_func, + )) + } +} + +impl ChumskyParse for TypeAlias { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let name = AliasName::parser().map_with(|name, e| (name, e.span())); + + just(Token::Type) + .ignore_then(name) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(AliasedType::parser()) + .then_ignore(just(Token::Semi)) + .map_with(|(name, ty), e| Self { + name: name.0, + ty, + span: e.span(), + }) } } -impl PestParse for MatchArm { - const RULE: Rule = Rule::match_arm; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let mut it = pair.into_inner(); - let pattern = MatchPattern::parse(it.next().unwrap())?; - let expression = Expression::parse(it.next().unwrap()).map(Arc::new)?; - Ok(MatchArm { - pattern, - expression, +impl ChumskyParse for Expression { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + recursive(|expr| { + let block = { + let statement = Statement::parser(expr.clone()).then_ignore(just(Token::Semi)); + + let block_recovery = nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::RAngle, Token::RAngle), + (Token::LBracket, Token::RBracket), + ], + |span| Expression::empty(span).inner().clone(), + ); + + let statements = statement + .repeated() + .collect::>() + .map(Arc::from) + .recover_with(skip_then_retry_until( + block_recovery.ignored().or(any().ignored()), + one_of([Token::Semi, Token::RParen, Token::RBracket, Token::RBrace]) + .ignored(), + )); + + let final_expr = expr.clone().map(Arc::new).or_not(); + + delimited_with_recovery( + statements.then(final_expr), + Token::LBrace, + Token::RBrace, + (Arc::from(Vec::new()), None), + ) + .map(|(stmts, end_expr)| ExpressionInner::Block(stmts, end_expr)) + }; + + let single = SingleExpression::parser(expr.clone()).map(ExpressionInner::Single); + + choice((block, single)) + .map_with(|inner, e| Expression { + inner, + span: e.span(), + }) + .labelled("expression") }) } } -impl PestParse for MatchPattern { - const RULE: Rule = Rule::match_pattern; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let pair = pair.into_inner().next().unwrap(); - let ret = match pair.as_rule() { - rule @ (Rule::left_pattern | Rule::right_pattern | Rule::some_pattern) => { - let mut it = pair.into_inner(); - let identifier = Identifier::parse(it.next().unwrap())?; - let ty = AliasedType::parse(it.next().unwrap())?; - - match rule { - Rule::left_pattern => MatchPattern::Left(identifier, ty), - Rule::right_pattern => MatchPattern::Right(identifier, ty), - Rule::some_pattern => MatchPattern::Some(identifier, ty), - _ => unreachable!("Covered by outer match"), - } - } - Rule::none_pattern => MatchPattern::None, - Rule::false_pattern => MatchPattern::False, - Rule::true_pattern => MatchPattern::True, - _ => unreachable!("Corrupt grammar"), +impl SingleExpression { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let wrapper = |name: &'static str| { + select! { Token::Ident(i) if i == name => i }.ignore_then( + expr.clone() + .delimited_by(just(Token::LParen), just(Token::RParen)), + ) }; - Ok(ret) - } -} - -impl PestParse for AliasedType { - const RULE: Rule = Rule::ty; - - fn parse(pair: pest::iterators::Pair) -> Result { - enum Item { - Type(AliasedType), - Size(usize), - Bound(NonZeroPow2Usize), - } - - impl Item { - fn unwrap_type(self) -> AliasedType { - match self { - Item::Type(ty) => ty, - _ => panic!("Not a type"), - } - } - fn unwrap_size(self) -> usize { - match self { - Item::Size(size) => size, - _ => panic!("Not a size"), - } - } + let left = + wrapper("Left").map(|e| SingleExpressionInner::Either(Either::Left(Arc::new(e)))); + + let right = + wrapper("Right").map(|e| SingleExpressionInner::Either(Either::Right(Arc::new(e)))); + + let some = wrapper("Some").map(|e| SingleExpressionInner::Option(Some(Arc::new(e)))); + + let none = select! { Token::Ident("None") => SingleExpressionInner::Option(None) }; + + let boolean = select! { Token::Bool(b) => SingleExpressionInner::Boolean(b) }; + + let comma_separated = expr + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(); + + let array = delimited_with_recovery( + comma_separated.clone(), + Token::LBracket, + Token::RBracket, + Vec::new(), + ) + .map(|es| SingleExpressionInner::Array(Arc::from(es))); + + let list = just(Token::Macro("list!")) + .ignore_then(delimited_with_recovery( + comma_separated.clone(), + Token::LBracket, + Token::RBracket, + Vec::new(), + )) + .map(|es| SingleExpressionInner::List(Arc::from(es))); + + let tuple = delimited_with_recovery( + comma_separated.clone(), + Token::LParen, + Token::RParen, + Vec::new(), + ) + .map(|es| SingleExpressionInner::Tuple(Arc::from(es))); + + let literal = select! { + Token::DecLiteral(s) => SingleExpressionInner::Decimal(s), + Token::HexLiteral(s) => SingleExpressionInner::Hexadecimal(s), + Token::BinLiteral(s) => SingleExpressionInner::Binary(s), + Token::Witness(s) => SingleExpressionInner::Witness(WitnessName::from_str_unchecked(s)), + Token::Param(s) => SingleExpressionInner::Parameter(WitnessName::from_str_unchecked(s)), + }; - fn unwrap_bound(self) -> NonZeroPow2Usize { - match self { - Item::Bound(size) => size, - _ => panic!("Not a bound"), - } - } - } + let call = Call::parser(expr.clone()).map(SingleExpressionInner::Call); - assert!(matches!(pair.as_rule(), Self::RULE)); - let pair = TyPair(pair); - let mut output = vec![]; + let match_expr = Match::parser(expr.clone()).map(SingleExpressionInner::Match); - for data in pair.post_order_iter() { - match data.node.0.as_rule() { - Rule::alias_name => { - let name = AliasName::parse(data.node.0)?; - output.push(Item::Type(AliasedType::alias(name))); - } - Rule::builtin_alias => { - let builtin = BuiltinAlias::parse(data.node.0)?; - output.push(Item::Type(AliasedType::builtin(builtin))); - } - Rule::unsigned_type => { - let uint_ty = UIntType::parse(data.node.0)?; - output.push(Item::Type(AliasedType::from(uint_ty))); - } - Rule::sum_type => { - let r = output.pop().unwrap().unwrap_type(); - let l = output.pop().unwrap().unwrap_type(); - output.push(Item::Type(AliasedType::either(l, r))); - } - Rule::option_type => { - let r = output.pop().unwrap().unwrap_type(); - output.push(Item::Type(AliasedType::option(r))); - } - Rule::boolean_type => { - output.push(Item::Type(AliasedType::boolean())); - } - Rule::tuple_type => { - let size = data.node.n_children(); - let elements: Vec = output - .split_off(output.len() - size) - .into_iter() - .map(Item::unwrap_type) - .collect(); - debug_assert_eq!(elements.len(), size); - output.push(Item::Type(AliasedType::tuple(elements))); - } - Rule::array_type => { - let size = output.pop().unwrap().unwrap_size(); - let el = output.pop().unwrap().unwrap_type(); - output.push(Item::Type(AliasedType::array(el, size))); - } - Rule::array_size => { - let size_str = data.node.0.as_str(); - let size = size_str.parse::().with_span(&data.node.0)?; - output.push(Item::Size(size)); - } - Rule::list_type => { - let bound = output.pop().unwrap().unwrap_bound(); - let el = output.pop().unwrap().unwrap_type(); - output.push(Item::Type(AliasedType::list(el, bound))); - } - Rule::list_bound => { - let bound = NonZeroPow2Usize::parse(data.node.0)?; - output.push(Item::Bound(bound)); - } - Rule::ty => {} - _ => unreachable!("Corrupt grammar"), - } - } + let variable = Identifier::parser().map(SingleExpressionInner::Variable); - debug_assert!(output.len() == 1); - Ok(output.pop().unwrap().unwrap_type()) + choice(( + left, right, some, none, boolean, match_expr, list, array, tuple, call, literal, + variable, + )) + .map_with(|inner, e| Self { + inner, + span: e.span(), + }) } } -impl PestParse for UIntType { - const RULE: Rule = Rule::unsigned_type; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let ret = match pair.as_str() { - "u1" => UIntType::U1, - "u2" => UIntType::U2, - "u4" => UIntType::U4, - "u8" => UIntType::U8, - "u16" => UIntType::U16, - "u32" => UIntType::U32, - "u64" => UIntType::U64, - "u128" => UIntType::U128, - "u256" => UIntType::U256, - _ => unreachable!("Corrupt grammar"), +impl ChumskyParse for MatchPattern { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let wrapper = |name: &'static str, ctor: fn(Identifier, AliasedType) -> Self| { + select! { Token::Ident(i) if i == name => i } + .ignore_then(delimited_with_recovery( + Identifier::parser() + .then_ignore(just(Token::Colon)) + .then(AliasedType::parser()), + Token::LParen, + Token::RParen, + ( + Identifier::from_str_unchecked(""), + AliasedType::alias(AliasName::from_str_unchecked("error")), + ), + )) + .map(move |(id, ty)| ctor(id, ty)) }; - Ok(ret) - } -} - -impl PestParse for BuiltinAlias { - const RULE: Rule = Rule::builtin_alias; - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - Self::from_str(pair.as_str()) - .map_err(Error::CannotParse) - .with_span(&pair) + choice(( + wrapper("Left", MatchPattern::Left), + wrapper("Right", MatchPattern::Right), + wrapper("Some", MatchPattern::Some), + select! { Token::Ident("None") => MatchPattern::None }, + select! { Token::Bool(true) => MatchPattern::True }, + select! { Token::Bool(false) => MatchPattern::False }, + )) } } -impl PestParse for NonZeroPow2Usize { - // FIXME: This equates NonZeroPow2Usize with list bounds. Create wrapper for list bounds? - const RULE: Rule = Rule::list_bound; +impl MatchArm { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + MatchPattern::parser() + .then_ignore(just(Token::FatArrow)) + .then(expr.map(Arc::new)) + .then(just(Token::Comma).or_not()) + .validate(|((pattern, expression), comma), e, emitter| { + let is_block = matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); + + if !is_block && comma.is_none() { + emitter.emit( + Error::Grammar( + "Missing ',' after a match arm that isn't block expression".to_string(), + ) + .with_span(e.span()), + ); + } - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let bound = pair.as_str().parse::().with_span(&pair)?; - NonZeroPow2Usize::new(bound) - .ok_or(Error::ListBoundPow2(bound)) - .with_span(&pair) + Self { + pattern, + expression, + } + }) } } -impl PestParse for ModuleProgram { - const RULE: Rule = Rule::program; +impl Match { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let scrutinee = expr.clone().map(Arc::new); + + let arm_recovery = any() + .filter(|t| !matches!(t, Token::Comma | Token::RBrace)) + .ignored() + .or(nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + ], + |_| (), + ) + .ignored()) + .repeated() + .map_with(|(), _| None); + + let arm_parser = MatchArm::parser(expr.clone()) + .map(Some) + .recover_with(via_parser(arm_recovery.clone())); + + let arms = delimited_with_recovery( + arm_parser.clone().then(arm_parser.clone()), + Token::LBrace, + Token::RBrace, + (None, None), + ); + + just(Token::Match) + .ignore_then(scrutinee) + .then(arms) + .validate(|(scrutinee, arms), e, emit| match arms { + (Some(first), Some(second)) => { + let (left, right) = match (&first.pattern, &second.pattern) { + (MatchPattern::Left(..), MatchPattern::Right(..)) => (first, second), + (MatchPattern::Right(..), MatchPattern::Left(..)) => (second, first), + + (MatchPattern::None, MatchPattern::Some(..)) => (first, second), + (MatchPattern::Some(..), MatchPattern::None) => (second, first), + + (MatchPattern::False, MatchPattern::True) => (first, second), + (MatchPattern::True, MatchPattern::False) => (second, first), + + (p1, p2) => { + emit.emit( + Error::IncompatibleMatchArms(p1.clone(), p2.clone()) + .with_span(e.span()), + ); + (first, second) + } + }; + + Self { + scrutinee, + left, + right, + span: e.span(), + } + } + _ => { + let match_arm_fallback = MatchArm { + expression: Arc::new(Expression::empty(Span::new(0, 0))), + pattern: MatchPattern::False, + }; - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let items = pair - .into_inner() - .filter_map(|pair| match pair.as_rule() { - Rule::item => Some(ModuleItem::parse(pair)), - _ => None, + let (left, right) = ( + arms.0.unwrap_or(match_arm_fallback.clone()), + arms.1.unwrap_or(match_arm_fallback.clone()), + ); + Self { + scrutinee, + left, + right, + span: e.span(), + } + } }) - .collect::, RichError>>()?; - Ok(Self { items, span }) } } -impl PestParse for ModuleItem { - const RULE: Rule = Rule::item; +impl ChumskyParse for ModuleItem { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let module = Module::parser().map(Self::Module); - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let pair = pair.into_inner().next().unwrap(); - match pair.as_rule() { - Rule::module => Module::parse(pair).map(Self::Module), - _ => Ok(Self::Ignored), - } + module } } -impl PestParse for Module { - const RULE: Rule = Rule::module; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let _mod_keyword = it.next().unwrap(); - let name = ModuleName::parse(it.next().unwrap())?; - let assignments = it - .map(ModuleAssignment::parse) - .collect::, RichError>>()?; - Ok(Self { - name, - assignments, - span, - }) - } -} - -impl PestParse for ModuleAssignment { - const RULE: Rule = Rule::module_assign; - - fn parse(pair: pest::iterators::Pair) -> Result { - assert!(matches!(pair.as_rule(), Self::RULE)); - let span = Span::from(&pair); - let mut it = pair.into_inner(); - let _const_keyword = it.next().unwrap(); - let name = WitnessName::parse(it.next().unwrap())?; - let ty = AliasedType::parse(it.next().unwrap())?; - let expression = Expression::parse(it.next().unwrap())?; - Ok(Self { - name, - ty, - expression, - span, - }) +impl ChumskyParse for ModuleProgram { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + ModuleItem::parser() + .repeated() + .collect::>() + .map_with(|items, e| Self { + items: Arc::from(items), + span: e.span(), + }) } } -/// Pair of tokens from the 'pattern' rule. -#[derive(Clone, Debug)] -struct PatternPair<'a>(pest::iterators::Pair<'a, Rule>); - -impl TreeLike for PatternPair<'_> { - fn as_node(&self) -> Tree { - let mut it = self.0.clone().into_inner(); - match self.0.as_rule() { - Rule::variable_pattern | Rule::ignore_pattern => Tree::Nullary, - Rule::pattern => { - let l = it.next().unwrap(); - Tree::Unary(PatternPair(l)) - } - Rule::tuple_pattern | Rule::array_pattern => { - let children: Arc<[PatternPair]> = it.map(PatternPair).collect(); - Tree::Nary(children) - } - _ => unreachable!("Corrupt grammar"), - } +impl ChumskyParse for Module { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let name = ModuleName::parser().map_with(|name, e| (name, e.span())); + + let assignments = ModuleAssignment::parser() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .recover_with(via_parser(nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + ], + |_| Vec::new(), + ))) + .map(Arc::from); + + just(Token::Mod) + .ignore_then(name) + .then(assignments) + .map_with(|(name, assignments), e| Self { + name: name.0, + assignments, + span: e.span(), + }) } } -/// Pair of tokens from the 'ty' rule. -#[derive(Clone, Debug)] -struct TyPair<'a>(pest::iterators::Pair<'a, Rule>); - -impl TreeLike for TyPair<'_> { - fn as_node(&self) -> Tree { - let mut it = self.0.clone().into_inner(); - match self.0.as_rule() { - Rule::boolean_type - | Rule::unsigned_type - | Rule::array_size - | Rule::list_bound - | Rule::alias_name - | Rule::builtin_alias => Tree::Nullary, - Rule::ty | Rule::option_type => { - let l = it.next().unwrap(); - Tree::Unary(TyPair(l)) - } - Rule::sum_type | Rule::array_type | Rule::list_type => { - let l = it.next().unwrap(); - let r = it.next().unwrap(); - Tree::Binary(TyPair(l), TyPair(r)) - } - Rule::tuple_type => Tree::Nary(it.map(TyPair).collect()), - _ => unreachable!("Corrupt grammar"), - } +impl ChumskyParse for ModuleAssignment { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let name = WitnessName::parser(); + + just(Token::Const) + .ignore_then(name) + .then_ignore(just(Token::Colon)) + .then(AliasedType::parser()) + .then_ignore(just(Token::Eq)) + .then(Expression::parser()) + .then_ignore(just(Token::Semi)) + .map_with(|((name, ty), expression), e| Self { + name, + ty, + expression, + span: e.span(), + }) } } From f8ffaff4e8344173af65f5ae09051f1d1c992a53 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Tue, 20 Jan 2026 18:07:47 +0200 Subject: [PATCH 3/6] add `ErrorCollector` --- src/error.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/error.rs b/src/error.rs index d30eeed..244fbf4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -334,6 +334,56 @@ where } } +#[derive(Debug, Clone, Hash)] +pub struct ErrorCollector { + /// File in which the error occurred. + file: Arc, + + /// Collected errors. + errors: Vec, +} + +impl ErrorCollector { + pub fn new(file: Arc) -> Self { + Self { + file, + errors: Vec::new(), + } + } + + /// Extend existing errors with slice of new errors. + pub fn update(&mut self, errors: impl IntoIterator) { + let new_errors = errors + .into_iter() + .map(|err| err.with_file(Arc::clone(&self.file))); + + self.errors.extend(new_errors); + } + + pub fn get(&self) -> &[RichError] { + &self.errors + } + + pub fn is_empty(&self) -> bool { + self.get().is_empty() + } +} + +impl fmt::Display for ErrorCollector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for err in self.get() { + writeln!(f, "{err}\n")?; + } + Ok(()) + } +} + +impl From for String { + fn from(handler: ErrorCollector) -> Self { + handler.to_string() + } +} + /// An individual error. /// /// Records _what_ happened but not where. From c24b9014ec77e27e73d96f9d53bc98e6dfc8a150 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Tue, 20 Jan 2026 18:11:08 +0200 Subject: [PATCH 4/6] add multiple error handling This adds `ParseFromStrWithErrors`, which would take `ErrorCollector` and return an `Option` of AST. Also changes `TemplateProgram` to use new trait with collector --- src/lib.rs | 22 ++++++++++++++-------- src/main.rs | 9 ++++++++- src/parse.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a815d20..d7b890d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,8 +30,8 @@ pub extern crate simplicity; pub use simplicity::elements; use crate::debug::DebugSymbols; -use crate::error::WithFile; -use crate::parse::ParseFromStr; +use crate::error::{ErrorCollector, WithFile}; +use crate::parse::ParseFromStrWithErrors; pub use crate::types::ResolvedType; pub use crate::value::Value; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; @@ -53,12 +53,17 @@ impl TemplateProgram { /// The string is not a valid SimplicityHL program. pub fn new>>(s: Str) -> Result { let file = s.into(); - let parse_program = parse::Program::parse_from_str(&file)?; - let ast_program = ast::Program::analyze(&parse_program).with_file(Arc::clone(&file))?; - Ok(Self { - simfony: ast_program, - file, - }) + let mut error_handler = ErrorCollector::new(Arc::clone(&file)); + let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); + if let Some(program) = parse_program { + let ast_program = ast::Program::analyze(&program).with_file(Arc::clone(&file))?; + Ok(Self { + simfony: ast_program, + file, + }) + } else { + Err(error_handler)? + } } /// Access the parameters of the program. @@ -266,6 +271,7 @@ pub trait ArbitraryOfType: Sized { #[cfg(test)] pub(crate) mod tests { + use crate::parse::ParseFromStr; use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use simplicity::BitMachine; diff --git a/src/main.rs b/src/main.rs index 1135a31..fdd090c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,7 +69,14 @@ fn main() -> Result<(), Box> { let include_debug_symbols = matches.get_flag("debug"); let output_json = matches.get_flag("json"); - let compiled = CompiledProgram::new(prog_text, Arguments::default(), include_debug_symbols)?; + let compiled = + match CompiledProgram::new(prog_text, Arguments::default(), include_debug_symbols) { + Ok(program) => program, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; #[cfg(feature = "serde")] let witness_opt = matches diff --git a/src/parse.rs b/src/parse.rs index d8b5d3d..28fa4a7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -11,6 +11,7 @@ use chumsky::prelude::*; use either::Either; use miniscript::iter::{Tree, TreeLike}; +use crate::error::ErrorCollector; use crate::error::{Error, RichError, Span}; use crate::impl_eq_hash; use crate::lexer::Token; @@ -865,6 +866,12 @@ pub trait ParseFromStr: Sized { fn parse_from_str(s: &str) -> Result; } +/// Trait for parsing with collection of errors. +pub trait ParseFromStrWithErrors: Sized { + /// Parse a value from the string `s` with Errors. + fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option; +} + /// Trait for generating parsers of themselves. /// /// Replacement for previous `PestParse` trait. @@ -909,6 +916,34 @@ impl ParseFromStr for A { } } +impl ParseFromStrWithErrors for A { + fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option { + let (tokens, lex_errs) = crate::lexer::lex(s); + + handler.update(lex_errs); + let tokens = tokens?; + + let (ast, parse_errs) = A::parser() + .map_with(|parsed, _| parsed) + .parse( + tokens + .as_slice() + .map((s.len()..s.len()).into(), |(t, s)| (t, s)), + ) + .into_output_errors(); + + handler.update(parse_errs); + + // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module + // is not handling poisoned tree right now, we don't return parsed result + if handler.get().is_empty() { + ast + } else { + None + } + } +} + /// Parse a token, and, if not found, place itself in place of missing one. /// /// Should be only used when we know that this token should be there. For example, type of From 183fdaee74ca037f91acd8b14f64babe4b4aab78 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Tue, 20 Jan 2026 18:11:43 +0200 Subject: [PATCH 5/6] remove #[ignore] above `fuzz_slow_unit_1()` it's not slow anymore --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d7b890d..73ae9c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -639,7 +639,6 @@ fn main() { } #[test] - #[ignore] fn fuzz_slow_unit_1() { parse::Program::parse_from_str("fn fnnfn(MMet:(((sssss,((((((sssss,ssssss,ss,((((((sssss,ss,((((((sssss,ssssss,ss,((((((sssss,ssssss,((((((sssss,sssssssss,(((((((sssss,sssssssss,(((((ssss,((((((sssss,sssssssss,(((((((sssss,ssss,((((((sssss,ss,((((((sssss,ssssss,ss,((((((sssss,ssssss,((((((sssss,sssssssss,(((((((sssss,sssssssss,(((((ssss,((((((sssss,sssssssss,(((((((sssss,sssssssssssss,(((((((((((u|(").unwrap_err(); } From e53f005d8d348a6c77ac85fd8bed2d67080201e3 Mon Sep 17 00:00:00 2001 From: Volodymyr Herashchenko Date: Thu, 29 Jan 2026 15:39:33 +0200 Subject: [PATCH 6/6] add regression tests for the parser This adds tests to ensure that the compiler using the `chumsky` parser produces the same Simplicity program as when using the `pest` parser for the default examples. The programs were compiled using an old `simc` version with debug symbols into .json files, and located in `test-data/` folder. --- src/lib.rs | 163 ++++++++++++++++++ test-data/array_fold.json | 4 + test-data/array_fold_2n.json | 4 + test-data/cat.json | 4 + test-data/ctv.json | 4 + test-data/escrow_with_delay.json | 4 + test-data/hash_loop.json | 4 + test-data/hodl_vault.json | 4 + test-data/htlc.json | 4 + test-data/last_will.json | 4 + test-data/non_interactive_fee_bump.json | 4 + test-data/p2ms.json | 4 + test-data/p2pkh.json | 4 + test-data/presigned_vault.json | 4 + test-data/reveal_collision.json | 4 + test-data/reveal_fix_point.json | 4 + test-data/sighash_all_anyonecanpay.json | 4 + test-data/sighash_all_anyprevout.json | 4 + .../sighash_all_anyprevoutanyscript.json | 4 + test-data/sighash_none.json | 4 + test-data/sighash_single.json | 4 + test-data/transfer_with_timeout.json | 4 + 22 files changed, 247 insertions(+) create mode 100644 test-data/array_fold.json create mode 100644 test-data/array_fold_2n.json create mode 100644 test-data/cat.json create mode 100644 test-data/ctv.json create mode 100644 test-data/escrow_with_delay.json create mode 100644 test-data/hash_loop.json create mode 100644 test-data/hodl_vault.json create mode 100644 test-data/htlc.json create mode 100644 test-data/last_will.json create mode 100644 test-data/non_interactive_fee_bump.json create mode 100644 test-data/p2ms.json create mode 100644 test-data/p2pkh.json create mode 100644 test-data/presigned_vault.json create mode 100644 test-data/reveal_collision.json create mode 100644 test-data/reveal_fix_point.json create mode 100644 test-data/sighash_all_anyonecanpay.json create mode 100644 test-data/sighash_all_anyprevout.json create mode 100644 test-data/sighash_all_anyprevoutanyscript.json create mode 100644 test-data/sighash_none.json create mode 100644 test-data/sighash_single.json create mode 100644 test-data/transfer_with_timeout.json diff --git a/src/lib.rs b/src/lib.rs index 73ae9c1..b02adbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -372,6 +372,11 @@ pub(crate) mod tests { include_fee_output: self.include_fee_output, } } + + pub fn get_encoding(self) -> String { + let program_bytes = self.program.commit().to_vec_without_witness(); + Base64Display::new(&program_bytes, &STANDARD).to_string() + } } impl TestCase { @@ -428,6 +433,14 @@ pub(crate) mod tests { Err(error) => panic!("Unexpected error: {error}"), } } + + pub fn get_encoding_with_witness(self) -> (String, String) { + let (program_bytes, witness_bytes) = self.program.redeem().to_vec_with_witness(); + ( + Base64Display::new(&program_bytes, &STANDARD).to_string(), + Base64Display::new(&witness_bytes, &STANDARD).to_string(), + ) + } } #[test] @@ -670,4 +683,154 @@ fn main() { .with_witness_values(WitnessValues::default()) .assert_run_success(); } + + #[cfg(feature = "serde")] + mod regression { + use super::TestCase; + + #[derive(serde::Deserialize)] + struct Program { + program: String, + witness: Option, + } + + fn regression_test(name: &str) { + let program = serde_json::from_str::( + std::fs::read_to_string(format!("./test-data/{}.json", name)) + .unwrap() + .as_str(), + ) + .unwrap(); + + let test_case = TestCase::program_file(format!("./examples/{}.simf", name)); + match program.witness { + Some(wit) => { + let (new_program, new_witness) = test_case + .with_witness_file(format!("./examples/{}.wit", name)) + .get_encoding_with_witness(); + assert_eq!( + program.program, new_program, + "Byte code of programs should be the same" + ); + assert_eq!( + wit, new_witness, + "Byte code of witnesses should be the same" + ); + } + None => { + let new_program = test_case.get_encoding(); + + assert_eq!( + program.program, new_program, + "Byte code of programs should be the same" + ) + } + } + } + + #[test] + fn array_fold_2n_regression() { + regression_test("array_fold_2n"); + } + + #[test] + fn array_fold_regression() { + regression_test("array_fold"); + } + + #[test] + fn cat_regression() { + regression_test("cat"); + } + + #[test] + fn ctv_regression() { + regression_test("ctv"); + } + + #[test] + fn escrow_with_delay_regression() { + regression_test("escrow_with_delay"); + } + + #[test] + fn hash_loop_regression() { + regression_test("hash_loop"); + } + + #[test] + fn hodl_vault_regression() { + regression_test("hodl_vault"); + } + + #[test] + fn htlc_regression() { + regression_test("htlc"); + } + + #[test] + fn last_will_regression() { + regression_test("last_will"); + } + + #[test] + fn non_interactive_fee_bump_regression() { + regression_test("non_interactive_fee_bump"); + } + + #[test] + fn p2ms_regression() { + regression_test("p2ms"); + } + + #[test] + fn p2pkh_regression() { + regression_test("p2pkh"); + } + + #[test] + fn presigned_vault_regression() { + regression_test("presigned_vault"); + } + + #[test] + fn reveal_collision_regression() { + regression_test("reveal_collision"); + } + + #[test] + fn reveal_fix_point_regression() { + regression_test("reveal_fix_point"); + } + + #[test] + fn sighash_all_anyonecanpay_regression() { + regression_test("sighash_all_anyonecanpay"); + } + + #[test] + fn sighash_all_anyprevoutanyscript_regression() { + regression_test("sighash_all_anyprevoutanyscript"); + } + + #[test] + fn sighash_all_anyprevout_regression() { + regression_test("sighash_all_anyprevout"); + } + + #[test] + fn sighash_none_regression() { + regression_test("sighash_none"); + } + + #[test] + fn sighash_single_regression() { + regression_test("sighash_single"); + } + + #[test] + fn transfer_with_timeout_regression() { + regression_test("transfer_with_timeout"); + } + } } diff --git a/test-data/array_fold.json b/test-data/array_fold.json new file mode 100644 index 0000000..dc81f32 --- /dev/null +++ b/test-data/array_fold.json @@ -0,0 +1,4 @@ +{ + "program": "5WJsgAAAAEEWQAAAAEDBZAAAAAYMgqFksgAAAAQGpZAAAAAoNgVWQAAAAMDesgAAAAcHAQqFkFsIFCDE2QAAAAACBYEHGIMMQcKkHGGyMPoFCSDbD7RQLBpcTmcVRCRXQmYAIq4WDF4UsJ7AwqF1hL9cVIyFlhwCtu/AEMAxCcCPwYwFA4AFwUbcbgB+GigDMWIDSDQBxCFxQPxEYbiQw/FIoA1CyAbAagONiBQkg3GlkAAAAOAwCoWLNuJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYha2Dmuu5eD0J1PRkDjOX+NuSvKMp89lTLaq09lrtGKI8VGIcMAxCcCPyTBAODAclQ=", + "witness": null +} diff --git a/test-data/array_fold_2n.json b/test-data/array_fold_2n.json new file mode 100644 index 0000000..e3405e5 --- /dev/null +++ b/test-data/array_fold_2n.json @@ -0,0 +1,4 @@ +{ + "program": "5JpsgAAAAEECBAhAoQYmyAAAAAAQLAg4w1MQcKkHGGph9AoPqMNsMPuFCSBZaBRpcTmcVRCRXQmYAIq4WDF4UsJ7AwqF1hL9cVIyFlhwCtu/AEMAxCcGPwowFA3BcDAUHBwuGAKDiELicBQcWEChJBuK7IAAAAIBgFQsWbcTmBd77UKPutD6OLpAkNFbS8SYUsZldxGEoA3o23T+XYAMMAxC1sHNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiE4EfkSCAcGA5Fg", + "witness": null +} diff --git a/test-data/cat.json b/test-data/cat.json new file mode 100644 index 0000000..c44f97a --- /dev/null +++ b/test-data/cat.json @@ -0,0 +1,4 @@ +{ + "program": "5DJsBAEWACDAKkChNiIAIIQKEkIMONRQLJm3A5rruXg9CdT0ZA4zl/jbkryjKfPZUy2qtPZa7RiiPFRiHDAMQtjBzOKohIroTMAEVcLBi8KWE9gYVC6wl+uKkZCyw4BW3fgCGAYq2Dgiug4MFSBQmwvQQgUJIQYcaigWTNtOYIi/1AcaRNMBhc3vyX09ErCRo/sv5CAbaGAJlp1bvfigwDELYYF3vtQo+60Po4ukCQ0VtLxJhSxmV3EYSgDejbdP5dgAw8PAoTgJ+OAQDgoHCwuIwGIOOAOPgA==", + "witness": null +} diff --git a/test-data/ctv.json b/test-data/ctv.json new file mode 100644 index 0000000..d11d012 --- /dev/null +++ b/test-data/ctv.json @@ -0,0 +1,4 @@ +{ + "program": "5wEkCTVkcziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQgwVw+HGBzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYhZhamq05rruXg9CdT0ZA4zl/jbkryjKfPZUy2qtPZa7RiiPFRiHDAMSBQkg3AxS/DI5n4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDAMQswtJgiL/UBxpE0wGFze/JfT0SsJGj+y/kIBtoYAmWnVu9+KDwICkChJBuIxS9wI5u8lZwLSPPkvMpefT82f85Cc97MlOKr7Djoj7EAHmx0TDAMQswtLVcjm8ToYSOAZ74jeLno8FWHRgos74PKQ3vn+6/VNqUJJRywMAxIFCSDcail+GBzfQgn7m8T+SlBrmrOch86kHUDO438Z7mfcnzyKpQtkIOGAYhZhaTaCHQWKequSoQRbKj9ZMNUNP9TZslL2ACY8pcCYeALJMHjQCkChJBuQope4Ac0vqVxn0dNTcJGVHIKgZVnO4Ri5KL+OnrThZFmsP0velhgGIWYWk20qztTpHBhF8ugDjaMMsLl9/jQllEBh7MsI4rcUNpyvh4iApAoSQbkqKX4YnNKg3OgxzUB7/tnnkIYePSA4b9lUGNvmRsOpjaQAmHTtYYBiFmFpMDLqyIAl/t8mPC4rRGEfM1lVH7CAxNfptKDrOKJGwAHAeSgFIFCSDcpRS9gcxgts54X+MRHKrfq2DJAzmcXWPmk3AyFTFgxcx5QC1WKBgGIWYWkwYulQXr0Fu/BAWgC5j7LWeBAl+pI9pt9ZFn0rcXaN1Fh5BAUgUJINy1FL8KcxsXX6Q/gR6bfNv+QIkq2TSNzaS+85toDfqr3uY18KY5BgGIWYWk2R+ra7Pj7bD7ngvTZ8puEzNaYgGZeeHhbJwsZpxe6Uuh5aAUgUJINzCijVicxaVg5c7bCv5v4Efeky6pdqN8+a2UbNm+82P8XJkfgUkBgGJAoTaa49AZswUpxgRNKz1+4uDuXbUafwXtXbjwic1dRV8fxdBCBQkhBhxqKBZM26Dmf7mfDw7DxJRotWTkSgnKVskuvR6itvdUy8omlW0PLqAMAxC2MHNuPwXvzv9stXb3QFgqI0+EkTeqbTD8hwv1sedPf8PVoQYBiE4GfmrBAODgcNA4pA46A5FgcnAOVoHL4DmRA5ogObYA==", + "witness": null +} diff --git a/test-data/escrow_with_delay.json b/test-data/escrow_with_delay.json new file mode 100644 index 0000000..aca444c --- /dev/null +++ b/test-data/escrow_with_delay.json @@ -0,0 +1,4 @@ +{ + "program": "56XQKEGJtLzfMz987l3WKtAxSudDhYOBTf5tlucUbKz5QK2LfAvMAgTFNpxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUEIFCbT8mEUAySxhiCSaJ8L8TqkU2pjkIsG3zNhDAPiJ3nAbfIIQKEGHHGwONkKBYj8JOOFgQYgUH4hOQKE2AACDcIMOQYYLALQPicgUIMONxOccgwVJIEnDmuu5eD0J1PRkDjOX+NuSvKMp89lTLaq09lrtGKI8VGIcMAxCcUJIPxAbjkUH4CcKhZtjDmBd77UKPutD6OLpAkNFbS8SYUsZldxGEoA3o23T+XYAMMAxCbj8lwQDgQHDCSD8TCjTGnMERf6gONImmAwub35L6eiVhI0f2X8hANtDAEy06t3vxQYBiQKEkIMMSQJjmA5nFUQkV0JmACKuFgxeFLCewMKhdYS/XFSMhZYcArbvwBDAMQNIW1g5n4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDAMR+QhgqPyHBAOFBcTAMA8igORwHJMgUG5Im5bHGHHG5QHGHCxC1gOAIFBuUB+YU4w444/LA4w44WQW0BxCgUJsAIEIFCSDcuDjcuhQLFm2nN3krOBaR58l5lLz6fmz/nITnvZkpxVfYcdEfYgA82OiYYBiFrN4nQwkcAz3xG8XPR4KsOjBRZ3weUhvfP91+qbUoSSjlgeQgFCcAA5TA4GBwkDisDjsDmcA5oQOaYkUAcsAc24HN6Bzhgc5A3MgkDnJE5kxc03MGBy4sQPoBzKBObVJCDBV5Tm0EOgsU9VclQgi2VH6yYaoaf6mzZKXsAEx5S4Ew8AWSYMAxCagOYkG0LgIDmMBwUDhIPCwOeYA=", + "witness": null +} diff --git a/test-data/hash_loop.json b/test-data/hash_loop.json new file mode 100644 index 0000000..b53b985 --- /dev/null +++ b/test-data/hash_loop.json @@ -0,0 +1,4 @@ +{ + "program": "6EZJAk1ZHN3krOBaR58l5lLz6fmz/nITnvZkpxVfYcdEfYgA82OiYYBiQKEGJFB8iUUCyG0IOSQKhZDbzcCMOQcYcfA4VC0BbBuGIOSQKhZDcRoOSQKhZDcVG3GHH4WYcfhxwqFmFrG42NxyYcg4w4+BwqFoC2Dcg0HJIFQshuRSDkkCoWQ3JE24w4/IIw4/IU4VCzC1jcnUHJIFQshuUiDkkCoWQ3Ks24w4/Jww4/J84VCzC1jctzcaGHH5AmHH5CHCoWYWsbmDNzDGHIOMOPgcKhaAthJBuZJBxwsAsmqjmcVRCRXQmYAIq4WDF4UsJ7AwqF1hL9cVIyFlhwCtu/AEMAxIFCSD7BRmynNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiQKDc1RxSSBMNWJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYiA6gNwHCgOKgOOAOQoHJIJyXQYQXk0Lk4FyfAaQ4gaQOUgTlSbWQXlWLlaFywAaA4gaAOWoHMEE5hzcGILzEi5iwuY4BoDiBoA5kwnMwbiAgvM0LmcC5nwGgOIGgDmkA5sgOb8JziG4sILzjC5yAucsBoDiBoA50AnOqbjYgvOuLnZC53AGgOIGgDnhA57wnPobkCQXn2Fz8Bc/YDQHEDQB0AAToBDciyC9ASLoCAugMAaA4gaAOgOIFCbSgV/l06WxJFyPX6jJHNLOkuKw8L96O1DhziIEzX8okQAIQKEkG5OoMOFiSLEzohnOQs8/+3xpn819ObmnDkhDQn925GJoUwiWnfmwmLh4UBRzbSrO1OkcGEXy6AONowywuX3+NCWUQGHsywjitxQ2nK+GAaQtwXAGbdBzfQgn7m8T+SlBrmrOch86kHUDO438Z7mfcnzyKpQtkIOGAYhcGYObQQ6CxT1VyVCCLZUfrJhqhp/qbNkpewATHlLgTDwBZJgwDEJw0/QcggHEIHE4HQeAA==", + "witness": null +} diff --git a/test-data/hodl_vault.json b/test-data/hodl_vault.json new file mode 100644 index 0000000..32c42e8 --- /dev/null +++ b/test-data/hodl_vault.json @@ -0,0 +1,4 @@ +{ + "program": "53k2QAAAfQCECh0ChJCDDjUUCyacAxOb6EE/c3ifyUoNc1ZzkPnUg6gZ3G/jPcz7k+eRVKFshBwwDELYwc2gh0FinqrkqEEWyo/WTDVDT/U2bJS9gAmPKXAmHgCyTBgGIXAt3g5tpVnanSODCL5dAHG0YZYXL7/GhLKIDD2ZYRxW4obTlfDAMVkAAMNQA4UQKHQKEkG4gONxEKBYmBl1ZEAS/2+THhcVojCPmayqj9hAYmv02lB1nFEjYADgPDwKFpNL6lcZ9HTU3CRlRyCoGVZzuEYuSi/jp604WRZrD9L3pYeGgVaXm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYBvIFDoFCDDj8ZnHHH4QcKkGCoWsgw5AoSQJNWRzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYkChJCDDclDDhYBZtVpzBEX+oDjSJpgMLm9+S+nolYSNH9l/IQDbQwBMtOrd78UGAYkChJBuAH5RmHHCxCzM/BVazZBMsdgY/lS0WYcYfsNRJVmhtHQmf/PVrNEOe4weAAUgUJINw4UasTm8ToYSOAZ74jeLno8FWHRgos74PKQ3vn+6/VNqUJJRywMAxIFCSDciDjjjjjcjhQfkMccccccLQFwFsYc3eSs4FpHnyXmUvPp+bP+chOe9mSnFV9hx0R9iADzY6JhgGIThB+ZkEA4YBxQBxuByIA5JgcmSbTjAj/KIPa+tpgioDdK4D5sLjvHJcZ3nlPV1gTcrjhPcoIQKHQKDcrzjcpxQkgScOZxVEJFdCZgAirhYMXhSwnsDCoXWEv1xUjIWWHAK278AQwDEgUJINy/ONzAig/Lk4VC0Guu5eD0J1PRkDjOX+NuSvKMp89lTLaq09lrtGKI8VGIcPFwFCbQOLAcAA4WSKAOMQcRgcThcWgONQcywHM0FzPgOOQc14HNmFzcAOPRc4ADj8HOqBzsg==", + "witness": "AAAD6AABhqCQIxuN6WoflA3c9Ab+g4lBfKj7CwMVFgji+UsxtEOn4NJqEuQ332kCjwkCfDfV9nQqEMHohkBh0Rm4u86WLSbT8jQfVx8Gkhbt/HKCL2CUuOwznC9y3GSuoO7R49YKv0Vy/dBGGOW1vGcszXHPrxJbbBsQGuyjp7k4/oOTKrOHQw==" +} diff --git a/test-data/htlc.json b/test-data/htlc.json new file mode 100644 index 0000000..fd00814 --- /dev/null +++ b/test-data/htlc.json @@ -0,0 +1,4 @@ +{ + "program": "5lnQKEGIFCDDEHGCwJIEmrI5nFUQkV0JmACKuFgxeFLCewMKhdYS/XFSMhZYcArbvwBDAMQm9JCDD8EFAsWq5HNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiQKEkG3CjVicwLvfahR91ofRxdIEhoraXiTCljMruIwlAG9G26fy7ABhgGIGoDhIHFATi5NpZmh6rfhivXdsj8GLjp+OIAiXFIVu4jOzkCpZHQ1fKSUEIFCSDcZHG40FAsWbdBzd5KzgWkefJeZS8+n5s/5yE572ZKcVX2HHRH2IAPNjomGAYha2Dm8ToYSOAZ74jeLno8FWHRgos74PKQ3vn+6/VNqUJJRywMAxWl5vmZ++dy7rFWgYpXOhwsHApv82y3OKNlZ8oFbFvgXmAcDIFBuQx+QZxwsCSBJw5giL/UBxpE0wGFze/JfT0SsJGj+y/kIBtoYAmWnVu9+KDAMQnJcnFJ+SgnEgnABYNjDmfgqtZsgmWOwMfypaLMOMP2GokqzQ2joTP/nq1miHPcYMAxCcZn5SggG4DhBIoAyBxCFxMA0g46A5AAcpSRQBvBywG5WpsgAAA+gECcsUkIMFXg5tBDoLFPVXJUIItlR+smGqGn+ps2Sl7ABMeUuBMPAFkmDAMVpxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUGsgUG5gz8wJwqA4mBkFrAcVg4OBwwHhwHMmA==", + "witness": null +} diff --git a/test-data/last_will.json b/test-data/last_will.json new file mode 100644 index 0000000..3dace1a --- /dev/null +++ b/test-data/last_will.json @@ -0,0 +1,4 @@ +{ + "program": "5wnQKEGJsWVABAmKSEGCrynMGLpUF69BbvwQFoAuY+y1ngQJfqSPabfWRZ9K3F2jdRYYBitLzfMz987l3WKtAxSudDhYOBTf5tlucUbKz5QK2LfAvMA1kChBh+DHCpJAk4cziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQbgJxuBig/FJwqFobGHNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiE3n41BAOBgcOJFAGQOJwuLAGkHHAHHpBiBQbkHacYEf5RB7X1tMEVAbpXAfNhcd45LjO88p6usCblccJ7lByDCchhcRcOA4GJxgBwcGIGlafkwigGSWMMQSTRPhfidUim1MchFg2+ZsIYB8RO84Db5ByMCcj0kCT4YnM/BVazZBMsdgY/lS0WYcYfsNRJVmhtHQmf/PVrNEOe4wYBisgAAAAIGkKhambcTmCIv9QHGkTTAYXN78l9PRKwkaP7L+QgG2hgCZadW734oMAxC4AwcwLvfahR91ofRxdIEhoraXiTCljMruIwlAG9G26fy7ABhgGL4cEObxOhhI4BnviN4uejwVYdGCizvg8pDe+f7r9U2pQklHLAwDhITlckiyAAAAAAUKvhqObQQ6CxT1VyVCCLZUfrJhqhp/qbNkpewATHlLgTDwBZJgwDELSQkUM6IZzkLPP/t8aZ/NfTm5pw5IQ0J/duRiaFMIlp35sJi5uVAYBgObvJWcC0jz5LzKXn0/Nn/OQnPezJTiq+w46I+xAB5sdEwwDWQKEkH1m5aCgWDNug5tpVnanSODCL5dAHG0YZYXL7/GhLKIDD2ZYRxW4obTlfDAMQtRvoQT9zeJ/JSg1zVnOQ+dSDqBncb+M9zPuT55FUoWyEHDxeBVkAAAAAg4AFwB8NhzSoNzoMc1Ae/7Z55CGHj0gOG/ZVBjb5kbDqY2kAJh07WGAYhcGISKEGB4nAwHMDLqyIAl/t8mPC4rRGEfM1lVH7CAxNfptKDrOKJGwAHAYBrC4iNL6lcZ9HTU3CRlRyCoGVZzuEYuSi/jp604WRZrD9L3pYeQ4FCcTgcuhcLAcvAcWAchQuRgDmABzNhOZwDmDFyvAcwoOVQHLcHleBy6B5fgc34A==", + "witness": null +} diff --git a/test-data/non_interactive_fee_bump.json b/test-data/non_interactive_fee_bump.json new file mode 100644 index 0000000..e329468 --- /dev/null +++ b/test-data/non_interactive_fee_bump.json @@ -0,0 +1,4 @@ +{ + "program": "6FtJAk1ZHMGLpUF69BbvwQFoAuY+y1ngQJfqSPabfWRZ9K3F2jdRYYBiQKEkIMFcPhxic2R+ra7Pj7bD7ngvTZ8puEzNaYgGZeeHhbJwsZpxe6UuhgGIWYWpquRzGC2znhf4xEcqt+rYMkDOZxdY+aTcDIVMWDFzHlALVYoGAYkChJBuBs4qiEiuhMwARVwsGLwpYT2BhULrCX64qRkLLDgFbd+AIeGgcQBOF/DjA5gXe+1Cj7rQ+ji6QJDRW0vEmFLGZXcRhKAN6Nt0/l2ADDAOFBcMC4e1WnNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiE4KLgfBOxOZ+Cq1myCZY7Ax/Klosw4w/YaiSrNDaOhM/+erWaIc9xgwDELhAXCjBEX+oDjSJpgMLm9+S+nolYSNH9l/IQDbQwBMtOrd78UHiMCkChJFkAAAAAAoVdoObxOhhI4BnviN4uejwVYdGCizvg8pDe+f7r9U2pQklHLAwDEgUJIEigAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5u8lZwLSPPkvMpefT82f85Cc97MlOKr7Djoj7EAHmx0TDAMSSDciDkGCoWRtBDoLFPVXJUIItlR+smGqGn+ps2Sl7ABMeUuBMPAFkmDyEAoOwDgoTg6SDclhS9qObaVZ2p0jgwi+XQBxtGGWFy+/xoSyiAw9mWEcVuKG05XwwDELMLSb6EE/c3ifyUoNc1ZzkPnUg6gZ3G/jPcz7k+eRVKFshBw8lQKQKEkG5TCl7M5gZdWRAEv9vkx4XFaIwj5msqo/YQGJr9NpQdZxRI2AA4DAMQswtJpfUrjPo6am4SMqOQVAyrOdwjFyUX8dPWnCyLNYfpe9LDylApAoSQblsKNWJzSoNzoMc1Ae/7Z55CGHj0gOG/ZVBjb5kbDqY2kAJh07WGAYgagOFgcWAcjwOToHKsDlkFyxC5ZmNi6/SH8CPTb5t/yBElWyaRubSX3nNtAb9Ve9zGvhTHIPLwChOTIuS/Jjqc24/Be/O/2y1dvdAWCojT4SRN6ptMPyHC/Wx509/w9WhBgGIXIgLk8YtKwcudthX838CPvSZdUu1G+fNbKNmzfebH+LkyPwKSA8xAFCcmfhTmb7B9JpOOvjzvkKphS8NtLvwwbUVNndvXlfFNOmqzOGsMA5BhchQuQ5n+5nw8Ow8SUaLVk5EoJylbJLr0eorb3VMvKJpVtDy6gDy+AoTj4XHnHbRWUSWl08PCaXtsZqXile+vysLSXJSK/YvyaDzmRBx9SHioCgYgcBA4cBzMAc1gHNsE5r0+ZHNpqpAItHKrs2xH5eK86mYeAbd+MQutrvrA8MwoShaA0gYBzdhObZNpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEIFCSDcsBR8N5zWa0KAyBMXrJ2UBojLw7OBjrZ0EbMpXskhjjF92sbQtsGAYkChNkztL/hghAoSQgw4442IoFoaaMTmtEl5A4Z3ZSM0hoHMj/FV7/8/Z68+Dm0wBXPZawQlCdcMAxIFCDDEkCY5wJTmqVRe0D8SUO6jtVao1FB17aQ8lLuOQxt7+3/8rw1C9SoMAxWAADSFQtTNtOZ03GjZDW7Ftn60asj836Lt9HupDDp2uz1U/XBdavUDlQwDELgDBzAv2aelCyR1GDcx2SY+gQtQ00SgKpNwMYBiJyyVY9pwKGAYhODn5+AQDhZNmAAAAAAAAB9AIQKEkIMPxsYcLJnAshzX9G/tSATW0YkPScXlEdsUOEAfcwsJM88ltF3cmoyL9sGAYhagtrS5HMsYPv9fumQQOGTcadno3VyfJMNL6AKNsiQnkqiPybFXAYBiQKDccmAcPSQbhJxxxx+HGCoWlpwHI5jZp+Z5zc+ZtdrGOgHNi/BIOR+PffaUEGJqgDEH4VhfkDAMQt5nKaJX6zFdMeufriqUp0DTwxgwwS1u/oeoheaBmR2H4LDxwBQnBAONhcHAccA4YBxmFxyA48ByZA5VgcsQOXoHMGBzFLTN98aqtsByHXPwX2WdPzQcXK738ixLdrA63cga6DZMsgOYwgUJINz5m59DhU4oFm2MOYtNB6xYfNxwI3ft475qKr3JHVqA8Jvj4kUVX65L0SrKgwDEJuA5LA4CFwYByZBzRgA==", + "witness": "5mCM62b2KJbKB2YZZN0qsIZ9+Uyq6wiawJCJ0Twjz2TuOg1C+PhML2J9QjDJ81eRnEiidBF+OMnD0yoOh1cLRQ==" +} diff --git a/test-data/p2ms.json b/test-data/p2ms.json new file mode 100644 index 0000000..8087902 --- /dev/null +++ b/test-data/p2ms.json @@ -0,0 +1,4 @@ +{ + "program": "5uk2l5vmZ++dy7rFWgYpXOhwsHApv82y3OKNlZ8oFbFvgXmARacYEf5RB7X1tMEVAbpXAfNhcd45LjO88p6usCblccJ7lBgtPyYRQDJLGGIJJonwvxOqRTamOQiwbfM2EMA+InecBt8gyCoWRAoQY4oNggUIOOQKE2AACEGGHIMMFgFpHxOQKEGHG4AccgwVJIEnDmuu5eD0J1PRkDjOX+NuSvKMp89lTLaq09lrtGKI8VGIcMAxCcWpIPxAbiIUH4CcKhZtjDmBd77UKPutD6OLpAkNFbS8SYUsZldxGEoA3o23T+XYAMMAxCbj8fggHAgOGEkH4mFGmNOYIi/1AcaRNMBhc3vyX09ErCRo/sv5CAbaGAJlp1bvfigwDEgUJIQYYkgTHMBzOKohIroTMAEVcLBi8KWE9gYVC6wl+uKkZCyw4BW3fgCGAYgaQtrBzPwVWs2QTLHYGP5UtFmHGH7DUSVZobR0Jn/z1azRDnuMGAYj8hDBUfkOCAcKC4mAYB5FAcjgOSZAoNyRNyfOMOONygOMOFiFrAcAQKDcoD8rjjDjjj8sDjDjhZBbQHEKBQmwAgQgUJINy4ONy6FAsWbac3eSs4FpHnyXmUvPp+bP+chOe9mSnFV9hx0R9iADzY6JhgGIWs3idDCRwDPfEbxc9Hgqw6MFFnfB5SG98/3X6ptShJKOWB5CAUJwADlMDgYHCQOKwOOwOZ0DmjA5pyRQBywBzXg=", + "witness": "+6WeUroyP8LKsSWJSZJX0XnFrMVODj5+L4RU4Bt2LWaeB93Pae1y5RHQUy0aWutmZutdEkTC6wIPvZCTFYvXt6U7fVasUVyOV5x8EOUdWjMv3vE6nglrfHOYEWbFuEU+qn+mp/FBWf+/e7qOOitBu0dmDQhILf5I14DoxcrM/XEg" +} diff --git a/test-data/p2pkh.json b/test-data/p2pkh.json new file mode 100644 index 0000000..1a8911e --- /dev/null +++ b/test-data/p2pkh.json @@ -0,0 +1,4 @@ +{ + "program": "5PugUJtImXnNTGGN1W7dMpL66h+UqiOQS/0Kky+iiYX+24co73ghAoQYckgSasjmcVRCRXQmYAIq4WDF4UsJ7AwqF1hL9cVIyFlhwCtu/AEMAxCa0kIMPwAUCxarkc113LwehOp6MgcZy/xtyV5RlPnsqZbVWnstdoxRHioxDhgGJAoSQbcKNWJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYgagOEgcRkChJBuKjcVnCoWLNug5n4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDAMQtbBzBEX+oDjSJpgMLm9+S+nolYSNH9l/IQDbQwBMtOrd78UGAYhcB4Fw5vE6GEjgGe+I3i56PBVh0YKLO+DykN75/uv1TalCSUcsDAMSBQkg/II43IYUOKBZtjDm7yVnAtI8+S8yl59PzZ/zkJz3syU4qvsOOiPsQAebHRMMAxA3hcIH5MggHFoHJYDk4A==", + "witness": "eb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5j3SzyldGR/hZViSxKTJK+i84tZipwcfPxfCKnANuxazTwPu57T2uXKI6CmWjS11szN1rokiYXWBB97ISYrF69v" +} diff --git a/test-data/presigned_vault.json b/test-data/presigned_vault.json new file mode 100644 index 0000000..2d0c80a --- /dev/null +++ b/test-data/presigned_vault.json @@ -0,0 +1,4 @@ +{ + "program": "5FugUIMTYgfQCBMUkIMFXlOYF3vtQo+60Po4ukCQ0VtLxJhSxmV3EYSgDejbdP5dgAwwDFaXm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYBrIFCDD8GOFSSBJw5nFUQkV0JmACKuFgxeFLCewMKhdYS/XFSMhZYcArbvwBDAMSBQkg3ATjcDFB+KThULQ2MOa67l4PQnU9GQOM5f425K8oynz2VMtqrT2Wu0YojxUYhwwDEJvPxqCAcDA4cSKAMgcThcWANIOOAOPVpxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUHHwTkCLhvCQNwKByEB0AciAA==", + "witness": null +} diff --git a/test-data/reveal_collision.json b/test-data/reveal_collision.json new file mode 100644 index 0000000..421f776 --- /dev/null +++ b/test-data/reveal_collision.json @@ -0,0 +1,4 @@ +{ + "program": "5RugUOgUJIQYcaigWTNug5vE6GEjgGe+I3i56PBVh0YKLO+DykN75/uv1TalCSUcsDAMSSECjMBzOKohIroTMAEVcLBi8KWE9gYVC6wl+uKkZCyw4BW3fgCGAYgaguDMHM/BVazZBMsdgY/lS0WYcYfsNRJVmhtHQmf/PVrNEOe4wYBiSQJNWRzXXcvB6E6noyBxnL/G3JXlGU+eypltVaey12jFEeKjEOGAYhOI0kIMPxQKBYtVyOYF3vtQo+60Po4ukCQ0VtLxJhSxmV3EYSgDejbdP5dgAwwDEgUJINuFGrE5giL/UBxpE0wGFze/JfT0SsJGj+y/kIBtoYAmWnVu9+KDAMQNQHCQOOyBQfj0DFAoSQbkMcbkQKBYm0EOgsU9VclQgi2VH6yYaoaf6mzZKXsAEx5S4Ew8AWSYPIUChaTd5KzgWkefJeZS8+n5s/5yE572ZKcVX2HHRH2IAPNjomHjkChNx+TQIBwIDg4XHoDEHJwDlAA=", + "witness": null +} diff --git a/test-data/reveal_fix_point.json b/test-data/reveal_fix_point.json new file mode 100644 index 0000000..9a51321 --- /dev/null +++ b/test-data/reveal_fix_point.json @@ -0,0 +1,4 @@ +{ + "program": "5AOgUIMSQJNWRzOKohIroTMAEVcLBi8KWE9gYVC6wl+uKkZCyw4BW3fgCGAYhNSSEGH3igWLVcjmuu5eD0J1PRkDjOX+NuSvKMp89lTLaq09lrtGKI8VGIcMAxIFCSDbhRqxOYF3vtQo+60Po4ukCQ0VtLxJhSxmV3EYSgDejbdP5dgAwwDEDUBwkDiMgUJIPxQbioUCwZt0HM/BVazZBMsdgY/lS0WYcYfsNRJVmhtHQmf/PVrNEOe4wYBiFqYOYIi/1AcaRNMBhc3vyX09ErCRo/sv5CAbaGAJlp1bvfigwDEJwE/HYIBwUDj0", + "witness": null +} diff --git a/test-data/sighash_all_anyonecanpay.json b/test-data/sighash_all_anyonecanpay.json new file mode 100644 index 0000000..2706e60 --- /dev/null +++ b/test-data/sighash_all_anyonecanpay.json @@ -0,0 +1,4 @@ +{ + "program": "5+0kCTVkcziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQgwVw+HGJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYhZhamq5HNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiQKEkG4GKVn4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDwIChYhZmCIv9QHGkTTAYXN78l9PRKwkaP7L+QgG2hgCZadW734oPAAKQKEkG4hFL8OMDm7yVnAtI8+S8yl59PzZ/zkJz3syU4qvsOOiPsQAebHRMMAxCzC0tVpzeJ0MJHAM98RvFz0eCrDowUWd8HlIb3z/dfqm1KEko5YGAYkChJBuMxS/DI5voQT9zeJ/JSg1zVnOQ+dSDqBncb+M9zPuT55FUoWyEHDAMQswtJtBDoLFPVXJUIItlR+smGqGn+ps2Sl7ABMeUuBMPAFkmDwICkChJBuQYpepzS+pXGfR01NwkZUcgqBlWc7hGLkov46etOFkWaw/S96WGAYhZhaTbSrO1OkcGEXy6AONowywuX3+NCWUQGHsywjitxQ2nK+HkEBSBQkg3JMUvwpzGC2znhf4xEcqt+rYMkDOZxdY+aTcDIVMWDFzHlALVYoGAYhaHbjmDF0qC9egt34IC0AXMfZazwIEv1JHtNvrIs+lbi7RuosMAxC3EJFDOiGc5Czz/7fGmfzX05uacOSENCf3bkYmhTCJad+bCYugwNAxHNKg3OgxzUB7/tnnkIYePSA4b9lUGNvmRsOpjaQAmHTtYYBsC4UFwwwMurIgCX+3yY8LitEYR8zWVUfsIDE1+m0oOs4okbAAcB5SgUgUJINy2FK24/Be/O/2y1dvdAWCojT4SRN6ptMPyHC/Wx509/w9WhB4jAoWTuEHMWlYOXO2wr+b+BH3pMuqXajfPmtlGzZvvNj/FyZH4FJAYBiFsMbF1+kP4Eem3zb/kCJKtk0jc2kvvObaA36q97mNfCmOQeFAULeFwE2R+ra7Pj7bD7ngvTZ8puEzNaYgGZeeHhbJwsZpxe6Uuh5egUgUJINzHCl7A5m+wfSaTjr4875CqYUvDbS78MG1FTZ3b15XxTTpqszhrDAMQswtJn+5nw8Ow8SUaLVk5EoJylbJLr0eorb3VMvKJpVtDy6gDzGgUgUJINzPCl7I5gX7NPShZI6jBuY7JMfQIWoaaJQFUm4GMAxE5ZKse04FDAMQswtJorKJLS6eHhNL22M1LxSvfX5WFpLkpFfsX5NB5zIg4+pDzOgUgUJINzXCl7ac1SqL2gfiSh3UdqrVGooOvbSHkpdxyGNvf2//leGoXqVBgGIWYWkzpuNGyGt2LbP1o1ZH5v0Xb6PdSGHTtdnqp+uC61eoHKh5rQKQKEkG5vhRqxObTVSARaOVXZtiPy8V51Mw8A278YhdbXfWB4ZhQlC0BpAwDELQPyhazWhQGQJi9ZOygNEZeHZwMdbOgjZlK9kkMcYvu1jaFtgwDAgUJtLzfMz987l3WKtAxSudDhYOBTf5tlucUbKz5QK2LfAvMAhAoSQblcblecKnFAs2xhzWiS8gcM7spGaQ0DmR/iq9/+fs9efBzaYArnstYIShOuGAYhNx+eQEA4EBwgDikDjoDkWBycA5agczIHNUBzdgc5wHO6Bz4g", + "witness": "6uDgKrlIWLWlcSUQB09YGfwpnWKmQSpjc8v5LrQqYnT7cIJ6DW2oTPVJ2ZsURU8VA9+CoU1RQZ+k+wje5jdFMQ==" +} diff --git a/test-data/sighash_all_anyprevout.json b/test-data/sighash_all_anyprevout.json new file mode 100644 index 0000000..ee00f6a --- /dev/null +++ b/test-data/sighash_all_anyprevout.json @@ -0,0 +1,4 @@ +{ + "program": "6AZJAk1ZHM4qiEiuhMwARVwsGLwpYT2BhULrCX64qRkLLDgFbd+AIYBiQKEkIMFcPhxicwLvfahR91ofRxdIEhoraXiTCljMruIwlAG9G26fy7ABhgGIWYWpquRzXXcvB6E6noyBxnL/G3JXlGU+eypltVaey12jFEeKjEOGAYkChJBuBilZ+Cq1myCZY7Ax/Klosw4w/YaiSrNDaOhM/+erWaIc9xg8CAoWIWZgiL/UBxpE0wGFze/JfT0SsJGj+y/kIBtoYAmWnVu9+KDwACkChJBuIRS/DjA5u8lZwLSPPkvMpefT82f85Cc97MlOKr7Djoj7EAHmx0TDAMQswtLVac3idDCRwDPfEbxc9Hgqw6MFFnfB5SG98/3X6ptShJKOWBgGJAoSQbjMUvwyOb6EE/c3ifyUoNc1ZzkPnUg6gZ3G/jPcz7k+eRVKFshBwwDELMLSbQQ6CxT1VyVCCLZUfrJhqhp/qbNkpewATHlLgTDwBZJg8CApAoSQbkGKXqc0vqVxn0dNTcJGVHIKgZVnO4Ri5KL+OnrThZFmsP0velhgGIWYWk20qztTpHBhF8ugDjaMMsLl9/jQllEBh7MsI4rcUNpyvh5BAUgUJINyTFL8OCnNKg3OgxzUB7/tnnkIYePSA4b9lUGNvmRsOpjaQAmHTtYYBiFmFpMDLqyIAl/t8mPC4rRGEfM1lVH7CAxNfptKDrOKJGwAHAeMgKQKEkG5Ril+HBjmMFtnPC/xiI5Vb9WwZIGczi6x80m4GQqYsGLmPKAWqxQMAxCzC0u4gOYMXSoL16C3fggLQBcx9lrPAgS/Uke02+siz6VuLtG6iwwDEgUJINy1FL8Kc24/Be/O/2y1dvdAWCojT4SRN6ptMPyHC/Wx509/w9WhBgGIWh3CDmLSsHLnbYV/N/Aj70mXVLtRvnzWyjZs33mx/i5Mj8CkgMAxC3EJFDOiGc5Czz/7fGmfzX05uacOSENCf3bkYmhTCJad+bCYugwNAxHMbF1+kP4Eem3zb/kCJKtk0jc2kvvObaA36q97mNfCmOQYBsC4UFww2R+ra7Pj7bD7ngvTZ8puEzNaYgGZeeHhbJwsZpxe6Uuh5hwKQKEkG5mBS9gczfYPpNJx18ed8hVMKXhtpd+GDaips7t68r4pp01WZw1hgGIWYWkz/cz4eHYeJKNFqyciUE5Stkl16PUVt7qmXlE0q2h5dQB5lwKQKEkG5qBS9kcwL9mnpQskdRg3MdkmPoELUNNEoCqTcDGAYicslWPacChgGIWYWk0VlElpdPDwml7bGal4pXvr8rC0lyUiv2L8mg85kQcfUh5pwKQKEkG5uBS9tOapVF7QPxJQ7qO1VqjUUHXtpDyUu45DG3v7f/yvDUL1KgwDELMLSZ03GjZDW7Ftn60asj836Lt9HupDDp2uz1U/XBdavUDlQ824FIFCSDc5Ao1YnNpqpAItHKrs2xH5eK86mYeAbd+MQutrvrA8MwoShaA0gYBiFoH5EtZrQoDIExesnZQGiMvDs4GOtnQRsyleySGOMX3axtC2wYBgQKE2l5vmZ++dy7rFWgYpXOhwsHApv82y3OKNlZ8oFbFvgXmAQgUJINyWNyXOFTigWbYw5rRJeQOGd2UjNIaBzI/xVe//P2evPg5tMAVz2WsEJQnXDAMQm4/PcCAcCA4QBxSBx0ByLA5OAcwAHMmBzSAc2oHOMBzsgc9AHP4A", + "witness": "l0dMPuxHNzcWAEoplAWtCm3m0LGK6y2Wbk6Ib0XeL41TBH/Ba4v/y0WxbddZqTl8jGofXViD4ty7bYm5Cuxd/Q==" +} diff --git a/test-data/sighash_all_anyprevoutanyscript.json b/test-data/sighash_all_anyprevoutanyscript.json new file mode 100644 index 0000000..d8e6e2a --- /dev/null +++ b/test-data/sighash_all_anyprevoutanyscript.json @@ -0,0 +1,4 @@ +{ + "program": "540kCTVkcziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQgwVw+HGJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYhZhamq5HNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiQKEkG4GKVn4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDwIChYhZmCIv9QHGkTTAYXN78l9PRKwkaP7L+QgG2hgCZadW734oPAAKQKEkG4hFL8OMDm7yVnAtI8+S8yl59PzZ/zkJz3syU4qvsOOiPsQAebHRMMAxCzC0tVpzeJ0MJHAM98RvFz0eCrDowUWd8HlIb3z/dfqm1KEko5YGAYkChJBuMxS/DI5voQT9zeJ/JSg1zVnOQ+dSDqBncb+M9zPuT55FUoWyEHDAMQswtJtBDoLFPVXJUIItlR+smGqGn+ps2Sl7ABMeUuBMPAFkmDwICkChJBuQYpepzS+pXGfR01NwkZUcgqBlWc7hGLkov46etOFkWaw/S96WGAYhZhaTbSrO1OkcGEXy6AONowywuX3+NCWUQGHsywjitxQ2nK+HkEBSBQkg3JMUvw4Kc0qDc6DHNQHv+2eeQhh49IDhv2VQY2+ZGw6mNpACYdO1hgGIWYWkwMurIgCX+3yY8LitEYR8zWVUfsIDE1+m0oOs4okbAAcB4yApAoSQblGKX4cGOYwW2c8L/GIjlVv1bBkgZzOLrHzSbgZCpiwYuY8oBarFAwDELMLS7iA5gxdKgvXoLd+CAtAFzH2Ws8CBL9SR7Tb6yLPpW4u0bqLDAMSBQkg3LUUvYHMbF1+kP4Eem3zb/kCJKtk0jc2kvvObaA36q97mNfCmOQYBiFmFpNkfq2uz4+2w+54L02fKbhMzWmIBmXnh4WycLGacXulLoeWgFIFCSDcwopeyObcfgvfnf7Zau3ugLBURp8JIm9U2mH5DhfrY86e/4erQgwDELMLSYtKwcudthX838CPvSZdUu1G+fNbKNmzfebH+LkyPwKSA8wgFIFCSDcyope2nM32D6TScdfHnfIVTCl4baXfhg2oqbO7evK+KadNVmcNYYBiFmFpM/3M+Hh2HiSjRasnIlBOUrZJdej1Fbe6pl5RNKtoeXUAeZQCkChJBuaUUasTmisoktLp4eE0vbYzUvFK99flYWkuSkV+xfk0HnMiDj6kMAxC0EHMC/Zp6ULJHUYNzHZJj6BC1DTRKAqk3AxgGInLJVj2nAoYBiQKE2l5vmZ++dy7rFWgYpXOhwsHApv82y3OKNlZ8oFbFvgXmAQgUJINuNvOFTigWbYw5nTcaNkNbsW2frRqyPzfou30e6kMOna7PVT9cF1q9QOVDAMQm4/OYCAcCA4QBxUBx2ByMA5OgcsAOYEDmSA5owObYDnFA52QA==", + "witness": "goe1wgj38QRpwLi+Ez8zRk3CCCrfE76Ayc7OoVy+958RLhIlM7wemd5lEFkafXwyCT46Mb9g1qaE+H9orEKTyg==" +} diff --git a/test-data/sighash_none.json b/test-data/sighash_none.json new file mode 100644 index 0000000..23ca93a --- /dev/null +++ b/test-data/sighash_none.json @@ -0,0 +1,4 @@ +{ + "program": "5v0kCTVkcziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQgwVw+HGJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYhZhamq5HNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiQKEkG4GKVn4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDwIChYhZmCIv9QHGkTTAYXN78l9PRKwkaP7L+QgG2hgCZadW734oPAAKQKEkG4hFL8OMDm7yVnAtI8+S8yl59PzZ/zkJz3syU4qvsOOiPsQAebHRMMAxCzC0tVpzeJ0MJHAM98RvFz0eCrDowUWd8HlIb3z/dfqm1KEko5YGAYkChJBuMxS/DI5voQT9zeJ/JSg1zVnOQ+dSDqBncb+M9zPuT55FUoWyEHDAMQswtJtBDoLFPVXJUIItlR+smGqGn+ps2Sl7ABMeUuBMPAFkmDwICkChJBuQYpepzS+pXGfR01NwkZUcgqBlWc7hGLkov46etOFkWaw/S96WGAYhZhaTbSrO1OkcGEXy6AONowywuX3+NCWUQGHsywjitxQ2nK+HkEBSBQkg3JMUvYnNKg3OgxzUB7/tnnkIYePSA4b9lUGNvmRsOpjaQAmHTtYYBiFmFpMDLqyIAl/t8mPC4rRGEfM1lVH7CAxNfptKDrOKJGwAHAeSQFIFCSDcoxS9mcxgts54X+MRHKrfq2DJAzmcXWPmk3AyFTFgxcx5QC1WKBgGIWYWkwYulQXr0Fu/BAWgC5j7LWeBAl+pI9pt9ZFn0rcXaN1Fh5RAUgUJINyzFL8KcxsXX6Q/gR6bfNv+QIkq2TSNzaS+85toDfqr3uY18KY5BgGIWYWk2R+ra7Pj7bD7ngvTZ8puEzNaYgGZeeHhbJwsZpxe6Uuh5JAUgUJINzBijVicxaVg5c7bCv5v4Efeky6pdqN8+a2UbNm+82P8XJkfgUkBgGIWgg5tx+C9+d/tlq7e6AsFRGnwkib1TaYfkOF+tjzp7/h6tCDAMSBQm0vN8zP3zuXdYq0DFK50OFg4FN/m2W5xRsrPlArYt8C8wCEChJBtxt5wqcUCzbGHM/3M+Hh2HiSjRasnIlBOUrZJdej1Fbe6pl5RNKtoeXUAYBiE3H5qgQDgQHCAOKgOOwORgHJ0DlcBy/A5kgOaEDm1A=", + "witness": "daDW/7G3k77Wd5aIA/Fch5teU8DWAHEmSw+YMK1NSTeVY31OKTXGLjlBJSpD0FqypkrpPf6PdiLfEAHHGaePkQ==" +} diff --git a/test-data/sighash_single.json b/test-data/sighash_single.json new file mode 100644 index 0000000..7917d71 --- /dev/null +++ b/test-data/sighash_single.json @@ -0,0 +1,4 @@ +{ + "program": "54EkCTVkcziqISK6EzABFXCwYvClhPYGFQusJfripGQssOAVt34AhgGJAoSQgwVw+HGJzAu99qFH3Wh9HF0gSGitpeJMKWMyu4jCUAb0bbp/LsAGGAYhZhamq5HNddy8HoTqejIHGcv8bcleUZT57KmW1Vp7LXaMUR4qMQ4YBiQKEkG4GKVn4KrWbIJljsDH8qWizDjD9hqJKs0No6Ez/56tZohz3GDwIChYhZmCIv9QHGkTTAYXN78l9PRKwkaP7L+QgG2hgCZadW734oPAAKQKEkG4hFL8OMDm7yVnAtI8+S8yl59PzZ/zkJz3syU4qvsOOiPsQAebHRMMAxCzC0tVpzeJ0MJHAM98RvFz0eCrDowUWd8HlIb3z/dfqm1KEko5YGAYkChJBuMxS/DI5voQT9zeJ/JSg1zVnOQ+dSDqBncb+M9zPuT55FUoWyEHDAMQswtJtBDoLFPVXJUIItlR+smGqGn+ps2Sl7ABMeUuBMPAFkmDwICkChJBuQYpepzS+pXGfR01NwkZUcgqBlWc7hGLkov46etOFkWaw/S96WGAYhZhaTbSrO1OkcGEXy6AONowywuX3+NCWUQGHsywjitxQ2nK+HkEBSBQkg3JMUvYnNKg3OgxzUB7/tnnkIYePSA4b9lUGNvmRsOpjaQAmHTtYYBiFmFpMDLqyIAl/t8mPC4rRGEfM1lVH7CAxNfptKDrOKJGwAHAeSQFIFCSDcoxS9mcxgts54X+MRHKrfq2DJAzmcXWPmk3AyFTFgxcx5QC1WKBgGIWYWkwYulQXr0Fu/BAWgC5j7LWeBAl+pI9pt9ZFn0rcXaN1Fh5RAUgUJINyzFL8Kc24/Be/O/2y1dvdAWCojT4SRN6ptMPyHC/Wx509/w9WhBgGIWh2g5i0rBy522FfzfwI+9Jl1S7Ub581so2bN95sf4uTI/ApIDAMQtxCRQzohnOQs8/+3xpn819ObmnDkhDQn925GJoUwiWnfmwmLoMDQMRzGxdfpD+BHpt82/5AiSrZNI3NpL7zm2gN+qve5jXwpjkGAbAuFBcMNkfq2uz4+2w+54L02fKbhMzWmIBmXnh4WycLGacXulLoeYUCkChJBuZYUrN9g+k0nHXx53yFUwpeG2l34YNqKmzu3ryvimnTVZnDWHiMChYhZmf7mfDw7DxJRotWTkSgnKVskuvR6itvdUy8omlW0PLqAPLMCkChJBuaQUasTmisoktLp4eE0vbYzUvFK99flYWkuSkV+xfk0HnMiDj6kMAxC0D8TsC/Zp6ULJHUYNzHZJj6BC1DTRKAqk3AxgGInLJVj2nAoYBgQKE2l5vmZ++dy7rFWgYpXOhwsHApv82y3OKNlZ8oFbFvgXmAQgUJINxobjU4VOKBZtjDmdNxo2Q1uxbZ+tGrI/N+i7fR7qQw6drs9VP1wXWr1A5UMAxCbj85QIBwIDhAHFIHHAHJwDlaBy+A5jwOaADmzA5wgOdYA==", + "witness": "iq5tGhmZAujxnHi4VlEY4etEjfom0kOWFvaJJ+oy3TOpZMgstdMAETEH1YtSUC9qfba8NQ395QRLSbAamO6OjQ==" +} diff --git a/test-data/transfer_with_timeout.json b/test-data/transfer_with_timeout.json new file mode 100644 index 0000000..9337008 --- /dev/null +++ b/test-data/transfer_with_timeout.json @@ -0,0 +1,4 @@ +{ + "program": "5RugUOgUIMOTaXm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYBAmSDD6RQkgScOZxVEJFdCZgAirhYMXhSwnsDCoXWEv1xUjIWWHAK278AQwDEgUJIPvNwEUH4CKBZNjDmuu5eD0J1PRkDjOX+NuSvKMp89lTLaq09lrtGKI8VGIcMAxCbT8UAgHAQOGLIAAAPoBw0JwZJCDBV4OYF3vtQo+60Po4ukCQ0VtLxJhSxmV3EYSgDejbdP5dgAwwDEJqA4EDaFwEBwUHGYHHA/HRuPBQkDjtAoNx2bkGcKgOIU2nGBH+UQe19bTBFQG6VwHzYXHeOS4zvPKerrAm5XHCe5QQgUG5Cn5GnHCwAcWJFAHGINQXAAHGgODgcMB4gA5OAcoA", + "witness": null +}