Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ what remains in each layer.
| --- | --- | --- |
| L0/L1 lexing, layout, parsing | 904/908 | non-FFI `layout`, `passing`, `failing`, `warning` files; the four differences are recorded DEC-16 intentional differences |
| L2 resolution | 72/72 failing, 403/413 passing | official `errorCode`s; 6 passing files stop at P3 and 4 at P0; no harness blockers |
| L3 kinds | 39/48 failing | official kind `errorCode`s |
| L4 types | 39/50 failing | official `errorCode`s |
| L5 classes | 81/97 failing | official `errorCode`s, measured 2026-10-17 |
| L3 kinds | 44/48 failing | official kind `errorCode`s; measured 2026-10-18 |
| L4 types | 41/50 failing | official `errorCode`s; `ExpectedType` 2/2 since the annotation-position rule |
| L5 classes | 98/99 failing | official `errorCode`s; only `ClassHeadNoVTA3` (#123) remains; measured 2026-10-18 |
| L6/M7 runtime | 287/340 programs | all exit 0; 53 do not agree; 74 support files remain program inputs, 25 FFI groups excluded |
| M8 warnings, optimization | not measured | no scoreboard exists |

Expand Down
8 changes: 6 additions & 2 deletions crates/psrs-driver/src/tests/coercion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,11 @@ fn foreign_data_roles_default_to_nominal_and_accept_explicit_signatures() {
}

#[test]
fn rejects_coercion_between_type_constructors_with_different_kinds() {
fn reports_a_coercion_between_type_constructors_with_different_kinds() {
// `failing/CoercibleKindMismatch.purs`: the constraint's role decomposition
// needs `Coercible Unary Binary`, and official solving unifies a picked-up
// constraint's argument kinds before refusing it, so the failure is the
// kind equation's verdict, not a missing instance.
let source = r#"module Main where

import Safe.Coerce (coerce)
Expand All @@ -387,7 +391,7 @@ type role Proxy representational
bad :: Proxy Unary -> Proxy Binary
bad = coerce
"#;
rejects(&[("Main.purs", source)], "NoInstanceFound");
rejects(&[("Main.purs", source)], "KindsDoNotUnify");
}

#[test]
Expand Down
62 changes: 62 additions & 0 deletions crates/psrs-driver/src/tests/declarations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,42 @@ fn reports_decl_conflict_between_a_class_and_a_type() {
);
}

#[test]
fn reports_duplicate_type_class_for_two_classes_of_one_name() {
// Two class declarations sharing one name are the official
// `DuplicateTypeClass`, not the ordinary declaration conflict.
let source = "module Main where\nclass C\nclass C\n";
let errors = check_program_lenient(&[("Main.purs", source)]).unwrap_err();
let codes: Vec<_> = errors
.iter()
.filter_map(|error| error.diagnostic.code)
.collect();
assert!(
codes.contains(&"DuplicateTypeClass") && !codes.contains(&"DeclConflict"),
"{codes:?}"
);
}

#[test]
fn still_reports_decl_conflict_when_a_class_meets_another_declaration() {
// Only a class meeting an earlier class is `DuplicateTypeClass`; a class
// name colliding with a data type, a synonym, or a constructor keeps the
// ordinary `DeclConflict` code.
for source in [
"module Main where\nclass Fail\ndata Fail\n",
"module Main where\ndata Fail\nclass Fail\n",
"module Main where\ndata T = Fail\nclass Fail\n",
] {
let errors = check_program_lenient(&[("Main.purs", source)]).unwrap_err();
assert!(
errors
.iter()
.any(|error| error.diagnostic.code == Some("DeclConflict")),
"{source}: {errors:?}"
);
}
}

#[test]
fn imports_a_type_and_its_constructors_across_modules() {
let library = "module Library where\ndata Maybe a = Nothing | Just a\n";
Expand Down Expand Up @@ -346,3 +382,29 @@ fn reports_a_scope_conflict_for_a_duplicated_qualifier() {
.any(|error| error.diagnostic.code == Some("ScopeConflict"))
);
}

#[test]
fn an_export_shadowing_an_unused_import_conflicts_with_nothing() {
// The corpus shape `NonConflictingExports`: `Main (thing, main)` exports
// its own `thing` while `A`, imported but never used, also defines
// `thing`. The export refers to `Main.thing` only, so no export or scope
// conflict is reported, the program type checks, and `main` executes.
let a = "module A where\nthing :: Int\nthing = 1\n";
let main = "module Main (thing, main) where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport A\nimport WASI.Console\nthing :: Int\nthing = 2\nmain = let ignored = unsafePerformEffect (log \"Done\") in 0\n";
let sources = [("A.purs", a), ("Main.purs", main)];
// Compiling includes resolution, so acceptance here covers the export
// resolution as well as the type check.
crate::compile_program_sources_with_prelude(&sources)
.expect("a local declaration shadows an unused import");
let Some(output) = run_program_with_prelude_wasmtime(&sources) else {
eprintln!("skipping: wasmtime is not installed");
return;
};
assert!(output.status.success(), "the guest must run to completion");
assert!(
String::from_utf8_lossy(&output.stdout)
.trim_end()
.ends_with("Done"),
"the program must execute with its own export in place: {output:?}"
);
}
5 changes: 4 additions & 1 deletion crates/psrs-driver/src/tests/deriving/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,13 @@ fn fold_deriving_reports_missing_core_values_at_the_declaration() {
#[test]
fn deriving_rejects_non_constructor_heads_and_invalid_class_arity() {
for (library, main, expected) in [
// A class the deriving module does not own and a primitive head make
// the declaration an official orphan: `checkOrphanInstance` throws
// before the derivation runs, so the orphan code is the whole report.
(
"module Data.Eq where\nclass Eq a where\n eq :: a -> a -> Boolean\n",
"module Main where\nimport Data.Eq\nderive instance eqInt :: Eq Int\nmain = 0\n",
"ExpectedTypeConstructor",
"OrphanInstance",
),
(
"module Data.Eq where\nclass Eq a b where\n eq :: a -> b -> Boolean\n",
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-driver/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod number_rounding;
mod number_sqrt;
mod number_trunc;
mod operators;
mod orphan_instances;
mod partial_application;
mod primitive_foreign;
mod record_storage;
Expand Down
200 changes: 200 additions & 0 deletions crates/psrs-driver/src/tests/orphan_instances.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//! The official orphan instance rule, from `purs`'s
//! `TypeChecker.typeCheckAll` (`findNonOrphanModules` and
//! `checkOrphanInstance`): an instance declared in a module that defines
//! neither its class nor a type constructor covering one of the class's
//! functional-dependency covering sets is an orphan, and the official
//! `OrphanInstance` code is raised at the common instance recorder before the
//! instance is recorded, so it can neither overlap with another visible
//! instance nor submit a derived dictionary.

#[test]
fn rejects_an_instance_whose_head_names_no_declared_type() {
// The class is foreign and the head argument is a primitive the declaring
// module does not own, so no covering set is covered by the declaring
// module.
let errors = crate::check_program(&[
("Library.purs", "module Library where\nclass C a where\n op :: a -> a\n"),
(
"Main.purs",
"module Main where\nimport Library\ninstance cInt :: C Int where\n op a = a\nmain = 0\n",
),
])
.expect_err("a foreign-class instance over a primitive argument is an orphan");
assert!(
errors
.iter()
.any(|error| error.diagnostic.code == Some("OrphanInstance")),
"{errors:?}"
);
}

#[test]
fn accepts_an_instance_of_a_class_declared_in_the_same_module() {
// A module that declares its own class is never an orphan, whatever the
// head argument names.
crate::check_program(&[(
"Main.purs",
"module Main where\nclass C a where\n op :: a -> a\ninstance cInt :: C Int where\n op a = a\nmain = op 1\n",
)])
.expect("a locally-declared class's instance is not an orphan");
}

#[test]
fn accepts_an_instance_covered_by_a_local_head_constructor() {
// The class is foreign, but the instance head's own type constructor is
// declared in the declaring module, which satisfies the class's only
// covering set.
crate::check_program(&[
("Library.purs", "module Library where\nclass C a where\n op :: a -> a\n"),
(
"Main.purs",
"module Main where\nimport Library\ndata Local = Local\ninstance cLocal :: C Local where\n op a = a\nmain = 0\n",
),
])
.expect("a locally-declared head constructor makes the instance non-orphan");
}

#[test]
fn rejects_a_nullary_foreign_class_instance() {
// A class with no parameters contributes no covering positions, so a
// foreign nullary instance is always an orphan.
let errors = crate::check_program(&[
(
"Library.purs",
"module Library where\nclass C where\n op :: Int\n",
),
(
"Main.purs",
"module Main where\nimport Library\ninstance c :: C where\n op = 1\nmain = 0\n",
),
])
.expect_err("a foreign nullary class instance is an orphan");
assert!(
errors
.iter()
.any(|error| error.diagnostic.code == Some("OrphanInstance")),
"{errors:?}"
);
}

#[test]
fn rejects_a_head_argument_that_names_no_module() {
// A head argument that is a decided literal names no declaring module, so
// it covers nothing — the official type-level `Symbol` shape.
let errors = crate::check_program(&[
(
"Library.purs",
"module Library where\nclass Foo (s :: Symbol) where\n foo :: String\n",
),
(
"Main.purs",
"module Main where\nimport Library\ninstance fooX :: Foo \"x\" where\n foo = \"\"\nmain = 0\n",
),
])
.expect_err("a literal head argument covers no covering set");
assert!(
errors
.iter()
.any(|error| error.diagnostic.code == Some("OrphanInstance")),
"{errors:?}"
);
}

#[test]
fn rejects_an_instance_outside_the_covering_set_intersection() {
// The official fundep shape: the class's dependencies give it two covering
// sets, and the declaring module covers one but not the other, so it is
// in no intersection — the intersection of covering-set modules is empty.
let errors = crate::check_program(&[
(
"Library.purs",
"module Library where\nclass C l r | l -> r, r -> l\ndata R = R\n",
),
(
"Main.purs",
"module Main where\nimport Library\ndata L = L\ninstance clr :: C L R\nmain = 0\n",
),
])
.expect_err("a module that covers only one covering set declares an orphan");
assert!(
errors
.iter()
.any(|error| error.diagnostic.code == Some("OrphanInstance")),
"{errors:?}"
);
}

#[test]
fn accepts_an_instance_covering_every_set_through_a_local_constructor() {
// The complement of the intersection rule: a locally-declared head
// constructor in every covering set keeps the instance non-orphan, even
// though the remaining arguments are foreign.
crate::check_program(&[
(
"Library.purs",
"module Library where\nclass C a b\ndata Foreign = Foreign\n",
),
(
"Main.purs",
"module Main where\nimport Library\ndata Local = Local\ninstance cab :: C Local Foreign\nmain = 0\n",
),
])
.expect("a locally-declared constructor in the only covering set is not an orphan");
}

#[test]
fn an_orphan_derived_instance_reports_only_the_orphan_code() {
// A `derive instance` whose head, after synonym expansion, names a
// primitive type is an orphan before any derivation runs, mirroring the
// official throw order: the deriving half is never started, so the
// report set is exactly `OrphanInstance`.
let errors = crate::check_program(&[
("Data.Eq.purs", "module Data.Eq where\nclass Eq a where\n eq :: a -> a -> Boolean\n"),
(
"Main.purs",
"module Main where\nimport Data.Eq\ntype Something = Int\nderive instance eqSomething :: Eq Something\nmain = 0\n",
),
])
.expect_err("deriving over a foreign primitive type is an orphan");
assert!(
errors
.iter()
.all(|error| error.diagnostic.code == Some("OrphanInstance")),
"an orphan must be rejected before the deriving half runs: {errors:?}"
);
}

#[test]
fn accepts_and_runs_a_foreign_fundep_class_instance_over_local_heads() {
// The corpus shape `NonOrphanInstanceFunDepExtra`: the class
// `C f l r | l -> r` and `L` live in `Lib`, while `Main` declares `F` and
// `R` and the instance. The class's one official covering set `{f, l}`
// contains the declaring module's `F`, so the instance is accepted and
// recorded, and `main` executes with it.
let sources = [
(
"Lib.purs",
"module Lib where\nclass C f l r | l -> r\ndata L\n",
),
(
"Main.purs",
"module Main where\nimport Effect.Unsafe (unsafePerformEffect)\nimport Prelude\nimport Lib\nimport WASI.Console\ndata F\ndata R\ninstance cflr :: C F L R\nmain = let ignored = unsafePerformEffect (log \"Done\") in 0\n",
),
];
// Compiling runs the whole pipeline, whose typechecker applies the orphan
// rule at the common instance recorder; acceptance means the instance was
// recorded, not merely parsed.
crate::compile_program_sources_with_prelude(&sources)
.expect("a declaring module that covers the class's covering set is not an orphan");
let Some(output) = super::run_program_with_prelude_wasmtime(&sources) else {
eprintln!("skipping: wasmtime is not installed");
return;
};
assert!(output.status.success(), "the guest must run to completion");
assert!(
String::from_utf8_lossy(&output.stdout)
.trim_end()
.ends_with("Done"),
"the program must execute with the instance recorded: {output:?}"
);
}
9 changes: 6 additions & 3 deletions crates/psrs-driver/src/tests/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,11 @@ fn a_source_partial_constraint_reaches_the_registry_declaration() {

#[test]
fn a_source_partial_type_is_rejected_as_a_constraint_kinded_type() {
// `Partial` is a constraint, not a type of kind `Type`: `purs` reports the
// kind of `Partial` as `Constraint` for this same declaration.
// `Partial` has kind `Constraint`, so the value annotation's equation
// `kind ~ Type` fails. Official `TypeChecker/Kinds.hs` raises that
// equation through `checkTypeKind`, whose failure handler is
// `ExpectedType`; the kind unifier's own mismatch is reserved for kinds
// two types impose on each other.
let source = "module Main where\n\
import Prim\n\
value :: Partial\n\
Expand All @@ -232,7 +235,7 @@ fn a_source_partial_type_is_rejected_as_a_constraint_kinded_type() {
assert!(
errors
.iter()
.any(|error| error.diagnostic.code == Some("KindsDoNotUnify")),
.any(|error| error.diagnostic.code == Some("ExpectedType")),
"{errors:?}"
);
}
Expand Down
Loading
Loading