diff --git a/docs/c-modules.md b/docs/c-modules.md index e2aa7027..006cf18f 100644 --- a/docs/c-modules.md +++ b/docs/c-modules.md @@ -52,10 +52,13 @@ Where: Examples: - `mypackage/math.c` -> `js_mypackage_math_use` - `gitea.pockle.world/john/lib/render.c` -> `js_gitea_pockle_world_john_lib_render_use` +- `mypackage/internal/helpers.c` -> `js_mypackage_internal_helpers_use` - `mypackage/game.ce` (AOT actor) -> `js_mypackage_game_program` Actor files (`.ce`) use the `_program` suffix instead of `_use`. +Internal modules (in `internal/` subdirectories) follow the same convention — the `internal` directory name becomes part of the symbol. For example, `internal/os.c` in the core package has the symbol `js_core_internal_os_use`. + **Note:** Having both a `.cm` and `.c` file with the same stem at the same scope is a build error. ## Required Headers diff --git a/docs/compiler-tools.md b/docs/compiler-tools.md index e91eb605..774df046 100644 --- a/docs/compiler-tools.md +++ b/docs/compiler-tools.md @@ -72,6 +72,7 @@ cell streamline --stats # summary stats per function cell streamline --ir # human-readable IR cell streamline --check # warnings only cell streamline --types # IR with type annotations +cell streamline --diagnose # compile-time diagnostics ``` | Flag | Description | @@ -81,6 +82,7 @@ cell streamline --types # IR with type annotations | `--ir` | Human-readable canonical IR (same format as `ir_report.ce`) | | `--check` | Warnings only (e.g. `nr_slots > 200` approaching 255 limit) | | `--types` | Optimized IR with inferred type annotations per slot | +| `--diagnose` | Run compile-time diagnostics (type errors and warnings) | Flags can be combined. diff --git a/docs/nota.md b/docs/nota.md index c04047c8..af96de58 100644 --- a/docs/nota.md +++ b/docs/nota.md @@ -5,7 +5,7 @@ weight: 85 type: "docs" --- -Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols. +Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols. Nota is an internal module: `use('internal/nota')`. Nota stands for Network Object Transfer Arrangement. diff --git a/docs/shop.md b/docs/shop.md index 73963074..8727f76b 100644 --- a/docs/shop.md +++ b/docs/shop.md @@ -230,4 +230,4 @@ If `shop.toml` is missing or has no `[policy]` section, all methods are enabled | `internal/os.c` | OS intrinsics: dylib ops, internal symbol lookup, embedded modules | | `package.cm` | Package directory detection, alias resolution, file listing | | `link.cm` | Development link management (link.toml read/write) | -| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, bootstrap) | +| `boot/*.cm.mcode` | Pre-compiled pipeline seeds (tokenize, parse, fold, mcode, streamline, bootstrap) | diff --git a/docs/spec/pipeline.md b/docs/spec/pipeline.md index 15d251cb..802ed9e9 100644 --- a/docs/spec/pipeline.md +++ b/docs/spec/pipeline.md @@ -69,6 +69,7 @@ Optimizes the Mcode IR through a series of independent passes. Operates per-func 6. **Move elimination**: Removes self-moves (`move a, a`). 7. **Unreachable elimination**: Nops dead code after `return` until the next label. 8. **Dead jump elimination**: Removes jumps to the immediately following label. +9. **Compile-time diagnostics** (optional): When `_warn` is set on the mcode input, emits errors for provably wrong operations (storing named property on array, invoking null, etc.) and warnings for suspicious patterns (named property access on array/text). The engine aborts compilation if any error-severity diagnostics are emitted. See [Streamline Optimizer](streamline.md) for detailed pass descriptions. @@ -134,6 +135,7 @@ Seeds are used during cold start (empty cache) to compile the pipeline modules f | `mcode.ce --pretty` | Print raw Mcode IR before streamlining | | `streamline.ce --types` | Print streamlined IR with type annotations | | `streamline.ce --stats` | Print IR after streamlining with before/after stats | +| `streamline.ce --diagnose` | Print compile-time diagnostics (type errors and warnings) | ## Test Files @@ -146,3 +148,4 @@ Seeds are used during cold start (empty cache) to compile the pipeline modules f | `qbe_test.ce` | End-to-end QBE IL generation | | `test_intrinsics.cm` | Inlined intrinsic opcodes (is_array, length, push, etc.) | | `test_backward.cm` | Backward type propagation for parameters | +| `tests/compile.cm` | Compile-time diagnostics (type errors and warnings) | diff --git a/docs/spec/streamline.md b/docs/spec/streamline.md index 33e5f0a5..0167dc49 100644 --- a/docs/spec/streamline.md +++ b/docs/spec/streamline.md @@ -178,6 +178,36 @@ Removes `jump L` instructions where `L` is the immediately following label (skip **Nop prefix:** `_nop_dj_` +### 9. diagnose_function (compile-time diagnostics) + +Optional pass that runs when `_warn` is set on the mcode input. Performs a forward type-tracking scan and emits diagnostics for provably wrong operations. Diagnostics are collected in `ir._diagnostics` as `{severity, file, line, col, message}` records. + +This pass does not modify instructions — it only emits diagnostics. + +**Errors** (compilation is aborted): + +| Pattern | Message | +|---------|---------| +| `store_field` on T_ARRAY | storing named property on array | +| `store_index` on T_RECORD | storing numeric index on record | +| `store_field` / `store_index` on T_TEXT | storing property/index on text | +| `push` on T_TEXT / T_RECORD | push on text/record | +| `invoke` on T_NULL / T_INT / T_FLOAT / T_NUM / T_TEXT / T_BOOL / T_ARRAY | invoking null/number/text/bool/array | +| arity mismatch (module imports only) | function expects N arguments, got M | + +**Warnings** (compilation continues): + +| Pattern | Message | +|---------|---------| +| `load_field` on T_ARRAY | named property access on array | +| `load_field` on T_TEXT | named property access on text | +| `load_dynamic` with T_TEXT key on T_RECORD | text key on record | +| `load_dynamic` with T_RECORD / T_ARRAY / T_BOOL / T_NULL key on T_RECORD | record/array/bool/null key on record | + +The engine (`internal/engine.cm`) prints all diagnostics and aborts compilation if any have severity `"error"`. Warnings are printed but do not block compilation. + +**Nop prefix:** none (diagnostics only, does not modify instructions) + ## Pass Composition All passes run in sequence in `optimize_function`: @@ -191,6 +221,7 @@ simplify_booleans eliminate_moves eliminate_unreachable eliminate_dead_jumps +diagnose_function → optional, when _warn is set ``` Each pass is independent and can be commented out for testing or benchmarking. diff --git a/docs/testing.md b/docs/testing.md index 38e98e48..6e5c7dcf 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -192,6 +192,36 @@ Failures saved to tests/fuzz_failures/ Saved failure files are valid `.cm` modules that can be run directly or added to the test suite. +## Compile-Time Diagnostics Tests + +The `tests/compile.cm` test suite verifies that the type checker catches provably wrong operations at compile time. It works by compiling source snippets through the pipeline with `_warn` enabled and checking that the expected diagnostics are emitted. + +```javascript +var shop = use('internal/shop') +var streamline = use('streamline') + +function get_diagnostics(src) { + fd.slurpwrite(tmpfile, stone(blob(src))) + var compiled = shop.mcode_file(tmpfile) + compiled._warn = true + var optimized = streamline(compiled) + if (optimized._diagnostics == null) return [] + return optimized._diagnostics +} +``` + +The suite covers: +- **Store errors**: storing named property on array, numeric index on record, property/index on text, push on text/record +- **Invoke errors**: invoking null, number, text +- **Warnings**: named property access on array/text, record key on record +- **Clean code**: valid operations produce no diagnostics + +Run the compile diagnostics tests with: + +```bash +pit test compile +``` + ## Test File Organization Tests live in the `tests/` directory of a package: diff --git a/docs/wota.md b/docs/wota.md index 3beff594..ba3c19e7 100644 --- a/docs/wota.md +++ b/docs/wota.md @@ -5,7 +5,7 @@ weight: 86 type: "docs" --- -Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume. +Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume. Wota is an internal module: `use('internal/wota')`. Wota stands for Word Object Transfer Arrangement. diff --git a/tests/compile.cm b/tests/compile.cm new file mode 100644 index 00000000..16f32cab --- /dev/null +++ b/tests/compile.cm @@ -0,0 +1,129 @@ +// Compile-time diagnostics tests — verify the type checker catches errors +var fd = use('fd') +var shop = use('internal/shop') +var streamline = use('streamline') + +var tmpfile = "/tmp/_cell_compile_test.cm" + +function write_source(src) { + fd.slurpwrite(tmpfile, stone(blob(src))) +} + +function get_diagnostics(src) { + write_source(src) + var compiled = shop.mcode_file(tmpfile) + compiled._warn = true + var optimized = streamline(compiled) + if (optimized._diagnostics == null) return [] + return optimized._diagnostics +} + +function has_diagnostic(diags, severity, pattern) { + var i = 0 + while (i < length(diags)) { + if (diags[i].severity == severity && search(diags[i].message, pattern) != null) { + return true + } + i = i + 1 + } + return false +} + +function expect(diags, severity, pattern) { + if (!has_diagnostic(diags, severity, pattern)) { + return `expected ${severity} matching '${pattern}', got ${text(length(diags))} diagnostic(s)` + } + return null +} + +function expect_clean(diags) { + if (length(diags) > 0) { + return `expected no diagnostics, got ${text(length(diags))}` + } + return null +} + +return { + // === Store errors === + + test_store_field_on_array: function() { + var d = get_diagnostics("var a = []\na[\"x\"] = 1") + return expect(d, "error", "storing named property on array") + }, + + test_store_index_on_record: function() { + var d = get_diagnostics("var a = {}\na[1] = 1") + return expect(d, "error", "storing numeric index on record") + }, + + test_store_field_on_text: function() { + var d = get_diagnostics("var s = \"hello\"\ns.x = 1") + return expect(d, "error", "storing property on text") + }, + + test_store_index_on_text: function() { + var d = get_diagnostics("var s = \"hello\"\ns[0] = 1") + return expect(d, "error", "storing index on text") + }, + + test_push_on_text: function() { + var d = get_diagnostics("var s = \"hello\"\ns[] = 1") + return expect(d, "error", "push on text") + }, + + test_push_on_record: function() { + var d = get_diagnostics("var r = {}\nr[] = 1") + return expect(d, "error", "push on record") + }, + + // === Invoke errors === + + test_invoke_null: function() { + var d = get_diagnostics("var x = null\nx()") + return expect(d, "error", "invoking null") + }, + + test_invoke_number: function() { + var d = get_diagnostics("var x = 42\nx()") + return expect(d, "error", "invoking") + }, + + test_invoke_text: function() { + var d = get_diagnostics("var x = \"hello\"\nx()") + return expect(d, "error", "invoking") + }, + + // === Warnings === + + test_field_on_array_warns: function() { + var d = get_diagnostics("var a = [1, 2]\nvar x = a.name") + return expect(d, "warning", "named property access on array") + }, + + test_field_on_text_warns: function() { + var d = get_diagnostics("var s = \"hello\"\nvar x = s.name") + return expect(d, "warning", "named property access on text") + }, + + test_record_key_on_record_warns: function() { + var d = get_diagnostics("var r = {a: 1}\nvar k = {}\nvar x = r[k]") + return expect(d, "warning", "record key on record") + }, + + // === Clean code produces no diagnostics === + + test_clean_array_ops: function() { + var d = get_diagnostics("var a = [1, 2, 3]\nvar x = a[0]\na[1] = 5\na[] = 4") + return expect_clean(d) + }, + + test_clean_record_ops: function() { + var d = get_diagnostics("var r = {a: 1, b: 2}\nvar x = r.a\nr.c = 3") + return expect_clean(d) + }, + + test_clean_function_call: function() { + var d = get_diagnostics("function f(a, b) { return a + b }\nvar x = f(1, 2)") + return expect_clean(d) + } +} diff --git a/vm_suite.ce b/vm_suite.ce index 553456b4..5eaf66de 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -1927,7 +1927,8 @@ run("array for", function() { // ============================================================================ run("array string key disrupts", function() { - if (!should_disrupt(function() { var a = []; a["a"] = 1 })) fail("array should not use string as key") + var f = function(a) { a["a"] = 1 } + if (!should_disrupt(function() { f([]) })) fail("array should not use string as key") }) run("array object key disrupts", function() { @@ -1947,7 +1948,8 @@ run("array array key disrupts", function() { }) run("obj number key disrupts", function() { - if (!should_disrupt(function() { var a = {}; a[1] = 1 })) fail("object should not use number as key") + var f = function(a) { a[1] = 1 } + if (!should_disrupt(function() { f({}) })) fail("object should not use number as key") }) run("obj array key disrupts", function() {