fix(check): honour the written element type-arg on polymorphic (new T …)
A `(new RawBuf (con Int) 3)` whose buffer is never observed by a later
get/set passed `ail check` but crashed `ail build` with
`RawBuf_new__Unit has no registered intercept`, and a forbidden
`(new RawBuf (con Unit) 3)` was wrongly accepted at check. Both legs
share one root: the `Term::New` desugar discarded the author's written
`NewArg::Type` before anything could use it.
This was never an inference problem. The element type is EXPLICITLY
known — the author wrote `(con Int)`. The defect was that the desugar
threw that known type away and relied on re-discovering it from
downstream use; with no use (size-only), `RawBuf.new`'s result-only
forall var stayed unpinned, mono Unit-defaulted it, and codegen aborted
on the unregistered `RawBuf_new__Unit` intercept. The fix USES the
written type instead of inferring it.
Mechanism:
- desugar.rs: a polymorphic `(new T <elem> …)` (carrying a written
`NewArg::Type`) now SURVIVES into check instead of being lowered to
`(app T.new …)`. A monomorphic `(new Counter 42)` (no type-arg) keeps
the raw-buf.4 app-lowering unchanged.
- lib.rs synth `Term::New` arm: (a) validates the written element type
against the constructed type's `param_in` set via
`check_type_well_formed`, firing `ParamNotInRestrictedSet` naming the
forbidden element (leg 2) — synth is the correct site because it has
Env/registry access, unlike `pre_desugar_validation`; (b) binds the
result-only forall var DIRECTLY to the written type (`?a := Int`, a
trivial binding, no inference channel) by pushing a `FreeFnCall` whose
metas are the written concrete types, so mono mints
`RawBuf_new__<elem>` and never `__Unit`.
- mono.rs: `rewrite_mono_calls` and `interleave_slots` gain matching
`Term::New` arms that each consume exactly one free-fn slot at a
type-arg-bearing `Term::New` (mirroring synth's single observation,
pushed before the value-args), and `rewrite_mono_calls` reduces the
node to `(app <mangled> <value-args>)` so codegen never sees
`Term::New` and the lib.rs:2200 `unreachable!` stays valid. The two
walkers stay in lockstep.
The Unit-default policy in mono stays correct for genuinely-unobservable
vars (`is_empty(Nil)` etc.); only the case with a written `NewArg::Type`
is changed.
Alternatives rejected: an additive `type_args` field on `Term::App`
(~100 construction sites across 7 crates — a detour that builds a new
transport slot only to copy the already-known type into it); a runtime
type-witness parameter on `RawBuf.new` (invents a runtime value for a
pure type property); an identity-lambda wrapper carrying the element in
its param-type (hides a semantic property in a runtime construct). The
written type belongs where the author put it, carried to the one point
that consumes it.
Verification: both RED legs (committed 61b9d3f) now green; full
check/codegen/core/surface suites green; intercepts bijection and
hash-pin tests green; existing raw-buf int/float/bool fixtures still
build and run leak-clean (live=0).
Out of scope (pre-existing at HEAD, separate issue): a monomorphic
`(new Counter 42)` over a user ADT fails with `unknown variable:
Counter.new` — unrelated to the type-arg drop fixed here.
closes #51
This commit is contained in:
@@ -4390,16 +4390,60 @@ pub(crate) fn synth(
|
||||
}
|
||||
}
|
||||
// Step 4: build the Forall-var → concrete-Type mapping
|
||||
// from the Type-positional args (in order).
|
||||
// from the Type-positional args (in order). The written type
|
||||
// is the binding declaration of each result-only forall var
|
||||
// (#51): the author's `(con Int)` IS the element type, never
|
||||
// inferred from later use.
|
||||
let mut mapping: BTreeMap<String, Type> = BTreeMap::new();
|
||||
let mut written_type_args: Vec<Type> = Vec::with_capacity(vars.len());
|
||||
for (var, arg) in vars.iter().zip(args.iter()) {
|
||||
if let NewArg::Type(t) = arg {
|
||||
mapping.insert(var.clone(), t.clone());
|
||||
written_type_args.push(t.clone());
|
||||
}
|
||||
// The kind-check loop above guarantees this branch
|
||||
// matches; defensive: a Value arg in a Type slot
|
||||
// would have raised NewArgKindMismatch already.
|
||||
}
|
||||
// Steps 4b/4c (#51): only for a polymorphic `new` (the call
|
||||
// carries written type-args binding result-only forall vars).
|
||||
if !vars.is_empty() {
|
||||
// Step 4b: the written element type is subject to the
|
||||
// constructed type's `param-in` restricted set. synth is
|
||||
// the correct site for this enforcement (unlike
|
||||
// `pre_desugar_validation`, which has no registry
|
||||
// access): construct the result `Type::Con` with the
|
||||
// written args and run it through
|
||||
// `check_type_well_formed`, which fires
|
||||
// `ParamNotInRestrictedSet` naming the forbidden element
|
||||
// (e.g. `Unit`). This is the leg-2 reject; it must run
|
||||
// before codegen ever mints a `RawBuf_new__<elem>` symbol.
|
||||
let constructed = Type::Con {
|
||||
name: type_name.clone(),
|
||||
args: written_type_args.clone(),
|
||||
};
|
||||
check_type_well_formed(&constructed, env)?;
|
||||
// Step 4c: push a `FreeFnCall` observation so the mono
|
||||
// pass mints the `RawBuf_new__<elem>` specialisation for
|
||||
// the written element type — bound DIRECTLY (`?a := Int`),
|
||||
// not via an inference channel. The metas carry the
|
||||
// written concrete types, so `subst.apply` recovers them
|
||||
// verbatim and the unobservable-var Unit-default (which
|
||||
// still applies to genuinely-unpinned vars elsewhere) is
|
||||
// never reached here. Pushed BEFORE the value-args are
|
||||
// synthesised so the observation order matches the mono
|
||||
// walker's `Term::New` arm (which consumes this site's
|
||||
// slot before descending into the value-args). `new`
|
||||
// carries no class constraints, so no residuals are
|
||||
// pushed alongside.
|
||||
free_fn_calls.push(FreeFnCall {
|
||||
name: "new".to_string(),
|
||||
owner_module: home_module.clone(),
|
||||
forall_vars: vars.clone(),
|
||||
metas: written_type_args.clone(),
|
||||
scope: Some(bare_type.clone()),
|
||||
});
|
||||
}
|
||||
// Step 5: synth each value-arg, unify against the
|
||||
// (substituted) corresponding param type.
|
||||
let value_args = &args[vars.len()..];
|
||||
|
||||
@@ -1328,16 +1328,67 @@ fn rewrite_mono_calls(
|
||||
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
||||
}
|
||||
}
|
||||
// prep.2 (kernel-extension-mechanics): walker through
|
||||
// NewArg::Value subterms; type args carry no callable term.
|
||||
// The Term::New site itself does not consume a mono cursor
|
||||
// slot — synth desugars it to a global-fn call in a later
|
||||
// milestone, at which point the call site reads as a Term::App
|
||||
// and rides the existing Var-arm channel.
|
||||
Term::New { args, .. } => {
|
||||
// #51: a `Term::New` carrying a written `NewArg::Type` survives
|
||||
// desugar (polymorphic `new`, e.g. `(new RawBuf (con Int) 3)`)
|
||||
// and is lowered to a monomorphised global-fn call HERE. synth
|
||||
// pushed exactly one `FreeFnCall` observation for this site
|
||||
// (BEFORE its value-args), so the cursor slot at `*cursor` is the
|
||||
// mono target for `RawBuf_new__<elem>`. We rename the call to the
|
||||
// mangled symbol, then descend into the value-args (which become
|
||||
// the App args) so their own cursor advances stay aligned.
|
||||
//
|
||||
// A type-arg-free `Term::New` (monomorphic `new`, e.g.
|
||||
// `(new Counter 42)`) was already lowered to `(app T.new …)` at
|
||||
// desugar and never reaches this arm — synth pushed no
|
||||
// observation, so no slot is consumed.
|
||||
Term::New { type_name, args, .. } => {
|
||||
let has_type_arg = args.iter().any(|a| matches!(a, NewArg::Type(_)));
|
||||
if has_type_arg {
|
||||
let callee_name = if let Some(Some(MonoTarget::FreeFn {
|
||||
name: fn_name,
|
||||
type_args,
|
||||
defining_module,
|
||||
scope,
|
||||
})) = ordered_targets.get(*cursor)
|
||||
{
|
||||
let sym = mono_symbol_n(&scoped_base(scope, fn_name), type_args.as_slice());
|
||||
if defining_module == caller_module {
|
||||
sym
|
||||
} else {
|
||||
format!("{}.{}", defining_module, sym)
|
||||
}
|
||||
} else {
|
||||
// Slot is None / non-concrete: leave an unmangled
|
||||
// type-scoped spelling. The written type-arg is
|
||||
// concrete (it is the author's annotation), so this
|
||||
// branch is not expected; fall back to the
|
||||
// type-scoped name (`RawBuf` from `raw_buf.RawBuf`)
|
||||
// rather than panic.
|
||||
let bare_type =
|
||||
type_name.rsplit('.').next().unwrap_or(type_name.as_str());
|
||||
format!("{bare_type}.new")
|
||||
};
|
||||
*cursor += 1;
|
||||
// Rewrite value-args in place (advances the cursor for
|
||||
// any nested mono calls), then replace the node with the
|
||||
// equivalent app to the monomorphised `new`.
|
||||
let mut value_args: Vec<Term> = Vec::new();
|
||||
for arg in args.iter_mut() {
|
||||
if let NewArg::Value(v) = arg {
|
||||
rewrite_mono_calls(v, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
||||
value_args.push(v.clone());
|
||||
}
|
||||
}
|
||||
*body = Term::App {
|
||||
callee: Box::new(Term::Var { name: callee_name }),
|
||||
args: value_args,
|
||||
tail: false,
|
||||
};
|
||||
} else {
|
||||
for arg in args.iter_mut() {
|
||||
if let NewArg::Value(v) = arg {
|
||||
rewrite_mono_calls(v, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1719,10 +1770,20 @@ fn interleave_slots(
|
||||
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
|
||||
}
|
||||
}
|
||||
// prep.2 (kernel-extension-mechanics): walker through
|
||||
// NewArg::Value subterms; Term::New itself does not push a
|
||||
// mono slot — see `rewrite_mono_calls`'s arm for the rationale.
|
||||
// #51: a `Term::New` carrying a written `NewArg::Type` survives
|
||||
// desugar and synth pushed exactly one `FreeFnCall` observation
|
||||
// for it (BEFORE its value-args), so it consumes one free-fn
|
||||
// slot HERE — mirroring `rewrite_mono_calls`'s `Term::New` arm
|
||||
// so the two walkers stay in lockstep. A type-arg-free `Term::New`
|
||||
// was lowered to `(app T.new …)` at desugar and never reaches
|
||||
// this arm, pushing no slot.
|
||||
Term::New { args, .. } => {
|
||||
let has_type_arg = args.iter().any(|a| matches!(a, NewArg::Type(_)));
|
||||
if has_type_arg {
|
||||
let slot = free_fn_slots.get(*free_cur).cloned().unwrap_or(None);
|
||||
out.push(slot);
|
||||
*free_cur += 1;
|
||||
}
|
||||
for arg in args {
|
||||
if let NewArg::Value(v) = arg {
|
||||
interleave_slots(v, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
|
||||
|
||||
@@ -1051,16 +1051,36 @@ impl Desugarer {
|
||||
in_full
|
||||
}
|
||||
Term::New { type_name, args } => {
|
||||
// raw-buf.4: (new T <types…> <values…>) desugars to
|
||||
// (app T.new <values…>) — a type-scoped call so synth's
|
||||
// dotted-name branch resolves it through the
|
||||
// TypeDef-first ladder and the raw-buf.3 scope threading
|
||||
// mints `T_new__<elem>`. The leading NewArg::Type is
|
||||
// dropped: the AST carries no type-args on App, and the
|
||||
// element type is recovered by ordinary inference from
|
||||
// how the result is used (spec § Term::New desugar).
|
||||
// Runs before check, so no Term::New reaches the
|
||||
// type-checker or codegen.
|
||||
// #51: honour the written element type-arg. When the
|
||||
// `new` is polymorphic (the call carries a leading
|
||||
// `NewArg::Type`, e.g. `(new RawBuf (con Int) 3)`), the
|
||||
// written type is the binding declaration of the result's
|
||||
// forall var — it must NOT be discarded. We keep
|
||||
// `Term::New` surviving into synth, which binds that var
|
||||
// directly to the written type (`?a := Int`) and pushes
|
||||
// the matching mono observation so codegen mints
|
||||
// `RawBuf_new__Int` (and enforces `param-in` on the
|
||||
// written type). The value-args are desugared in place.
|
||||
//
|
||||
// When the `new` is monomorphic (no `NewArg::Type`, e.g.
|
||||
// `(new Counter 42)`) there is no var to bind, so we keep
|
||||
// the raw-buf.4 lowering to `(app T.new <values…>)` — a
|
||||
// type-scoped call that synth's dotted-name branch
|
||||
// resolves through the TypeDef-first ladder.
|
||||
let has_type_arg = args.iter().any(|a| matches!(a, NewArg::Type(_)));
|
||||
if has_type_arg {
|
||||
let desugared_args: Vec<NewArg> = args
|
||||
.iter()
|
||||
.map(|arg| match arg {
|
||||
NewArg::Value(v) => NewArg::Value(self.desugar_term(v, scope)),
|
||||
NewArg::Type(ty) => NewArg::Type(ty.clone()),
|
||||
})
|
||||
.collect();
|
||||
Term::New {
|
||||
type_name: type_name.clone(),
|
||||
args: desugared_args,
|
||||
}
|
||||
} else {
|
||||
let value_args: Vec<Term> = args
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
@@ -1076,6 +1096,7 @@ impl Desugarer {
|
||||
tail: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Term::Intrinsic => t.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user