12e9a9c0cc
The 15a tester surfaced a real compiler limitation: cross-module
type and ctor references were not implemented. Iter 5b only
carried fns + consts via module_globals; types/ctors stayed
module-local with an explicit DESIGN comment. This iter completes
the cross-module mechanism using the Iter-5b convention:
qualified-only access via module.Name.
Implementation:
- ailang-check: Env.module_types populated by build_module_types.
Qualified resolution in Type::Con, Term::Ctor, with cross-module
fallback for pat-ctor (local wins, multi-import collision -> new
ambiguous-ctor diagnostic). Four new unit tests.
- ailang-codegen: workspace-level module_ctor_index replaces
per-Emitter table. lookup_ctor_by_type / lookup_ctor_in_pattern
thread qualified type names through box-tag and field-type
resolution.
- examples/std_maybe_demo.{ailx,ail.json}: type-name slots now
qualified (std_maybe.Maybe).
- New e2e test cross_module_maybe_demo asserts the demo prints
["7","99","true","true","42"].
Net diff ~550 LOC. Tests 80 -> 85. All Iter 14a regressions
(parameterised_box_round_trip, parameterised_maybe_match,
list_map_poly_inc_then_prints, polymorphic_id_at_int_and_bool)
verified green — the 14h derive_substitution change (default
unpinned forall vars to Unit for monomorphiser) sits on a
different layer than 14a's $u-wildcard fix and they coexist.
Hash invariance: all five std_maybe def hashes unchanged. All
80-test-suite fixtures retain bit-identical hashes — cross-module
support is purely additive at the language level.
Process note: the std_maybe.ailx file landed in the 14g commit
via a sloppy git-add-A; should have spotted it before staging.
Not a correctness issue but a hygiene one.
Implementer flagged Unit-default monomorphisation as wasteful-
but-correct; rethink if stdlib grows toward overload-resolution-
style cases needing distinct unconstrained instantiations.
std_maybe stdlib effectively ships: module + four combinators +
e2e-tested consumer demo. Plan 15b: std_list importing std_maybe,
exercising Maybe-returning head/tail and tail-call-marked
fold_left.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
147 lines
5.8 KiB
Rust
147 lines
5.8 KiB
Rust
//! Structured diagnostics for `ail check --json`.
|
|
//!
|
|
//! A [`Diagnostic`] is the machine-readable representation of a problem
|
|
//! reported by the typechecker (or an upstream load step).
|
|
//! [`Severity`] serializes as a lowercase string
|
|
//! (`"error"` / `"warning"`); the `code` field is a stable kebab-case identifier
|
|
//! that tooling can consume without parsing the `message` text.
|
|
//!
|
|
//! Convention: each call to [`crate::check_module`] reports at most one
|
|
//! diagnostic (single-shot). Multiple diagnostics per run are a future
|
|
//! feature; the current format already allows them.
|
|
//!
|
|
//! Stable codes (as of iteration 5b):
|
|
//! - `schema-mismatch`
|
|
//! - `unknown-type`
|
|
//! - `unbound-var`
|
|
//! - `type-mismatch` — `ctx`: `{"expected": "...", "actual": "..."}`
|
|
//! - `arity-mismatch` — `ctx`: `{"expected": N, "actual": M}`
|
|
//! - `unknown-ctor`
|
|
//! - `unknown-effect-op`
|
|
//! - `non-exhaustive-match` — `ctx`: `{"missing": ["..."]}`
|
|
//! - `unknown-ctor-in-pattern`
|
|
//! - `nested-ctor-pattern-not-allowed`
|
|
//! - `duplicate-def`
|
|
//! - `not-a-function`
|
|
//! - `undeclared-effect`
|
|
//! - `fn-type-required`
|
|
//! - `param-count-mismatch`
|
|
//! - `polymorphic-not-supported`
|
|
//! - `const-has-effects`
|
|
//! - `pattern-type-mismatch`
|
|
//! - `primitive-needs-wildcard`
|
|
//! - `duplicate-type`
|
|
//! - `duplicate-ctor`
|
|
//! - `unknown-module` — `ctx`: `{"module": "<prefix>"}` (Iter 5b)
|
|
//! - `unknown-import` — `ctx`: `{"module": "<m>", "name": "<def>"}` (Iter 5b)
|
|
//! - `invalid-def-name` — `ctx`: `{"name": "<n>", "reason": "contains-dot"}` (Iter 5b)
|
|
//! - `module-not-found` — workspace loader (Iter 5b, in the CLI path)
|
|
//! - `module-cycle` — workspace loader (Iter 5b, in the CLI path)
|
|
//! - `module-name-mismatch` — workspace loader (Iter 5b, in the CLI path)
|
|
//! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
|
|
//! - `tail-call-not-in-tail-position` (Iter 14e, see Decision 8)
|
|
//! - `ambiguous-ctor` — `ctx`: `{"ctor": "<n>", "candidates": ["m1.T", "m2.T"]}` (Iter 15a)
|
|
|
|
use serde::Serialize;
|
|
|
|
/// Severity of a [`Diagnostic`].
|
|
///
|
|
/// Serializes as a lowercase string (`"error"` / `"warning"`) so that
|
|
/// `ail check --json` consumers can branch on it without parsing prose.
|
|
/// The MVP only emits [`Severity::Error`]; [`Severity::Warning`] is
|
|
/// reserved for future lints.
|
|
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum Severity {
|
|
/// Hard failure. The module did not typecheck.
|
|
Error,
|
|
/// Non-fatal observation. Reserved; the typechecker does not emit
|
|
/// warnings yet.
|
|
Warning,
|
|
}
|
|
|
|
/// Machine-readable problem report from the typechecker.
|
|
///
|
|
/// Stable JSON shape: every field is always emitted (no `skip_serializing_if`),
|
|
/// so a tool can decode into a fixed schema without conditional handling.
|
|
/// See the module-level doc for the list of `code` values.
|
|
#[derive(Serialize, Debug, Clone)]
|
|
pub struct Diagnostic {
|
|
/// Severity of the diagnostic; see [`Severity`].
|
|
pub severity: Severity,
|
|
/// Stable kebab-case identifier (e.g. `"unbound-var"`,
|
|
/// `"type-mismatch"`). Tool consumers should branch on this rather
|
|
/// than on `message`. The full enumeration is listed in the module
|
|
/// doc.
|
|
pub code: String,
|
|
/// Human-readable description. Free-form; do not parse. The
|
|
/// `code` + `ctx` pair carries the structured equivalent.
|
|
pub message: String,
|
|
/// Which top-level definition is affected (if known). Always emitted in
|
|
/// the JSON — `null` when unknown — so consumers don't have to handle
|
|
/// the field conditionally.
|
|
pub def: Option<String>,
|
|
/// Free structured context. Empty = `{}`.
|
|
pub ctx: serde_json::Value,
|
|
}
|
|
|
|
impl Diagnostic {
|
|
/// Builds an [`Severity::Error`] diagnostic with the given `code` and
|
|
/// `message`. `def` is left unset and `ctx` defaults to `{}` — chain
|
|
/// [`Diagnostic::with_def`] / [`Diagnostic::with_ctx`] to fill them
|
|
/// in.
|
|
pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
|
|
Self {
|
|
severity: Severity::Error,
|
|
code: code.into(),
|
|
message: message.into(),
|
|
def: None,
|
|
ctx: serde_json::Value::Object(serde_json::Map::new()),
|
|
}
|
|
}
|
|
|
|
/// Sets the affected top-level def name. Builder-style: returns
|
|
/// `self`. Used by [`crate::CheckError::to_diagnostic`] when the
|
|
/// error was wrapped in [`crate::CheckError::Def`].
|
|
pub fn with_def(mut self, def: impl Into<String>) -> Self {
|
|
self.def = Some(def.into());
|
|
self
|
|
}
|
|
|
|
/// Replaces the `ctx` payload. Builder-style: returns `self`. Pass a
|
|
/// `serde_json::Value` whose shape matches the `code`-specific
|
|
/// schema documented at the module level (e.g.
|
|
/// `{"expected": "...", "actual": "..."}` for `type-mismatch`).
|
|
pub fn with_ctx(mut self, ctx: serde_json::Value) -> Self {
|
|
self.ctx = ctx;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn serializes_with_stable_shape() {
|
|
let d = Diagnostic::error("unbound-var", "unknown identifier: `x`")
|
|
.with_def("main");
|
|
let s = serde_json::to_string(&d).unwrap();
|
|
// All fields must be present, severity lowercase, ctx is an
|
|
// empty object (not null, not omitted).
|
|
assert!(s.contains("\"severity\":\"error\""), "{s}");
|
|
assert!(s.contains("\"code\":\"unbound-var\""), "{s}");
|
|
assert!(s.contains("\"def\":\"main\""), "{s}");
|
|
assert!(s.contains("\"ctx\":{}"), "{s}");
|
|
}
|
|
|
|
#[test]
|
|
fn ctx_can_carry_structured_data() {
|
|
let d = Diagnostic::error("type-mismatch", "type mismatch")
|
|
.with_ctx(serde_json::json!({"expected": "Int", "actual": "Bool"}));
|
|
let s = serde_json::to_string(&d).unwrap();
|
|
assert!(s.contains("\"expected\":\"Int\""), "{s}");
|
|
assert!(s.contains("\"actual\":\"Bool\""), "{s}");
|
|
}
|
|
}
|