Merge pull request #619 from aiken-lang/first-class-binary-operators

First class binary operators
This commit is contained in:
Matthias Benkort 2023-06-17 17:23:34 +02:00 committed by GitHub
commit 42519d3965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1335 additions and 35 deletions

View File

@ -1,5 +1,16 @@
# Changelog # 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 ## v1.0.10-alpha - 2023-06-13
### Added ### Added

View File

@ -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 { pub fn is_logically_equal(&self, other: &Annotation) -> bool {
match self { match self {
Annotation::Constructor { Annotation::Constructor {

View File

@ -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)] #[derive(Debug, Clone, PartialEq)]
pub enum UntypedExpr { pub enum UntypedExpr {
Int { Int {
@ -424,7 +432,7 @@ pub enum UntypedExpr {
Fn { Fn {
location: Span, location: Span,
is_capture: bool, fn_style: FnStyle,
arguments: Vec<Arg<()>>, arguments: Vec<Arg<()>>,
body: Box<Self>, body: Box<Self>,
return_annotation: Option<Annotation>, return_annotation: Option<Annotation>,

View File

@ -8,7 +8,7 @@ use crate::{
Use, Validator, CAPTURE_VARIABLE, Use, Validator, CAPTURE_VARIABLE,
}, },
docvec, docvec,
expr::{UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR}, expr::{FnStyle, UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR},
parser::{ parser::{
extra::{Comment, ModuleExtra}, extra::{Comment, ModuleExtra},
token::Base, token::Base,
@ -768,12 +768,18 @@ impl<'comments> Formatter<'comments> {
UntypedExpr::UnOp { value, op, .. } => self.un_op(value, op), UntypedExpr::UnOp { value, op, .. } => self.un_op(value, op),
UntypedExpr::Fn { UntypedExpr::Fn {
is_capture: true, fn_style: FnStyle::Capture,
body, body,
.. ..
} => self.fn_capture(body), } => self.fn_capture(body),
UntypedExpr::Fn { UntypedExpr::Fn {
fn_style: FnStyle::BinOp(op),
..
} => op.to_doc(),
UntypedExpr::Fn {
fn_style: FnStyle::Plain,
return_annotation, return_annotation,
arguments: args, arguments: args,
body, body,
@ -1061,7 +1067,9 @@ impl<'comments> Formatter<'comments> {
let right = self.expr(right); let right = self.expr(right);
self.operator_side(left, precedence, left_precedence) self.operator_side(left, precedence, left_precedence)
.append(" ")
.append(name) .append(name)
.append(" ")
.append(self.operator_side(right, precedence, right_precedence - 1)) .append(self.operator_side(right, precedence, right_precedence - 1))
} }
@ -1093,7 +1101,7 @@ impl<'comments> Formatter<'comments> {
let comments = self.pop_comments(expr.location().start); let comments = self.pop_comments(expr.location().start);
let doc = match expr { let doc = match expr {
UntypedExpr::Fn { UntypedExpr::Fn {
is_capture: true, fn_style: FnStyle::Capture,
body, body,
.. ..
} => self.pipe_capture_right_hand_side(body), } => self.pipe_capture_right_hand_side(body),

View File

@ -935,11 +935,91 @@ pub fn expr_parser(
arguments, arguments,
body: Box::new(body), body: Box::new(body),
location: span, location: span,
is_capture: false, fn_style: expr::FnStyle::Plain,
return_annotation, 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() let when_clause_parser = pattern_parser()
.then( .then(
just(Token::Vbar) just(Token::Vbar)
@ -1083,6 +1163,7 @@ pub fn expr_parser(
bytearray, bytearray,
list_parser, list_parser,
anon_fn_parser, anon_fn_parser,
anon_binop_parser,
block_parser, block_parser,
when_parser, when_parser,
let_parser, let_parser,
@ -1205,7 +1286,7 @@ pub fn expr_parser(
} else { } else {
expr::UntypedExpr::Fn { expr::UntypedExpr::Fn {
location: call.location(), location: call.location(),
is_capture: true, fn_style: expr::FnStyle::Capture,
arguments: holes, arguments: holes,
body: Box::new(call), body: Box::new(call),
return_annotation: None, return_annotation: None,
@ -1239,7 +1320,20 @@ pub fn expr_parser(
// Negate // Negate
let op = choice(( let op = choice((
just(Token::Bang).to(UnOp::Not), 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 let unary = op

View File

@ -864,3 +864,26 @@ fn hex_and_numeric_underscore() {
assert_fmt(src, src); 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);
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ use crate::{
UntypedRecordUpdateArg, UntypedRecordUpdateArg,
}, },
builtins::{bool, byte_array, function, int, list, string, tuple}, builtins::{bool, byte_array, function, int, list, string, tuple},
expr::{TypedExpr, UntypedExpr}, expr::{FnStyle, TypedExpr, UntypedExpr},
format, format,
tipo::fields::FieldMap, tipo::fields::FieldMap,
}; };
@ -220,12 +220,19 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
UntypedExpr::Fn { UntypedExpr::Fn {
location, location,
is_capture, fn_style,
arguments: args, arguments: args,
body, body,
return_annotation, return_annotation,
.. ..
} => self.infer_fn(args, &[], *body, is_capture, return_annotation, location), } => self.infer_fn(
args,
&[],
*body,
fn_style == FnStyle::Capture,
return_annotation,
location,
),
UntypedExpr::If { UntypedExpr::If {
location, location,
@ -1011,17 +1018,19 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
body, body,
return_annotation, return_annotation,
location, location,
is_capture: false, fn_style,
.. ..
}, },
) if expected_arguments.len() == arguments.len() => self.infer_fn( ) if fn_style != FnStyle::Capture && expected_arguments.len() == arguments.len() => {
self.infer_fn(
arguments, arguments,
expected_arguments, expected_arguments,
*body, *body,
false, false,
return_annotation, return_annotation,
location, location,
), )
}
// Otherwise just perform normal type inference. // Otherwise just perform normal type inference.
(_, value) => self.infer(value), (_, value) => self.infer(value),