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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/psrs-cst",
"crates/psrs-ast",
"crates/psrs-hir",
"crates/psrs-coverage",
"crates/psrs-resolve",
"crates/psrs-thir",
"crates/psrs-typecheck",
Expand All @@ -30,6 +31,7 @@ psrs-span = { path = "crates/psrs-span" }
psrs-cst = { path = "crates/psrs-cst" }
psrs-ast = { path = "crates/psrs-ast" }
psrs-hir = { path = "crates/psrs-hir" }
psrs-coverage = { path = "crates/psrs-coverage" }
psrs-resolve = { path = "crates/psrs-resolve" }
psrs-thir = { path = "crates/psrs-thir" }
psrs-typecheck = { path = "crates/psrs-typecheck" }
Expand Down
36 changes: 15 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ See [package and conformance setup](docs/workflow/stdlib-conformance.md).
library from the locked `psrs-stdlib` package and writes the artifact.
`wat <file.purs>...` renders the text form. `dump <core|cc|mir> <file.purs>` prints an intermediate IR for
debugging. A selected `main :: Int` returns its value as the process exit code.
A selected `main :: Effect Unit` runs that action once and returns 0; a trap
still propagates.
The package-configured runner executes a selected `main :: Effect a` once,
discards its result, and returns 0; a trap still propagates.

For a guided, interactive walkthrough of P0 through P11, use the React
application in [`psrs-explorer/`](psrs-explorer/). It labels compact teaching
Expand All @@ -53,10 +53,15 @@ what remains in each layer.
| 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 | 58/81 failing | official `errorCode`s |
| L6/M7 runtime | 210/413 passing | all 210 exit 0; 203 do not agree, including 47 with no selected `main` |
| L5 classes | 81/97 failing | official `errorCode`s, measured 2026-10-17 |
| 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 |

Frontend counts use source files; runtime counts use program roots with their
support sources. The 2026-10-17 runtime remeasurement uses the release harness
and records completion, not agreement with upstream output goldens. Its program
denominator is not directly comparable with the earlier per-file totals.

Run the scoreboards yourself:

```sh
Expand Down Expand Up @@ -100,30 +105,19 @@ Verified working subsets, each with source tests and Wasmtime execution:

### What does not

- **surface lowering** — guards on equations and `case` alternatives, operator
and constructor-operator aliases, operator sections, unary minus, type
wildcards, pattern bindings, and multi-scrutinee `case`; 106 `passing` files
stop here;
- **resolution** — instance declarations are not resolved yet, the `Prim`
module hierarchy is not provided, and unary minus is not desugared;
- **diagnostics** — the type checker now emits official `errorCode`s for a type
mismatch, an occurs check, an out-of-range literal, a missing or overlapping
instance, a missing class member, an escaping skolem, and an ambiguous
constraint, so 49 M4/M5 cases are measurable; the eight class checks D-04
lists as `0/n` do not exist yet;
- **frontend agreement** — kind, type, and class checking still have the gaps
recorded in D-04; warning-code agreement is not measured;
- **runtime** — open rows have no runtime layout, variants are not implemented,
and the scalar and numeric operation set is partial
([scalars](docs/design/backend/fp/scalars-and-primitives.md));
- **aggregates at the ABI boundary** — the canonical ABI covers the mapped
scalar, string, list, flags, handle, and variant shapes
([canonical ABI](docs/design/backend/wasm/canonical-abi-and-wit.md)), and the
synthesized aggregate fixtures validate but do not yet execute;
- **the standard library** — the independent `psrs-stdlib` package holds the
pinned official core libraries (215 source modules). A module the compiler owns,
such as `Safe.Coerce`,
resolves through its primitive interface rather than the vendored file, which
stays faithful to upstream; `Unsafe.Coerce.unsafeCoerce` has no interface yet
and is a recorded gap.
- **the standard library** — the independent `psrs-stdlib` package holds pinned
official sources and target adapters; several foreign bindings remain
unimplemented. Compiler-owned modules such as `Safe.Coerce` and
`Unsafe.Coerce` resolve through their declared primitive interfaces.

## Workspace

Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ license.workspace = true
[dependencies]
psrs-core.workspace = true
psrs-hir.workspace = true
psrs-coverage.workspace = true
psrs-span.workspace = true
psrs-linker.workspace = true
psrs-runtime = { workspace = true, default-features = false, features = ["catalog", "gc-storage"] }
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/abi/link/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn module() -> CoreModule {
CoreType::Constructor(psrs_core::TypeConstructor::Unit),
],
newtype_ids: Vec::new(),
ordinary_data_ids: Vec::new(),
opaque_ids: Vec::new(),
callable_types: Vec::new(),
constructors: Vec::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/abi/tests/aggregates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn aggregate_module() -> psrs_core::Module {
let maybe = user(0);
let either = user(1);
let shape = user(2);
module.ordinary_data_ids = vec![maybe, either, shape];
module.type_names = vec![
(maybe, "Data.Maybe.Maybe".into()),
(either, "Data.Either.Either".into()),
Expand Down
3 changes: 3 additions & 0 deletions crates/psrs-backend/src/abi/tests/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ fn maps_nullary_source_constructors_to_matching_wit_enum_cases() {
let span = TextRange::new(0, 1);
let type_id = HirTypeId::new(ModuleId(0), 0);
let mut core = empty_core_module();
core.ordinary_data_ids.push(type_id);
core.constructors = ["Red", "GreenBlue"]
.into_iter()
.enumerate()
Expand Down Expand Up @@ -167,6 +168,7 @@ fn maps_nullary_source_constructors_to_matching_wit_enum_cases() {
// The same enum with reversed constructor tags must not match the WIT
// enum case order.
let mut reversed_core = empty_core_module();
reversed_core.ordinary_data_ids.push(type_id);
reversed_core.constructors = ["Red", "GreenBlue"]
.into_iter()
.rev()
Expand Down Expand Up @@ -197,6 +199,7 @@ fn validates_a_list_of_nullary_enums() {
let type_id = HirTypeId::new(ModuleId(0), 0);
let cases = ["Red", "GreenBlue"];
let mut module = empty_core_module();
module.ordinary_data_ids.push(type_id);
module.constructors = cases
.iter()
.enumerate()
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/abi/tests/lists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fn empty_core() -> CoreModule {
external_types: Vec::new(),
types: Vec::new(),
newtype_ids: Vec::new(),
ordinary_data_ids: Vec::new(),
opaque_ids: Vec::new(),
callable_types: Vec::new(),
constructors: Vec::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/abi/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fn empty_core_module() -> psrs_core::Module {
external_types: Vec::new(),
types: Vec::new(),
newtype_ids: Vec::new(),
ordinary_data_ids: Vec::new(),
opaque_ids: Vec::new(),
callable_types: Vec::new(),
constructors: Vec::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub use runtime::metadata::RuntimeBinding;
pub(crate) use runtime::metadata::{
from_core as runtime_metadata_from_core, validate_core as validate_runtime_bindings,
};
pub(crate) use runtime::record_projection::{operation_for_call, operation_matches};
pub(crate) use runtime::target::{
plan_import as plan_runtime_import, verify_import as verify_runtime_import,
};
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/bindings/primitives/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn fixture(types: &[TypeId]) -> Module {
Type::Application(TypeId(3), TypeId(0)),
],
newtype_ids: Vec::new(),
ordinary_data_ids: Vec::new(),
opaque_ids: Vec::new(),
callable_types: Vec::new(),
constructors: Vec::new(),
Expand Down
97 changes: 96 additions & 1 deletion crates/psrs-backend/src/bindings/runtime/call/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Ordinary CC call signatures for the runtime storage ABI.
use crate::{BackendError, RuntimeBinding, cc};
use psrs_runtime::{StorageOperation, StorageValue};
use psrs_runtime::{RecordSourceValue, StorageOperation, StorageValue};

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -39,6 +39,9 @@ pub(crate) fn canonical_signature(
signature: &cc::Signature,
table: &cc::RepresentationTable,
) -> Result<cc::Signature, Vec<BackendError>> {
if binding.module == psrs_runtime::RECORD_STORAGE_MODULE {
return canonical_record_signature(binding, signature, table);
}
if binding.module != psrs_runtime::STORAGE_MODULE {
return Err(error(binding, "unknown runtime storage provider"));
}
Expand Down Expand Up @@ -89,3 +92,95 @@ pub(crate) fn canonical_signature(
result: shape(contract.payload, signature.result)?,
})
}

fn canonical_record_signature(
binding: &RuntimeBinding,
signature: &cc::Signature,
table: &cc::RepresentationTable,
) -> Result<cc::Signature, Vec<BackendError>> {
let operation = psrs_runtime::RecordPrimitive::from_export(&binding.function)
.ok_or_else(|| error(binding, "unknown record-storage export"))?;
let storage = table.record_storage().ok_or_else(|| {
error(
binding,
"record-storage provider has no shared owner schema",
)
})?;
let contract = operation.source_contract();
if signature.parameters.len() != contract.parameters.len() {
return Err(error(
binding,
"record primitive CC signature has wrong arity",
));
}
let record_shape = cc::ValueShape::Reference(cc::Reference {
nullable: false,
heap: cc::RefShape::Repr(storage.representation()),
});
let shape = |expected, actual| -> Result<_, Vec<BackendError>> {
match expected {
RecordSourceValue::Record => {
let cc::ValueShape::Reference(cc::Reference {
nullable: false,
heap: cc::RefShape::Repr(representation),
}) = actual
else {
return Err(error(
binding,
"record operand has no canonical record shape",
));
};
if representation != storage.representation()
&& !matches!(
table.representation(representation),
Some(cc::Representation::Product { .. })
)
{
return Err(error(
binding,
"record operand is not a closed record product",
));
}
Ok(record_shape)
}
RecordSourceValue::Int if actual == cc::ValueShape::Integer => Ok(actual),
RecordSourceValue::String if actual == cc::ValueShape::String => Ok(actual),
RecordSourceValue::Slot if actual == cc::payload::erased_shape() => Ok(actual),
RecordSourceValue::ArrayString | RecordSourceValue::ArraySlot => {
if let cc::ValueShape::Reference(cc::Reference {
nullable: false,
heap: cc::RefShape::Repr(id),
}) = actual
&& matches!(table.representation(id), Some(cc::Representation::Array { element }) if *element == cc::payload::erased_shape())
{
Ok(actual)
} else {
Err(error(
binding,
"record primitive arrays require erased-element storage",
))
}
}
_ => Err(error(
binding,
"record primitive CC shape disagrees with source policy",
)),
}
};
let parameters = contract
.parameters
.iter()
.copied()
.zip(signature.parameters.iter().copied())
.map(|(expected, actual)| shape(expected, actual))
.collect::<Result<Vec<_>, _>>()?;
let result = shape(contract.result, signature.result)?;
let canonical = cc::Signature { parameters, result };
if canonical.parameters.len() != signature.parameters.len() {
return Err(error(
binding,
"record primitive CC signature has the wrong parameter count",
));
}
Ok(canonical)
}
38 changes: 38 additions & 0 deletions crates/psrs-backend/src/bindings/runtime/call/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,41 @@ fn canonical_runtime_call_rejects_malformed_cc_signatures() {
module.externals.clear();
assert!(checked(&binding, &module).is_err());
}

#[test]
fn canonical_record_call_packs_closed_record_arguments_through_shared_storage() {
let binding = RuntimeBinding {
symbol: psrs_hir::SymbolId::new(psrs_hir::ModuleId(7), 2),
source_module: psrs_hir::ModuleId(7),
module: psrs_runtime::RECORD_STORAGE_MODULE.into(),
function: "length".into(),
type_id: None,
span: psrs_span::TextRange::new(0, 1),
};
let mut table = cc::RepresentationTable::default();
let storage = table.ensure_record_storage();
let closed_record = table.reserve();
table.set(
closed_record,
cc::Representation::Product {
fields: vec![cc::ValueShape::Integer],
},
);
let source = cc::Signature {
parameters: vec![cc::ValueShape::Reference(cc::Reference {
nullable: false,
heap: cc::RefShape::Repr(closed_record),
})],
result: cc::ValueShape::Integer,
};
let canonical = canonical_signature(&binding, &source, &table)
.expect("a checked closed record is adapted through the shared owner");
assert_eq!(
canonical.parameters,
vec![cc::ValueShape::Reference(cc::Reference {
nullable: false,
heap: cc::RefShape::Repr(storage.representation()),
})]
);
assert_eq!(canonical.result, cc::ValueShape::Integer);
}
14 changes: 14 additions & 0 deletions crates/psrs-backend/src/bindings/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use psrs_runtime::StorageOperation;

pub(super) mod call;
pub(super) mod metadata;
pub(super) mod record_projection;
mod record_source;
mod source;
pub(super) mod target;

Expand Down Expand Up @@ -36,6 +38,18 @@ pub(crate) fn validate(module: &Module) -> Result<(), Vec<BackendError>> {
let error = |message| {
vec![BackendError::new("P8 runtime linking", span, message).with_module(source_module)]
};
if provider == psrs_runtime::RECORD_STORAGE_MODULE {
let operation =
psrs_runtime::RecordPrimitive::from_export(function).ok_or_else(|| {
error(format!(
"runtime provider `{provider}` has no export `{function}`"
))
})?;
let checked = checked
.ok_or_else(|| error("runtime binding has no checked source signature".into()))?;
record_source::validate(module, checked.ty, operation).map_err(error)?;
continue;
}
if provider != psrs_runtime::STORAGE_MODULE {
return Err(error(format!("unknown runtime provider `{provider}`")));
}
Expand Down
Loading
Loading