Skip to content

Commit

Permalink
Allow pattern-matching on bytearrays
Browse files Browse the repository at this point in the history
  - Doesn't allow pattern-matching on G1/G2 elements and strings,
    because the use cases for those is unclear and it adds complexity to
    the feature.

  - We still _parse_ patterns on G1/G2 elements and strings, but emit an
    error in those cases.

  - The syntax is the same as for bytearray literals (i.e. supports hex,
    utf-8 strings or plain arrays of bytes).
  • Loading branch information
KtorZ committed Aug 3, 2024
1 parent ea032c9 commit f14dfdf
Show file tree
Hide file tree
Showing 24 changed files with 605 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- **aiken-lang**: add support for `mk_cons` and `mk_pair_data` builtins. See [#964](https://github.com/aiken-lang/aiken/issues/964). @KtorZ
- **aiken-lang**: pattern-matching on bytearrays is now available. See [#989](https://github.com/aiken-lang/aiken/issues/989). @KtorZ

### Changed

Expand Down
9 changes: 9 additions & 0 deletions crates/aiken-lang/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,12 @@ pub enum Pattern<Constructor, Type> {
base: Base,
},

ByteArray {
location: Span,
value: Vec<u8>,
preferred_format: ByteArrayFormatPreference,
},

/// The creation of a variable.
/// e.g. `expect [this_is_a_var, .._] = x`
/// e.g. `let foo = 42`
Expand Down Expand Up @@ -1330,6 +1336,7 @@ impl<A, B> Pattern<A, B> {
| Pattern::Discard { location, .. }
| Pattern::Tuple { location, .. }
| Pattern::Pair { location, .. }
| Pattern::ByteArray { location, .. }
| Pattern::Constructor { location, .. } => *location,
}
}
Expand Down Expand Up @@ -1383,6 +1390,7 @@ impl TypedPattern {
Pattern::Int { .. }
| Pattern::Var { .. }
| Pattern::Assign { .. }
| Pattern::ByteArray { .. }
| Pattern::Discard { .. } => Some(Located::Pattern(self, value.clone())),

Pattern::List { elements, .. }
Expand Down Expand Up @@ -1438,6 +1446,7 @@ impl TypedPattern {
pub fn tipo(&self, value: &TypedExpr) -> Option<Rc<Type>> {
match self {
Pattern::Int { .. } => Some(builtins::int()),
Pattern::ByteArray { .. } => Some(builtins::byte_array()),
Pattern::Constructor { tipo, .. } => Some(tipo.clone()),
Pattern::Var { .. } | Pattern::Assign { .. } | Pattern::Discard { .. } => {
Some(value.tipo())
Expand Down
6 changes: 6 additions & 0 deletions crates/aiken-lang/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,12 @@ impl<'comments> Formatter<'comments> {
let doc = match pattern {
Pattern::Int { value, base, .. } => self.int(value, base),

Pattern::ByteArray {
value,
preferred_format,
..
} => self.bytearray(value, None, preferred_format),

Pattern::Var { name, .. } => name.to_doc(),

Pattern::Assign { name, pattern, .. } => {
Expand Down
53 changes: 35 additions & 18 deletions crates/aiken-lang/src/gen_uplc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
Span, TraceLevel, Tracing, TypedArg, TypedClause, TypedDataType, TypedFunction,
TypedPattern, TypedValidator, UnOp,
},
builtins::{bool, data, int, list, void, PRELUDE},
builtins::{bool, byte_array, data, int, list, void, PRELUDE},
expr::TypedExpr,
gen_uplc::{
air::ExpectLevel,
Expand Down Expand Up @@ -915,26 +915,30 @@ impl<'a> CodeGenerator<'a> {
value: expected_int,
location,
..
} => {
assert!(props.kind.is_expect());

let name = format!(
} => AirTree::assign_literal_pattern(
format!(
"__expected_by_{}_span_{}_{}",
expected_int, location.start, location.end
);

let expect = AirTree::binop(
BinOp::Eq,
bool(),
AirTree::int(expected_int),
AirTree::local_var(&name, int()),
int(),
);

let expr = AirTree::let_assignment(name, value, expect);
),
AirTree::int(expected_int),
value,
int(),
props,
then,
),

AirTree::assert_bool(true, expr, then, props.otherwise.clone())
}
Pattern::ByteArray {
location,
value: expected_bytes,
..
} => AirTree::assign_literal_pattern(
format!("__expected_bytes_span_{}_{}", location.start, location.end),
AirTree::byte_array(expected_bytes.clone()),
value,
byte_array(),
props,
then,
),

Pattern::Var { name, .. } => {
if props.full_check {
Expand Down Expand Up @@ -2321,6 +2325,10 @@ impl<'a> CodeGenerator<'a> {
assert!(!props.final_clause);
(AirTree::int(value), then)
}
Pattern::ByteArray { value, .. } => {
assert!(!props.final_clause);
(AirTree::byte_array(value.clone()), then)
}
Pattern::Var { name, .. } => (
AirTree::void(),
AirTree::let_assignment(
Expand Down Expand Up @@ -2839,6 +2847,15 @@ impl<'a> CodeGenerator<'a> {
then,
)
}
Pattern::ByteArray { value, .. } => {
props.complex_clause = true;
AirTree::clause_guard(
&props.original_subject_name,
AirTree::byte_array(value.clone()),
byte_array(),
then,
)
}
Pattern::Var { name, .. } => AirTree::let_assignment(
name,
AirTree::local_var(&props.clause_var_name, subject_tipo.clone()),
Expand Down
2 changes: 1 addition & 1 deletion crates/aiken-lang/src/gen_uplc/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ pub fn pattern_has_conditions(
data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
) -> bool {
match pattern {
Pattern::List { .. } | Pattern::Int { .. } => true,
Pattern::List { .. } | Pattern::Int { .. } | Pattern::ByteArray { .. } => true,
Pattern::Tuple { elems, .. } => elems
.iter()
.any(|elem| pattern_has_conditions(elem, data_types)),
Expand Down
24 changes: 24 additions & 0 deletions crates/aiken-lang/src/gen_uplc/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::air::{Air, ExpectLevel};
use crate::{
ast::{BinOp, Curve, Span, UnOp},
builtins::{bool, byte_array, data, int, list, string, void},
gen_uplc::AssignmentProperties,
tipo::{Type, ValueConstructor, ValueConstructorVariant},
};
use indexmap::IndexSet;
Expand Down Expand Up @@ -588,6 +589,29 @@ impl AirTree {
}
}

pub fn assign_literal_pattern(
name: String,
pattern: AirTree,
rhs: AirTree,
tipo: Rc<Type>,
props: AssignmentProperties,
then: AirTree,
) -> AirTree {
assert!(props.kind.is_expect());

let expect = AirTree::binop(
BinOp::Eq,
bool(),
pattern,
AirTree::local_var(&name, tipo.clone()),
tipo,
);

let expr = AirTree::let_assignment(name, rhs, expect);

AirTree::assert_bool(true, expr, then, props.otherwise.clone())
}

pub fn cast_from_data(
value: AirTree,
tipo: Rc<Type>,
Expand Down
32 changes: 32 additions & 0 deletions crates/aiken-lang/src/parser/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ impl ParseError {
label: None,
}
}

pub fn match_on_curve(span: Span) -> Self {
Self {
kind: ErrorKind::PatternMatchOnCurvePoint,
span,
while_parsing: None,
expected: HashSet::new(),
label: Some("cannot pattern-match on curve point"),
}
}

pub fn match_string(span: Span) -> Self {
Self {
kind: ErrorKind::PatternMatchOnString,
span,
while_parsing: None,
expected: HashSet::new(),
label: Some("cannot pattern-match on string"),
}
}
}

impl PartialEq for ParseError {
Expand Down Expand Up @@ -260,6 +280,18 @@ pub enum ErrorKind {
"#
}))]
DeprecatedWhenClause,

#[error("I choked on a curve point in a bytearray pattern.")]
#[diagnostic(help(
"You can pattern-match on bytearrays just fine, but not on G1 nor G2 elements. Use if/else with an equality if you have to compare those."
))]
PatternMatchOnCurvePoint,

#[error("I refuse to cooperate and match a utf-8 string.")]
#[diagnostic(help(
"You can pattern-match on bytearrays but not on strings. Note that I can parse utf-8 encoded bytearrays just fine, so you probably want to drop the extra '@' and only manipulate bytearrays wherever you need to. On-chain, strings shall be avoided as much as possible."
))]
PatternMatchOnString,
}

fn fmt_curve_type(curve: &CurveType) -> String {
Expand Down
63 changes: 63 additions & 0 deletions crates/aiken-lang/src/parser/pattern/bytearray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::{
ast::UntypedPattern,
parser::{error::ParseError, literal, token::Token},
};
use chumsky::prelude::*;

pub fn parser() -> impl Parser<Token, UntypedPattern, Error = ParseError> {
literal::bytearray(|value, preferred_format, curve, location, emit| {
if curve.is_some() {
emit(ParseError::match_on_curve(location));
}

UntypedPattern::ByteArray {
location,
value,
preferred_format,
}
})
}

#[cfg(test)]
mod tests {
use crate::assert_expr;

#[test]
fn pattern_bytearray() {
assert_expr!(
r#"
when foo is {
#"00abcd" -> True
"Aiken, rocks!" -> True
#[1, 2, 3, 4] -> True
#[0x00, 0xab, 0xcd] -> True
_ -> False
}
"#
);
}

#[test]
fn pattern_bytearray_g1_element() {
assert_expr!(
r#"
when foo is {
#<Bls12_381, G1>"950dfd33da2682260c76038dfb8bad6e84ae9d599a3c151815945ac1e6ef6b1027cd917f3907479d20d636ce437a41f5" -> False
_ -> True
}
"#
);
}

#[test]
fn pattern_bytearray_g2_element() {
assert_expr!(
r#"
when foo is {
#<Bls12_381, G2>"b0629fa1158c2d23a10413fe91d381a84d25e31d041cd0377d25828498fd02011b35893938ced97535395e4815201e67108bcd4665e0db25d602d76fa791fab706c54abf5e1a9e44b4ac1e6badf3d2ac0328f5e30be341677c8bac5dda7682f1" -> False
_ -> True
}
"#
);
}
}
6 changes: 6 additions & 0 deletions crates/aiken-lang/src/parser/pattern/mod.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
use chumsky::prelude::*;

mod bytearray;
mod constructor;
mod discard;
mod int;
mod list;
mod pair;
mod string;
mod tuple;
mod var;

use crate::{
ast::UntypedPattern,
parser::{error::ParseError, token::Token},
};
pub use bytearray::parser as bytearray;
pub use constructor::parser as constructor;
pub use discard::parser as discard;
pub use int::parser as int;
pub use list::parser as list;
pub use pair::parser as pair;
pub use string::parser as string;
pub use tuple::parser as tuple;
pub use var::parser as var;

Expand All @@ -28,8 +32,10 @@ pub fn parser() -> impl Parser<Token, UntypedPattern, Error = ParseError> {
constructor(pattern.clone()),
discard(),
int(),
bytearray(),
tuple(pattern.clone()),
list(pattern),
string(),
))
.then(
just(Token::As)
Expand Down
Loading

0 comments on commit f14dfdf

Please sign in to comment.