sx: step 8 — non-exhaustive match warnings

Emit a warning when a `match` expression on an ADT value misses one
or more constructors and lacks an `else`/`_` clause. Behaviour is
non-fatal — the match still runs, the warning goes to stderr.

- spec/evaluator.sx: helpers `match-clause-is-else?`, `match-clause-ctor-name`,
  `match-warn-non-exhaustive`, `match-check-exhaustiveness`. The latter
  reads the `*adt-registry*` (already populated by `define-type`),
  collects constructor patterns from clauses, and dedupes via an
  `*adt-warned*` env-bound dict so each (type, missing-set) warns once.
  Wired into `step-sf-match` via a `do` block before clause dispatch.

- hosts/javascript/platform.py: `host-warn` primitive (`console.warn`)
  + matching `hostWarn` js-id helper so the JS-transpiled spec code
  can call it directly. Spec code reaches JS via `sx_build target=js`.

- hosts/ocaml/lib/sx_runtime.ml + sx_primitives.ml: `host-warn` runtime
  helper (`prerr_endline`) and registered primitive.

- hosts/ocaml/lib/sx_ref.ml: HAND-PATCHED. `step_sf_match` now calls
  a hand-written `match_check_exhaustiveness` that handles both
  `AdtValue` and back-compat dict-shape ADT values. The OCaml side
  is *not* retranspiled because regenerating sx_ref.ml drops
  several preamble fixes (seq_to_list, string->symbol mangling,
  empty-dict literal bug). Future retranspile must reapply this patch.

- spec/tests/test-adt.sx: 5 new tests covering exhaustive,
  non-exhaustive (warning is non-fatal), `else` suppression,
  partial coverage with one missing constructor, and `_` wildcard
  suppression. Tests assert return values only — warnings go to
  stderr and are not captured.

Warning format: `[sx] match: non-exhaustive — TypeName: missing Ctor1, Ctor2`
Both hosts emit identical messages.

Tests: OCaml 4540 → 4545 (+5), JS 2586 → 2591 (+5). Zero regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 00:13:41 +00:00
parent 7b050fb217
commit 6d39111992
8 changed files with 281 additions and 15 deletions

View File

@@ -41,7 +41,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-05-06T23:01:54Z";
var SX_VERSION = "2026-05-07T00:02:13Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -378,6 +378,13 @@
// hostError — throw a host-level error that propagates out of cekRun.
function hostError(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); }
// hostWarn — emit a host-level warning to console (no-op if console missing).
function hostWarn(msg) {
var m = typeof msg === "string" ? msg : inspect(msg);
if (typeof console !== "undefined" && console.warn) console.warn(m);
return NIL;
}
// Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering).
var _renderExprFn = null;
@@ -3301,14 +3308,58 @@ PRIMITIVES["match-find-clause"] = matchFindClause;
})()) : sxEq(pattern, value))))))))); };
PRIMITIVES["match-pattern"] = matchPattern;
// match-clause-is-else?
var matchClauseIsElse_p = function(clause) { return (function() {
var p = first(clause);
return sxOr(sxEq(p, new Symbol("_")), sxEq(p, new Symbol("else")), sxEq(p, "else"));
})(); };
PRIMITIVES["match-clause-is-else?"] = matchClauseIsElse_p;
// match-clause-ctor-name
var matchClauseCtorName = function(clause) { return (function() {
var p = first(clause);
return (isSxTruthy((isSxTruthy(isList(p)) && isSxTruthy(!isSxTruthy(isEmpty(p))) && symbol_p(first(p)))) ? symbolName(first(p)) : (isSxTruthy((isSxTruthy(symbol_p(p)) && isSxTruthy(!isSxTruthy(sxEq(p, new Symbol("_")))) && !isSxTruthy(sxEq(p, new Symbol("else"))))) ? NIL : NIL));
})(); };
PRIMITIVES["match-clause-ctor-name"] = matchClauseCtorName;
// match-warn-non-exhaustive
var matchWarnNonExhaustive = function(env, typeName, registered, clauseCtors) { return (function() {
var missing = filter(function(c) { return !isSxTruthy(contains(clauseCtors, c)); }, registered);
if (isSxTruthy(!isSxTruthy(isEmpty(missing)))) {
if (isSxTruthy(!isSxTruthy(envHas(env, "*adt-warned*")))) {
envBind(env, "*adt-warned*", {});
}
(function() {
var warned = envGet(env, "*adt-warned*");
var key = (String(typeName) + String("|") + String(join(",", missing)));
return (isSxTruthy(!isSxTruthy(get(warned, key))) ? (dictSet(warned, key, true), hostWarn((String("[sx] match: non-exhaustive — ") + String(typeName) + String(": missing ") + String(join(", ", missing))))) : NIL);
})();
}
return NIL;
})(); };
PRIMITIVES["match-warn-non-exhaustive"] = matchWarnNonExhaustive;
// match-check-exhaustiveness
var matchCheckExhaustiveness = function(val, clauses, env) { return (isSxTruthy((isSxTruthy(isDict(val)) && get(val, "_adt"))) ? (function() {
var typeName = get(val, "_type");
return (isSxTruthy((isSxTruthy(envHas(env, "*adt-registry*")) && typeName)) ? (function() {
var registered = get(envGet(env, "*adt-registry*"), typeName);
return (isSxTruthy((isSxTruthy(registered) && !isSxTruthy(some(matchClauseIsElse_p, clauses)))) ? (function() {
var clauseCtors = filter(function(n) { return !isSxTruthy(isNil(n)); }, map(matchClauseCtorName, clauses));
return matchWarnNonExhaustive(env, typeName, registered, clauseCtors);
})() : NIL);
})() : NIL);
})() : NIL); };
PRIMITIVES["match-check-exhaustiveness"] = matchCheckExhaustiveness;
// step-sf-match
var stepSfMatch = function(args, env, kont) { return (function() {
var val = trampoline(evalExpr(first(args), env));
var clauses = rest(args);
return (function() {
return (matchCheckExhaustiveness(val, clauses, env), (function() {
var result = matchFindClause(val, clauses, env);
return (isSxTruthy(isNil(result)) ? makeCekValue((String("match: no clause matched ") + String(inspect(val))), env, kontPush(makeRaiseEvalFrame(env, false), kont)) : makeCekState(nth(result, 1), first(result), kont));
})();
})());
})(); };
PRIMITIVES["step-sf-match"] = stepSfMatch;
@@ -4874,6 +4925,11 @@ PRIMITIVES["boot-init"] = bootInit;
// -----------------------------------------------------------------------
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
PRIMITIVES["host-error"] = function(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); };
PRIMITIVES["host-warn"] = function(msg) {
var m = typeof msg === "string" ? msg : inspect(msg);
if (typeof console !== "undefined" && console.warn) console.warn(m);
return NIL;
};
PRIMITIVES["try-catch"] = function(tryFn, catchFn) {
try {
return cekRun(continueWithCall(tryFn, [], makeEnv(), [], []));