diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9999721..00830753 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
### Changed
+- **aiken-lang**: Forbid constants evaluating to generic or unbound functions. Same restrictions as for validators or any exported UPLC programs apply here. @KtorZ & @MicroProofs
- **aiken-lang**: Fix compiler crash on trace + expect as last expression of a clause. See #1029. @KtorZ
- **aiken-lang**: Fix redundant warning on introduced identifiers when destructuring validator params. @KtorZ
- **aiken-lsp**: Compile project using verbose tracing, to avoid having the language server complain about unused imports. @KtorZ
diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs
index ed4ae19c..6a525456 100644
--- a/crates/aiken-lang/src/tests/check.rs
+++ b/crates/aiken-lang/src/tests/check.rs
@@ -3394,3 +3394,54 @@ fn destructuring_validator_params_record() {
"should be empty: {warnings:#?}"
);
}
+
+#[test]
+fn constant_generic_lambda() {
+ let source_code = r#"const foo: fn(a) -> List = fn(x: a) { [x] }"#;
+ assert!(matches!(
+ check(parse(source_code)),
+ Err((_, Error::GenericLeftAtBoundary { .. }))
+ ))
+}
+
+#[test]
+fn constant_generic_mismatch() {
+ let source_code = r#"const foo: List = [42]"#;
+ assert!(matches!(
+ check(parse(source_code)),
+ Err((_, Error::CouldNotUnify { .. }))
+ ))
+}
+
+#[test]
+fn constant_generic_inferred_1() {
+ let source_code = r#"const foo = [42]"#;
+ assert!(check(parse(source_code)).is_ok());
+}
+
+#[test]
+fn constant_generic_inferred_2() {
+ let source_code = r#"
+ const foo = fn(x) { [x] }(42)
+
+ test my_test() {
+ foo == [42]
+ }
+ "#;
+ assert!(check(parse(source_code)).is_ok());
+}
+
+#[test]
+fn constant_generic_inferred_3() {
+ let source_code = r#"const foo: List = fn(x) { [x] }(42)"#;
+ assert!(matches!(
+ check(parse(source_code)),
+ Err((_, Error::CouldNotUnify { .. }))
+ ))
+}
+
+#[test]
+fn constant_generic_empty() {
+ let source_code = r#"const foo: List = []"#;
+ assert!(check_validator(parse(source_code)).is_ok());
+}
diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs
index 66655537..849d2571 100644
--- a/crates/aiken-lang/src/tipo/error.rs
+++ b/crates/aiken-lang/src/tipo/error.rs
@@ -1044,7 +1044,7 @@ The best thing to do from here is to remove it."#))]
#[error("I choked on a generic type left in an outward-facing interface.\n")]
#[diagnostic(code("illegal::generic_in_abi"))]
#[diagnostic(help(
- "Functions of the outer-most parts of a project, such as a validator or a property-based test, must be fully instantiated. That means they can no longer carry unbound generic variables. The type must be fully-known at this point since many structural validation must occur to ensure a safe boundary between the on-chain and off-chain worlds."
+ "Elements of the outer-most parts of a project, such as a validator, constants or a property-based test, must be fully instantiated. That means they can no longer carry unbound or generic variables. The type must be fully-known at this point since many structural validation must occur to ensure a safe boundary between the on-chain and off-chain worlds."
))]
GenericLeftAtBoundary {
#[label("unbound generic at boundary")]
diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs
index d69ef03e..6f59bd96 100644
--- a/crates/aiken-lang/src/tipo/infer.rs
+++ b/crates/aiken-lang/src/tipo/infer.rs
@@ -655,6 +655,10 @@ fn infer_definition(
let tipo = typed_expr.tipo();
+ if tipo.is_function() && !tipo.is_monomorphic() {
+ return Err(Error::GenericLeftAtBoundary { location });
+ }
+
let variant = ValueConstructor {
public,
variant: ValueConstructorVariant::ModuleConstant {