iter 22b.1.1: ClassDef/InstanceDef AST + downstream match arms

Adds Def::Class(ClassDef) and Def::Instance(InstanceDef) variants per
Decision 11 §"Form-A schema". All optional fields (superclass, doc,
default body) carry skip_serializing_if so canonical-JSON bytes of
pre-22b fixtures stay bit-identical.

Downstream match-on-Def sites are extended with explicit Class/Instance
arms. Behaviour for 22b.1 is placeholder (skip in typecheck/codegen,
one-line summary in pretty/manifest, placeholder marker in prose/print)
with TODO comments naming the deferred sub-iters (22b.2 typecheck,
22b.3 codegen, 22b.4 prose-projection arms).
This commit is contained in:
2026-05-09 12:31:23 +02:00
parent 522500a6f6
commit f25d7b6cd6
12 changed files with 264 additions and 4 deletions
+27
View File
@@ -1007,6 +1007,14 @@ fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
}
}
}
// Iter 22b.1: `ail deps` does not yet trace references inside
// class/instance defs. Walking method signatures and bodies
// (default bodies, instance method bodies) lands in 22b.2 along
// with the typecheck arms — until then a class/instance def
// contributes no entries to the dependency graph. Tools that
// call `collect_refs` (`ail deps`, the cross-module-fan-out
// diagnostics) therefore treat them as leaves.
ailang_core::Def::Class(_) | ailang_core::Def::Instance(_) => {}
}
out
}
@@ -1416,6 +1424,25 @@ fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec<String>) {
.join(" | ");
("type", s, vec![])
}
// Iter 22b.1: a one-line `class C a` / `instance C T` summary
// for `ail manifest`. The `effects` slot is empty for both —
// class methods carry their own effect annotations on each
// method signature, but the class itself does not (it is a
// collection of method signatures, not a single function).
ailang_core::Def::Class(c) => (
"class",
format!("{} {}", c.name, c.param),
vec![],
),
ailang_core::Def::Instance(i) => (
"instance",
format!(
"{} {}",
i.class,
ailang_core::pretty::type_to_string(&i.type_)
),
vec![],
),
}
}
+24
View File
@@ -751,6 +751,18 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
name: def.name().to_string(),
args: vec![],
},
// Iter 22b.1: class/instance defs are not yet checked.
// The symbols map is keyed by definition name; class/
// instance contribute their class name, but with no
// concrete pre-monomorphisation type we represent them
// as a placeholder Con. Tools that consume this map
// (`ail diff`, `ail manifest`) read kind separately
// via `def_kind`, so the placeholder type does not
// mislead them.
Def::Class(_) | Def::Instance(_) => Type::Con {
name: def.name().to_string(),
args: vec![],
},
};
symbols.insert(def.name().to_string(), (ty, h));
}
@@ -840,6 +852,10 @@ fn build_module_globals(
name: def_name.to_string(),
args: vec![],
},
// Iter 22b.1: class/instance defs do not contribute
// monomorphic globals to the workspace symbol table
// before 22b.3 monomorphisation runs. Skip them here.
Def::Class(_) | Def::Instance(_) => continue,
};
globals.insert(def_name.to_string(), ty);
}
@@ -944,6 +960,14 @@ fn check_def(def: &Def, env: &Env) -> Result<()> {
Def::Fn(f) => check_fn(f, env),
Def::Const(c) => check_const(c, env),
Def::Type(td) => check_type_def(td, env),
// Iter 22b.1: schema-only landing for class/instance defs.
// Class-schema validation (KindMismatch, InvalidSuperclassParam,
// ConstraintReferencesUnboundTypeVar) and instance-body
// typechecking (with class-method substitution) land in 22b.2.
// Workspace-load coherence checks (Orphan / Duplicate /
// MissingMethod) already fire from `workspace::build_registry`,
// so a malformed instance never reaches this point.
Def::Class(_) | Def::Instance(_) => Ok(()),
}
}
+9
View File
@@ -99,6 +99,11 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
name: def.name().to_string(),
args: vec![],
},
// Iter 22b.1: skip class/instance defs in the global-type
// collection. Class methods become globally typed names
// only after 22b.3 monomorphisation; until then there is
// no concrete `Type` to register here.
Def::Class(_) | Def::Instance(_) => continue,
};
env.globals.insert(def.name().to_string(), ty);
}
@@ -186,6 +191,10 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
c.value = lifter.lift_in_term(&c.value, &mut locals, &c.name)?;
}
Def::Type(_) => {}
// Iter 22b.1: class/instance defs do not enter the lift
// pass yet. 22b.2 will lift method-body lambdas in instance
// methods; for 22b.1 this is a passthrough.
Def::Class(_) | Def::Instance(_) => {}
}
}
+5
View File
@@ -205,6 +205,11 @@ pub(crate) fn check_module(m: &Module) -> Vec<Diagnostic> {
ctors.insert(name.clone(), fields.clone());
}
}
// Iter 22b.1: class/instance defs are skipped here. Once
// 22b.3 monomorphisation materialises class methods as
// ordinary `FnDef`s, the lift's globals map will pick
// them up automatically.
Def::Class(_) | Def::Instance(_) => {}
}
}
+6
View File
@@ -118,6 +118,12 @@ pub fn infer_module(m: &Module) -> UniquenessTable {
globals.insert(c.name.clone(), strip_forall(&c.ty).clone());
}
Def::Type(_) => {}
// Iter 22b.1: class/instance defs are skipped here. Class
// method signatures contribute global names once
// monomorphisation (22b.3) materialises them; the
// pre-monomorphisation form has no concrete `Type` to
// hand to the uniqueness pass.
Def::Class(_) | Def::Instance(_) => {}
}
}
+7
View File
@@ -765,6 +765,13 @@ impl<'a> Emitter<'a> {
// logical type. Heap boxes are allocated ad hoc via
// GC_malloc (Boehm conservative collector, Iter 14f).
}
// Iter 22b.1: class/instance defs do not emit IR yet.
// 22b.3 monomorphisation will rewrite class-method
// calls into calls against synthesised monomorphic
// FnDefs; once that pass runs, class/instance bodies
// never reach the emit path on their own — they only
// appear inlined into the synthesised fns.
Def::Class(_) | Def::Instance(_) => {}
}
}
// Drain the monomorphisation queue. Specialised fns may
+115 -4
View File
@@ -57,9 +57,16 @@ pub struct Import {
/// A top-level definition — the unit that gets a content hash.
///
/// Discriminated by the JSON `kind` tag: `"fn"`, `"const"`, or
/// `"type"`. Use [`def_name`] / [`def_kind`] (or the [`Def::name`]
/// method) to inspect generically without matching every variant.
/// Discriminated by the JSON `kind` tag: `"fn"`, `"const"`, `"type"`,
/// `"class"`, or `"instance"`. Use [`def_name`] / [`def_kind`] (or the
/// [`Def::name`] method) to inspect generically without matching every
/// variant.
///
/// Iter 22b.1 (Decision 11): adds `Class` and `Instance`. Both are
/// additive — pre-22b fixtures never produce these tags, so their
/// canonical-JSON bytes (and therefore their content hashes) are
/// unchanged. The `Def`-level match exhaustiveness in downstream
/// crates is the only caller that has to acknowledge the variants.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Def {
@@ -69,15 +76,26 @@ pub enum Def {
Const(ConstDef),
/// Type (ADT) definition; see [`TypeDef`].
Type(TypeDef),
/// Iter 22b.1: typeclass declaration; see [`ClassDef`].
Class(ClassDef),
/// Iter 22b.1: instance declaration; see [`InstanceDef`].
Instance(InstanceDef),
}
impl Def {
/// Source-level name of the definition, regardless of kind.
///
/// For `Def::Instance` the "name" is the class being instantiated;
/// instances are not separately named at the source level. The
/// workspace registry keys instances by `(class, type-hash)`, so
/// the class name alone is the closest analogue of a "name".
pub fn name(&self) -> &str {
match self {
Def::Fn(f) => &f.name,
Def::Const(c) => &c.name,
Def::Type(t) => &t.name,
Def::Class(c) => &c.name,
Def::Instance(i) => &i.class,
}
}
}
@@ -92,7 +110,7 @@ pub fn def_name(def: &Def) -> &str {
}
/// External helper: discriminator tag of a definition (`"fn"`,
/// `"const"`, `"type"`).
/// `"const"`, `"type"`, `"class"`, `"instance"`).
///
/// Identical to the `kind` field in the JSON representation. Useful
/// for tools that group or filter definitions by kind.
@@ -101,6 +119,8 @@ pub fn def_kind(def: &Def) -> &'static str {
Def::Fn(_) => "fn",
Def::Const(_) => "const",
Def::Type(_) => "type",
Def::Class(_) => "class",
Def::Instance(_) => "instance",
}
}
@@ -220,6 +240,97 @@ pub struct Suppress {
pub because: String,
}
/// Iter 22b.1: a typeclass declaration (Decision 11).
///
/// Single-parameter, multi-method, optional-default, optional-superclass
/// typeclass. The `param` is a single string — multi-param classes are
/// rejected by Decision 11 axis 1, and the schema enforces it by shape
/// (`param: String`, not `Vec<String>`).
///
/// `superclass`, when present, MUST have its `type` field equal to
/// `param`. The check is enforced in 22b.2 via the
/// `InvalidSuperclassParam` diagnostic; the schema does not encode it.
///
/// All optional fields are omitted from canonical JSON when absent /
/// empty, so future schema evolution can land here without disturbing
/// pre-22b hashes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassDef {
/// Class name (capitalised by convention).
pub name: String,
/// Single type-parameter name.
pub param: String,
/// Optional single-superclass relation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub superclass: Option<SuperclassRef>,
/// Methods of the class, in declaration order.
pub methods: Vec<ClassMethod>,
/// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
/// Iter 22b.1: reference to a superclass relation in [`ClassDef`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuperclassRef {
/// Superclass name.
pub class: String,
/// Type the superclass is applied to. MUST equal the parent
/// `ClassDef.param` (validated in 22b.2 — schema does not enforce).
#[serde(rename = "type")]
pub type_: String,
}
/// Iter 22b.1: one method declared in a [`ClassDef`].
///
/// `default` is `None` when the method is abstract-required (every
/// instance must specify it); `Some(body)` when the method has a
/// default body (instances may inherit or override).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassMethod {
/// Method name.
pub name: String,
/// Full method signature including any mode annotations. The
/// class parameter appears as a [`Type::Var`] inside this signature.
#[serde(rename = "type")]
pub ty: Type,
/// Default body. `None` ⇒ abstract-required; `Some(body)` ⇒
/// default-with-fallback.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<Term>,
}
/// Iter 22b.1: an instance declaration (Decision 11).
///
/// `class` is the name of the class being instantiated. `type_` is the
/// concrete type expression the class is applied to — never the class
/// param. `methods` contains bodies for the required methods plus any
/// overrides of default-bearing methods.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceDef {
/// Class being instantiated.
pub class: String,
/// Concrete type the class is applied to.
#[serde(rename = "type")]
pub type_: Type,
/// Method bodies in declaration order.
pub methods: Vec<InstanceMethod>,
/// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
/// Iter 22b.1: one method body in an [`InstanceDef`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceMethod {
/// Method name (must match a method in the corresponding class).
pub name: String,
/// Method body. The class's declared method type with the class
/// param substituted to the instance type is the body's expected
/// type — checked in 22b.2.
pub body: Term,
}
/// A constant (top-level binding to a value).
///
/// Differs from a zero-arg [`FnDef`] in that it is evaluated once and
+17
View File
@@ -169,6 +169,18 @@ pub fn desugar_module(m: &Module) -> Module {
Def::Type(td) => {
used.insert(td.name.clone());
}
// Iter 22b.1: class/instance defs are passthrough for the
// desugar pass — their bodies (default methods, instance
// method bodies) will be desugared once 22b.2 wires the
// class/instance arms into typecheck. For 22b.1 the names
// still go into `used` so the synthetic-name generator
// does not collide.
Def::Class(c) => {
used.insert(c.name.clone());
}
Def::Instance(i) => {
used.insert(i.class.clone());
}
}
}
// Iter 16b.1: pre-collect every top-level def name. The LetRec
@@ -248,6 +260,11 @@ pub fn desugar_module(m: &Module) -> Module {
c.value = d.desugar_term(&c.value, &scope);
}
Def::Type(_) => {}
// Iter 22b.1: class/instance defs are not desugared yet.
// Their bodies (default methods, instance method bodies)
// will be processed once 22b.2 lands the class/instance
// arms in typecheck and 22b.3 in codegen.
Def::Class(_) | Def::Instance(_) => {}
}
}
// Iter 16b.1: append every lifted fn to the desugared module.
+5
View File
@@ -63,6 +63,11 @@ pub fn manifest(m: &Module) -> String {
};
("type", body)
}
// Iter 22b.1: one-line summary `class C a` / `instance C T`.
// Full pretty rendering of method signatures and bodies
// lands in 22b.4 alongside the prose projection arms.
Def::Class(c) => ("class", format!("{} {}", c.name, c.param)),
Def::Instance(i) => ("instance", format!("{} {}", i.class, type_to_string(&i.type_))),
};
writeln!(
s,
+8
View File
@@ -272,6 +272,14 @@ fn spec_mentions_every_def_kind() {
Def::Fn(_) => "fn",
Def::Const(_) => "const",
Def::Type(_) => "type",
// Iter 22b.1: class/instance Def variants exist but are
// not yet anchored in the prose-spec block. Once 22b.4
// adds prose projection for them, the FORM_A_SPEC text
// gains `(class ` / `(instance ` anchors and this match
// will be exercised. For 22b.1 the exemplars list above
// does not produce these variants.
Def::Class(_) => "class",
Def::Instance(_) => "instance",
};
assert!(
FORM_A_SPEC.contains(anchor),
+22
View File
@@ -77,6 +77,28 @@ fn write_def(out: &mut String, def: &Def, level: usize) {
Def::Type(td) => write_type_def(out, td, level),
Def::Fn(fd) => write_fn_def(out, fd, level),
Def::Const(cd) => write_const_def(out, cd, level),
// Iter 22b.1: ClassDef / InstanceDef render as a single-line
// placeholder so prose round-trip survives a module that
// contains them. The full Form-B projection (mirror of the
// ClassDef / InstanceDef Form-A schema, with method bodies
// re-indented and superclass / default markers spelled out)
// is deferred to 22b.4 alongside the prose-roundtrip parser
// arms. The placeholder is informational only — the prose
// round-trip mediator does not yet read these forms back.
Def::Class(c) => {
indent(out, level);
out.push_str("// (class ");
out.push_str(&c.name);
out.push(' ');
out.push_str(&c.param);
out.push_str(") -- 22b.4 will render full Form-B\n");
}
Def::Instance(i) => {
indent(out, level);
out.push_str("// (instance ");
out.push_str(&i.class);
out.push_str(") -- 22b.4 will render full Form-B\n");
}
}
}
+19
View File
@@ -94,6 +94,25 @@ fn write_def(out: &mut String, def: &Def, level: usize) {
Def::Type(td) => write_type_def(out, td, level),
Def::Fn(fd) => write_fn_def(out, fd, level),
Def::Const(cd) => write_const_def(out, cd, level),
// Iter 22b.1: surface (Form-B) printer arms for ClassDef /
// InstanceDef are deferred to 22b.4 (along with the parse-side
// arms in `surface/src/parse.rs`). For 22b.1 we emit a
// single-line placeholder so a module containing these defs
// still round-trips through `ail render` in print-only mode.
Def::Class(c) => {
indent(out, level);
out.push_str("(class ");
out.push_str(&c.name);
out.push(' ');
out.push_str(&c.param);
out.push(')');
}
Def::Instance(i) => {
indent(out, level);
out.push_str("(instance ");
out.push_str(&i.class);
out.push(')');
}
}
}