Prevent non-default fallback on exhaustive validator
Technically, we always need a fallback just because the way the UPLC is going to work. The last case in the handler pattern matching is always going to be else ... We could optimize that away and when the validator is exhaustive, make the last handler the fallback. Yet, it's really a micro optimization that saves us one extra if/else. So the sake of getting things working, we always assume that there's a fallback but, with the extra condition that when the validator is exhaustive (i.e. there's a handler covering all purposes), the fallback HAS TO BE the default fallback (i.e. (_) => fail). This allows us to gracefully format it out, and also raise an error in case where there's an extraneous custom fallback.
This commit is contained in:
parent
48535636ed
commit
efeda9a998
|
@ -242,6 +242,19 @@ fn str_to_keyword(word: &str) -> Option<Token> {
|
|||
pub type TypedFunction = Function<Rc<Type>, TypedExpr, TypedArg>;
|
||||
pub type UntypedFunction = Function<(), UntypedExpr, UntypedArg>;
|
||||
|
||||
impl UntypedFunction {
|
||||
pub fn is_default_fallback(&self) -> bool {
|
||||
matches!(
|
||||
&self.arguments[..],
|
||||
[UntypedArg {
|
||||
by: ArgBy::ByName(ArgName::Discarded { .. }),
|
||||
..
|
||||
}]
|
||||
) && matches!(&self.body, UntypedExpr::ErrorTerm { .. })
|
||||
&& self.name.as_str() == well_known::VALIDATOR_ELSE
|
||||
}
|
||||
}
|
||||
|
||||
pub type TypedTest = Function<Rc<Type>, TypedExpr, TypedArgVia>;
|
||||
pub type UntypedTest = Function<(), UntypedExpr, UntypedArgVia>;
|
||||
|
||||
|
@ -490,6 +503,33 @@ impl<T, Arg, Expr> Validator<T, Arg, Expr> {
|
|||
}
|
||||
}
|
||||
|
||||
impl UntypedValidator {
|
||||
pub fn default_fallback(location: Span) -> UntypedFunction {
|
||||
Function {
|
||||
arguments: vec![UntypedArg {
|
||||
by: ArgBy::ByName(ArgName::Discarded {
|
||||
name: "_".to_string(),
|
||||
label: "_".to_string(),
|
||||
location,
|
||||
}),
|
||||
location,
|
||||
annotation: None,
|
||||
doc: None,
|
||||
is_validator_param: false,
|
||||
}],
|
||||
body: UntypedExpr::fail(None, location),
|
||||
doc: None,
|
||||
location,
|
||||
end_position: location.end - 1,
|
||||
name: well_known::VALIDATOR_ELSE.to_string(),
|
||||
public: true,
|
||||
return_annotation: Some(Annotation::boolean(location)),
|
||||
return_type: (),
|
||||
on_test_failure: OnTestFailure::FailImmediately,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedValidator {
|
||||
pub fn available_handler_names() -> Vec<String> {
|
||||
vec![
|
||||
|
@ -621,8 +661,6 @@ impl TypedValidator {
|
|||
},
|
||||
}
|
||||
})
|
||||
// FIXME: This is only needed if there's non-exhaustive patterns
|
||||
// above.
|
||||
.chain(std::iter::once(&self.fallback).map(|fallback| {
|
||||
let arg = fallback.arguments.first().unwrap();
|
||||
|
||||
|
|
|
@ -256,8 +256,7 @@ impl Type {
|
|||
pub fn list(t: Rc<Type>) -> Rc<Type> {
|
||||
Rc::new(Type::App {
|
||||
public: true,
|
||||
// FIXME: We should probably have t.contains_opaque here?
|
||||
contains_opaque: false,
|
||||
contains_opaque: t.contains_opaque(),
|
||||
name: LIST.to_string(),
|
||||
module: "".to_string(),
|
||||
args: vec![t],
|
||||
|
@ -290,8 +289,7 @@ impl Type {
|
|||
pub fn option(a: Rc<Type>) -> Rc<Type> {
|
||||
Rc::new(Type::App {
|
||||
public: true,
|
||||
// FIXME: We should probably have t.contains_opaque here?
|
||||
contains_opaque: false,
|
||||
contains_opaque: a.contains_opaque(),
|
||||
name: OPTION.to_string(),
|
||||
module: "".to_string(),
|
||||
args: vec![a],
|
||||
|
|
|
@ -3,10 +3,10 @@ use crate::{
|
|||
Annotation, ArgBy, ArgName, ArgVia, AssignmentKind, AssignmentPattern, BinOp,
|
||||
ByteArrayFormatPreference, CallArg, Constant, CurveType, DataType, Definition, Function,
|
||||
LogicalOpChainKind, ModuleConstant, OnTestFailure, Pattern, RecordConstructor,
|
||||
RecordConstructorArg, RecordUpdateSpread, Span, TraceKind, TypeAlias, TypedArg, UnOp,
|
||||
UnqualifiedImport, UntypedArg, UntypedArgVia, UntypedAssignmentKind, UntypedClause,
|
||||
UntypedDefinition, UntypedFunction, UntypedIfBranch, UntypedModule, UntypedPattern,
|
||||
UntypedRecordUpdateArg, Use, Validator, CAPTURE_VARIABLE,
|
||||
RecordConstructorArg, RecordUpdateSpread, Span, TraceKind, TypeAlias, TypedArg,
|
||||
TypedValidator, UnOp, UnqualifiedImport, UntypedArg, UntypedArgVia, UntypedAssignmentKind,
|
||||
UntypedClause, UntypedDefinition, UntypedFunction, UntypedIfBranch, UntypedModule,
|
||||
UntypedPattern, UntypedRecordUpdateArg, Use, Validator, CAPTURE_VARIABLE,
|
||||
},
|
||||
docvec,
|
||||
expr::{FnStyle, UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR},
|
||||
|
@ -643,27 +643,31 @@ impl<'comments> Formatter<'comments> {
|
|||
handler_docs.push(first_fn);
|
||||
}
|
||||
|
||||
let fallback_comments = self.pop_comments(fallback.location.start);
|
||||
let fallback_doc_comments = self.doc_comments(fallback.location.start);
|
||||
let is_exhaustive = handlers.len() >= TypedValidator::available_handler_names().len() - 1;
|
||||
|
||||
let fallback_fn = self
|
||||
.definition_fn(
|
||||
&fallback.public,
|
||||
&fallback.name,
|
||||
&fallback.arguments,
|
||||
&fallback.return_annotation,
|
||||
&fallback.body,
|
||||
fallback.end_position,
|
||||
true,
|
||||
)
|
||||
.group();
|
||||
if !is_exhaustive || !fallback.is_default_fallback() {
|
||||
let fallback_comments = self.pop_comments(fallback.location.start);
|
||||
let fallback_doc_comments = self.doc_comments(fallback.location.start);
|
||||
|
||||
let fallback_fn = commented(
|
||||
fallback_doc_comments.append(fallback_fn).group(),
|
||||
fallback_comments,
|
||||
);
|
||||
let fallback_fn = self
|
||||
.definition_fn(
|
||||
&fallback.public,
|
||||
&fallback.name,
|
||||
&fallback.arguments,
|
||||
&fallback.return_annotation,
|
||||
&fallback.body,
|
||||
fallback.end_position,
|
||||
true,
|
||||
)
|
||||
.group();
|
||||
|
||||
handler_docs.push(fallback_fn);
|
||||
let fallback_fn = commented(
|
||||
fallback_doc_comments.append(fallback_fn).group(),
|
||||
fallback_comments,
|
||||
);
|
||||
|
||||
handler_docs.push(fallback_fn);
|
||||
}
|
||||
|
||||
let v_body = line().append(join(handler_docs, lines(2)));
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::function::param;
|
||||
use crate::{
|
||||
ast::{self, well_known, ArgBy, ArgName},
|
||||
ast::{self, well_known},
|
||||
expr::UntypedExpr,
|
||||
parser::{annotation, error::ParseError, expr, token::Token},
|
||||
};
|
||||
|
@ -63,28 +63,8 @@ pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError
|
|||
location,
|
||||
params,
|
||||
end_position: span.end - 1,
|
||||
fallback: opt_catch_all.unwrap_or(ast::Function {
|
||||
arguments: vec![ast::UntypedArg {
|
||||
by: ArgBy::ByName(ArgName::Discarded {
|
||||
name: "_".to_string(),
|
||||
label: "_".to_string(),
|
||||
location,
|
||||
}),
|
||||
location,
|
||||
annotation: None,
|
||||
doc: None,
|
||||
is_validator_param: false,
|
||||
}],
|
||||
body: UntypedExpr::fail(None, location),
|
||||
doc: None,
|
||||
location,
|
||||
end_position: location.end - 1,
|
||||
name: well_known::VALIDATOR_ELSE.to_string(),
|
||||
public: true,
|
||||
return_annotation: Some(ast::Annotation::boolean(location)),
|
||||
return_type: (),
|
||||
on_test_failure: ast::OnTestFailure::FailImmediately,
|
||||
}),
|
||||
fallback: opt_catch_all
|
||||
.unwrap_or(ast::UntypedValidator::default_fallback(location)),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
|
@ -3147,3 +3147,76 @@ fn validator_by_name_validator_duplicate_2() {
|
|||
Err((_, Error::DuplicateName { .. }))
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhaustive_handlers() {
|
||||
let source_code = r#"
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
assert!(check_validator(parse(source_code)).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extraneous_fallback_on_exhaustive_handlers() {
|
||||
let source_code = r#"
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
else (_) -> Bool {
|
||||
fail
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
assert!(matches!(
|
||||
check_validator(parse(source_code)),
|
||||
Err((_, Error::UnexpectedValidatorFallback { .. }))
|
||||
))
|
||||
}
|
||||
|
|
|
@ -1140,3 +1140,110 @@ fn format_long_pair() {
|
|||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_validator_exhaustive_handlers() {
|
||||
assert_format!(
|
||||
r#"
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_validator_exhaustive_handlers_extra_default_fallback() {
|
||||
assert_format!(
|
||||
r#"
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
else(_) {
|
||||
fail
|
||||
}
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_validator_exhaustive_handlers_extra_non_default_fallback() {
|
||||
assert_format!(
|
||||
r#"
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
else(_) {
|
||||
True
|
||||
}
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
|
29
crates/aiken-lang/src/tests/snapshots/format_validator_exhaustive_handlers.snap
vendored
Normal file
29
crates/aiken-lang/src/tests/snapshots/format_validator_exhaustive_handlers.snap
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: crates/aiken-lang/src/tests/format.rs
|
||||
description: "Code:\n\nvalidator foo {\n mint(_redeemer, _policy_id, _self) {\n True\n }\n\n spend(_datum, _redeemer, _policy_id, _self) {\n True\n }\n\n withdraw(_redeemer, _account, _self) {\n True\n }\n\n publish(_redeemer, _certificate, _self) {\n True\n }\n\n vote(_redeemer, _voter, _self) {\n True\n }\n\n propose(_redeemer, _proposal, _self) {\n True\n }\n}\n"
|
||||
---
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: crates/aiken-lang/src/tests/format.rs
|
||||
description: "Code:\n\nvalidator foo {\n mint(_redeemer, _policy_id, _self) {\n True\n }\n\n spend(_datum, _redeemer, _policy_id, _self) {\n True\n }\n\n withdraw(_redeemer, _account, _self) {\n True\n }\n\n publish(_redeemer, _certificate, _self) {\n True\n }\n\n vote(_redeemer, _voter, _self) {\n True\n }\n\n propose(_redeemer, _proposal, _self) {\n True\n }\n\n else(_) {\n fail\n }\n}\n"
|
||||
---
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
source: crates/aiken-lang/src/tests/format.rs
|
||||
description: "Code:\n\nvalidator foo {\n mint(_redeemer, _policy_id, _self) {\n True\n }\n\n spend(_datum, _redeemer, _policy_id, _self) {\n True\n }\n\n withdraw(_redeemer, _account, _self) {\n True\n }\n\n publish(_redeemer, _certificate, _self) {\n True\n }\n\n vote(_redeemer, _voter, _self) {\n True\n }\n\n propose(_redeemer, _proposal, _self) {\n True\n }\n\n else(_) {\n True\n }\n}\n"
|
||||
---
|
||||
validator foo {
|
||||
mint(_redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
spend(_datum, _redeemer, _policy_id, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
withdraw(_redeemer, _account, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
publish(_redeemer, _certificate, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
vote(_redeemer, _voter, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
propose(_redeemer, _proposal, _self) {
|
||||
True
|
||||
}
|
||||
|
||||
else(_) {
|
||||
True
|
||||
}
|
||||
}
|
|
@ -1101,6 +1101,16 @@ The best thing to do from here is to remove it."#))]
|
|||
location: Span,
|
||||
available_handlers: Vec<String>,
|
||||
},
|
||||
|
||||
#[error("I caught an extraneous fallback handler in an already exhaustive validator\n")]
|
||||
#[diagnostic(code("extraneous::fallback"))]
|
||||
#[diagnostic(help(
|
||||
"Validator handlers must be exhaustive and either cover all purposes, or provide a fallback handler. Here, you have successfully covered all script purposes with your handler, but left an extraneous fallback branch. I cannot let that happen, but removing it for you would probably be deemed rude. So please, remove the fallback."
|
||||
))]
|
||||
UnexpectedValidatorFallback {
|
||||
#[label("redundant fallback handler")]
|
||||
fallback: Span,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExtraData for Error {
|
||||
|
@ -1162,6 +1172,7 @@ impl ExtraData for Error {
|
|||
| Error::ValidatorMustReturnBool { .. }
|
||||
| Error::UnknownPurpose { .. }
|
||||
| Error::UnknownValidatorHandler { .. }
|
||||
| Error::UnexpectedValidatorFallback { .. }
|
||||
| Error::MustInferFirst { .. } => None,
|
||||
|
||||
Error::UnknownType { name, .. }
|
||||
|
|
|
@ -9,7 +9,8 @@ use crate::{
|
|||
ast::{
|
||||
Annotation, ArgName, ArgVia, DataType, Definition, Function, ModuleConstant, ModuleKind,
|
||||
RecordConstructor, RecordConstructorArg, Tracing, TypeAlias, TypedArg, TypedDefinition,
|
||||
TypedModule, TypedValidator, UntypedArg, UntypedDefinition, UntypedModule, Use, Validator,
|
||||
TypedModule, TypedValidator, UntypedArg, UntypedDefinition, UntypedModule,
|
||||
UntypedValidator, Use, Validator,
|
||||
},
|
||||
tipo::{expr::infer_function, Span, Type, TypeVar},
|
||||
IdGenerator,
|
||||
|
@ -247,6 +248,20 @@ fn infer_definition(
|
|||
typed_handlers.push(typed_fun);
|
||||
}
|
||||
|
||||
// NOTE: Duplicates are handled when registering handler names. So if we have N
|
||||
// typed handlers, they are different. The -1 represents takes out the fallback
|
||||
// handler name.
|
||||
let is_exhaustive =
|
||||
typed_handlers.len() >= TypedValidator::available_handler_names().len() - 1;
|
||||
|
||||
if is_exhaustive
|
||||
&& fallback != UntypedValidator::default_fallback(fallback.location)
|
||||
{
|
||||
return Err(Error::UnexpectedValidatorFallback {
|
||||
fallback: fallback.location,
|
||||
});
|
||||
}
|
||||
|
||||
let (typed_params, typed_fallback) = environment.in_new_scope(|environment| {
|
||||
let temp_params = params.iter().cloned().chain(fallback.arguments);
|
||||
fallback.arguments = temp_params.collect();
|
||||
|
|
Loading…
Reference in New Issue