Skip to content

Commit c587aa4

Browse files
hyperpolymathclaude
andcommitted
feat(diagnostics): prelude-hint for UndefinedVariable / Unbound (stdlib #5)
Closes #415. Stdlib roadmap item #5 was framed as "cross-file `use` resolvability from standalone check", but the actual underlying issue turned out to be **discoverability**: cross-module `use` already works (both `use Deno::{Json}` and `use prelude::*` resolve from a standalone single-file check). What trips users is that 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. This change: 1. Replaces face.ml's `Canonical` branch's `show_resolve_error` (raw record dump) with a real per-error-class formatter. Other faces were already formatted; only Canonical hadn't been touched here. 2. Adds a `prelude_hint` helper in both face.ml and typecheck.ml that, when an undefined name matches a known prelude export, appends: hint: `Some` is defined in `stdlib/prelude.affine` — add `use prelude::{Some};` (or `use prelude::*;`) at the top of the file. Covers both the resolve-time path (face.ml) and the typecheck-time path (typecheck.ml's UnboundVariable — fires e.g. when a match arm references an unbound constructor the resolver missed). 3. Hard-codes the prelude export list as the well-known 20 names (Option, Result, Some, None, Ok, Err + the function helpers). A missing name here means a worse error message, not incorrect behaviour. Comment in both files notes the list must mirror `stdlib/prelude.affine`. Effect for users: - BEFORE: `Resolution error: (Resolve.UndefinedVariable { Ast.name = "Some"; span = {...} })` - 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. ``` Doesn't auto-import prelude (would revert #138). Doesn't change resolve behaviour at all — only error rendering. Test plan: - [x] codegen-deno suite stays 7/7 green - [x] Manual reproduction of the idaptik PR #107 case shows the hint - [x] Non-prelude names (e.g. `nonexistentFn`) still get a clean error with NO inappropriate prelude hint - [x] `dune build bin/main.exe` clean Updates stdlib roadmap row #5 status `◑` partial → `●` usable in a follow-up PR (so this PR stays focused on the diagnostic). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b95852f commit c587aa4

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)