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
## 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

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 {
match self {
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)]
pub enum UntypedExpr {
Int {
@ -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>,

View File

@ -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,
@ -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,
@ -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))
}
@ -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),
@ -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()
}

View File

@ -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)
@ -1083,6 +1163,7 @@ pub fn expr_parser(
bytearray,
list_parser,
anon_fn_parser,
anon_binop_parser,
block_parser,
when_parser,
let_parser,
@ -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,
@ -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

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

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