Merge pull request #1114 from aiken-lang/fixes

Fix #1111 & #1113
This commit is contained in:
Matthias Benkort 2025-03-02 13:51:28 +01:00 committed by GitHub
commit 97c382715f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 108 additions and 101 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## v1.1.14 - UNRELEASED
### Changed
- **aiken-lang**: Prevent (type error) backpassing blocks with empty continuation. See [#1111](https://github.com/aiken-lang/aiken/issues/1111). @KtorZ
- **aiken-lang**: Change default placeholder for `trace` to `Void` instead of `todo`. @KtorZ
- **aiken-lang**: Disallow (parse error) dangling colon `:` in traces. See [#1113](https://github.com/aiken-lang/aiken/issues/1113). @KtorZ
## v1.1.13 - 2025-02-26 ## v1.1.13 - 2025-02-26
### Added ### Added

View File

@ -3,6 +3,7 @@ use crate::{
parser::token::Token, parser::token::Token,
}; };
use indoc::formatdoc; use indoc::formatdoc;
use itertools::Itertools;
use miette::Diagnostic; use miette::Diagnostic;
use owo_colors::{OwoColorize, Stream::Stdout}; use owo_colors::{OwoColorize, Stream::Stdout};
use std::collections::HashSet; use std::collections::HashSet;
@ -18,6 +19,7 @@ use std::collections::HashSet;
"I am looking for one of the following patterns:\n{}", "I am looking for one of the following patterns:\n{}",
expected expected
.iter() .iter()
.sorted()
.map(|x| format!( .map(|x| format!(
"→ {}", "→ {}",
x.to_aiken() x.to_aiken()
@ -320,7 +322,7 @@ fn fmt_unknown_curve(curve: &String, point: &Option<String>) -> String {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Diagnostic, thiserror::Error)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Diagnostic, thiserror::Error)]
pub enum Pattern { pub enum Pattern {
#[error("I found an unexpected char '{0:?}'.")] #[error("I found an unexpected char '{0:?}'.")]
#[diagnostic(help("Try removing it!"))] #[diagnostic(help("Try removing it!"))]

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
ast::TraceKind, ast::{well_known, TraceKind},
expr::UntypedExpr, expr::UntypedExpr,
parser::{ parser::{
error::{ParseError, Pattern}, error::{ParseError, Pattern},
@ -32,7 +32,8 @@ pub fn parser<'a>(
choice((just(Token::Colon), just(Token::Comma))) choice((just(Token::Colon), just(Token::Comma)))
.then( .then(
choice((string::hybrid(), expression.clone())) choice((string::hybrid(), expression.clone()))
.separated_by(just(Token::Comma)), .separated_by(just(Token::Comma))
.at_least(1),
) )
.validate(|(token, arguments), span, emit| { .validate(|(token, arguments), span, emit| {
if token != Token::Colon { if token != Token::Colon {
@ -53,7 +54,10 @@ pub fn parser<'a>(
|((label, arguments), continuation), span| UntypedExpr::Trace { |((label, arguments), continuation), span| UntypedExpr::Trace {
kind: TraceKind::Trace, kind: TraceKind::Trace,
location: span, location: span,
then: Box::new(continuation.unwrap_or_else(|| UntypedExpr::todo(None, span))), then: Box::new(continuation.unwrap_or_else(|| UntypedExpr::Var {
location: span,
name: well_known::VOID.to_string(),
})),
label: Box::new(label), label: Box::new(label),
arguments, arguments,
}, },
@ -193,4 +197,15 @@ mod tests {
"# "#
); );
} }
#[test]
fn trace_dangling_colons() {
assert_expr!(
r#"
let debug = fn() {
trace "foo":
}
"#
);
}
} }

View File

@ -2,14 +2,4 @@
source: crates/aiken-lang/src/parser/expr/assignment.rs source: crates/aiken-lang/src/parser/expr/assignment.rs
description: "Invalid code (parse error):\n\nlet a =\n// foo\nlet b = 42\n" description: "Invalid code (parse error):\n\nlet a =\n// foo\nlet b = 42\n"
--- ---
[ I spotted an unfinished assignment.
ParseError {
kind: UnfinishedAssignmentRightHandSide,
span: 0..25,
while_parsing: None,
expected: {},
label: Some(
"invalid assignment right-hand side",
),
},
]

View File

@ -0,0 +1,5 @@
---
source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs
description: "Invalid code (parse error):\n\nlet debug = fn() {\n trace \"foo\":\n}\n"
---
I found an unexpected token '}'.

View File

@ -5,17 +5,9 @@ description: "Code:\n\ntrace some_var\n"
Trace { Trace {
kind: Trace, kind: Trace,
location: 0..14, location: 0..14,
then: Trace { then: Var {
kind: Todo,
location: 0..14, location: 0..14,
then: ErrorTerm { name: "Void",
location: 0..14,
},
label: String {
location: 0..14,
value: "aiken::todo",
},
arguments: [],
}, },
label: Var { label: Var {
location: 6..14, location: 6..14,

View File

@ -5,17 +5,9 @@ description: "Code:\n\ntrace foo: \"bar\"\n"
Trace { Trace {
kind: Trace, kind: Trace,
location: 0..16, location: 0..16,
then: Trace { then: Var {
kind: Todo,
location: 0..16, location: 0..16,
then: ErrorTerm { name: "Void",
location: 0..16,
},
label: String {
location: 0..16,
value: "aiken::todo",
},
arguments: [],
}, },
label: Var { label: Var {
location: 6..9, location: 6..9,

View File

@ -5,17 +5,9 @@ description: "Code:\n\ntrace \"foo\": @\"bar\", baz\n"
Trace { Trace {
kind: Trace, kind: Trace,
location: 0..24, location: 0..24,
then: Trace { then: Var {
kind: Todo,
location: 0..24, location: 0..24,
then: ErrorTerm { name: "Void",
location: 0..24,
},
label: String {
location: 0..24,
value: "aiken::todo",
},
arguments: [],
}, },
label: String { label: String {
location: 6..11, location: 6..11,

View File

@ -2,14 +2,4 @@
source: crates/aiken-lang/src/parser/expr/when/mod.rs source: crates/aiken-lang/src/parser/expr/when/mod.rs
description: "Invalid code (parse error):\n\nwhen a is {\n 2 if x > 1 -> 3\n _ -> 1\n}\n" description: "Invalid code (parse error):\n\nwhen a is {\n 2 if x > 1 -> 3\n _ -> 1\n}\n"
--- ---
[ I found a now-deprecated clause guard in a when/is expression.
ParseError {
kind: DeprecatedWhenClause,
span: 14..29,
while_parsing: None,
expected: {},
label: Some(
"deprecated",
),
},
]

View File

@ -2,14 +2,4 @@
source: crates/aiken-lang/src/parser/pattern/bytearray.rs source: crates/aiken-lang/src/parser/pattern/bytearray.rs
description: "Invalid code (parse error):\n\nwhen foo is {\n #<Bls12_381, G1>\"950dfd33da2682260c76038dfb8bad6e84ae9d599a3c151815945ac1e6ef6b1027cd917f3907479d20d636ce437a41f5\" -> False\n _ -> True\n}\n" description: "Invalid code (parse error):\n\nwhen foo is {\n #<Bls12_381, G1>\"950dfd33da2682260c76038dfb8bad6e84ae9d599a3c151815945ac1e6ef6b1027cd917f3907479d20d636ce437a41f5\" -> False\n _ -> True\n}\n"
--- ---
[ I choked on a curve point in a bytearray pattern.
ParseError {
kind: PatternMatchOnCurvePoint,
span: 18..132,
while_parsing: None,
expected: {},
label: Some(
"cannot pattern-match on curve point",
),
},
]

View File

@ -2,14 +2,4 @@
source: crates/aiken-lang/src/parser/pattern/bytearray.rs source: crates/aiken-lang/src/parser/pattern/bytearray.rs
description: "Invalid code (parse error):\n\nwhen foo is {\n #<Bls12_381, G2>\"b0629fa1158c2d23a10413fe91d381a84d25e31d041cd0377d25828498fd02011b35893938ced97535395e4815201e67108bcd4665e0db25d602d76fa791fab706c54abf5e1a9e44b4ac1e6badf3d2ac0328f5e30be341677c8bac5dda7682f1\" -> False\n _ -> True\n}\n" description: "Invalid code (parse error):\n\nwhen foo is {\n #<Bls12_381, G2>\"b0629fa1158c2d23a10413fe91d381a84d25e31d041cd0377d25828498fd02011b35893938ced97535395e4815201e67108bcd4665e0db25d602d76fa791fab706c54abf5e1a9e44b4ac1e6badf3d2ac0328f5e30be341677c8bac5dda7682f1\" -> False\n _ -> True\n}\n"
--- ---
[ I choked on a curve point in a bytearray pattern.
ParseError {
kind: PatternMatchOnCurvePoint,
span: 18..228,
while_parsing: None,
expected: {},
label: Some(
"cannot pattern-match on curve point",
),
},
]

View File

@ -2,14 +2,4 @@
source: crates/aiken-lang/src/parser/pattern/string.rs source: crates/aiken-lang/src/parser/pattern/string.rs
description: "Invalid code (parse error):\n\nwhen foo is {\n @\"foo\" -> True\n}\n" description: "Invalid code (parse error):\n\nwhen foo is {\n @\"foo\" -> True\n}\n"
--- ---
[ I refuse to cooperate and match a utf-8 string.
ParseError {
kind: PatternMatchOnString,
span: 16..22,
while_parsing: None,
expected: {},
label: Some(
"cannot pattern-match on string",
),
},
]

View File

@ -1,12 +1,14 @@
use std::fmt; use std::fmt;
#[derive(Clone, Debug, PartialEq, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)] #[derive(
Clone, Debug, PartialEq, PartialOrd, Ord, Hash, Eq, Copy, serde::Serialize, serde::Deserialize,
)]
pub enum Base { pub enum Base {
Decimal { numeric_underscore: bool }, Decimal { numeric_underscore: bool },
Hexadecimal, Hexadecimal,
} }
#[derive(Clone, Debug, PartialEq, Hash, Eq)] #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Hash, Eq)]
pub enum Token { pub enum Token {
Error(char), Error(char),
Name { name: String }, Name { name: String },

View File

@ -45,7 +45,7 @@ macro_rules! assert_expr {
prepend_module_to_snapshot => false, prepend_module_to_snapshot => false,
omit_expression => true omit_expression => true
}, { }, {
insta::assert_debug_snapshot!(err); insta::assert_snapshot!(err.into_iter().map(|e| e.to_string()).collect::<Vec<_>>().join("\n"));
}) })
} }
} }

View File

@ -33,6 +33,7 @@ fn check_module(
for (package, module) in extra { for (package, module) in extra {
let mut warnings = vec![]; let mut warnings = vec![];
let typed_module = module let typed_module = module
.infer( .infer(
&id_gen, &id_gen,
@ -3309,6 +3310,44 @@ fn softcasting_unused_let_binding() {
assert!(warnings.is_empty(), "should not contain any warnings"); assert!(warnings.is_empty(), "should not contain any warnings");
} }
#[test]
fn dangling_let_in_block() {
let source_code = r#"
fn for_each(xs: List<Int>, with: fn(Int) -> a) -> a {
todo
}
test foo() {
{
let _ <- for_each([1, 2, 3])
}
}
"#;
let result = check_validator(parse(source_code));
assert!(
matches!(result, Err((_, Error::LastExpressionIsAssignment { .. }))),
"{result:?}"
)
}
#[test]
fn default_trace_return() {
let source_code = r#"
fn debug() {
trace @"patate": Void
}
test foo() {
debug()
True
}
"#;
assert!(matches!(check_validator(parse(source_code)), Ok(..)))
}
#[test] #[test]
fn dangling_trace_let_standalone() { fn dangling_trace_let_standalone() {
let source_code = r#" let source_code = r#"

View File

@ -1970,11 +1970,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
PipeTyper::infer(self, expressions) PipeTyper::infer(self, expressions)
} }
#[allow(clippy::result_large_err)]
fn backpass( fn backpass(
&mut self, &mut self,
breakpoint: UntypedExpr, breakpoint: UntypedExpr,
mut continuation: Vec<UntypedExpr>, mut continuation: Vec<UntypedExpr>,
) -> UntypedExpr { ) -> Result<UntypedExpr, Error> {
let UntypedExpr::Assignment { let UntypedExpr::Assignment {
location, location,
value, value,
@ -1985,6 +1986,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
unreachable!("backpass misuse: breakpoint isn't an Assignment ?!"); unreachable!("backpass misuse: breakpoint isn't an Assignment ?!");
}; };
if continuation.is_empty() {
return Err(Error::LastExpressionIsAssignment {
location,
expr: *value,
patterns: patterns.clone(),
kind,
});
}
let value_location = value.location(); let value_location = value.location();
let call_location = Span { let call_location = Span {
@ -2101,11 +2111,11 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
value: UntypedExpr::lambda(names, continuation, lambda_span), value: UntypedExpr::lambda(names, continuation, lambda_span),
}); });
UntypedExpr::Call { Ok(UntypedExpr::Call {
location: call_location, location: call_location,
fun, fun,
arguments: new_arguments, arguments: new_arguments,
} })
} }
// This typically occurs on function captures. We do not try to assert anything on the // This typically occurs on function captures. We do not try to assert anything on the
@ -2136,15 +2146,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
}; };
if arguments.is_empty() { if arguments.is_empty() {
call Ok(call)
} else { } else {
UntypedExpr::Fn { Ok(UntypedExpr::Fn {
location: call_location, location: call_location,
fn_style, fn_style,
arguments, arguments,
body: call.into(), body: call.into(),
return_annotation, return_annotation,
} })
} }
} }
@ -2152,7 +2162,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
// with our continuation. If the expression isn't callable? No problem, the // with our continuation. If the expression isn't callable? No problem, the
// type-checker will catch that eventually in exactly the same way as if the code was // type-checker will catch that eventually in exactly the same way as if the code was
// written like that to begin with. // written like that to begin with.
_ => UntypedExpr::Call { _ => Ok(UntypedExpr::Call {
location: call_location, location: call_location,
fun: value, fun: value,
arguments: vec![CallArg { arguments: vec![CallArg {
@ -2160,7 +2170,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
label: None, label: None,
value: UntypedExpr::lambda(names, continuation, lambda_span), value: UntypedExpr::lambda(names, continuation, lambda_span),
}], }],
}, }),
} }
} }
@ -2203,7 +2213,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
} }
if let Some(breakpoint) = breakpoint { if let Some(breakpoint) = breakpoint {
prefix.push(self.backpass(breakpoint, suffix)); prefix.push(self.backpass(breakpoint, suffix)?);
return self.infer_seq(location, prefix); return self.infer_seq(location, prefix);
} }