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

View File

@ -3,6 +3,7 @@ use crate::{
parser::token::Token,
};
use indoc::formatdoc;
use itertools::Itertools;
use miette::Diagnostic;
use owo_colors::{OwoColorize, Stream::Stdout};
use std::collections::HashSet;
@ -18,6 +19,7 @@ use std::collections::HashSet;
"I am looking for one of the following patterns:\n{}",
expected
.iter()
.sorted()
.map(|x| format!(
"→ {}",
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 {
#[error("I found an unexpected char '{0:?}'.")]
#[diagnostic(help("Try removing it!"))]

View File

@ -1,5 +1,5 @@
use crate::{
ast::TraceKind,
ast::{well_known, TraceKind},
expr::UntypedExpr,
parser::{
error::{ParseError, Pattern},
@ -32,7 +32,8 @@ pub fn parser<'a>(
choice((just(Token::Colon), just(Token::Comma)))
.then(
choice((string::hybrid(), expression.clone()))
.separated_by(just(Token::Comma)),
.separated_by(just(Token::Comma))
.at_least(1),
)
.validate(|(token, arguments), span, emit| {
if token != Token::Colon {
@ -53,7 +54,10 @@ pub fn parser<'a>(
|((label, arguments), continuation), span| UntypedExpr::Trace {
kind: TraceKind::Trace,
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),
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
description: "Invalid code (parse error):\n\nlet a =\n// foo\nlet b = 42\n"
---
[
ParseError {
kind: UnfinishedAssignmentRightHandSide,
span: 0..25,
while_parsing: None,
expected: {},
label: Some(
"invalid assignment right-hand side",
),
},
]
I spotted an unfinished assignment.

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 {
kind: Trace,
location: 0..14,
then: Trace {
kind: Todo,
then: Var {
location: 0..14,
then: ErrorTerm {
location: 0..14,
},
label: String {
location: 0..14,
value: "aiken::todo",
},
arguments: [],
name: "Void",
},
label: Var {
location: 6..14,

View File

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

View File

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

View File

@ -2,14 +2,4 @@
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"
---
[
ParseError {
kind: DeprecatedWhenClause,
span: 14..29,
while_parsing: None,
expected: {},
label: Some(
"deprecated",
),
},
]
I found a now-deprecated clause guard in a when/is expression.

View File

@ -2,14 +2,4 @@
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"
---
[
ParseError {
kind: PatternMatchOnCurvePoint,
span: 18..132,
while_parsing: None,
expected: {},
label: Some(
"cannot pattern-match on curve point",
),
},
]
I choked on a curve point in a bytearray pattern.

View File

@ -2,14 +2,4 @@
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"
---
[
ParseError {
kind: PatternMatchOnCurvePoint,
span: 18..228,
while_parsing: None,
expected: {},
label: Some(
"cannot pattern-match on curve point",
),
},
]
I choked on a curve point in a bytearray pattern.

View File

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

View File

@ -1,12 +1,14 @@
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 {
Decimal { numeric_underscore: bool },
Hexadecimal,
}
#[derive(Clone, Debug, PartialEq, Hash, Eq)]
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Hash, Eq)]
pub enum Token {
Error(char),
Name { name: String },

View File

@ -45,7 +45,7 @@ macro_rules! assert_expr {
prepend_module_to_snapshot => false,
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 {
let mut warnings = vec![];
let typed_module = module
.infer(
&id_gen,
@ -3309,6 +3310,44 @@ fn softcasting_unused_let_binding() {
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]
fn dangling_trace_let_standalone() {
let source_code = r#"

View File

@ -1970,11 +1970,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
PipeTyper::infer(self, expressions)
}
#[allow(clippy::result_large_err)]
fn backpass(
&mut self,
breakpoint: UntypedExpr,
mut continuation: Vec<UntypedExpr>,
) -> UntypedExpr {
) -> Result<UntypedExpr, Error> {
let UntypedExpr::Assignment {
location,
value,
@ -1985,6 +1986,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
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 call_location = Span {
@ -2101,11 +2111,11 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
value: UntypedExpr::lambda(names, continuation, lambda_span),
});
UntypedExpr::Call {
Ok(UntypedExpr::Call {
location: call_location,
fun,
arguments: new_arguments,
}
})
}
// 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() {
call
Ok(call)
} else {
UntypedExpr::Fn {
Ok(UntypedExpr::Fn {
location: call_location,
fn_style,
arguments,
body: call.into(),
return_annotation,
}
})
}
}
@ -2152,7 +2162,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
// 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
// written like that to begin with.
_ => UntypedExpr::Call {
_ => Ok(UntypedExpr::Call {
location: call_location,
fun: value,
arguments: vec![CallArg {
@ -2160,7 +2170,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
label: None,
value: UntypedExpr::lambda(names, continuation, lambda_span),
}],
},
}),
}
}
@ -2203,7 +2213,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
}
if let Some(breakpoint) = breakpoint {
prefix.push(self.backpass(breakpoint, suffix));
prefix.push(self.backpass(breakpoint, suffix)?);
return self.infer_seq(location, prefix);
}