Skip to content

Commit

Permalink
Merge pull request #619 from aiken-lang/first-class-binary-operators
Browse files Browse the repository at this point in the history
First class binary operators
  • Loading branch information
KtorZ authored Jun 17, 2023
2 parents 93135ce + 41b2bf1 commit 42519d3
Show file tree
Hide file tree
Showing 8 changed files with 1,335 additions and 35 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## v1.0.11-alpha - unreleased

### Added

- **aiken-lang**: Binary operator are now treated like first-class citizen in
expressions. In particular, they can be used as function arguments directly:

```
compare_with(a, >=, b) == compare_with(a, fn(l, r) { l >= r }, b)
```

## v1.0.10-alpha - 2023-06-13

### Added
Expand Down
18 changes: 18 additions & 0 deletions crates/aiken-lang/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,24 @@ impl Annotation {
}
}

pub fn boolean(location: Span) -> Self {
Annotation::Constructor {
name: "Bool".to_string(),
module: None,
arguments: vec![],
location,
}
}

pub fn int(location: Span) -> Self {
Annotation::Constructor {
name: "Int".to_string(),
module: None,
arguments: vec![],
location,
}
}

pub fn is_logically_equal(&self, other: &Annotation) -> bool {
match self {
Annotation::Constructor {
Expand Down
10 changes: 9 additions & 1 deletion crates/aiken-lang/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,14 @@ impl TypedExpr {
}
}

// Represent how a function was written so that we can format it back.
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum FnStyle {
Plain,
Capture,
BinOp(BinOp),
}

#[derive(Debug, Clone, PartialEq)]
pub enum UntypedExpr {
Int {
Expand All @@ -424,7 +432,7 @@ pub enum UntypedExpr {

Fn {
location: Span,
is_capture: bool,
fn_style: FnStyle,
arguments: Vec<Arg<()>>,
body: Box<Self>,
return_annotation: Option<Annotation>,
Expand Down
40 changes: 24 additions & 16 deletions crates/aiken-lang/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
Use, Validator, CAPTURE_VARIABLE,
},
docvec,
expr::{UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR},
expr::{FnStyle, UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR},
parser::{
extra::{Comment, ModuleExtra},
token::Base,
Expand Down Expand Up @@ -768,12 +768,18 @@ impl<'comments> Formatter<'comments> {
UntypedExpr::UnOp { value, op, .. } => self.un_op(value, op),

UntypedExpr::Fn {
is_capture: true,
fn_style: FnStyle::Capture,
body,
..
} => self.fn_capture(body),

UntypedExpr::Fn {
fn_style: FnStyle::BinOp(op),
..
} => op.to_doc(),

UntypedExpr::Fn {
fn_style: FnStyle::Plain,
return_annotation,
arguments: args,
body,
Expand Down Expand Up @@ -1061,7 +1067,9 @@ impl<'comments> Formatter<'comments> {
let right = self.expr(right);

self.operator_side(left, precedence, left_precedence)
.append(" ")
.append(name)
.append(" ")
.append(self.operator_side(right, precedence, right_precedence - 1))
}

Expand Down Expand Up @@ -1093,7 +1101,7 @@ impl<'comments> Formatter<'comments> {
let comments = self.pop_comments(expr.location().start);
let doc = match expr {
UntypedExpr::Fn {
is_capture: true,
fn_style: FnStyle::Capture,
body,
..
} => self.pipe_capture_right_hand_side(body),
Expand Down Expand Up @@ -1717,19 +1725,19 @@ impl<'a> Documentable<'a> for &'a UnqualifiedImport {
impl<'a> Documentable<'a> for &'a BinOp {
fn to_doc(self) -> Document<'a> {
match self {
BinOp::And => " && ",
BinOp::Or => " || ",
BinOp::LtInt => " < ",
BinOp::LtEqInt => " <= ",
BinOp::Eq => " == ",
BinOp::NotEq => " != ",
BinOp::GtEqInt => " >= ",
BinOp::GtInt => " > ",
BinOp::AddInt => " + ",
BinOp::SubInt => " - ",
BinOp::MultInt => " * ",
BinOp::DivInt => " / ",
BinOp::ModInt => " % ",
BinOp::And => "&&",
BinOp::Or => "||",
BinOp::LtInt => "<",
BinOp::LtEqInt => "<=",
BinOp::Eq => "==",
BinOp::NotEq => "!=",
BinOp::GtEqInt => ">=",
BinOp::GtInt => ">",
BinOp::AddInt => "+",
BinOp::SubInt => "-",
BinOp::MultInt => "*",
BinOp::DivInt => "/",
BinOp::ModInt => "%",
}
.to_doc()
}
Expand Down
100 changes: 97 additions & 3 deletions crates/aiken-lang/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -935,11 +935,91 @@ pub fn expr_parser(
arguments,
body: Box::new(body),
location: span,
is_capture: false,
fn_style: expr::FnStyle::Plain,
return_annotation,
},
);

let anon_binop_parser = select! {
Token::EqualEqual => BinOp::Eq,
Token::NotEqual => BinOp::NotEq,
Token::Less => BinOp::LtInt,
Token::LessEqual => BinOp::LtEqInt,
Token::Greater => BinOp::GtInt,
Token::GreaterEqual => BinOp::GtEqInt,
Token::VbarVbar => BinOp::Or,
Token::AmperAmper => BinOp::And,
Token::Plus => BinOp::AddInt,
Token::Minus => BinOp::SubInt,
Token::Slash => BinOp::DivInt,
Token::Star => BinOp::MultInt,
Token::Percent => BinOp::ModInt,
}
.map_with_span(|name, location| {
use BinOp::*;

let arg_annotation = match name {
Or | And => Some(ast::Annotation::boolean(location)),
Eq | NotEq => None,
LtInt | LtEqInt | GtInt | GtEqInt | AddInt | SubInt | MultInt | DivInt | ModInt => {
Some(ast::Annotation::int(location))
}
};

let return_annotation = match name {
Or | And | Eq | NotEq | LtInt | LtEqInt | GtInt | GtEqInt => {
Some(ast::Annotation::boolean(location))
}
AddInt | SubInt | MultInt | DivInt | ModInt => Some(ast::Annotation::int(location)),
};

let arguments = vec![
ast::Arg {
arg_name: ast::ArgName::Named {
name: "left".to_string(),
label: "left".to_string(),
location,
is_validator_param: false,
},
annotation: arg_annotation.clone(),
location,
tipo: (),
},
ast::Arg {
arg_name: ast::ArgName::Named {
name: "right".to_string(),
label: "right".to_string(),
location,
is_validator_param: false,
},
annotation: arg_annotation,
location,
tipo: (),
},
];

let body = expr::UntypedExpr::BinOp {
location,
name,
left: Box::new(expr::UntypedExpr::Var {
location,
name: "left".to_string(),
}),
right: Box::new(expr::UntypedExpr::Var {
location,
name: "right".to_string(),
}),
};

expr::UntypedExpr::Fn {
arguments,
body: Box::new(body),
return_annotation,
fn_style: expr::FnStyle::BinOp(name),
location,
}
});

let when_clause_parser = pattern_parser()
.then(
just(Token::Vbar)
Expand Down Expand Up @@ -1083,6 +1163,7 @@ pub fn expr_parser(
bytearray,
list_parser,
anon_fn_parser,
anon_binop_parser,
block_parser,
when_parser,
let_parser,
Expand Down Expand Up @@ -1205,7 +1286,7 @@ pub fn expr_parser(
} else {
expr::UntypedExpr::Fn {
location: call.location(),
is_capture: true,
fn_style: expr::FnStyle::Capture,
arguments: holes,
body: Box::new(call),
return_annotation: None,
Expand Down Expand Up @@ -1239,7 +1320,20 @@ pub fn expr_parser(
// Negate
let op = choice((
just(Token::Bang).to(UnOp::Not),
just(Token::Minus).to(UnOp::Negate),
just(Token::Minus)
// NOTE: Prevent conflict with usage for '-' as a standalone binary op.
// This will make '-' parse when used as standalone binop in a function call.
// For example:
//
// foo(a, -, b)
//
// but it'll fail in a let-binding:
//
// let foo = -
//
// which seems acceptable.
.then_ignore(just(Token::Comma).not().rewind())
.to(UnOp::Negate),
));

let unary = op
Expand Down
23 changes: 23 additions & 0 deletions crates/aiken-lang/src/tests/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,3 +864,26 @@ fn hex_and_numeric_underscore() {

assert_fmt(src, src);
}

#[test]
fn first_class_binop() {
let src = indoc! { r#"
fn foo() {
compare_with(a, >, b)
compare_with(a, >=, b)
compare_with(a, <, b)
compare_with(a, <=, b)
compare_with(a, ==, b)
compare_with(a, !=, b)
combine_with(a, &&, b)
combine_with(a, ||, b)
compute_with(a, +, b)
compute_with(a, -, b)
compute_with(a, /, b)
compute_with(a, *, b)
compute_with(a, %, b)
}
"#};

assert_fmt(src, src);
}
Loading

0 comments on commit 42519d3

Please sign in to comment.