Skip to content

Commit cb9704a

Browse files
feat(diagnostics): prelude-hint for UndefinedVariable / Unbound (stdlib #5) (#427)
Closes #415. Stdlib roadmap item #5 (`docs/stdlib-roadmap.adoc`). ## TL;DR The original framing of stdlib #5 was *"cross-file \`use\` resolvability from standalone check"*. Reproducing the bug turned up something different: cross-module \`use\` **already works** (both \`use Deno::{Json}\` and \`use prelude::*\` resolve fine from a standalone single-file check). What actually trips users is **discoverability** — prelude is NOT auto-opened (deliberate, per issue #138), and the error when an unimported \`Some\` is referenced was a raw OCaml record dump with no hint about the fix. ## Before / after **Before:** \`\`\` Resolution error: (Resolve.UndefinedVariable { Ast.name = "Some"; span = { Span.start_pos = { Span.line = 1; col = 37; ... }; ... } }) \`\`\` **After:** \`\`\` Resolution error: undefined value: \`Some\` hint: \`Some\` is defined in \`stdlib/prelude.affine\` — add \`use prelude::{Some};\` (or \`use prelude::*;\`) at the top of the file. \`\`\` ## Files | File | Change | |---|---| | \`lib/face.ml\` | Canonical face: replace \`show_resolve_error\` dump with full per-error-class formatter; add \`prelude_hint\` for UndefinedVariable + UndefinedType | | \`lib/typecheck.ml\` | Add same \`prelude_hint\` on \`UnboundVariable\` (typecheck-time path; fires for match arms whose constructor reference the resolver missed) | ## What this does NOT do - Does NOT auto-import prelude (that would revert issue #138 which intentionally removed the band-aid) - Does NOT change resolve behaviour at all — only the error message - Does NOT touch the other faces (Python / Js / Pseudocode / Lucid / Cafe) — they already had their own formatted hints; just Canonical was raw-dumping ## Test plan - [x] codegen-deno suite: 7/7 harnesses green (no behaviour change) - [x] Manual: idaptik PR #107's case (missing \`Some\`) now shows the hint - [x] Manual: non-prelude names (e.g. \`nonexistentFn\`) still get a clean error with NO inappropriate prelude hint - [x] \`dune build bin/main.exe\` clean - [x] \`dune build @runtest\` — 340/342 pass; the 2 failures are pre-existing E2E Node-CJS Codegen baseline flakes (CLAUDE.md "Known-failing baseline checks"), unchanged by this PR ## Follow-ups Once this lands, a follow-up PR updates \`docs/stdlib-roadmap.adoc\` row #5 status \`◑\` partial → \`●\` usable (kept out of this PR so the diff stays focused on the diagnostic itself). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 263be56 commit cb9704a

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

lib/face.ml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,10 +549,49 @@ let format_borrow_error (face : face) (err : Borrow.borrow_error) : string =
549549

550550
(* ─── Resolve errors ─────────────────────────────────────────────────── *)
551551

552+
(* Known exports of stdlib/prelude.affine. When an UndefinedVariable /
553+
UndefinedType fires with one of these names, the error formatter
554+
suggests `use prelude::{...}` — the discoverability gap that issue
555+
#138's "no flat builtin seeding" + stdlib roadmap #5 surfaced.
556+
Keep this list in sync with stdlib/prelude.affine; a missing name
557+
here means a worse error message, not incorrect behaviour. *)
558+
let prelude_exports = [
559+
(* Types *)
560+
"Option"; "Result";
561+
(* Constructors *)
562+
"Some"; "None"; "Ok"; "Err";
563+
(* Functions *)
564+
"map"; "filter"; "fold"; "contains";
565+
"sum"; "product"; "min"; "max"; "clamp";
566+
"not"; "all"; "any"; "range"; "repeat";
567+
]
568+
569+
let prelude_hint (name : string) : string =
570+
if List.mem name prelude_exports then
571+
Printf.sprintf "\n hint: `%s` is defined in `stdlib/prelude.affine` — add `use prelude::{%s};` (or `use prelude::*;`) at the top of the file."
572+
name name
573+
else ""
574+
552575
(** Format a name-resolution error for the given face. *)
553576
let format_resolve_error (face : face) (err : Resolve.resolve_error) : string =
554577
match face with
555-
| Canonical -> Resolve.show_resolve_error err
578+
| Canonical ->
579+
(match err with
580+
| Resolve.UndefinedVariable id ->
581+
Printf.sprintf "undefined value: `%s`%s" id.name (prelude_hint id.name)
582+
| Resolve.UndefinedType id ->
583+
Printf.sprintf "undefined type: `%s`%s" id.name (prelude_hint id.name)
584+
| Resolve.UndefinedEffect id ->
585+
Printf.sprintf "undefined effect: `%s`" id.name
586+
| Resolve.UndefinedModule id ->
587+
Printf.sprintf "undefined module: `%s`\n hint: add `use %s::{...};` at the top of the file."
588+
id.name id.name
589+
| Resolve.DuplicateDefinition id ->
590+
Printf.sprintf "duplicate definition of `%s`" id.name
591+
| Resolve.VisibilityError (id, msg) ->
592+
Printf.sprintf "`%s` is not accessible here: %s" id.name msg
593+
| Resolve.ImportError msg ->
594+
Printf.sprintf "import error: %s" msg)
556595
| Python ->
557596
(match err with
558597
| Resolve.UndefinedVariable id ->

lib/typecheck.ml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,29 @@ type type_error =
113113
raised when a row is declared; an undeclared row stays
114114
permissive under tracking-only v1. *)
115115

116+
(* Known exports of stdlib/prelude.affine. Mirrors the same list in
117+
lib/face.ml — when an UnboundVariable fires at type-check time with
118+
one of these names, the formatter suggests `use prelude::{...}`.
119+
Same discoverability fix as the resolver path; the typechecker has
120+
its own miss path (e.g. match arms reference unbound constructors
121+
that the resolver missed). Keep in sync with stdlib/prelude.affine. *)
122+
let prelude_exports = [
123+
"Option"; "Result";
124+
"Some"; "None"; "Ok"; "Err";
125+
"map"; "filter"; "fold"; "contains";
126+
"sum"; "product"; "min"; "max"; "clamp";
127+
"not"; "all"; "any"; "range"; "repeat";
128+
]
129+
130+
let prelude_hint (name : string) : string =
131+
if List.mem name prelude_exports then
132+
Printf.sprintf "\n hint: `%s` is defined in `stdlib/prelude.affine` — add `use prelude::{%s};` (or `use prelude::*;`) at the top of the file."
133+
name name
134+
else ""
135+
116136
(** Format a type error for human consumption. *)
117137
let show_type_error = function
118-
| UnboundVariable v -> "Unbound variable: " ^ v
138+
| UnboundVariable v -> "Unbound variable: " ^ v ^ prelude_hint v
119139
| TypeMismatch { expected; got } ->
120140
Printf.sprintf "Type mismatch: expected %s, got %s"
121141
(ty_to_string expected) (ty_to_string got)

0 commit comments

Comments
 (0)