Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to EigenScript are documented here.
## [Unreleased]

### Added
- **E003 — undefined-name lint, increment one of the scope-aware
name-resolution pass** (#404): `--lint` (and the LSP, as red squiggles)
now reports a name that is read but bound nowhere — the typo'd name in a
cold branch that a dynamic language otherwise carries to runtime. The
binding set is a deliberate whole-file over-approximation (silent on
outward-`is`, `local` shadowing, sibling-branch first assignment; a false
positive is the failure mode that breaks consumer CI, so the pass always
fails open). Builtins come from `register_builtins()` itself on a scratch
env — never a hand list (the old lint.c copy had drifted ~120 names and
carried a phantom `"error"` entry). Extension builtins bind by name via
the new `src/ext_names.h` X-lists, which the gfx/http/db/model registrars
now expand too, so registration and name-resolution cannot drift; the
lint describes the language surface, not one binary's build flags.
Literal `load_file of "path"` is resolved with the runtime's own
resolution chain and its top-level binders collected transitively, so
entry-point files assembling a program from parts lint clean. `eval` or a
computed `load_file` path disables the pass for the file (documented
dynamic escape). E003 is error-severity: it fails `--lint` even at
`--lint-level error`. Full model in docs/DIAGNOSTICS.md.
- **`# lint: allow-file <CODE>...` file-wide suppression** — the escape for
module *fragments* (files a loader `load_file`s into scope the loader
provides, e.g. `lib/ui_w_*.eigs`), honored by both `--lint` and the LSP.
`lib/invariant.eigs` instead became self-contained (it now loads
`lib/observer.eigs` for `cs_signature` — pure defines, idempotent).
- **Interactive REPL line editor** (#392) — the REPL on a tty now has
history (in-memory + `EIGS_HISTORY` file, default
`~/.eigenscript_history`, created `0600`), arrow-key cursor editing,
Expand Down
47 changes: 46 additions & 1 deletion docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ a code's meaning never changes, and retired codes are not reused.
| `E000` | error | File cannot be read (lint). |
| `E001` | error | *Reserved* — tokenizer/syntax error. Not currently emitted: lexer-level failures surface through the parser as `E002`. The code is held so `E002` keeps its meaning if a distinct tokenizer diagnostic is added later. |
| `E002` | error | Parse error (parser). `--lint --json` reports the first one. |
| `E003` | error | Undefined name — **no binding on any path** (#404). A name is read somewhere but bound nowhere: not by any assignment in any scope, not a parameter/binder (function/lambda params, `for`/comprehension variables, `catch` names, list-pattern names, `import` module names), not a builtin, and not a top-level name of a file pulled in by a literal `load_file` (resolved with the runtime's own resolution chain, transitively). The runtime raises `undefined variable` the moment such a path executes; `E003` surfaces it statically, including on cold branches. See "Name resolution (`E003`)" below for the exact model and escapes. |
| `E100` | error | Uncaught runtime error (category code; see note below). |
| `W001` | warning | Unused variable. |
| `W002` | warning | Unused function parameter. |
Expand Down Expand Up @@ -237,7 +238,51 @@ nothing:

A suppressed diagnostic is removed from both the human and `--json` output.

(Per-file allow-lists in `eigs.json` are not yet wired up — see #399.)
- **`# lint: allow-file <CODE>...`** silences the listed codes (or `all`) for
the **whole file**, in both `--lint` and the LSP. Conventionally the first
line. This is the escape for module *fragments* — files that a loader
`load_file`s into scope the loader provides (e.g. `lib/ui_w_*.eigs` expect
`lib/ui.eigs` to have bound `_theme`/`_ui` first), where `E003` would flag
every loader-bound name and per-line allows would drown the file:

```eigenscript
# lint: allow-file E003 -- fragment of lib/ui.eigs: loader binds _theme/_ui
```

(Per-file allow-lists in `eigs.json` are not yet wired up — see #455.)

## Name resolution (`E003`)

`E003` is increment one of the scope-aware name-resolution pass (#404) —
ROADMAP's sanctioned alternative to a type system. Its model:

- **Binding set = whole-file over-approximation.** Every binder anywhere in
the file counts (mutate-outward makes most bindings cross-scope visible
anyway). Over-collecting can only *miss* a bug, never invent one — a false
positive would break consumer CI, so the approximation always fails open.
Consequently `E003` is silent on outward-`is`, `local` shadowing, and
sibling-branch first assignments; path-precise analysis is later #404 work.
- **Builtins come from the runtime's own registry** — the linter calls
`register_builtins()` on a scratch environment rather than keeping a name
list that can drift. Extension builtins (`gfx_*`/`audio_*`, `http_*`,
`db_*`, model) bind by name regardless of the binary's build flags, from
the same `ext_names.h` lists their registrars expand: the lint describes
the language surface, not one build. `report_value` (a compiler special
form) and the observed-loop bindings `__loop_exit__`/`__loop_iterations__`
are also known.
- **Literal `load_file` is followed.** `load_file of "path"` resolves with
the runtime's resolution chain (anchored at the linted file's directory)
and the loaded file's top-level binders are collected transitively, so an
entry-point file that assembles its program from parts lints clean.
- **Dynamic escape.** `eval` appearing anywhere, or a `load_file` whose
argument is not a string literal, can bind names invisible to static
analysis — the pass disables itself for that file. A `load_file` the
linter cannot resolve, read, or parse also fails open.
- **Module members are not checked.** `import m` binds `m`; `m.anything` is
a dict key, outside this pass.
- For a file that is *deliberately* a fragment of a larger program, use
`# lint: allow-file E003` (above). For a single host-injected name, a
per-line `# lint: allow E003` works as usual.

`E100` is the **category code** for an uncaught runtime error. It is
deliberately *not* injected into the `Error line N: ...` text, because
Expand Down
4 changes: 4 additions & 0 deletions lib/invariant.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
# r is run_descent of [inv, Q0, lr, eps_change, max_iter]
# r is run_preserving of [inv, Q0, n_steps]

# Self-contained: pull in cs_signature (pure defines, idempotent to re-load
# when the caller already followed the two-line usage above).
load_file of "lib/observer.eigs"

# ---- constructor: bundle three callables into an Invariant record ----
define make_invariant(ds2_fn, grad_fn, step_fn) as:
return {"ds2": ds2_fn, "grad": grad_fn, "step": step_fn}
Expand Down
1 change: 1 addition & 0 deletions lib/ui_draw.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Draw — low-level drawing helpers for the UI toolkit
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_focus.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Focus system — build focus list, navigate, handle keys
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_basic.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Basic: label, separator, section, color_dot, badge, progress_bar
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_button.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Buttons: button, toggle_button, checkbox, toggle
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_container.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Containers: panel, hbox, vbox, toolbar, statusbar, scroll_panel
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_data.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Data: table, tree, item_list, grid
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_dialog.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Dialog: dialog, property_editor, toast, color_picker + HSV
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_input.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# UI Widgets — Input: text_input, editable_label, spinbox, combobox

define text_input(id, x, y, w, text, on_change) as:
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_menu.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Menu: dropdown, menu, radio_group, tabs
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_slider.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Sliders: slider, vslider, knob, scrollbar + drag math
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_special.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# ============================================================
# UI Widgets — Special: splitter, piano_kb
# ============================================================
Expand Down
1 change: 1 addition & 0 deletions lib/ui_w_viz.eigs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# lint: allow-file E003 -- fragment of lib/ui.eigs: the loader binds the shared ui state (_theme, _ui, widget registry, sibling widgets) before this file runs
# UI Widgets — Viz: chart, bar_chart, gauge, meter, canvas, waveform_view, code_view

define chart(id, x, y, w, h) as:
Expand Down
25 changes: 17 additions & 8 deletions src/eigenlsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -751,19 +751,26 @@ static void send_diagnostics(Document *doc) {
json_escape_to(&sb, full);
strbuf_append_char(&sb, '}');
} else if (doc->ast) {
/* Parse OK → run the linter and publish each warning as a
* yellow squiggle carrying its stable code (#3 taxonomy), so the
* editor shows the same diagnostics as `--lint`. */
/* Parse OK → run the linter and publish each diagnostic as a
* squiggle carrying its stable code (#3 taxonomy), so the editor
* shows the same diagnostics as `--lint` — warnings yellow,
* E-class errors red. The doc's filesystem path anchors E003's
* load_file resolution (as-you-type typo squiggles). */
LintDiag diags[256];
int n = lint_collect(doc->ast, diags, 256);
const char *fs_path =
strncmp(doc->uri, "file://", 7) == 0 ? doc->uri + 7 : NULL;
int n = lint_collect(doc->ast, fs_path, diags, 256);
int emitted = 0;
for (int i = 0; i < n; i++) {
if (lint_file_allows(doc->text, diags[i].code)) continue;
int ln = diags[i].line - 1;
if (ln < 0) ln = 0;
if (i > 0) strbuf_append_char(&sb, ',');
int sev = strcmp(diags[i].severity, "error") == 0 ? 1 : 2;
if (emitted++ > 0) strbuf_append_char(&sb, ',');
strbuf_append_fmt(&sb,
"{\"range\":{\"start\":{\"line\":%d,\"character\":0},"
"\"end\":{\"line\":%d,\"character\":1000}},\"severity\":2,\"code\":\"%s\","
"\"source\":\"eigenscript\",\"message\":", ln, ln, diags[i].code);
"\"end\":{\"line\":%d,\"character\":1000}},\"severity\":%d,\"code\":\"%s\","
"\"source\":\"eigenscript\",\"message\":", ln, ln, sev, diags[i].code);
json_escape_to(&sb, diags[i].message);
strbuf_append_char(&sb, '}');
}
Expand Down Expand Up @@ -1593,7 +1600,9 @@ static void handle_code_action(int id, const char *params) {
/* Recompute diagnostics from the AST rather than parsing the client's
* context.diagnostics array — more robust, and the codes are ours. */
LintDiag diags[256];
int n = lint_collect(doc->ast, diags, 256);
const char *fs_path =
strncmp(doc->uri, "file://", 7) == 0 ? doc->uri + 7 : NULL;
int n = lint_collect(doc->ast, fs_path, diags, 256);

strbuf sb;
strbuf_init(&sb);
Expand Down
10 changes: 8 additions & 2 deletions src/eigenscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -1031,8 +1031,14 @@ typedef struct {
char message[256];
} LintDiag;
/* Run all lint checks on an already-parsed AST; fill out[] (up to max),
* return the count. No I/O. Used by the LSP to publish diagnostics. */
int lint_collect(ASTNode *ast, LintDiag *out, int max);
* return the count. `path` = the source file's filesystem path (NULL if
* unknown); it anchors E003's literal-load_file resolution, which reads the
* loaded files — otherwise no I/O. Used by the LSP to publish diagnostics. */
int lint_collect(ASTNode *ast, const char *path, LintDiag *out, int max);
/* 1 if the source carries a file-wide `# lint: allow-file <code>` directive
* for `code` (or `all`). Callers of lint_collect apply it themselves (the
* CLI and the LSP both do), since lint_collect never sees the source. */
int lint_file_allows(const char *source, const char *code);
int eigenscript_lint(const char *path, int json_mode, int fail_on_warning);

#endif /* EIGENSCRIPT_H */
11 changes: 7 additions & 4 deletions src/ext_db.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

#include "ext_db_internal.h"
#include "ext_names.h"
#if EIGENSCRIPT_EXT_DB

PGconn *g_db_conn = NULL;
Expand Down Expand Up @@ -163,10 +164,12 @@ Value* builtin_db_query_json(Value *arg) {
}

void register_db_builtins(Env *env) {
env_set_local_owned(env, "db_connect", make_builtin(builtin_db_connect));
env_set_local_owned(env, "db_query_value", make_builtin(builtin_db_query_value));
env_set_local_owned(env, "db_execute", make_builtin(builtin_db_execute));
env_set_local_owned(env, "db_query_json", make_builtin(builtin_db_query_json));
/* Expanded from ext_names.h — the shared name list the linter's E003
* binding base also expands, so registration and name-resolution
* cannot drift. Add a builtin there, not here. */
#define X(name, fn) env_set_local_owned(env, #name, make_builtin(fn));
EIGS_DB_BUILTINS(X)
#undef X
}

#endif /* EIGENSCRIPT_EXT_DB */
46 changes: 7 additions & 39 deletions src/ext_gfx.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

#include "eigenscript.h"
#include "ext_names.h"

#if EIGENSCRIPT_EXT_GFX

Expand Down Expand Up @@ -1422,45 +1423,12 @@ Value* builtin_ppu_render_frame(Value *arg) {
}

void register_gfx_builtins(Env *env) {
env_set_local_owned(env, "gfx_open", make_builtin(builtin_gfx_open));
env_set_local_owned(env, "gfx_close", make_builtin(builtin_gfx_close));
env_set_local_owned(env, "gfx_clear", make_builtin(builtin_gfx_clear));
env_set_local_owned(env, "gfx_rect", make_builtin(builtin_gfx_rect));
env_set_local_owned(env, "gfx_line", make_builtin(builtin_gfx_line));
env_set_local_owned(env, "gfx_point", make_builtin(builtin_gfx_point));
env_set_local_owned(env, "gfx_circle", make_builtin(builtin_gfx_circle));
env_set_local_owned(env, "gfx_rrect", make_builtin(builtin_gfx_rrect));
env_set_local_owned(env, "gfx_clip", make_builtin(builtin_gfx_clip));
env_set_local_owned(env, "gfx_present", make_builtin(builtin_gfx_present));
env_set_local_owned(env, "gfx_poll", make_builtin(builtin_gfx_poll));
env_set_local_owned(env, "gfx_ticks", make_builtin(builtin_gfx_ticks));
env_set_local_owned(env, "gfx_delay", make_builtin(builtin_gfx_delay));
env_set_local_owned(env, "gfx_title", make_builtin(builtin_gfx_title));
env_set_local_owned(env, "gfx_text", make_builtin(builtin_gfx_text));
env_set_local_owned(env, "gfx_fb", make_builtin(builtin_gfx_fb));
env_set_local_owned(env, "ppu_render_frame", make_builtin(builtin_ppu_render_frame));

/* Audio builtins */
env_set_local_owned(env, "audio_open", make_builtin(builtin_audio_open));
env_set_local_owned(env, "audio_close", make_builtin(builtin_audio_close));
env_set_local_owned(env, "audio_volume", make_builtin(builtin_audio_volume));
env_set_local_owned(env, "audio_stop", make_builtin(builtin_audio_stop));
env_set_local_owned(env, "audio_pause", make_builtin(builtin_audio_pause));
env_set_local_owned(env, "audio_play", make_builtin(builtin_audio_play));
env_set_local_owned(env, "audio_play_loop", make_builtin(builtin_audio_play_loop));
env_set_local_owned(env, "audio_music_play", make_builtin(builtin_audio_music_play));
env_set_local_owned(env, "audio_music_volume", make_builtin(builtin_audio_music_volume));
env_set_local_owned(env, "audio_music_stop", make_builtin(builtin_audio_music_stop));
env_set_local_owned(env, "audio_queue_size", make_builtin(builtin_audio_queue_size));
env_set_local_owned(env, "audio_clear", make_builtin(builtin_audio_clear));
env_set_local_owned(env, "audio_sine", make_builtin(builtin_audio_sine));
env_set_local_owned(env, "audio_saw", make_builtin(builtin_audio_saw));
env_set_local_owned(env, "audio_square", make_builtin(builtin_audio_square));
env_set_local_owned(env, "audio_sweep", make_builtin(builtin_audio_sweep));
env_set_local_owned(env, "audio_noise", make_builtin(builtin_audio_noise));
env_set_local_owned(env, "audio_mix", make_builtin(builtin_audio_mix));
env_set_local_owned(env, "audio_gain", make_builtin(builtin_audio_gain));
env_set_local_owned(env, "audio_envelope", make_builtin(builtin_audio_envelope));
/* Expanded from ext_names.h — the shared name list the linter's E003
* binding base also expands, so registration and name-resolution
* cannot drift. Add a builtin there, not here. */
#define X(name, fn) env_set_local_owned(env, #name, make_builtin(fn));
EIGS_GFX_BUILTINS(X)
#undef X
}

#endif /* EIGENSCRIPT_EXT_GFX */
Loading
Loading