Skip to content

Commit 5b5f39c

Browse files
committed
parser/runtime: Destructuring assignment syntax
1 parent 8f1f55a commit 5b5f39c

File tree

6 files changed

+116
-20
lines changed

6 files changed

+116
-20
lines changed

src/error.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use miniscript::policy::compiler::CompilerError;
66
use miniscript::{descriptor, TranslateErr};
77

88
use crate::parser::ast::{Ident, InfixOp};
9-
use crate::{stdlib, PrettyDisplay, Value};
9+
use crate::{ast, stdlib, PrettyDisplay, Value};
1010

1111
#[derive(thiserror::Error, Debug)]
1212
pub enum Error {
@@ -34,6 +34,12 @@ pub enum RuntimeError {
3434
#[error("Undefined variable: {0}")]
3535
VarNotFound(Ident),
3636

37+
#[error("Expected an array to unpack into {}, not {}", &.0, ValErrFmt(.1))]
38+
UnpackArrayExpected(Box<ast::AssignTarget>, Box<Value>),
39+
40+
#[error("Expected an array of length {1} to unpack into {2}, received an array of {0}")]
41+
UnpackInvalidArrayLen(usize, usize, Box<ast::AssignTarget>),
42+
3743
#[error("Expected a function, not {}", ValErrFmt(.0))]
3844
NotFn(Box<Value>),
3945

@@ -96,7 +102,7 @@ pub enum RuntimeError {
96102
#[error("Expected a Script (or script bytes), not {}. Perhaps you meant to use explicitScript()/scriptPubKey()", ValErrFmt(.0))]
97103
NotScriptLike(Box<Value>),
98104

99-
#[error("Expected a transaction, raw bytes or tagged list, not {}", ValErrFmt(.0))]
105+
#[error("Expected a Transaction (also accepted as raw bytes, tagged list or PSBT), not {}", ValErrFmt(.0))]
100106
NotTxLike(Box<Value>),
101107

102108
#[error("Expected txid bytes or tx, not {}", ValErrFmt(.0))]
@@ -452,6 +458,9 @@ impl<'a> fmt::Display for ValErrFmt<'a> {
452458

453459
#[derive(thiserror::Error, Debug)]
454460
pub enum ParseError {
461+
#[error("Invalid assignemnt target")]
462+
InvalidAssignTarget,
463+
455464
#[error("ParseFloatError: {0}")]
456465
ParseFloatError(#[from] std::num::ParseFloatError),
457466

src/parser/ast.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use miniscript::{bitcoin, descriptor};
33
use bitcoin::address::{self, Address};
44
use descriptor::{DescriptorPublicKey, DescriptorSecretKey};
55

6+
use crate::util::fmt_list;
7+
68
/// Expressions have no side-effects and produce a value
79
#[derive(Debug, Clone)]
810
pub enum Expr {
@@ -149,7 +151,7 @@ impl_from_variant!(ScriptFrag, Expr);
149151
/// An anonymous function expression
150152
#[derive(Debug, Clone)]
151153
pub struct FnExpr {
152-
pub params: Vec<Ident>,
154+
pub params: Vec<AssignTarget>,
153155
pub body: Box<Expr>,
154156
pub dynamic_scoping: bool,
155157
}
@@ -241,7 +243,7 @@ impl_from_variant!(BtcAmount, Expr);
241243
#[derive(Debug, Clone)]
242244
pub struct FnDef {
243245
pub ident: Ident,
244-
pub params: Vec<Ident>,
246+
pub params: Vec<AssignTarget>,
245247
pub body: Expr,
246248
pub dynamic_scoping: bool,
247249
}
@@ -254,10 +256,17 @@ impl_from_variant!(Assign, Stmt);
254256

255257
#[derive(Debug, Clone)]
256258
pub struct Assignment {
257-
pub lhs: Ident,
259+
pub lhs: AssignTarget,
258260
pub rhs: Expr,
259261
}
260262

263+
// Used for assignment and function parameters
264+
#[derive(Debug, Clone)]
265+
pub enum AssignTarget {
266+
Ident(Ident),
267+
List(Vec<AssignTarget>),
268+
}
269+
261270
/// A call statement whose return value is discarded
262271
#[derive(Debug, Clone)]
263272
pub struct ExprStmt(pub Expr);
@@ -284,3 +293,19 @@ impl Expr {
284293
}
285294
}
286295
}
296+
297+
impl From<&str> for AssignTarget {
298+
fn from(s: &str) -> Self {
299+
Self::Ident(s.into())
300+
}
301+
}
302+
impl std::fmt::Display for AssignTarget {
303+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304+
match self {
305+
AssignTarget::Ident(ident) => write!(f, "{}", ident),
306+
AssignTarget::List(items) => {
307+
fmt_list(f, items.iter(), None, |f, item, _| write!(f, "{}", item))
308+
}
309+
}
310+
}
311+
}

src/parser/grammar.lalrpop

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::convert::TryFrom;
12
use std::str::FromStr;
23
use crate::time;
34
use crate::parser::{ast, Expr, Stmt, ParseError, concat, prepend, call, self};
5+
use ast::AssignTarget;
46

57
grammar;
68

@@ -277,13 +279,13 @@ FnExpr = { FnExpr1, FnExpr2 };
277279
FnExpr1: Expr = <dyn_scoping:"dyn"?> <params:FnExpr1Params> <body:Either2<Expr, FnBlockNoReturn>> =>
278280
ast::FnExpr { params, body: body.into(), dynamic_scoping: dyn_scoping.is_some() }.into();
279281
FnExpr1Params = {
280-
"|" <List0<IdentTerm, ",">> "|",
282+
"|" <List0<AssignTarget, ",">> "|",
281283
// Zero arguments should be captures by the List0, but it isn't because `||` is pre-tokenized as OR
282284
"||" => vec![],
283285
};
284286

285287
// FnDef-like syntax, more suitable for chaining with FnDef: fn adder($a) = fn ($b) = $a+$b; adder(2)(3)
286-
FnExpr2: Expr = <dyn_scoping:"dyn"?> "fn" <params:Paren<List0<IdentTerm, ",">>> <body:Either3<("=" <Expr>), BlockExpr, FnBlockNoReturn>> =>
288+
FnExpr2: Expr = <dyn_scoping:"dyn"?> "fn" <params:Paren<List0<AssignTarget, ",">>> <body:Either3<("=" <Expr>), BlockExpr, FnBlockNoReturn>> =>
287289
ast::FnExpr { params, body: body.into(), dynamic_scoping: dyn_scoping.is_some() }.into();
288290

289291
// XXX Should probably settle on one syntax to rule them all. Maybe `($a, $b) => $a+$b`?
@@ -393,10 +395,10 @@ BtcAmount: Expr = <num:SExpr> <denom:BTC_DENOMINATION> =>
393395
Assign: Stmt = "let"? <assigns:List1<Assignment, ",">> ";" =>
394396
ast::Assign(assigns).into();
395397

396-
Assignment: ast::Assignment = <lhs:IdentTerm> "=" <rhs:Expr> =>
398+
Assignment: ast::Assignment = <lhs:AssignTarget> "=" <rhs:Expr> =>
397399
ast::Assignment { lhs, rhs };
398400

399-
FnDef: Stmt = <dyn_scoping:"dyn"?> "fn" <ident:IdentTerm> <params:Paren<List0<IdentTerm, ",">>> <body:FnDefBody> =>
401+
FnDef: Stmt = <dyn_scoping:"dyn"?> "fn" <ident:IdentTerm> <params:Paren<List0<AssignTarget, ",">>> <body:FnDefBody> =>
400402
ast::FnDef { ident, params, body, dynamic_scoping: dyn_scoping.is_some() }.into();
401403

402404
FnDefBody = { "=" <Expr> ";", <BlockExpr> ";"?, <FnBlockNoReturn> ";"? };
@@ -409,15 +411,23 @@ IfStmtElse: Vec<Stmt> = {
409411
"else" <IfStmt> => vec![<>],
410412
};
411413

412-
// An expression used in a statement position. The evaluated return value is
413-
// discarded, but this can be useful for expressions that produce side effects
414-
// like logging and exceptions.
414+
// An expression used in a statement position. The evaluated return value is discarded,
415+
// but this can be useful for expressions that produce logging/exceptions side effects.
415416
ExprStmt: Stmt = <Expr> ";" => ast::ExprStmt(<>).into();
416417

418+
// Used as the target for Assignment and function parameters
419+
AssignTarget: AssignTarget = {
420+
IdentTerm => AssignTarget::Ident(<>),
421+
// Reuses the existing Expr Array type as an AssignTarget to play nicely with LR(1) grammar.
422+
// ASsignTarget::try_from() will reject Arrays that are not valid as a target.
423+
// https://github.com/lalrpop/lalrpop/issues/552#issuecomment-778923903
424+
Array =>? Ok(AssignTarget::try_from(<>)?),
425+
};
426+
417427
// Helpers
418428

419429
Either2<A, B>: Expr = { A, B };
420-
Either3<A, B, C>: Expr = { A, B };
430+
Either3<A, B, C>: Expr = { A, B, C };
421431

422432
// A `S`-separated list of zero or more `T` values
423433
List0<T, S>: Vec<T> = <l:(<T> S)*> <t:T?> => concat(l, t);

src/parser/mod.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::str::FromStr;
44
pub use crate::error::ParseError;
55

66
pub mod ast;
7-
pub use ast::{Expr, Ident, Library, Stmt};
7+
pub use ast::{AssignTarget, Expr, Ident, Library, Stmt};
88

99
lalrpop_mod!(
1010
#[allow(clippy::all)]
@@ -30,6 +30,33 @@ impl FromStr for Library {
3030
}
3131
impl_tryfrom_fromstr!(Library);
3232

33+
// The grammar reuses the Expr Array/Ident types as an AssignTarget to play nicely with LR(1) grammar.
34+
// This converts them into an AssignTarget, or reject ones that are invalid.
35+
// See https://github.com/lalrpop/lalrpop/issues/552#issuecomment-778923903
36+
impl TryFrom<Expr> for AssignTarget {
37+
type Error = ParseError;
38+
fn try_from(expr: Expr) -> Result<Self, ParseError> {
39+
use ast::{Infix, InfixOp::Colon};
40+
Ok(match expr {
41+
Expr::Ident(ident) => Self::Ident(ident),
42+
Expr::Array(ast::Array(mut items)) => {
43+
// Colon tuple syntax is supported within Array brackets, as e.g. `[$txid:$vout]` (it cannot be
44+
// supported without it due to grammar conflicts). Must peek to check prior to taking ownership.
45+
if items.len() == 1 && matches!(items[0], Expr::Infix(Infix { op: Colon, .. })) {
46+
let Expr::Infix(ast::Infix { lhs, rhs, .. }) = items.remove(0) else {
47+
unreachable!()
48+
};
49+
Self::List(vec![Self::try_from(*lhs)?, Self::try_from(*rhs)?])
50+
} else {
51+
let targets = items.into_iter().map(Self::try_from);
52+
Self::List(targets.collect::<Result<_, _>>()?)
53+
}
54+
}
55+
_ => bail!(ParseError::InvalidAssignTarget),
56+
})
57+
}
58+
}
59+
3360
// Utility functions used by the grammar
3461

3562
pub fn concat<T>(mut list: Vec<T>, val: Option<T>) -> Vec<T> {

src/runtime/function.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::fmt;
22

3-
use crate::parser::{ast, Expr, Ident};
3+
use crate::parser::{ast, AssignTarget, Expr, Ident};
44
use crate::runtime::{Array, Error, Evaluate, Result, ScopeRef, Value};
55

66
#[derive(Debug, Clone)]
@@ -13,7 +13,7 @@ pub enum Function {
1313
#[derive(Clone)]
1414
pub struct UserFunction {
1515
pub ident: Option<Ident>,
16-
pub params: Vec<Ident>,
16+
pub params: Vec<AssignTarget>,
1717
pub body: Expr,
1818
pub scope: Option<ScopeRef>,
1919
}
@@ -55,9 +55,8 @@ impl Call for UserFunction {
5555
// For lexically-scoped functions, create a child scope of the scope where the function was defined.
5656
// For dynamically-scoped function, create a child of the caller scope.
5757
let mut scope = self.scope.as_ref().unwrap_or(caller_scope).child();
58-
for (index, value) in args.into_iter().enumerate() {
59-
let ident = self.params.get(index).unwrap();
60-
scope.set(ident.clone(), value)?;
58+
for (param_target, arg_value) in self.params.iter().zip(args) {
59+
param_target.unpack(arg_value, &mut scope)?;
6160
}
6261
self.body.eval(&scope.into_ref())
6362
};

src/runtime/mod.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::convert::TryInto;
33

44
use crate::parser::{ast, Expr, Stmt};
55
use crate::stdlib;
6+
pub use ast::AssignTarget;
67

78
pub use crate::error::{ResultExt, RuntimeError as Error};
89
pub type Result<T> = std::result::Result<T, Error>;
@@ -36,7 +37,7 @@ impl Execute for ast::Assign {
3637
let readonly = scope.as_readonly();
3738
for assignment in &self.0 {
3839
let value = assignment.rhs.eval(&readonly)?;
39-
scope.borrow_mut().set(assignment.lhs.clone(), value)?;
40+
assignment.lhs.unpack(value, &mut scope.borrow_mut())?;
4041
}
4142
Ok(())
4243
}
@@ -409,6 +410,31 @@ impl Evaluate for ast::Block {
409410
}
410411
}
411412

413+
impl ast::AssignTarget {
414+
pub fn unpack(&self, value: Value, scope: &mut Scope) -> Result<()> {
415+
match self {
416+
AssignTarget::Ident(ident) if ident.0 == "_" => Ok(()),
417+
AssignTarget::Ident(ident) => scope.set(ident.clone(), value),
418+
AssignTarget::List(targets) => {
419+
let Value::Array(array) = value else {
420+
bail!(Error::UnpackArrayExpected(
421+
self.clone().into(),
422+
value.into()
423+
));
424+
};
425+
ensure!(
426+
targets.len() == array.len(),
427+
Error::UnpackInvalidArrayLen(array.len(), targets.len(), self.clone().into())
428+
);
429+
for (target, array_el) in targets.iter().zip(array) {
430+
target.unpack(array_el, scope)?;
431+
}
432+
Ok(())
433+
}
434+
}
435+
}
436+
}
437+
412438
impl Evaluate for Expr {
413439
fn eval(&self, scope: &ScopeRef) -> Result<Value> {
414440
Ok(match self {

0 commit comments

Comments
 (0)