test: group examples into per-category folders
Move examples/*.sx and their expected/ snapshots into per-category subfolders (examples/<category>/...). Folder = leading filename token, with ffi-objc/ffi-jni kept whole; filenames are unchanged. The corpus runner and LSP sweep now discover each category's expected/ dir, while issues/ stays flat. Example 1058's repo-root-relative companion import is made file-relative. Path strings embedded in 164 snapshots were regenerated (path-only changes). Test-layout docs in CLAUDE.md updated.
This commit is contained in:
17
examples/modules/0700-modules-import.sx
Normal file
17
examples/modules/0700-modules-import.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
std :: #import "modules/std.sx";
|
||||
|
||||
//flat
|
||||
#import "modules/math";
|
||||
|
||||
main :: () -> i32 {
|
||||
{
|
||||
defer std.print("after hello");
|
||||
//expect stdout : hello there
|
||||
std.print("hello there");
|
||||
}
|
||||
|
||||
v:= std.Vector(3,f32).[1,2,3];
|
||||
|
||||
std.print("\n{}\n", v);
|
||||
0
|
||||
}
|
||||
14
examples/modules/0701-modules-c-import.sx
Normal file
14
examples/modules/0701-modules-c-import.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#include "vendors/test_c/test.h";
|
||||
#source "vendors/test_c/test.c";
|
||||
};
|
||||
|
||||
main :: () -> i32 {
|
||||
a := add_numbers(10, 20);
|
||||
b := multiply(5, 6);
|
||||
print("add_numbers(10, 20) = {}\n", a);
|
||||
print("multiply(5, 6) = {}\n", b);
|
||||
0
|
||||
}
|
||||
10
examples/modules/0702-modules-c-import-ns.sx
Normal file
10
examples/modules/0702-modules-c-import-ns.sx
Normal file
@@ -0,0 +1,10 @@
|
||||
#import "modules/std.sx";
|
||||
tc :: #import "tests/fixtures/test_c.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
a := tc.add_numbers(10, 20);
|
||||
b := tc.multiply(5, 6);
|
||||
print("tc.add_numbers(10, 20) = {}\n", a);
|
||||
print("tc.multiply(5, 6) = {}\n", b);
|
||||
0
|
||||
}
|
||||
12
examples/modules/0703-modules-into-impl-helper.sx
Normal file
12
examples/modules/0703-modules-into-impl-helper.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// Helper for 93-into-import-scope.sx: declares Wrap + an impl Into for it.
|
||||
// No paired tests/expected file — not executed standalone by the runner.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Wrap :: struct { v: i64 = 0; }
|
||||
|
||||
impl Into(Wrap) for i64 {
|
||||
convert :: (self: i64) -> Wrap {
|
||||
.{ v = self * 10 }
|
||||
}
|
||||
}
|
||||
13
examples/modules/0703-modules-into-import-scope.sx
Normal file
13
examples/modules/0703-modules-into-import-scope.sx
Normal file
@@ -0,0 +1,13 @@
|
||||
// Phase 4 (xx-via-Into mechanism): an `impl Into(...) for ...` lives in
|
||||
// a separate file and reaches the xx site through a direct `#import`.
|
||||
// The visibility filter accepts the impl because the user file
|
||||
// transitively imports the impl's defining module.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "./0703-modules-into-impl-helper.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
w : Wrap = xx 3;
|
||||
print("w.v = {}\n", w.v);
|
||||
0
|
||||
}
|
||||
20
examples/modules/0704-modules-inline-if-import-in-body.sx
Normal file
20
examples/modules/0704-modules-inline-if-import-in-body.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// Regression: `#import` inside the body of a top-level
|
||||
// `inline if OS == .X { ... }` block. The imports.zig flatten pass
|
||||
// (issue-0042) lifts these to the top level before resolution; the
|
||||
// parser arm in `parseStmt` that accepts them was missing on macOS /
|
||||
// iOS / linux until this commit, so chess's
|
||||
// `inline if OS == .android { #import "modules/platform/android.sx"; }`
|
||||
// pattern broke parse on every non-Android target.
|
||||
//
|
||||
// The body here also carries a global decl to mirror chess's shape —
|
||||
// the prior bug was specifically about hash_import inside an inline-if
|
||||
// body, not the global decl alongside it.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
inline if OS == .android {
|
||||
#import "modules/std.sx";
|
||||
g_android_only : i32 = 0;
|
||||
}
|
||||
|
||||
main :: () { print("ok\n"); }
|
||||
24
examples/modules/0705-modules-inline-if-hoist-toplevel.sx
Normal file
24
examples/modules/0705-modules-inline-if-hoist-toplevel.sx
Normal file
@@ -0,0 +1,24 @@
|
||||
// Regression: top-level `inline if OS == .X { ... }` body decls get
|
||||
// hoisted to the actual top level. Before this commit, the `if_expr`
|
||||
// landed in `root.decls` but `scanDecls` had no `.if_expr` arm, so the
|
||||
// body was silently dropped — chess's
|
||||
// `inline if OS == .android { SxApp :: #jni_main #jni_class(...) { ... } }`
|
||||
// was invisible to the compiler. Fix:
|
||||
// `imports.flattenComptimeConditionals` runs at the head of
|
||||
// `resolveImports` and replaces matching arms with their body stmts
|
||||
// (recursively, so a nested `inline if` inside a hoisted arm also
|
||||
// hoists).
|
||||
//
|
||||
// Three patterns covered: a global `var_decl`, an `#import`, and a
|
||||
// nested `inline if` whose else arm fires on host macOS.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
inline if OS == .android {
|
||||
#import "modules/std.sx";
|
||||
g_value : i64 = 99;
|
||||
} else {
|
||||
g_value : i64 = 42;
|
||||
}
|
||||
|
||||
main :: () { print("{}\n", g_value); }
|
||||
18
examples/modules/0706-modules-import-non-transitive.sx
Normal file
18
examples/modules/0706-modules-import-non-transitive.sx
Normal file
@@ -0,0 +1,18 @@
|
||||
// `#import` is non-transitive: when A imports B and B imports C, A
|
||||
// must NOT see C's top-level names. This file imports `b.sx` (which
|
||||
// in turn imports `c.sx`) and then deliberately references C's names
|
||||
// directly — the compiler is expected to reject the references with
|
||||
// "not visible; #import the module that declares it" diagnostics.
|
||||
//
|
||||
// `b.sx` ↔ `c.sx` together still compile: `b_only_fn`'s body sees
|
||||
// `c_only_fn` / `c_only_const` because b.sx directly imports c.sx.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0706-modules-import-non-transitive/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("b_only_fn: {}\n", b_only_fn());
|
||||
print("c_only_fn direct: {}\n", c_only_fn());
|
||||
print("c_only_const direct: {}\n", c_only_const);
|
||||
0
|
||||
}
|
||||
5
examples/modules/0706-modules-import-non-transitive/b.sx
Normal file
5
examples/modules/0706-modules-import-non-transitive/b.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
#import "c.sx";
|
||||
|
||||
b_only_fn :: () -> i32 {
|
||||
c_only_fn() + c_only_const
|
||||
}
|
||||
2
examples/modules/0706-modules-import-non-transitive/c.sx
Normal file
2
examples/modules/0706-modules-import-non-transitive/c.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
c_only_fn :: () -> i32 { 42 }
|
||||
c_only_const :: 7;
|
||||
29
examples/modules/0707-modules-import-dir-scan-order.sx
Normal file
29
examples/modules/0707-modules-import-dir-scan-order.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Regression test for issue-0039 — directory-import scan order.
|
||||
//
|
||||
// When a directory is imported, the resolver iterates files
|
||||
// alphabetically. Inside the directory, `aaa_uses.sx` comes BEFORE
|
||||
// `types.sx` but its `make_my` returns `MyEnum` (defined in
|
||||
// `types.sx`). The combined directory module must put `aaa_uses.sx`'s
|
||||
// transitive imports (which include `MyEnum`) into the global scan
|
||||
// stream BEFORE `aaa_uses.sx`'s own decls so the tagged_union for
|
||||
// MyEnum is registered before `make_my`'s return type is resolved.
|
||||
//
|
||||
// Pre-fix, the dir-import implementation appended each file's
|
||||
// `own_decls` before its `decls`, which inverted that order and
|
||||
// caused `MyEnum` to be registered as a placeholder struct via the
|
||||
// `resolveTypeName` fallback. The later `enum_decl` scan then
|
||||
// short-circuited via `findByName` and never upgraded the placeholder
|
||||
// to the real tagged_union, surfacing as "cannot infer enum type
|
||||
// for '.b'" at the `return .b(42)` site.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0707-modules-import-dir-scan-order";
|
||||
|
||||
main :: () -> i32 {
|
||||
e := make_my();
|
||||
if e == {
|
||||
case .a: { print("a\n"); }
|
||||
case .b: (v) { print("b={}\n", v); }
|
||||
}
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Alphabetically FIRST file in the directory. Its own decl uses
|
||||
// `MyEnum`, defined in a sibling file (`types.sx`). The directory
|
||||
// import pass must register `MyEnum` BEFORE this file's `make_my`
|
||||
// is scanned, or `MyEnum` falls back to a placeholder struct and
|
||||
// `return .b(42)` fails with "cannot infer enum type".
|
||||
|
||||
#import "types.sx";
|
||||
|
||||
make_my :: () -> MyEnum {
|
||||
return .b(42);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
MyEnum :: enum {
|
||||
a;
|
||||
b: i32;
|
||||
}
|
||||
13
examples/modules/0708-modules-xx-any-pack-cross-module.sx
Normal file
13
examples/modules/0708-modules-xx-any-pack-cross-module.sx
Normal file
@@ -0,0 +1,13 @@
|
||||
// Regression for issue 0057: `xx <int>` as a variadic `format` arg inside an
|
||||
// imported-module function used to segfault (mis-typed as the enclosing fn's
|
||||
// return type, monomorphizing __pack_string + ABI-coercing the int as a
|
||||
// 16-byte string fat pointer). Now it auto-boxes to Any like the bare form.
|
||||
// The companion module is examples/242-xx-any-pack-cross-module/fmt.sx.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "0708-modules-xx-any-pack-cross-module/fmt.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("{}", build(3));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Companion module for issue-0057 regression. The bug: an `xx <int>` argument
|
||||
// to a variadic `format` (a comptime `..$args` pack), inside a function that
|
||||
// lives in an IMPORTED module, was mis-typed as the enclosing fn's
|
||||
// `target_type` (here `string`) instead of auto-boxing to `Any` — so it
|
||||
// monomorphized `__pack_string` and ABI-coerced the 4-byte int as a 16-byte
|
||||
// string fat pointer, corrupting memory at runtime. Fixed by clearing
|
||||
// `target_type` while lowering pack args.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
build :: (n: i32) -> string {
|
||||
result := "items:\n";
|
||||
i : i32 = 0;
|
||||
while i < n {
|
||||
line := format(" item {}\n", xx i); // <-- the xx-to-Any pack arg
|
||||
result = concat(result, line);
|
||||
i = i + 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Regression (issue 0056): a parameterised-protocol (`Into`) impl living in a
|
||||
// module reached through more than one import path must register exactly once.
|
||||
//
|
||||
// main ─┬─ issue-0056/mid_a ─┐
|
||||
// └─ issue-0056/mid_b ─┴─ issue-0056/common (holds `impl Into(Wrapped) for i64`)
|
||||
//
|
||||
// Impl blocks are anonymous (no `declName`), so before the fix the diamond
|
||||
// dedup in imports.zig appended the cached node once per path and the second
|
||||
// registration tripped: `duplicate impl 'Into' for source 'i64'`. Now the flat
|
||||
// decl list also dedups by node identity, so this builds and prints 7.
|
||||
#import "modules/std.sx";
|
||||
// `Wrapped` lives in the shared `common.sx`; bare-import visibility is
|
||||
// non-transitive, so naming it here means importing it here (not via mid_a/b).
|
||||
#import "0709-modules-issue-0056/common.sx";
|
||||
#import "0709-modules-issue-0056/mid_a.sx";
|
||||
#import "0709-modules-issue-0056/mid_b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
w : Wrapped = xx 7;
|
||||
print("{}\n", w.v);
|
||||
0
|
||||
}
|
||||
12
examples/modules/0709-modules-issue-0056/common.sx
Normal file
12
examples/modules/0709-modules-issue-0056/common.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// A parameterised-protocol (`Into`) impl living in a module reachable through
|
||||
// more than one import path. Each path must NOT re-register the impl.
|
||||
// Impl blocks are anonymous (`declName() == null`), so the diamond-import
|
||||
// dedup in imports.zig (`mergeFlat`) skips them and appends the node once per
|
||||
// path — `registerParamImpl` then trips its same-file duplicate check.
|
||||
Wrapped :: struct { v: i64; }
|
||||
|
||||
impl Into(Wrapped) for i64 {
|
||||
convert :: (self: i64) -> Wrapped {
|
||||
return .{ v = self };
|
||||
}
|
||||
}
|
||||
3
examples/modules/0709-modules-issue-0056/mid_a.sx
Normal file
3
examples/modules/0709-modules-issue-0056/mid_a.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
#import "common.sx";
|
||||
|
||||
mid_a_marker :: () -> i64 { 1 }
|
||||
3
examples/modules/0709-modules-issue-0056/mid_b.sx
Normal file
3
examples/modules/0709-modules-issue-0056/mid_b.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
#import "common.sx";
|
||||
|
||||
mid_b_marker :: () -> i64 { 2 }
|
||||
40
examples/modules/0710-modules-sha256.sx
Normal file
40
examples/modules/0710-modules-sha256.sx
Normal file
@@ -0,0 +1,40 @@
|
||||
// Streaming SHA-256 (FIPS 180-4) from `modules/std/hash.sx`.
|
||||
//
|
||||
// Known-answer vectors (empty, "abc", the 112-byte NIST multi-block
|
||||
// vector) plus the streaming invariant: feeding the same bytes in
|
||||
// several `update` chunks yields the same digest as the one-shot call.
|
||||
//
|
||||
// The digest is a zero-heap `[64]u8` returned by value; tests build a
|
||||
// `string` view over it (no copy) to compare against the pinned hex.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/hash.sx";
|
||||
|
||||
check :: (label: string, got: [64]u8, want: string) {
|
||||
view := string.{ ptr = @got[0], len = 64 };
|
||||
if view == want {
|
||||
print("{}: ok\n", label);
|
||||
} else {
|
||||
print("{}: FAIL got {} want {}\n", label, view, want);
|
||||
}
|
||||
}
|
||||
|
||||
main :: () {
|
||||
check("empty", sha256_hex(""),
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
check("abc", sha256_hex("abc"),
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
|
||||
// 112-byte input — spans more than one 64-byte block.
|
||||
multi := "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu";
|
||||
check("multi", sha256_hex(multi),
|
||||
"cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1");
|
||||
|
||||
// Streaming must equal one-shot regardless of chunk boundaries.
|
||||
h := init();
|
||||
h.update("abcdefghbcdefghicdefghijdefghijke"); // 33 bytes
|
||||
h.update("fghijklfghijklmghijklmnhijklmno"); // crosses block edge
|
||||
h.update("ijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu");
|
||||
check("stream-eq-oneshot", h.final(),
|
||||
"cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1");
|
||||
}
|
||||
82
examples/modules/0711-modules-sha256-vectors.sx
Normal file
82
examples/modules/0711-modules-sha256-vectors.sx
Normal file
@@ -0,0 +1,82 @@
|
||||
// SHA-256 known-answer vectors for `modules/std/hash.sx`.
|
||||
//
|
||||
// Pins published digests for: the empty input, "abc", the two NIST
|
||||
// multi-block sample vectors, the padding/length boundaries around the
|
||||
// 56-byte (one-block-with-length) and 64-byte (block) edges, and the
|
||||
// classic large repeats (1000 and 1,000,000 'a'). Each expected hex is
|
||||
// hard-coded from FIPS 180-4 / NIST CAVP and cross-checked with
|
||||
// `shasum -a 256`.
|
||||
//
|
||||
// The digest is a zero-heap `[64]u8`; we compare it via a `string` view
|
||||
// (no copy). Repeat vectors are built by streaming an 'a'-filled stack
|
||||
// buffer, so even the 1,000,000 case allocates nothing on the heap.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/hash.sx";
|
||||
|
||||
check :: (label: string, got: [64]u8, want: string) {
|
||||
view := string.{ ptr = @got[0], len = 64 };
|
||||
if view == want {
|
||||
print("{}: ok\n", label);
|
||||
} else {
|
||||
print("{}: FAIL got {} want {}\n", label, view, want);
|
||||
}
|
||||
}
|
||||
|
||||
// Digest of `total` bytes of 'a', streamed in 1000-byte chunks so peak
|
||||
// memory stays O(chunk) for the 1,000,000 case. `total == 0` yields the
|
||||
// empty-input digest (the loop body never runs).
|
||||
hash_a :: (total: i64) -> [64]u8 {
|
||||
chunk : [1000]u8 = ---;
|
||||
k := 0;
|
||||
while k < 1000 { chunk[k] = 97; k += 1; } // 97 = 'a'
|
||||
|
||||
h := init();
|
||||
remaining := total;
|
||||
while remaining > 0 {
|
||||
take := if remaining < 1000 then remaining else 1000;
|
||||
h.update(string.{ ptr = @chunk[0], len = take });
|
||||
remaining -= take;
|
||||
}
|
||||
h.final()
|
||||
}
|
||||
|
||||
main :: () {
|
||||
check("empty", sha256_hex(""),
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
check("abc", sha256_hex("abc"),
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
|
||||
// NIST CAVP sample vectors (56-byte and 112-byte multi-block).
|
||||
check("nist-56", sha256_hex("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
|
||||
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
|
||||
check("nist-112", sha256_hex("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu"),
|
||||
"cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1");
|
||||
|
||||
// Padding/length boundaries around the 56- and 64-byte edges, using
|
||||
// 'a' repeats so the boundary is exercised independently of content.
|
||||
check("len-0", hash_a(0),
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
check("len-55", hash_a(55),
|
||||
"9f4390f8d30c2dd92ec9f095b65e2b9ae9b0a925a5258e241c9f1e910f734318");
|
||||
check("len-56", hash_a(56),
|
||||
"b35439a4ac6f0948b6d6f9e3c6af0f5f590ce20f1bde7090ef7970686ec6738a");
|
||||
check("len-57", hash_a(57),
|
||||
"f13b2d724659eb3bf47f2dd6af1accc87b81f09f59f2b75e5c0bed6589dfe8c6");
|
||||
check("len-63", hash_a(63),
|
||||
"7d3e74a05d7db15bce4ad9ec0658ea98e3f06eeecf16b4c6fff2da457ddc2f34");
|
||||
check("len-64", hash_a(64),
|
||||
"ffe054fe7ae0cb6dc65c3af9b61d5209f439851db43d0ba5997337df154668eb");
|
||||
check("len-65", hash_a(65),
|
||||
"635361c48bb9eab14198e76ea8ab7f1a41685d6ad62aa9146d301d4f17eb0ae0");
|
||||
check("len-119", hash_a(119),
|
||||
"31eba51c313a5c08226adf18d4a359cfdfd8d2e816b13f4af952f7ea6584dcfb");
|
||||
check("len-120", hash_a(120),
|
||||
"2f3d335432c70b580af0e8e1b3674a7c020d683aa5f73aaaedfdc55af904c21c");
|
||||
|
||||
// Large repeats (FIPS 180-4 / classic vectors).
|
||||
check("a-1000", hash_a(1000),
|
||||
"41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3");
|
||||
check("a-1000000", hash_a(1000000),
|
||||
"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
|
||||
}
|
||||
77
examples/modules/0712-modules-sha256-streaming.sx
Normal file
77
examples/modules/0712-modules-sha256-streaming.sx
Normal file
@@ -0,0 +1,77 @@
|
||||
// SHA-256 streaming-equivalence + file hashing for `modules/std/hash.sx`.
|
||||
//
|
||||
// The chunk boundary must not affect the result: feeding the same bytes
|
||||
// one-shot, one byte at a time, split mid-block, and split exactly on a
|
||||
// 64-byte block boundary all yield the same digest, anchored to a pinned
|
||||
// value. Then `sha256_file` of a written temp file must equal the
|
||||
// in-memory digest of the same bytes — the streaming file path agrees
|
||||
// with the buffered path.
|
||||
//
|
||||
// All comparisons go through `string` views over the zero-heap `[64]u8`
|
||||
// digests; the byte/split updates view directly into the input buffer
|
||||
// (no `substr`, no copies).
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/hash.sx";
|
||||
#import "modules/std/fs.sx";
|
||||
|
||||
// 112-byte NIST multi-block vector — long enough that a 64-byte split is
|
||||
// a genuine block boundary and a 30-byte split lands mid-block.
|
||||
MSG :: "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu";
|
||||
PIN :: "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1";
|
||||
|
||||
check :: (label: string, got: [64]u8, want: string) {
|
||||
view := string.{ ptr = @got[0], len = 64 };
|
||||
if view == want {
|
||||
print("{}: ok\n", label);
|
||||
} else {
|
||||
print("{}: FAIL got {} want {}\n", label, view, want);
|
||||
}
|
||||
}
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
// Absorb `data` one byte at a time (views of length 1 into the buffer).
|
||||
stream_by_byte :: (data: string) -> [64]u8 {
|
||||
h := init();
|
||||
i := 0;
|
||||
while i < data.len {
|
||||
h.update(string.{ ptr = @data[i], len = 1 });
|
||||
i += 1;
|
||||
}
|
||||
h.final()
|
||||
}
|
||||
|
||||
// Absorb `data` as two updates split at `at` (views into the buffer).
|
||||
stream_split :: (data: string, at: i64) -> [64]u8 {
|
||||
h := init();
|
||||
h.update(string.{ ptr = @data[0], len = at });
|
||||
h.update(string.{ ptr = @data[at], len = data.len - at });
|
||||
h.final()
|
||||
}
|
||||
|
||||
main :: () {
|
||||
check("oneshot-pinned", sha256_hex(MSG), PIN);
|
||||
check("byte-at-a-time", stream_by_byte(MSG), PIN);
|
||||
check("split-mid-block", stream_split(MSG, 30), PIN); // 30: mid first block
|
||||
check("split-on-boundary", stream_split(MSG, 64), PIN); // 64: exact block edge
|
||||
|
||||
// sha256_file (streaming) must equal the in-memory digest.
|
||||
if !create_dir_all(".sx-tmp") { print("mkdir: FAIL\n"); return; }
|
||||
path := ".sx-tmp/sx_0712_stream.bin";
|
||||
if !write_file(path, MSG) { print("file-write: FAIL\n"); return; }
|
||||
|
||||
maybe := sha256_file(path);
|
||||
if maybe == null { print("file-eq-memory: FAIL (open)\n"); return; }
|
||||
file_digest := maybe!;
|
||||
mem_digest := sha256_hex(MSG);
|
||||
|
||||
fv := string.{ ptr = @file_digest[0], len = 64 };
|
||||
mv := string.{ ptr = @mem_digest[0], len = 64 };
|
||||
report("file-eq-memory", fv == mv);
|
||||
check("file-pinned", file_digest, PIN);
|
||||
|
||||
delete_file(path);
|
||||
}
|
||||
107
examples/modules/0713-modules-json-writer.sx
Normal file
107
examples/modules/0713-modules-json-writer.sx
Normal file
@@ -0,0 +1,107 @@
|
||||
// JSON value model + writer from `modules/std/json.sx`.
|
||||
//
|
||||
// Builds a representative value — a nested object holding a string with
|
||||
// every escape kind (quote, newline, tab, backslash, a raw control byte),
|
||||
// integers spanning zero / a small negative / a small positive / i64 MIN
|
||||
// (-9223372036854775808) / i64 MAX (9223372036854775807), a bool, null, an
|
||||
// array, and a nested object — then serializes it two ways and asserts the
|
||||
// EXACT bytes:
|
||||
//
|
||||
// 1. into a caller-owned `[]u8` buffer (returns bytes written),
|
||||
// 2. streaming straight to a file through an 8-byte staging buffer
|
||||
// (small on purpose, so the writer flushes many times and no
|
||||
// whole-document string is ever held).
|
||||
//
|
||||
// Both must yield byte-for-byte the same pinned document, with keys in
|
||||
// INSERTION ORDER. A too-small buffer must raise `error.Overflow` rather
|
||||
// than truncate. The model is built through an explicit Arena allocator
|
||||
// and freed in one `deinit`; the writer path allocates nothing.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it.
|
||||
#import "modules/std/json.sx";
|
||||
#import "modules/std/fs.sx";
|
||||
|
||||
// The exact document the writer must produce (insertion order, escaping).
|
||||
EXPECT :: "{\"name\":\"a\\\"b\\n\",\"tab\":\"x\\ty\",\"bs\":\"c\\\\d\",\"ctrl\":\"\\u0001\",\"n\":-7,\"zero\":0,\"pos\":7,\"min\":-9223372036854775808,\"max\":9223372036854775807,\"ok\":true,\"nil\":null,\"xs\":[1,-2,3],\"nested\":{\"k\":\"v\"}}";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
build :: (alloc: Allocator) -> Value {
|
||||
// A raw control byte (0x01) viewed as a 1-byte string — exercises the
|
||||
// `\u00XX` path that has no named shorthand. String values are VIEWS,
|
||||
// so the bytes must outlive the writes: back them with `alloc` (the
|
||||
// arena), not a local that dies when `build` returns.
|
||||
cbytes : [*]u8 = xx alloc.alloc_bytes(1);
|
||||
cbytes[0] = 1;
|
||||
ctrl := string.{ ptr = cbytes, len = 1 };
|
||||
|
||||
nested : Object = .{};
|
||||
nested.put("k", .str("v"), alloc);
|
||||
|
||||
xs : Array = .{};
|
||||
xs.add(.int_(1), alloc);
|
||||
xs.add(.int_(0 - 2), alloc);
|
||||
xs.add(.int_(3), alloc);
|
||||
|
||||
obj : Object = .{};
|
||||
obj.put("name", .str("a\"b\n"), alloc); // quote + newline
|
||||
obj.put("tab", .str("x\ty"), alloc); // tab
|
||||
obj.put("bs", .str("c\\d"), alloc); // backslash
|
||||
obj.put("ctrl", .str(ctrl), alloc); // raw control byte ->
|
||||
obj.put("n", .int_(0 - 7), alloc); // small negative int
|
||||
obj.put("zero", .int_(0), alloc); // zero
|
||||
obj.put("pos", .int_(7), alloc); // small positive int
|
||||
// i64 MIN: its magnitude (9223372036854775808) is not a representable
|
||||
// positive i64 literal, so build it from MAX-positive minus one.
|
||||
obj.put("min", .int_(0 - 9223372036854775807 - 1), alloc);
|
||||
obj.put("max", .int_(9223372036854775807), alloc); // i64 MAX
|
||||
obj.put("ok", .bool_(true), alloc);
|
||||
obj.put("nil", .null_, alloc);
|
||||
obj.put("xs", .array(xs), alloc);
|
||||
obj.put("nested", .object(nested), alloc);
|
||||
|
||||
return .object(obj);
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 4096);
|
||||
defer arena.deinit();
|
||||
|
||||
root := build(xx arena);
|
||||
|
||||
// 1. Write into a caller buffer; assert exact bytes + byte count.
|
||||
buf : [512]u8 = ---;
|
||||
n := try write_to_buffer(root, string.{ ptr = @buf[0], len = 512 });
|
||||
view := string.{ ptr = @buf[0], len = n };
|
||||
print("doc: {}\n", view);
|
||||
report("buffer-exact", view == EXPECT);
|
||||
report("buffer-len", n == EXPECT.len);
|
||||
|
||||
// 2. A buffer that is one byte too small must raise Overflow.
|
||||
tight : []u8 = string.{ ptr = @buf[256], len = EXPECT.len - 1 };
|
||||
_, oerr := write_to_buffer(root, tight);
|
||||
report("overflow-raised", oerr == error.Overflow);
|
||||
|
||||
// 3. Stream to a file through a tiny staging buffer (forces flushes);
|
||||
// read it back and assert it equals the same document. Write into the
|
||||
// repo-local, gitignored scratch dir and unlink afterwards so nothing
|
||||
// leaks and concurrent runs don't fight over a shared /tmp name.
|
||||
if !create_dir_all(".sx-tmp") { print("mkdir: FAIL\n"); return; }
|
||||
path := ".sx-tmp/sx_0713_json.json";
|
||||
fh := open_file(path, .write);
|
||||
if fh == null { print("open: FAIL\n"); return; }
|
||||
f := fh!;
|
||||
stage : [8]u8 = ---;
|
||||
try write_to_file(root, @f, string.{ ptr = @stage[0], len = 8 });
|
||||
f.close();
|
||||
|
||||
back := read_file(path);
|
||||
delete_file(path);
|
||||
if back == null { print("file-read: FAIL\n"); return; }
|
||||
report("file-exact", back! == EXPECT);
|
||||
return;
|
||||
}
|
||||
155
examples/modules/0714-modules-json-reader.sx
Normal file
155
examples/modules/0714-modules-json-reader.sx
Normal file
@@ -0,0 +1,155 @@
|
||||
// JSON reader (parser) from `modules/std/json.sx` — the inverse of the
|
||||
// F2.1 writer.
|
||||
//
|
||||
// Parses a representative document (nested object + array + a
|
||||
// string-with-escapes + ints incl. negatives + bool + null) into the
|
||||
// shared value model, then proves:
|
||||
//
|
||||
// 1. STRUCTURE — the parsed tree has the expected keys (in INSERTION
|
||||
// order), values, and nesting.
|
||||
// 2. HEAP DISCIPLINE — an un-escaped string value is a zero-copy VIEW
|
||||
// into the input buffer (its bytes lie inside `src`), while an
|
||||
// escaped string is DECODED into a fresh `alloc`-ed buffer (its
|
||||
// bytes lie OUTSIDE `src`). Composite nodes + the decoded string are
|
||||
// the only allocations, all through the explicit Arena.
|
||||
// 3. ROUND-TRIP — feeding the parsed tree back to the writer reproduces
|
||||
// the canonical input byte-for-byte.
|
||||
// 4. UNICODE — `\uXXXX` (BMP + 2-byte) and a surrogate pair decode to
|
||||
// the right UTF-8 bytes.
|
||||
// 5. FAILURE SURFACING — every malformed input raises the right
|
||||
// `JsonParseError` variant on the error channel, never a bogus value.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it.
|
||||
#import "modules/std/json.sx";
|
||||
|
||||
// Canonical document: no insignificant whitespace, escapes in the writer's
|
||||
// own form — so re-serializing the parse must reproduce it exactly.
|
||||
DOC :: "{\"name\":\"plain\",\"esc\":\"a\\nb\",\"xs\":[10,-20],\"yes\":true,\"nil\":null,\"sub\":{\"k\":\"v\"}}";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
// Half-open containment [lo, hi).
|
||||
in_range :: (x: i64, lo: i64, hi: i64) -> bool {
|
||||
return x >= lo and x < hi;
|
||||
}
|
||||
|
||||
// True when `parse(src)` raised `want` — destructure captures the error
|
||||
// tag without `try`, so a malformed input never aborts the example.
|
||||
raises :: (src: string, want: JsonParseError, alloc: Allocator) -> bool {
|
||||
_, e := parse(src, alloc);
|
||||
e == want
|
||||
}
|
||||
|
||||
// True when parsing `"a<b>b"` (a string holding the RAW control byte `b`)
|
||||
// raises BadControlChar. Built from a byte buffer because a raw control
|
||||
// byte can't appear in an sx string literal.
|
||||
ctrl_raises :: (b: u8, alloc: Allocator) -> bool {
|
||||
raw : [5]u8 = ---;
|
||||
raw[0] = 34; raw[1] = 97; raw[2] = b; raw[3] = 98; raw[4] = 34; // "a<b>b"
|
||||
return raises(string.{ ptr = @raw[0], len = 5 }, error.BadControlChar, alloc);
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 8192);
|
||||
defer arena.deinit();
|
||||
|
||||
// ── 1. Structure ─────────────────────────────────────────────────
|
||||
src := DOC;
|
||||
root := try parse(src, xx arena);
|
||||
|
||||
is_object := if root == { case .object: true; else: false; };
|
||||
report("root-is-object", is_object);
|
||||
|
||||
o := root.object;
|
||||
report("member-count", o.len == 6);
|
||||
report("key-order-0", o.items[0].key == "name");
|
||||
report("string-plain", o.items[0].val.str == "plain");
|
||||
report("string-escaped", o.items[1].val.str == "a\nb"); // \n decoded to 0x0A
|
||||
|
||||
xs := o.items[2].val.array;
|
||||
report("array-len", xs.len == 2);
|
||||
report("array-pos", xs.items[0].int_ == 10);
|
||||
report("array-neg", xs.items[1].int_ == 0 - 20);
|
||||
|
||||
report("bool-value", o.items[3].val.bool_ == true);
|
||||
|
||||
is_null := if o.items[4].val == { case .null_: true; else: false; };
|
||||
report("null-value", is_null);
|
||||
|
||||
// The nested pair asserted as one expression — a string `==` on each
|
||||
// side of `and`.
|
||||
sub := o.items[5].val.object;
|
||||
report("nested-pair", sub.items[0].key == "k" and sub.items[0].val.str == "v");
|
||||
|
||||
// ── 2. Heap discipline: view vs decoded ──────────────────────────
|
||||
base : i64 = xx src.ptr;
|
||||
stop := base + src.len;
|
||||
p_plain : i64 = xx o.items[0].val.str.ptr; // "plain": no escape -> VIEW into src
|
||||
p_esc : i64 = xx o.items[1].val.str.ptr; // "a\nb": escaped -> DECODED into arena
|
||||
report("plain-is-view", in_range(p_plain, base, stop));
|
||||
report("escaped-allocated", !in_range(p_esc, base, stop));
|
||||
|
||||
// ── 3. Round-trip back through the writer ────────────────────────
|
||||
buf : [256]u8 = ---;
|
||||
n := try write_to_buffer(root, string.{ ptr = @buf[0], len = 256 });
|
||||
rt := string.{ ptr = @buf[0], len = n };
|
||||
report("round-trip", rt == src);
|
||||
|
||||
// ── 4. Leading/trailing/inner whitespace is insignificant ────────
|
||||
wsv := try parse(" [ 1 , 2 , 3 ] ", xx arena);
|
||||
wa := wsv.array;
|
||||
report("ws-count", wa.len == 3);
|
||||
report("ws-first", wa.items[0].int_ == 1);
|
||||
report("ws-last", wa.items[2].int_ == 3);
|
||||
|
||||
// Empty container literals (the manifest/db.json use these).
|
||||
ea := try parse("[]", xx arena);
|
||||
report("empty-array", ea.array.len == 0);
|
||||
eo := try parse("{}", xx arena);
|
||||
report("empty-object", eo.object.len == 0);
|
||||
|
||||
// ── 5. Unicode: \uXXXX (1- and 2-byte) + surrogate pair (4-byte) ──
|
||||
// JSON "Aé😀" -> 'A', 'é' (C3 A9), '😀' (F0 9F 98 80). One byte per report.
|
||||
univ := try parse("\"\\u0041\\u00e9\\uD83D\\uDE00\"", xx arena);
|
||||
u := univ.str;
|
||||
report("uni-len", u.len == 7);
|
||||
report("uni-A", u[0] == 0x41); // U+0041 -> 1 byte
|
||||
report("uni-e1", u[1] == 0xC3); // U+00E9 -> 2 bytes
|
||||
report("uni-e2", u[2] == 0xA9);
|
||||
report("uni-i0", u[3] == 0xF0); // U+1F600 (surrogate pair) -> 4 bytes
|
||||
report("uni-i1", u[4] == 0x9F);
|
||||
report("uni-i2", u[5] == 0x98);
|
||||
report("uni-i3", u[6] == 0x80);
|
||||
|
||||
// ── 6. Malformed inputs each surface the right error variant ─────
|
||||
report("err-truncated", raises("{\"a\":", error.UnexpectedEnd, xx arena));
|
||||
report("err-bad-escape", raises("\"a\\xb\"", error.BadEscape, xx arena));
|
||||
report("err-trailing-junk", raises("[1,2] x", error.TrailingGarbage, xx arena));
|
||||
report("err-bad-token", raises("xyz", error.UnexpectedToken, xx arena));
|
||||
report("err-fraction", raises("1.5", error.BadNumber, xx arena));
|
||||
report("err-leading-zero", raises("01", error.BadNumber, xx arena));
|
||||
report("err-overflow", raises("9223372036854775808", error.BadNumber, xx arena));
|
||||
report("err-unterminated", raises("\"abc", error.UnexpectedEnd, xx arena));
|
||||
|
||||
// ── 7. RFC 8259 §7: unescaped control bytes (U+0000..U+001F) ──────
|
||||
// A RAW control byte inside a string is invalid JSON -> BadControlChar.
|
||||
report("err-raw-tab", ctrl_raises(9, xx arena)); // raw 0x09
|
||||
report("err-raw-lf", ctrl_raises(10, xx arena)); // raw 0x0A
|
||||
report("err-raw-nul", ctrl_raises(0, xx arena)); // raw 0x00
|
||||
|
||||
// POSITIVE: the ESCAPED control forms stay valid and decode to the
|
||||
// exact bytes. JSON "\t\n\u0009" -> 0x09 0x0A 0x09 (3 bytes).
|
||||
esc := try parse("\"\\t\\n\\u0009\"", xx arena);
|
||||
es := esc.str;
|
||||
report("esc-ctrl-len", es.len == 3);
|
||||
report("esc-tab", es[0] == 0x09); // \t
|
||||
report("esc-lf", es[1] == 0x0A); // \n
|
||||
report("esc-u", es[2] == 0x09); // \u0009
|
||||
|
||||
print("=== DONE ===\n");
|
||||
return;
|
||||
}
|
||||
224
examples/modules/0715-modules-json-suite.sx
Normal file
224
examples/modules/0715-modules-json-suite.sx
Normal file
@@ -0,0 +1,224 @@
|
||||
// Comprehensive pinned suite for `modules/std/json.sx` (writer F2.1 +
|
||||
// reader F2.2). Mirrors what 0711 did for std.hash: it LOCKS IN the full
|
||||
// round-trip and the complete malformed-input matrix as one coherent
|
||||
// pinned example. (0713/0714 stay as the focused writer/reader demos with
|
||||
// their heap-discipline narrative; this file is the correctness lock-in.)
|
||||
//
|
||||
// PART A — ROUND-TRIP. Build a representative document covering EVERY
|
||||
// value kind (nested object + array, a string carrying every escape
|
||||
// kind `\" \\ \b \f \n \r \t` and a `\u00XX` control, integers 0 /
|
||||
// small-negative / i64 MIN (-9223372036854775808) / i64 MAX
|
||||
// (9223372036854775807), bool, null) through an explicit Arena, then
|
||||
// `build -> write -> parse -> write`: assert the writer's EXACT bytes,
|
||||
// assert `parse` then re-`write` reproduces them (idempotent), and
|
||||
// spot-check the parsed tree's STRUCTURE incl. INSERTION ORDER.
|
||||
// PART B — DECODE POSITIVES. `\/`, the full named-escape set, `\uXXXX`
|
||||
// (BMP 1- and 2-byte) and a SURROGATE PAIR, the escaped control forms,
|
||||
// and raw multi-byte UTF-8 round-tripping through writer + reader.
|
||||
// PART C — MALFORMED MATRIX. One assertion per `JsonParseError` variant
|
||||
// and its key edges, each asserted to RAISE (never crash, never accept).
|
||||
//
|
||||
// Every model is built through an explicit Arena allocator (heap
|
||||
// discipline): scalars carry no heap, string values are views, composites
|
||||
// and decoded strings go through `alloc`, and the writer allocates nothing.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it.
|
||||
#import "modules/std/json.sx";
|
||||
|
||||
// The writer's EXACT output for the PART A document (insertion order,
|
||||
// canonical escaping). Hand-pinned so a writer regression fails loudly in
|
||||
// the example itself, not only in the captured golden.
|
||||
EXPECT :: "{\"esc\":\"\\\"\\\\\\b\\t\\n\\f\\r\\u0001\",\"zero\":0,\"neg\":-7,\"min\":-9223372036854775808,\"max\":9223372036854775807,\"ok\":true,\"nil\":null,\"xs\":[1,-2,3],\"nested\":{\"k\":\"v\"}}";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
// Half-open containment [lo, hi).
|
||||
in_range :: (x: i64, lo: i64, hi: i64) -> bool {
|
||||
return x >= lo and x < hi;
|
||||
}
|
||||
|
||||
// True when `parse(src)` raised exactly `want`. Destructure captures the
|
||||
// error tag without `try`, so a malformed input never aborts the example.
|
||||
raises :: (src: string, want: JsonParseError, alloc: Allocator) -> bool {
|
||||
_, e := parse(src, alloc);
|
||||
e == want
|
||||
}
|
||||
|
||||
// True when parsing `"a<b>b"` (a string holding the RAW control byte `b`)
|
||||
// raises BadControlChar. Built from a byte buffer because a raw control
|
||||
// byte can't appear in an sx string literal.
|
||||
ctrl_raises :: (b: u8, alloc: Allocator) -> bool {
|
||||
raw : [5]u8 = ---;
|
||||
raw[0] = 34; raw[1] = 97; raw[2] = b; raw[3] = 98; raw[4] = 34; // "a<b>b"
|
||||
return raises(string.{ ptr = @raw[0], len = 5 }, error.BadControlChar, alloc);
|
||||
}
|
||||
|
||||
// Build the PART A document: every value kind, in the insertion order the
|
||||
// writer must emit. The `esc` value carries one byte per escape kind; its
|
||||
// bytes are backed by `alloc` (string values are VIEWS, so they must
|
||||
// outlive `build`).
|
||||
build :: (alloc: Allocator) -> Value {
|
||||
ebytes : [*]u8 = xx alloc.alloc_bytes(8);
|
||||
ebytes[0] = 34; // " -> \"
|
||||
ebytes[1] = 92; // \ -> \\
|
||||
ebytes[2] = 8; // BS -> \b
|
||||
ebytes[3] = 9; // TAB -> \t
|
||||
ebytes[4] = 10; // LF -> \n
|
||||
ebytes[5] = 12; // FF -> \f
|
||||
ebytes[6] = 13; // CR -> \r
|
||||
ebytes[7] = 1; // SOH -> (control with no named shorthand)
|
||||
esc := string.{ ptr = ebytes, len = 8 };
|
||||
|
||||
nested : Object = .{};
|
||||
nested.put("k", .str("v"), alloc);
|
||||
|
||||
xs : Array = .{};
|
||||
xs.add(.int_(1), alloc);
|
||||
xs.add(.int_(0 - 2), alloc);
|
||||
xs.add(.int_(3), alloc);
|
||||
|
||||
obj : Object = .{};
|
||||
obj.put("esc", .str(esc), alloc);
|
||||
obj.put("zero", .int_(0), alloc);
|
||||
obj.put("neg", .int_(0 - 7), alloc);
|
||||
// i64 MIN: |MIN| is not a representable positive i64 literal, so build
|
||||
// it as MAX-positive minus one.
|
||||
obj.put("min", .int_(0 - 9223372036854775807 - 1), alloc);
|
||||
obj.put("max", .int_(9223372036854775807), alloc);
|
||||
obj.put("ok", .bool_(true), alloc);
|
||||
obj.put("nil", .null_, alloc);
|
||||
obj.put("xs", .array(xs), alloc);
|
||||
obj.put("nested", .object(nested), alloc);
|
||||
return .object(obj);
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 16384);
|
||||
defer arena.deinit();
|
||||
a : Allocator = xx arena;
|
||||
|
||||
// ── PART A. build -> write -> parse -> write ─────────────────────────
|
||||
root := build(a);
|
||||
|
||||
buf : [512]u8 = ---;
|
||||
n := try write_to_buffer(root, string.{ ptr = @buf[0], len = 512 });
|
||||
canon := string.{ ptr = @buf[0], len = n };
|
||||
print("doc: {}\n", canon); // golden pins the exact bytes
|
||||
report("rt-exact", canon == EXPECT);
|
||||
report("rt-len", n == EXPECT.len);
|
||||
|
||||
// parse the writer's output, then re-serialize: must reproduce it byte
|
||||
// for byte (writer/reader are inverses on the canonical form).
|
||||
tree2 := try parse(canon, a);
|
||||
buf2 : [512]u8 = ---;
|
||||
n2 := try write_to_buffer(tree2, string.{ ptr = @buf2[0], len = 512 });
|
||||
canon2 := string.{ ptr = @buf2[0], len = n2 };
|
||||
report("rt-idempotent", canon2 == canon);
|
||||
|
||||
// Structure of the parsed tree: insertion order + every value kind.
|
||||
o := tree2.object;
|
||||
report("st-count", o.len == 9);
|
||||
report("st-order",
|
||||
o.items[0].key == "esc" and o.items[1].key == "zero" and
|
||||
o.items[2].key == "neg" and o.items[3].key == "min" and
|
||||
o.items[4].key == "max" and o.items[5].key == "ok" and
|
||||
o.items[6].key == "nil" and o.items[7].key == "xs" and
|
||||
o.items[8].key == "nested");
|
||||
// The escaped string survives the round-trip back to its 8 raw bytes.
|
||||
eexp : [8]u8 = ---;
|
||||
eexp[0] = 34; eexp[1] = 92; eexp[2] = 8; eexp[3] = 9;
|
||||
eexp[4] = 10; eexp[5] = 12; eexp[6] = 13; eexp[7] = 1;
|
||||
report("st-esc", o.items[0].val.str == string.{ ptr = @eexp[0], len = 8 });
|
||||
report("st-zero", o.items[1].val.int_ == 0);
|
||||
report("st-neg", o.items[2].val.int_ == 0 - 7);
|
||||
report("st-min", o.items[3].val.int_ == 0 - 9223372036854775807 - 1);
|
||||
report("st-max", o.items[4].val.int_ == 9223372036854775807);
|
||||
report("st-bool", o.items[5].val.bool_ == true);
|
||||
is_null := if o.items[6].val == { case .null_: true; else: false; };
|
||||
report("st-null", is_null);
|
||||
xs := o.items[7].val.array;
|
||||
report("st-xs", xs.len == 3 and xs.items[0].int_ == 1 and
|
||||
xs.items[1].int_ == 0 - 2 and xs.items[2].int_ == 3);
|
||||
sub := o.items[8].val.object;
|
||||
report("st-nested", sub.len == 1 and sub.items[0].key == "k" and
|
||||
sub.items[0].val.str == "v");
|
||||
|
||||
// ── PART B. decode positives ─────────────────────────────────────────
|
||||
// `\/` decodes to a bare slash (the writer emits it unescaped, so this
|
||||
// is a parse-only form).
|
||||
slash := try parse("\"\\/\"", a);
|
||||
report("dec-slash", slash.str == "/");
|
||||
|
||||
// The full named-escape set in one string: \" \\ \/ \b \f \n \r \t.
|
||||
esc := try parse("\"\\\"\\\\\\/\\b\\f\\n\\r\\t\"", a);
|
||||
sexp : [8]u8 = ---;
|
||||
sexp[0] = 34; sexp[1] = 92; sexp[2] = 47; sexp[3] = 8;
|
||||
sexp[4] = 12; sexp[5] = 10; sexp[6] = 13; sexp[7] = 9;
|
||||
report("dec-escapes", esc.str == string.{ ptr = @sexp[0], len = 8 });
|
||||
|
||||
// \uXXXX: BMP 1-byte (A), BMP 2-byte (é), and a SURROGATE PAIR (😀).
|
||||
// "Aé😀" -> 41 | C3 A9 | F0 9F 98 80 (7 bytes).
|
||||
uni := try parse("\"\\u0041\\u00e9\\uD83D\\uDE00\"", a);
|
||||
uexp : [7]u8 = ---;
|
||||
uexp[0] = 0x41; uexp[1] = 0xC3; uexp[2] = 0xA9;
|
||||
uexp[3] = 0xF0; uexp[4] = 0x9F; uexp[5] = 0x98; uexp[6] = 0x80;
|
||||
report("dec-surrogate", uni.str == string.{ ptr = @uexp[0], len = 7 });
|
||||
|
||||
// POSITIVE counterpart to BadControlChar: the ESCAPED control forms
|
||||
// backslash-t, backslash-n and backslash-u-0009 decode to 09 0A 09.
|
||||
ectrl := try parse("\"\\t\\n\\u0009\"", a);
|
||||
cexp : [3]u8 = ---;
|
||||
cexp[0] = 9; cexp[1] = 10; cexp[2] = 9;
|
||||
report("dec-esc-ctrl", ectrl.str == string.{ ptr = @cexp[0], len = 3 });
|
||||
|
||||
// Raw multi-byte UTF-8 (>= 0x80) round-trips writer -> reader unchanged.
|
||||
ubytes : [*]u8 = xx a.alloc_bytes(7);
|
||||
ubytes[0] = 0x41; ubytes[1] = 0xC3; ubytes[2] = 0xA9;
|
||||
ubytes[3] = 0xF0; ubytes[4] = 0x9F; ubytes[5] = 0x98; ubytes[6] = 0x80;
|
||||
uval : Value = .str(string.{ ptr = ubytes, len = 7 });
|
||||
ubuf : [64]u8 = ---;
|
||||
un := try write_to_buffer(uval, string.{ ptr = @ubuf[0], len = 64 });
|
||||
uback := try parse(string.{ ptr = @ubuf[0], len = un }, a);
|
||||
report("rt-utf8", uback.str == string.{ ptr = @ubytes[0], len = 7 });
|
||||
|
||||
// ── PART C. malformed-input matrix — one assertion per variant + edge ─
|
||||
// UnexpectedToken: bad literal, non-string key, missing comma.
|
||||
report("err-token-literal", raises("xyz", error.UnexpectedToken, a));
|
||||
report("err-token-key", raises("{1:2}", error.UnexpectedToken, a));
|
||||
report("err-token-comma", raises("[1 2]", error.UnexpectedToken, a));
|
||||
|
||||
// UnexpectedEnd: truncated object / array / string.
|
||||
report("err-end-object", raises("{\"a\":", error.UnexpectedEnd, a));
|
||||
report("err-end-array", raises("[1,", error.UnexpectedEnd, a));
|
||||
report("err-end-string", raises("\"abc", error.UnexpectedEnd, a));
|
||||
|
||||
// BadEscape: unknown escape, non-hex \u, high surrogate not followed by
|
||||
// a low surrogate.
|
||||
report("err-esc-unknown", raises("\"a\\xb\"", error.BadEscape, a));
|
||||
report("err-esc-bad-hex", raises("\"\\uZZZZ\"", error.BadEscape, a));
|
||||
report("err-esc-surrogate", raises("\"\\uD83D\\u0041\"", error.BadEscape, a));
|
||||
|
||||
// BadNumber: leading zero, lone minus, fraction, exponent, and an
|
||||
// integer just past i64 MAX (overflow).
|
||||
report("err-num-leadzero", raises("01", error.BadNumber, a));
|
||||
report("err-num-lonedash", raises("-", error.BadNumber, a));
|
||||
report("err-num-fraction", raises("1.5", error.BadNumber, a));
|
||||
report("err-num-exponent", raises("1e9", error.BadNumber, a));
|
||||
report("err-num-overflow", raises("9223372036854775808", error.BadNumber, a));
|
||||
|
||||
// TrailingGarbage: junk after a complete value.
|
||||
report("err-trail-array", raises("[1,2] x", error.TrailingGarbage, a));
|
||||
report("err-trail-scalar", raises("null x", error.TrailingGarbage, a));
|
||||
|
||||
// BadControlChar: a raw control byte (< 0x20) inside a string.
|
||||
report("err-ctrl-tab", ctrl_raises(9, a)); // raw 0x09
|
||||
report("err-ctrl-lf", ctrl_raises(10, a)); // raw 0x0A
|
||||
report("err-ctrl-nul", ctrl_raises(0, a)); // raw 0x00
|
||||
|
||||
print("=== DONE ===\n");
|
||||
return;
|
||||
}
|
||||
31
examples/modules/0716-modules-cli-argv.sx
Normal file
31
examples/modules/0716-modules-cli-argv.sx
Normal file
@@ -0,0 +1,31 @@
|
||||
// Real OS-argv accessor from `modules/std/cli.sx` (extern _NSGetArgv).
|
||||
//
|
||||
// Only DETERMINISTIC structural invariants are asserted — the actual arg
|
||||
// contents depend on how the test is invoked (under `sx run` the process
|
||||
// argv is the interpreter's: ["sx", "run", "<this file>"]), so we never
|
||||
// pin exact strings:
|
||||
// - argc >= 1 (every process has argv[0])
|
||||
// - argv[0] is non-empty (the executable path)
|
||||
// - os_argc() agrees with the filled slice length (no truncation)
|
||||
//
|
||||
// `buf` is a stack `[64]string`; `os_args` fills it with zero-copy views
|
||||
// over the C runtime's argv block — no heap, no per-arg allocation.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/cli.sx";
|
||||
|
||||
main :: () {
|
||||
buf : [64]string = ---;
|
||||
args := os_args(buf[0..64]);
|
||||
|
||||
if args.len >= 1 { print("argc>=1: ok\n"); }
|
||||
else { print("argc>=1: FAIL ({})\n", args.len); }
|
||||
|
||||
if args.len >= 1 {
|
||||
if args[0].len > 0 { print("arg0-nonempty: ok\n"); }
|
||||
else { print("arg0-nonempty: FAIL\n"); }
|
||||
}
|
||||
|
||||
if os_argc() == args.len { print("argc-consistent: ok\n"); }
|
||||
else { print("argc-consistent: FAIL (os_argc={} len={})\n", os_argc(), args.len); }
|
||||
}
|
||||
219
examples/modules/0717-modules-cli-parse.sx
Normal file
219
examples/modules/0717-modules-cli-parse.sx
Normal file
@@ -0,0 +1,219 @@
|
||||
// CLI argument PARSER from `modules/std/cli.sx` (F3.2) — subcommand
|
||||
// dispatch + `--flag` parsing over an EXPLICIT logical argv (`[]string`).
|
||||
//
|
||||
// Every argv vector below is an explicit `[]string` literal (the caller's
|
||||
// logical args, program name already removed). The suite proves:
|
||||
//
|
||||
// 1. DISPATCH — `<group> <command>` selects the right command in the
|
||||
// caller's table; group/command are VIEWS into argv.
|
||||
// 2. FLAGS — `--out VALUE` (value-taking) binds a VIEW of the next
|
||||
// token; `--verbose` (boolean) records presence; the
|
||||
// reserved `--json` mode flag surfaces as `parsed.json`.
|
||||
// 3. SEPARATORS — `--` and the first bare operand both stop flag
|
||||
// parsing; the remainder is `parsed.rest` (operand VIEWS).
|
||||
// 4. HEAP — flag values / group / command / rest all point INSIDE
|
||||
// the input argv (zero copy); `Parsed` is a stack value.
|
||||
// 5. FAILURES — unknown command, unknown flag, missing required flag,
|
||||
// and a value-flag with no value each raise the specific
|
||||
// `CliError` variant on the error channel, and the
|
||||
// caller-owned `Diag` names the offending token.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/cli.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
// Half-open containment [lo, hi) — used to prove a view points into argv.
|
||||
in_range :: (x: i64, lo: i64, hi: i64) -> bool {
|
||||
return x >= lo and x < hi;
|
||||
}
|
||||
|
||||
// True when `parse(args, cmds)` raised exactly `want`. Destructure binds
|
||||
// the error tag without `try`, so a bad vector never aborts the example;
|
||||
// the failing token is captured in the caller-owned `Diag`.
|
||||
raises :: (args: []string, cmds: []Command, want: CliError) -> bool {
|
||||
d : Diag = .{};
|
||||
_, e := parse(args, cmds, @d);
|
||||
return e == want;
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
// ── Command table (caller storage; flag specs passed as views) ────
|
||||
publish_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "out", takes_value = true, required = true },
|
||||
FlagSpec.{ name = "verbose", takes_value = false, required = false },
|
||||
];
|
||||
status_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "verbose", takes_value = false, required = false },
|
||||
];
|
||||
cmds : []Command = .[
|
||||
Command.{ group = "ci", command = "publish", flags = publish_flags },
|
||||
Command.{ group = "ci", command = "status", flags = status_flags },
|
||||
];
|
||||
|
||||
// ── 1. Valid: <group> <command> --flag v --bool --json ───────────
|
||||
d : Diag = .{};
|
||||
argv : []string = .["ci", "publish", "--out", "dist", "--verbose", "--json"];
|
||||
p := try parse(argv, cmds, @d);
|
||||
|
||||
report("dispatch-group", p.group == "ci");
|
||||
report("dispatch-command", p.command == "publish");
|
||||
report("dispatch-index", p.cmd_index == 0);
|
||||
report("flag-value", p.value_of("out") == "dist");
|
||||
report("flag-value-set", p.is_set("out"));
|
||||
report("bool-set", p.is_set("verbose"));
|
||||
report("json-set", p.json);
|
||||
report("no-rest", p.rest.len == 0);
|
||||
|
||||
// ── 2. Heap discipline: flag value is a VIEW into argv ────────────
|
||||
// "dist" is argv[3]; its bytes must lie inside that very element.
|
||||
src : i64 = xx argv[3].ptr;
|
||||
stop := src + argv[3].len;
|
||||
pview : i64 = xx p.value_of("out").ptr;
|
||||
report("value-is-view", in_range(pview, src, stop) or pview == src);
|
||||
// group/command are argv[0]/argv[1] verbatim (same pointer, no copy).
|
||||
g0 : i64 = xx argv[0].ptr;
|
||||
gp : i64 = xx p.group.ptr;
|
||||
report("group-is-view", gp == g0);
|
||||
|
||||
// ── 3. Dispatch to a different command in the table ──────────────
|
||||
s_argv : []string = .["ci", "status", "--verbose"];
|
||||
sp := try parse(s_argv, cmds, @d);
|
||||
report("dispatch-2nd", sp.command == "status" and sp.cmd_index == 1);
|
||||
report("2nd-bool", sp.is_set("verbose"));
|
||||
report("2nd-json-unset", !sp.json);
|
||||
|
||||
// ── 4. `--` separator: rest are operand views, flags stop there ──
|
||||
sep_argv : []string = .["ci", "publish", "--out", "dist", "--", "--raw", "x"];
|
||||
spv := try parse(sep_argv, cmds, @d);
|
||||
report("sep-value", spv.value_of("out") == "dist");
|
||||
report("sep-rest-len", spv.rest.len == 2);
|
||||
report("sep-rest-0", spv.rest.len == 2 and spv.rest[0] == "--raw");
|
||||
report("sep-rest-1", spv.rest.len == 2 and spv.rest[1] == "x");
|
||||
report("sep-no-bool", !spv.is_set("verbose"));
|
||||
|
||||
// ── 5. First bare operand also stops flag parsing ────────────────
|
||||
bare_argv : []string = .["ci", "publish", "--out", "dist", "extra", "tail"];
|
||||
bpv := try parse(bare_argv, cmds, @d);
|
||||
report("bare-rest-len", bpv.rest.len == 2);
|
||||
report("bare-rest-0", bpv.rest.len == 2 and bpv.rest[0] == "extra");
|
||||
|
||||
// ── 6. Value-flag accepts a single-dash value (not a long flag) ──
|
||||
dash_argv : []string = .["ci", "publish", "--out", "-5", "--verbose"];
|
||||
dpv := try parse(dash_argv, cmds, @d);
|
||||
report("dash-value", dpv.value_of("out") == "-5" and dpv.is_set("verbose"));
|
||||
|
||||
// ── 7. Failures: each surfaces the specific variant ──────────────
|
||||
a_zero_args : []string = .[]; // nothing at all
|
||||
a_unknown_cmd : []string = .["ci", "deploy", "--out", "x"];
|
||||
a_unknown_group : []string = .["zz", "publish", "--out", "x"];
|
||||
a_too_few : []string = .["ci"];
|
||||
a_unknown_flag : []string = .["ci", "publish", "--out", "x", "--nope"];
|
||||
a_missing_value : []string = .["ci", "publish", "--out"];
|
||||
a_value_eats : []string = .["ci", "publish", "--out", "--verbose"];
|
||||
a_missing_req : []string = .["ci", "publish", "--verbose"];
|
||||
|
||||
// A command whose FlagSpec list exceeds the inline `Parsed.values` cap
|
||||
// (16): the parser rejects it with TooManyFlags rather than silently
|
||||
// truncating. 17 specs (> 16) trips the check right after dispatch
|
||||
// matches (group, command), before any flag is read.
|
||||
over_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "f00", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f01", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f02", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f03", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f04", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f05", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f06", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f07", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f08", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f09", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f10", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f11", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f12", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f13", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f14", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f15", takes_value = false, required = false },
|
||||
FlagSpec.{ name = "f16", takes_value = false, required = false },
|
||||
];
|
||||
over_cmds : []Command = .[ Command.{ group = "big", command = "cmd", flags = over_flags } ];
|
||||
over_args : []string = .["big", "cmd"];
|
||||
|
||||
report("err-zero-args", raises(a_zero_args, cmds, error.UnknownCommand));
|
||||
report("err-unknown-cmd", raises(a_unknown_cmd, cmds, error.UnknownCommand));
|
||||
report("err-unknown-group", raises(a_unknown_group, cmds, error.UnknownCommand));
|
||||
report("err-too-few", raises(a_too_few, cmds, error.UnknownCommand));
|
||||
report("err-unknown-flag", raises(a_unknown_flag, cmds, error.UnknownFlag));
|
||||
report("err-missing-value", raises(a_missing_value, cmds, error.MissingValue));
|
||||
report("err-value-eats-flag", raises(a_value_eats, cmds, error.MissingValue));
|
||||
report("err-missing-req", raises(a_missing_req, cmds, error.MissingRequired));
|
||||
report("err-too-many-flags", raises(over_args, over_cmds, error.TooManyFlags));
|
||||
|
||||
// ── 8. Diag pins the offending (token, index) for EVERY raise site ─
|
||||
// Each failure records the exact offending token (a VIEW into `args`,
|
||||
// except missing-required / too-many which name the spec's flag / the
|
||||
// command) plus its `args` index, so a caller can report which token
|
||||
// failed. This covers ALL SIX raise sites in cli.sx, both UnknownCommand
|
||||
// sub-branches included:
|
||||
// - zero-arg -> index -1, token "" (args.len == 0)
|
||||
// - too-few -> index 0, token args[0] (args.len == 1)
|
||||
// - unknown pair -> index 1, token command (group OR command wrong)
|
||||
// - too-many -> index -1, token command (spec count > 16 cap)
|
||||
// - unknown flag -> index i, token flag tok
|
||||
// - missing val -> index i, token flag tok
|
||||
// - missing req -> index -1, token flag name
|
||||
// The three index==-1 cases (zero-arg, too-many, missing-req) COINCIDE
|
||||
// with `Diag`'s `.{}` defaults (index -1, token ""), so those Diags are
|
||||
// seeded with a sentinel first: the assertion then proves `parse`
|
||||
// actually WROTE the value, not that it merely left the default.
|
||||
de : Diag = .{};
|
||||
_, ue := parse(a_unknown_flag, cmds, @de);
|
||||
report("diag-flag-tag", ue == error.UnknownFlag);
|
||||
report("diag-flag-token", de.token == "--nope" and de.index == 4);
|
||||
|
||||
dc : Diag = .{};
|
||||
_, ce := parse(a_unknown_cmd, cmds, @dc);
|
||||
report("diag-cmd-tag", ce == error.UnknownCommand);
|
||||
report("diag-cmd-token", dc.token == "deploy" and dc.index == 1);
|
||||
|
||||
dg : Diag = .{};
|
||||
_, ge := parse(a_unknown_group, cmds, @dg);
|
||||
report("diag-group-tag", ge == error.UnknownCommand);
|
||||
report("diag-group-token", dg.token == "publish" and dg.index == 1);
|
||||
|
||||
df : Diag = .{};
|
||||
_, fe := parse(a_too_few, cmds, @df);
|
||||
report("diag-too-few-tag", fe == error.UnknownCommand);
|
||||
report("diag-too-few-token", df.token == "ci" and df.index == 0);
|
||||
|
||||
d0 : Diag = .{ index = 999, token = "<unset>" }; // sentinel: -1/"" are defaults
|
||||
_, z0e := parse(a_zero_args, cmds, @d0);
|
||||
report("diag-zero-args-tag", z0e == error.UnknownCommand);
|
||||
report("diag-zero-args-token", d0.token == "" and d0.index == -1);
|
||||
|
||||
dv : Diag = .{};
|
||||
_, ve := parse(a_missing_value, cmds, @dv);
|
||||
report("diag-missing-value-tag", ve == error.MissingValue);
|
||||
report("diag-missing-value-token", dv.token == "--out" and dv.index == 2);
|
||||
|
||||
dz : Diag = .{};
|
||||
_, ze := parse(a_value_eats, cmds, @dz);
|
||||
report("diag-value-eats-tag", ze == error.MissingValue);
|
||||
report("diag-value-eats-token", dz.token == "--out" and dz.index == 2);
|
||||
|
||||
dm : Diag = .{ index = 999, token = "<unset>" }; // sentinel: -1 is the default
|
||||
_, me := parse(a_missing_req, cmds, @dm);
|
||||
report("diag-req-tag", me == error.MissingRequired);
|
||||
report("diag-req-token", dm.token == "out");
|
||||
report("diag-req-index", dm.index == -1);
|
||||
|
||||
dt : Diag = .{ index = 999, token = "<unset>" }; // sentinel: -1 is the default
|
||||
_, te := parse(over_args, over_cmds, @dt);
|
||||
report("diag-too-many-tag", te == error.TooManyFlags);
|
||||
report("diag-too-many-token", dt.token == "cmd" and dt.index == -1);
|
||||
|
||||
print("=== DONE ===\n");
|
||||
return;
|
||||
}
|
||||
69
examples/modules/0718-modules-cli-exit-json.sx
Normal file
69
examples/modules/0718-modules-cli-exit-json.sx
Normal file
@@ -0,0 +1,69 @@
|
||||
// std.cli EXIT-CODE + `--json` contract (F3.3 — FOUNDATION MILESTONE CLOSE).
|
||||
//
|
||||
// The minimal contract `dist` (and any sx CLI front-end) relies on:
|
||||
//
|
||||
// 1. NAMED EXIT CODES — `EX_OK` (0) and `EX_USAGE` (64, the sysexits.h
|
||||
// usage-error code) are public constants in `std.cli`.
|
||||
// 2. TERMINATORS — `exit_ok()` / `exit_usage()` end the process with the
|
||||
// matching code, routing through the canonical `process.exit(code: u8)`.
|
||||
// 3. `--json` MODE — the reserved global `--json` flag surfaces as
|
||||
// `parsed.json`: TRUE when `--json` is in the argv, FALSE when it is not.
|
||||
// In json mode stdout carries ONLY the machine result; human text goes
|
||||
// to stderr (here via `log.err`, which writes `ERROR: …` to fd 2).
|
||||
//
|
||||
// The run DELIBERATELY ends on the usage path: after the assertions it
|
||||
// triggers an `UnknownCommand`, writes the human diagnostic to STDERR, and
|
||||
// terminates via `exit_usage()` — so the process exits 64 (EX_USAGE),
|
||||
// captured in expected/0718-….exit. (sx `print`/`out` are unbuffered, so the
|
||||
// stdout assertion lines still appear despite the `_exit`.)
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/cli.sx";
|
||||
log :: #import "modules/std/log.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
publish_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "out", takes_value = true, required = true },
|
||||
];
|
||||
cmds : []Command = .[
|
||||
Command.{ group = "ci", command = "publish", flags = publish_flags },
|
||||
];
|
||||
|
||||
// ── 1. Named exit-code constants (the dist contract) ──────────────
|
||||
report("ex-ok-is-0", EX_OK == 0);
|
||||
report("ex-usage-is-64", EX_USAGE == 64);
|
||||
|
||||
// ── 2. `--json` detection — true with the flag, false without ─────
|
||||
d : Diag = .{};
|
||||
with_json : []string = .["ci", "publish", "--out", "dist", "--json"];
|
||||
without_json : []string = .["ci", "publish", "--out", "dist"];
|
||||
pj := try parse(with_json, cmds, @d);
|
||||
pn := try parse(without_json, cmds, @d);
|
||||
report("json-set-true", pj.json);
|
||||
report("json-set-false", !pn.json);
|
||||
|
||||
// ── 3. json mode keeps stdout machine-pure ────────────────────────
|
||||
// When `parsed.json`, the front-end emits ONLY the machine result on
|
||||
// stdout. `out` writes the bytes verbatim (no `{}` interpolation), so
|
||||
// the JSON braces are literal.
|
||||
if pj.json {
|
||||
out("{\"out\":\"");
|
||||
out(pj.value_of("out"));
|
||||
out("\"}\n");
|
||||
}
|
||||
|
||||
// ── 4. Usage error → human text to stderr → exit_usage() (= 64) ───
|
||||
// A bad command raises a `CliError`. The front-end maps every usage
|
||||
// error to `EX_USAGE`: it writes the human diagnostic to STDERR (stdout
|
||||
// stays machine-clean) and terminates with the usage code via the
|
||||
// canonical `process.exit`.
|
||||
bad : []string = .["ci", "deploy", "--out", "x"]; // unknown command
|
||||
_, e := parse(bad, cmds, @d);
|
||||
report("usage-error-raised", e == error.UnknownCommand);
|
||||
log.err("unknown command '{}' (argv index {})", d.token, d.index);
|
||||
exit_usage(); // -> _exit(EX_USAGE = 64)
|
||||
}
|
||||
65
examples/modules/0719-modules-cli-and-json.sx
Normal file
65
examples/modules/0719-modules-cli-and-json.sx
Normal file
@@ -0,0 +1,65 @@
|
||||
// Two modules that each export a top-level `parse` — `std.cli.parse`
|
||||
// (3-param subcommand dispatch) and `std.json.parse` (2-param document
|
||||
// reader) — imported into ONE program under DISTINCT namespaces, with
|
||||
// BOTH `parse`s actually called.
|
||||
//
|
||||
// Regression (issue 0100): same-name cross-module functions collided in
|
||||
// the bare-name function table during IR lowering. Lowering re-resolved a
|
||||
// call by SHORT name, so importing both modules and calling one bound the
|
||||
// wrong-arity same-named function and tripped `lazyLowerFunction`'s
|
||||
// param-count assert (panic). The fix resolves each `pkg.parse(...)` to a
|
||||
// UNIQUE module-qualified FuncId, so `cli.parse` and `json.parse` are
|
||||
// independent identities.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/mem.sx"; // `Allocator` is non-transitive: name it, import it.
|
||||
// `cli` is imported BOTH flat (so its types — `FlagSpec` / `Command` / `Diag` —
|
||||
// are bare-visible) AND namespaced (so the same-name `cli.parse` stays a
|
||||
// distinct qualified identity from `json.parse`). Post-E1 a bare reference to a
|
||||
// namespaced-ONLY type is a "not visible" error, so the flat import is what makes
|
||||
// the bare type names below resolve; `json` stays namespaced-only (its `Value`
|
||||
// reaches `main` only as `json.parse`'s return type, resolved in `json.sx`'s own
|
||||
// context).
|
||||
#import "modules/std/cli.sx";
|
||||
cli :: #import "modules/std/cli.sx";
|
||||
json :: #import "modules/std/json.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 8192);
|
||||
defer arena.deinit();
|
||||
|
||||
// ── cli.parse: dispatch <group> <command> + a value flag ─────────
|
||||
publish_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "out", takes_value = true, required = true },
|
||||
];
|
||||
cmds : []Command = .[
|
||||
Command.{ group = "ci", command = "publish", flags = publish_flags },
|
||||
Command.{ group = "ci", command = "status", flags = .[] },
|
||||
];
|
||||
argv : []string = .["ci", "publish", "--out", "dist"];
|
||||
d : Diag = .{};
|
||||
p := try cli.parse(argv, cmds, @d);
|
||||
report("cli-group", p.group == "ci");
|
||||
report("cli-command", p.command == "publish");
|
||||
report("cli-index", p.cmd_index == 0);
|
||||
report("cli-flag", p.value_of("out") == "dist");
|
||||
|
||||
// ── json.parse: read a small document into the value model ───────
|
||||
doc := "{\"name\":\"sx\",\"xs\":[1,2,3]}";
|
||||
root := try json.parse(doc, xx arena);
|
||||
o := root.object;
|
||||
report("json-members", o.len == 2);
|
||||
report("json-key0", o.items[0].key == "name");
|
||||
report("json-str", o.items[0].val.str == "sx");
|
||||
xs := o.items[1].val.array;
|
||||
report("json-arr-len", xs.len == 3);
|
||||
report("json-arr-2", xs.items[2].int_ == 3);
|
||||
|
||||
print("=== DONE ===\n");
|
||||
return;
|
||||
}
|
||||
26
examples/modules/0720-modules-qualified-own-import.sx
Normal file
26
examples/modules/0720-modules-qualified-own-import.sx
Normal file
@@ -0,0 +1,26 @@
|
||||
// Regression (issue 0100 F1): a QUALIFIED imported function that calls a
|
||||
// function from its OWN flat import.
|
||||
//
|
||||
// `calc :: #import …` registers `calc.compute` as a module-qualified alias
|
||||
// with a unique FuncId (the identity fix that resolves the cross-module
|
||||
// same-name `parse` collision, issue 0100 / example 0719). That alias is
|
||||
// lowered through `lazyLowerFunction`'s null-FuncId `lowerFunction` path,
|
||||
// which has no declared `Function.source_file` to restore. Before the fix it
|
||||
// lowered `calc.compute`'s body in the CALLER's (this file's) visibility
|
||||
// context, so `compute`'s calls to `triple` / `base` — visible only from
|
||||
// calc.sx's own `#import "util.sx"` — were rejected "not visible". The fix
|
||||
// carries the alias's declaring source so it lowers in calc.sx's context.
|
||||
|
||||
#import "modules/std.sx";
|
||||
calc :: #import "0720-modules-qualified-own-import/calc.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
// 14 * 3 = 42, computed by calc.compute -> triple(base()), both of which
|
||||
// live in calc.sx's own flat import.
|
||||
report("qualified-own-import", calc.compute() == 42);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// `calc` is pulled in under a QUALIFIED namespace by the consumer
|
||||
// (`calc :: #import …`), yet its own body calls `triple` / `base` from
|
||||
// calc.sx's OWN flat `#import "util.sx"`. The qualified alias `calc.compute`
|
||||
// must lower in calc.sx's source context so those own-import callees stay
|
||||
// visible — issue 0100 F1.
|
||||
#import "util.sx";
|
||||
|
||||
compute :: () -> i64 { return triple(base()); }
|
||||
@@ -0,0 +1,2 @@
|
||||
triple :: (x: i64) -> i64 { return x * 3; }
|
||||
base :: () -> i64 { return 14; }
|
||||
@@ -0,0 +1,31 @@
|
||||
// Regression (issue 0100 F2): lowering a QUALIFIED imported function whose
|
||||
// body terminates must leave the CALLER's lowering state untouched.
|
||||
//
|
||||
// `m :: #import …` registers `m.foo` as a module-qualified alias with a unique
|
||||
// FuncId (the identity fix, issue 0100 / example 0719) and lowers it through
|
||||
// `lazyLowerFunction`'s null-FuncId `lowerFunction` path. `foo`'s body folds
|
||||
// `if true { return helper(); }` to an unconditional return, so its lowering
|
||||
// ends with `block_terminated = true`. The null-FuncId path used to restore
|
||||
// every saved caller field EXCEPT `block_terminated`, so that flag leaked back
|
||||
// into `main`, and `main`'s own trailing `print` / `return 0` were treated as
|
||||
// dead-after-terminator — the compiler rejected `return 0` with "body produces
|
||||
// no value". The fix routes all exit paths through one save/restore defer, so
|
||||
// the qualified alias is transparent to the caller. (`helper` also lives in
|
||||
// m.sx's own flat import, exercising the F1 source-context restore too.)
|
||||
|
||||
#import "modules/std.sx";
|
||||
m :: #import "0721-modules-qualified-terminating-callee/m.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
// Qualified callee whose body terminates via a constant-folded `if true`.
|
||||
x := m.foo();
|
||||
// Caller statements AFTER the call must still be emitted (not dead).
|
||||
report("terminating-callee", x == 7);
|
||||
print("after\n");
|
||||
// The caller's OWN return — rejected pre-fix because block_terminated leaked.
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Lives in m.sx's OWN flat import — reachable from `foo` but not from the
|
||||
// top-level consumer that imports m.sx under a qualified namespace.
|
||||
helper :: () -> i64 { return 7; }
|
||||
@@ -0,0 +1,10 @@
|
||||
// `foo` is pulled in QUALIFIED by the consumer (`m :: #import …`). Its body
|
||||
// terminates via a constant-folded `if true { return … }`, and the `return`
|
||||
// calls `helper` from m.sx's OWN flat import. Lowering `foo` as a qualified
|
||||
// alias must be transparent to the caller's lowering state — issue 0100 F2.
|
||||
#import "helper.sx";
|
||||
|
||||
foo :: () -> i64 {
|
||||
if true { return helper(); }
|
||||
return 0;
|
||||
}
|
||||
18
examples/modules/0722-modules-flat-same-name-own.sx
Normal file
18
examples/modules/0722-modules-flat-same-name-own.sx
Normal file
@@ -0,0 +1,18 @@
|
||||
// fix-0102c (issue 0102): two flat FILE imports each author a same-name free
|
||||
// function `greet`. The first-wins import merge keeps exactly one `greet` in
|
||||
// the merged scope, but each module's OWN code must bind its OWN author when it
|
||||
// calls `greet` bare. `from_a` (in a.sx) returns 1; `from_b` (in b.sx) returns
|
||||
// 2 — per-source binding, resolved by identity, not first-wins.
|
||||
#import "modules/std.sx";
|
||||
#import "0722-modules-flat-same-name-own/a.sx";
|
||||
#import "0722-modules-flat-same-name-own/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("from_a binds a.greet", from_a() == 1);
|
||||
report("from_b binds b.greet", from_b() == 2);
|
||||
0
|
||||
}
|
||||
5
examples/modules/0722-modules-flat-same-name-own/a.sx
Normal file
5
examples/modules/0722-modules-flat-same-name-own/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `greet`. Its own `from_a` calls `greet` bare — under fix-0102c
|
||||
// that binds a.sx's OWN author (own-author wins), even though b.sx also
|
||||
// authors `greet` and the first-wins merge keeps only one in the merged scope.
|
||||
greet :: () -> i64 { return 1; }
|
||||
from_a :: () -> i64 { return greet(); }
|
||||
4
examples/modules/0722-modules-flat-same-name-own/b.sx
Normal file
4
examples/modules/0722-modules-flat-same-name-own/b.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
// b.sx authors its OWN `greet`. `from_b`'s bare `greet` must bind b.sx's
|
||||
// author (2), not the first-wins winner from a.sx.
|
||||
greet :: () -> i64 { return 2; }
|
||||
from_b :: () -> i64 { return greet(); }
|
||||
17
examples/modules/0723-modules-flat-vs-namespaced.sx
Normal file
17
examples/modules/0723-modules-flat-vs-namespaced.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// fix-0102c (issue 0102): one FLAT and one NAMESPACED author of `value`. The
|
||||
// bare call `value()` binds the FLAT author (10); the namespaced author is
|
||||
// reached only through `nm.value()` (20). A namespaced author must NOT make the
|
||||
// bare call ambiguous — only flat authors collide.
|
||||
#import "modules/std.sx";
|
||||
#import "0723-modules-flat-vs-namespaced/flat.sx";
|
||||
nm :: #import "0723-modules-flat-vs-namespaced/named.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("bare binds flat", value() == 10);
|
||||
report("nm.value binds named", nm.value() == 20);
|
||||
0
|
||||
}
|
||||
3
examples/modules/0723-modules-flat-vs-namespaced/flat.sx
Normal file
3
examples/modules/0723-modules-flat-vs-namespaced/flat.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Flat-imported author of `value`. A bare `value()` in the consumer binds THIS
|
||||
// one — the only bare (flat) author of the name.
|
||||
value :: () -> i64 { return 10; }
|
||||
@@ -0,0 +1,4 @@
|
||||
// Namespaced-imported author of `value`. Reachable only as `nm.value`; it never
|
||||
// enters the flat merge, so it neither shadows the flat author nor makes the
|
||||
// bare call ambiguous.
|
||||
value :: () -> i64 { return 20; }
|
||||
12
examples/modules/0724-modules-flat-same-name-ambiguous.sx
Normal file
12
examples/modules/0724-modules-flat-same-name-ambiguous.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// fix-0102c (issue 0102): a genuinely-ambiguous bare call. `main` flat-imports
|
||||
// two modules that each author `dup` and neither is `main`'s own — a bare
|
||||
// `dup()` can't pick one, so the compiler rejects it with a loud diagnostic
|
||||
// instead of silently first-wins-binding one. Qualify the call to disambiguate.
|
||||
#import "modules/std.sx";
|
||||
#import "0724-modules-flat-same-name-ambiguous/a.sx";
|
||||
#import "0724-modules-flat-same-name-ambiguous/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("{}\n", dup());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// One of two flat authors of `dup`. A consumer that flat-imports BOTH and calls
|
||||
// `dup` bare cannot pick between them.
|
||||
dup :: () -> i64 { return 1; }
|
||||
@@ -0,0 +1,2 @@
|
||||
// The second flat author of `dup`.
|
||||
dup :: () -> i64 { return 2; }
|
||||
17
examples/modules/0725-modules-flat-dir-same-name.sx
Normal file
17
examples/modules/0725-modules-flat-dir-same-name.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// fix-0102c (issue 0102): two flat DIRECTORY imports each author a same-name
|
||||
// `tag`. A directory flat-import exposes the directory's authored functions, so
|
||||
// `caller1`/`caller2` are visible here, and each binds its OWN directory's `tag`
|
||||
// when it calls bare — per-source binding across directory imports (100 / 200).
|
||||
#import "modules/std.sx";
|
||||
#import "0725-modules-flat-dir-same-name/d1";
|
||||
#import "0725-modules-flat-dir-same-name/d2";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("caller1 binds d1.tag", caller1() == 100);
|
||||
report("caller2 binds d2.tag", caller2() == 200);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// d1's author of `tag`. `caller1` (also in d1) binds d1's own `tag` (100).
|
||||
tag :: () -> i64 { return 100; }
|
||||
caller1 :: () -> i64 { return tag(); }
|
||||
@@ -0,0 +1,4 @@
|
||||
// d2's author of `tag`. `caller2` (also in d2) binds d2's own `tag` (200),
|
||||
// even though d1's `tag` is the first-wins merge winner.
|
||||
tag :: () -> i64 { return 200; }
|
||||
caller2 :: () -> i64 { return tag(); }
|
||||
23
examples/modules/0726-modules-flat-same-name-variadic.sx
Normal file
23
examples/modules/0726-modules-flat-same-name-variadic.sx
Normal file
@@ -0,0 +1,23 @@
|
||||
// fix-0102c F1 (issue 0102): two flat FILE imports each author same-name free
|
||||
// functions whose VARIADIC SHAPE differs. `combine` is fixed-arity in a.sx
|
||||
// (the first-wins winner) but variadic in b.sx (the shadow); `pick` is the
|
||||
// reverse. Each module's bare call must pack arguments against ITS OWN
|
||||
// author's signature — not the first-wins author's. Pre-fix the call path
|
||||
// re-fetched the first-wins AST by name to drive variadic packing, so b.sx's
|
||||
// variadic `combine` was packed as if fixed (and its fixed `pick` as if
|
||||
// variadic) → wrong lowering. Regression for the F1 review finding.
|
||||
#import "modules/std.sx";
|
||||
#import "0726-modules-flat-same-name-variadic/a.sx";
|
||||
#import "0726-modules-flat-same-name-variadic/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("from_a combine fixed", from_a_combine() == 30);
|
||||
report("from_b combine variadic", from_b_combine() == 10);
|
||||
report("from_a pick variadic", from_a_pick() == 6);
|
||||
report("from_b pick fixed", from_b_pick() == 5);
|
||||
0
|
||||
}
|
||||
11
examples/modules/0726-modules-flat-same-name-variadic/a.sx
Normal file
11
examples/modules/0726-modules-flat-same-name-variadic/a.sx
Normal file
@@ -0,0 +1,11 @@
|
||||
// a.sx is the first-wins winner for both names. `combine` is FIXED arity;
|
||||
// `pick` is VARIADIC. `from_a_*` call them bare — a authors the winner, so
|
||||
// they resolve through the existing path and pack against a's own shapes.
|
||||
combine :: (x: i64, y: i64) -> i64 { return x + y; }
|
||||
pick :: (..xs: []i64) -> i64 {
|
||||
result := 0;
|
||||
for xs (it) { result = result + it; }
|
||||
result
|
||||
}
|
||||
from_a_combine :: () -> i64 { return combine(10, 20); }
|
||||
from_a_pick :: () -> i64 { return pick(1, 2, 3); }
|
||||
12
examples/modules/0726-modules-flat-same-name-variadic/b.sx
Normal file
12
examples/modules/0726-modules-flat-same-name-variadic/b.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// b.sx is the SHADOW author for both names, with the OPPOSITE shapes:
|
||||
// `combine` is VARIADIC, `pick` is FIXED. Each `from_b_*` bare call must pack
|
||||
// against b's OWN author's signature (the F1 fix) — combine sums its variadic
|
||||
// pack, pick subtracts its two fixed args.
|
||||
combine :: (..xs: []i64) -> i64 {
|
||||
result := 0;
|
||||
for xs (it) { result = result + it; }
|
||||
result
|
||||
}
|
||||
pick :: (a: i64, b: i64) -> i64 { return b - a; }
|
||||
from_b_combine :: () -> i64 { return combine(1, 2, 3, 4); }
|
||||
from_b_pick :: () -> i64 { return pick(2, 7); }
|
||||
20
examples/modules/0727-modules-user-ns-m0.sx
Normal file
20
examples/modules/0727-modules-user-ns-m0.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// fix-0102c (issue 0102): a user namespace alias literally named `__m0`
|
||||
// coexists with flat same-name imports. fix-0102 resolves same-name authors by
|
||||
// FnDecl IDENTITY — there are no synthetic `__m0`-style names to collide with —
|
||||
// so a user namespace spelled `__m0` is just an ordinary namespace: `call_a`
|
||||
// binds a.ping (1), `call_b` binds b.ping (2), and `__m0.ping` reaches m.ping (99).
|
||||
#import "modules/std.sx";
|
||||
#import "0727-modules-user-ns-m0/a.sx";
|
||||
#import "0727-modules-user-ns-m0/b.sx";
|
||||
__m0 :: #import "0727-modules-user-ns-m0/m.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("call_a binds a.ping", call_a() == 1);
|
||||
report("call_b binds b.ping", call_b() == 2);
|
||||
report("__m0.ping binds m.ping", __m0.ping() == 99);
|
||||
0
|
||||
}
|
||||
3
examples/modules/0727-modules-user-ns-m0/a.sx
Normal file
3
examples/modules/0727-modules-user-ns-m0/a.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Flat author of `ping`; `call_a` binds a.sx's own `ping` (1).
|
||||
ping :: () -> i64 { return 1; }
|
||||
call_a :: () -> i64 { return ping(); }
|
||||
3
examples/modules/0727-modules-user-ns-m0/b.sx
Normal file
3
examples/modules/0727-modules-user-ns-m0/b.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Second flat author of `ping`; `call_b` binds b.sx's own `ping` (2).
|
||||
ping :: () -> i64 { return 2; }
|
||||
call_b :: () -> i64 { return ping(); }
|
||||
3
examples/modules/0727-modules-user-ns-m0/m.sx
Normal file
3
examples/modules/0727-modules-user-ns-m0/m.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Imported under a user namespace literally named `__m0`. Reached as
|
||||
// `__m0.ping` (99); coexists with the flat `ping` collision.
|
||||
ping :: () -> i64 { return 99; }
|
||||
22
examples/modules/0728-modules-flat-same-name-paramtype.sx
Normal file
22
examples/modules/0728-modules-flat-same-name-paramtype.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// fix-0102c F2 (issue 0102): two flat FILE imports each author a same-name free
|
||||
// function `apply` with a DIFFERENT parameter TYPE — a.sx takes a value
|
||||
// (`x: i64`), b.sx takes a pointer (`x: *i64`). The first-wins import merge
|
||||
// keeps a.sx's value-typed `apply`, but each module's bare call must type its
|
||||
// arguments against ITS OWN author. b.sx's `from_b` passes a local `v` to its
|
||||
// pointer-param `apply` via implicit address-of; before the fix the arg was
|
||||
// typed against the first-wins (value) winner, lowered as a value, then the
|
||||
// resolved pointer-param author was called with that value bit-cast to a
|
||||
// pointer — a segfault. Regression: per-source parameter target typing.
|
||||
#import "modules/std.sx";
|
||||
#import "0728-modules-flat-same-name-paramtype/a.sx";
|
||||
#import "0728-modules-flat-same-name-paramtype/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("from_a binds a.apply (value param)", from_a() == 11);
|
||||
report("from_b binds b.apply (pointer param)", from_b() == 42);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `apply` taking a VALUE. It is imported first, so it is the
|
||||
// first-wins merge winner. `from_a` calls `apply` bare on a value local — its
|
||||
// own author wins (own == winner → existing path, byte-for-byte unchanged).
|
||||
apply :: (x: i64) -> i64 { return x + 1; }
|
||||
from_a :: () -> i64 { v : i64 = 10; return apply(v); }
|
||||
@@ -0,0 +1,7 @@
|
||||
// b.sx authors its OWN `apply` taking a POINTER. `from_b` passes a value local
|
||||
// `v` bare; the pointer param must drive implicit address-of so the callee
|
||||
// mutates `v` in place (×2 → 42). Before the fix, `v` was typed against a.sx's
|
||||
// value-param winner, lowered as a value, then the resolved pointer-param
|
||||
// author was called with that value forced to a pointer (segfault).
|
||||
apply :: (x: *i64) { x.* = x.* * 2; }
|
||||
from_b :: () -> i64 { v : i64 = 21; apply(v); return v; }
|
||||
14
examples/modules/0729-modules-flat-same-name-extern.sx
Normal file
14
examples/modules/0729-modules-flat-same-name-extern.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
// fix-0102c (issue 0102) F3 regression: two flat FILE imports each `extern`
|
||||
// the SAME libc symbol under the SAME sx name `absval`. The bare-call resolver
|
||||
// must NOT count `extern` (non-plain) authors when deciding ambiguity — it
|
||||
// filters them out, returns "no rerouting", and the existing first-wins extern
|
||||
// dispatch binds the call. A same-name extern collision therefore compiles and
|
||||
// runs (master behavior), it does NOT error as ambiguous.
|
||||
#import "modules/std.sx";
|
||||
#import "0729-modules-flat-same-name-extern/a.sx";
|
||||
#import "0729-modules-flat-same-name-extern/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("absval = {}\n", absval(-7));
|
||||
0
|
||||
}
|
||||
5
examples/modules/0729-modules-flat-same-name-extern/a.sx
Normal file
5
examples/modules/0729-modules-flat-same-name-extern/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// One of two flat authors of `absval`, a `extern` libc binding. A consumer
|
||||
// flat-importing BOTH must NOT see this as an ambiguous bare-call collision —
|
||||
// extern authors are never rerouted by the bare-call resolver, so the call
|
||||
// falls to the existing first-wins extern dispatch.
|
||||
absval :: (n: i32) -> i32 extern libc "abs";
|
||||
2
examples/modules/0729-modules-flat-same-name-extern/b.sx
Normal file
2
examples/modules/0729-modules-flat-same-name-extern/b.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
// The second flat author of `absval` — the identical `extern` libc binding.
|
||||
absval :: (n: i32) -> i32 extern libc "abs";
|
||||
20
examples/modules/0730-modules-flat-same-name-default-arg.sx
Normal file
20
examples/modules/0730-modules-flat-same-name-default-arg.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// fix-0102d site 1 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `cfg` with a DIFFERENT default value for its trailing param —
|
||||
// a.sx defaults to 10, b.sx to 20. Each module calls `cfg()` bare with the arg
|
||||
// OMITTED. The omitted trailing arg must be filled from the RESOLVED author's
|
||||
// default (own-author wins), not the first-wins winner's. Before the fix,
|
||||
// `from_b`'s `cfg()` expanded to the winner a.sx's default (10) and returned 10.
|
||||
// Regression: per-source default-argument expansion.
|
||||
#import "modules/std.sx";
|
||||
#import "0730-modules-flat-same-name-default-arg/a.sx";
|
||||
#import "0730-modules-flat-same-name-default-arg/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("from_a binds a.cfg default (10)", from_a() == 10);
|
||||
report("from_b binds b.cfg default (20)", from_b() == 20);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `cfg` defaulting to 10. Imported first, so it is the first-wins
|
||||
// merge winner. `from_a` calls `cfg()` with the arg omitted — own == winner →
|
||||
// existing default-expansion path, byte-for-byte unchanged.
|
||||
cfg :: (n: i64 = 10) -> i64 { return n; }
|
||||
from_a :: () -> i64 { return cfg(); }
|
||||
@@ -0,0 +1,5 @@
|
||||
// b.sx authors its OWN `cfg` defaulting to 20. `from_b`'s `cfg()` omits the
|
||||
// arg; the omitted trailing default must come from b.sx's author (20), not the
|
||||
// first-wins winner from a.sx (10).
|
||||
cfg :: (n: i64 = 20) -> i64 { return n; }
|
||||
from_b :: () -> i64 { return cfg(); }
|
||||
22
examples/modules/0731-modules-flat-same-name-closure.sx
Normal file
22
examples/modules/0731-modules-flat-same-name-closure.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// fix-0102d site 2 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `pick` (a.sx returns 1, b.sx returns 2). Each module takes
|
||||
// `pick` as a function VALUE — both as `closure(pick)` and as a bare-name
|
||||
// fn-pointer binding (`g : () -> i64 = pick`). The captured FuncId must be the
|
||||
// RESOLVED author's (own-author wins), not the first-wins winner's. Before the
|
||||
// fix, b.sx's `closure(pick)` / `pick`-as-value both captured a.sx's winner
|
||||
// (1). Regression: per-source function-value conversion (closure + func_ref).
|
||||
#import "modules/std.sx";
|
||||
#import "0731-modules-flat-same-name-closure/a.sx";
|
||||
#import "0731-modules-flat-same-name-closure/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("from_a closure binds a.pick (1)", from_a_closure() == 1);
|
||||
report("from_b closure binds b.pick (2)", from_b_closure() == 2);
|
||||
report("from_a fn-value binds a.pick (1)", from_a_value() == 1);
|
||||
report("from_b fn-value binds b.pick (2)", from_b_value() == 2);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// a.sx authors `pick` returning 1. Imported first → first-wins winner.
|
||||
// `from_a_closure` / `from_a_value` take a.sx's own author (own == winner →
|
||||
// existing path, byte-for-byte unchanged).
|
||||
pick :: () -> i64 { return 1; }
|
||||
from_a_closure :: () -> i64 { f := closure(pick); return f(); }
|
||||
from_a_value :: () -> i64 { g : () -> i64 = pick; return g(); }
|
||||
@@ -0,0 +1,6 @@
|
||||
// b.sx authors its OWN `pick` returning 2. Taking `pick` as a value —
|
||||
// `closure(pick)` or `g : () -> i64 = pick` — must capture b.sx's author (2),
|
||||
// not the first-wins winner from a.sx (1).
|
||||
pick :: () -> i64 { return 2; }
|
||||
from_b_closure :: () -> i64 { f := closure(pick); return f(); }
|
||||
from_b_value :: () -> i64 { g : () -> i64 = pick; return g(); }
|
||||
19
examples/modules/0732-modules-flat-same-name-ufcs.sx
Normal file
19
examples/modules/0732-modules-flat-same-name-ufcs.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
// fix-0102d site 3 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `bump` (a.sx adds 1, b.sx adds 100). Each module dispatches it
|
||||
// via free-function UFCS — `v.bump()` lowers to `bump(v)`. The dispatched
|
||||
// author must be the RESOLVED one for the receiver's source (own-author wins),
|
||||
// not the first-wins winner. Before the fix, b.sx's `v.bump()` dispatched
|
||||
// a.sx's winner (+1 → 11). Regression: per-source free-function UFCS dispatch.
|
||||
#import "modules/std.sx";
|
||||
#import "0732-modules-flat-same-name-ufcs/a.sx";
|
||||
#import "0732-modules-flat-same-name-ufcs/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("from_a v.bump() binds a.bump (+1)", from_a_ufcs() == 11);
|
||||
report("from_b v.bump() binds b.bump (+100)", from_b_ufcs() == 110);
|
||||
0
|
||||
}
|
||||
5
examples/modules/0732-modules-flat-same-name-ufcs/a.sx
Normal file
5
examples/modules/0732-modules-flat-same-name-ufcs/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `bump` adding 1. Imported first → first-wins winner. `from_a`'s
|
||||
// `v.bump()` resolves a.sx's own author (own == winner → existing UFCS path,
|
||||
// byte-for-byte unchanged).
|
||||
bump :: ufcs (x: i64) -> i64 { return x + 1; }
|
||||
from_a_ufcs :: () -> i64 { v : i64 = 10; return v.bump(); }
|
||||
4
examples/modules/0732-modules-flat-same-name-ufcs/b.sx
Normal file
4
examples/modules/0732-modules-flat-same-name-ufcs/b.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
// b.sx authors its OWN `bump` adding 100. `from_b`'s `v.bump()` must dispatch
|
||||
// b.sx's author (+100 → 110), not the first-wins winner from a.sx (+1).
|
||||
bump :: ufcs (x: i64) -> i64 { return x + 100; }
|
||||
from_b_ufcs :: () -> i64 { v : i64 = 10; return v.bump(); }
|
||||
21
examples/modules/0733-modules-flat-same-name-comptime-run.sx
Normal file
21
examples/modules/0733-modules-flat-same-name-comptime-run.sx
Normal file
@@ -0,0 +1,21 @@
|
||||
// fix-0102d site 4 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `compute` (a.sx returns 7, b.sx returns 70) and each evaluates
|
||||
// it at comptime via `NAME :: #run compute();`. The #run body must resolve the
|
||||
// bare callee from ITS OWN module's source context (own-author wins), so a.sx's
|
||||
// const is 7 and b.sx's is 70. Before the fix, the #run body lowered with the
|
||||
// main file's source perspective, where `compute` is authored by two flat
|
||||
// imports and neither is main's own — so it was reported AMBIGUOUS and the
|
||||
// build failed. Regression: per-source comptime #run callee resolution.
|
||||
#import "modules/std.sx";
|
||||
#import "0733-modules-flat-same-name-comptime-run/a.sx";
|
||||
#import "0733-modules-flat-same-name-comptime-run/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
report("a.sx #run binds a.compute (7)", get_a() == 7);
|
||||
report("b.sx #run binds b.compute (70)", get_b() == 70);
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// a.sx authors `compute` returning 7 and evaluates it at comptime. Imported
|
||||
// first → first-wins winner; own == winner, but the #run must still lower in
|
||||
// a.sx's source context so the bare `compute` resolves at all (not ambiguous).
|
||||
compute :: () -> i64 { return 7; }
|
||||
A_VAL :: #run compute();
|
||||
get_a :: () -> i64 { return A_VAL; }
|
||||
@@ -0,0 +1,6 @@
|
||||
// b.sx authors its OWN `compute` returning 70. Its `#run compute()` must bind
|
||||
// b.sx's author (70) — own-author wins in b.sx's source context — not the
|
||||
// first-wins winner from a.sx (7).
|
||||
compute :: () -> i64 { return 70; }
|
||||
B_VAL :: #run compute();
|
||||
get_b :: () -> i64 { return B_VAL; }
|
||||
@@ -0,0 +1,16 @@
|
||||
// fix-0102d site 3 ambiguity (issue 0102): two flat FILE imports each author a
|
||||
// same-name free function `dup`, and the MAIN file (which authors neither)
|
||||
// dispatches it via free-function UFCS `v.dup()`. With two distinct flat
|
||||
// authors reachable and no own-author to prefer, the call is ambiguous — the
|
||||
// UFCS dispatch site must emit the loud "qualify the call" diagnostic rather
|
||||
// than silently binding the first-wins winner. Mirrors 0724 (the bare-call
|
||||
// ambiguity) one site over.
|
||||
#import "modules/std.sx";
|
||||
#import "0734-modules-flat-same-name-ufcs-ambiguous/a.sx";
|
||||
#import "0734-modules-flat-same-name-ufcs-ambiguous/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
v : i64 = 10;
|
||||
print("{}\n", v.dup());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// a.sx authors `dup` (+1). One of two distinct flat authors of `dup`.
|
||||
dup :: ufcs (x: i64) -> i64 { return x + 1; }
|
||||
@@ -0,0 +1,3 @@
|
||||
// b.sx authors its OWN `dup` (+2) — the second distinct flat author. Main
|
||||
// imports both and authors neither, so `v.dup()` from main is ambiguous.
|
||||
dup :: ufcs (x: i64) -> i64 { return x + 2; }
|
||||
@@ -0,0 +1,17 @@
|
||||
// fix-0102d site 2 / attempt-2 (issue 0102): the first-wins winner's body is
|
||||
// independently BROKEN (references an undefined symbol) and is never used. A
|
||||
// shadow author from a later flat import takes its OWN `pick` as a function
|
||||
// VALUE (`g : () -> i64 = pick`). The value must bind the shadow (own-author
|
||||
// wins) and the broken winner must NOT be lowered — a rerouted fn value never
|
||||
// uses the winner. Before the fix the fn-value site eagerly lazily-lowered the
|
||||
// name-keyed winner BEFORE the resolver rerouted, surfacing the winner's
|
||||
// `unresolved 'missing_from_a'` for a function the value never touches.
|
||||
// Regression: per-source function-value conversion must not pre-lower the winner.
|
||||
#import "modules/std.sx";
|
||||
#import "0735-modules-flat-same-name-fn-value-winner/a.sx";
|
||||
#import "0735-modules-flat-same-name-fn-value-winner/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("from_b_value = {}\n", from_b_value());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// a.sx authors `pick` (imported first → the first-wins name-keyed winner) but
|
||||
// its body references an undefined symbol, so lowering a.pick AT ALL is an
|
||||
// error. Nothing uses a.pick — taking b.pick as a value must not pre-lower it.
|
||||
pick :: () -> i64 { return missing_from_a(); }
|
||||
@@ -0,0 +1,7 @@
|
||||
// b.sx authors its OWN `pick` (returns 2) and takes it as a function VALUE. The
|
||||
// value binds b.pick (own-author wins), never the broken winner from a.sx.
|
||||
pick :: () -> i64 { return 2; }
|
||||
from_b_value :: () -> i64 {
|
||||
g : () -> i64 = pick;
|
||||
return g();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Bare-name visibility under a NAMESPACED-only import (regression, issue 0106).
|
||||
// `a.sx` is imported only as `m :: #import` — its top-level `secret` is reachable
|
||||
// ONLY as `m.secret`. A BARE `secret()` must error: bare-name visibility joins
|
||||
// over the FLAT import edges (`flat_import_graph`), and a namespaced alias is not
|
||||
// a flat edge. (Before the fix, `isNameVisible` walked `import_graph`, which also
|
||||
// records namespaced edges, so the bare call silently resolved.)
|
||||
m :: #import "0736-modules-namespaced-only-bare-not-visible/a.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
x := secret();
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
secret :: () -> i64 { 7 }
|
||||
15
examples/modules/0737-modules-insert-bare-not-visible.sx
Normal file
15
examples/modules/0737-modules-insert-bare-not-visible.sx
Normal file
@@ -0,0 +1,15 @@
|
||||
// A bare name inside a USER `#insert <expr>` is visibility-checked in the
|
||||
// USER's module, not skipped (regression, issue 0106). `a.sx` is imported only
|
||||
// as `m :: #import` — its top-level `secret` is reachable ONLY as `m.secret`.
|
||||
// A BARE `secret()` driving a `#insert` must error just like any other bare
|
||||
// reference into a namespaced-only import: `#insert` expansion does NOT exempt
|
||||
// user-typed code from visibility. (Library metaprograms like `std.print` keep
|
||||
// working because their bodies lower in their OWN module's context — see
|
||||
// `monomorphizePackFn` / `lowerComptimeCall` pinning `current_source_file` to
|
||||
// the body's defining module — not because `#insert` is exempt.)
|
||||
m :: #import "0737-modules-insert-bare-not-visible/a.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
#insert secret();
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
secret :: () -> string { "leaked := 1;" }
|
||||
24
examples/modules/0738-modules-comptime-arg-caller-context.sx
Normal file
24
examples/modules/0738-modules-comptime-arg-caller-context.sx
Normal file
@@ -0,0 +1,24 @@
|
||||
// A caller-owned helper passed as a comptime-ONLY `$`-arg to a NAMESPACED
|
||||
// imported metaprogram resolves in the CALLER's visibility context — not the
|
||||
// metaprogram's defining module (regression, issue 0106 follow-up).
|
||||
//
|
||||
// `emit` is reachable only as `m.emit`; the comptime arg `caller_name()` is
|
||||
// authored HERE in the caller. When `emit` splices that arg into its `#insert`
|
||||
// body and lowers it, the bare name `caller_name` must stay visible in the
|
||||
// caller's context. Before the fix, the body's defining-module pin also covered
|
||||
// the substituted caller arg, so `caller_name` was wrongly checked against
|
||||
// `emit.sx` and rejected as "not visible". The metaprogram's OWN code still
|
||||
// resolves in `emit.sx` (where `concat`/`print` are flat-imported), so this
|
||||
// stays compatible with the 0106 defining-context pin.
|
||||
//
|
||||
// Comptime-ONLY: `caller_name()` is evaluated at compile time and its value is
|
||||
// embedded as a literal in the generated `print(...)` statement — it is never
|
||||
// materialized at runtime (so this does NOT exercise issue 0107).
|
||||
m :: #import "0738-modules-comptime-arg-caller-context/emit.sx";
|
||||
|
||||
caller_name :: () -> string { return "world"; }
|
||||
|
||||
main :: () -> i32 {
|
||||
m.emit(caller_name());
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Library metaprogram: builds a `print(...)` statement at compile time from the
|
||||
// comptime `$who` value. `concat`/`print` are flat-imported here and resolve in
|
||||
// THIS module's context (the 0106 defining-module pin). The substituted caller
|
||||
// `$`-arg, by contrast, resolves in the CALLER's context.
|
||||
#import "modules/std.sx";
|
||||
|
||||
emit :: ($who: string) {
|
||||
#insert concat(concat("print(\"hello ", who), "\\n\");");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Caller-owned helpers passed as VARIADIC comptime-pack args (`..$args`) to a
|
||||
// NAMESPACED imported metaprogram resolve in the CALLER's visibility context —
|
||||
// not the metaprogram's defining module (regression, issue 0106 follow-up).
|
||||
//
|
||||
// `std.print :: ($fmt: string, ..$args)` is authored in `std.sx`; the pack args
|
||||
// `caller_num()` / `caller_two()` are authored HERE in the caller. The body's
|
||||
// typed `args[i]` substitution (via packArgNodeAt) lowers each pack arg under
|
||||
// the metaprogram's defining-module pin, so without stamping the pack-arg nodes
|
||||
// with the caller's source, the bare names `caller_num` / `caller_two` were
|
||||
// wrongly checked against `std.sx` and rejected as "not visible". The fixed
|
||||
// comptime param (`$fmt`) already got this treatment; this extends it to every
|
||||
// node in the variadic pack. The metaprogram's OWN code (build_format / out)
|
||||
// still resolves in `std.sx`, so the defining-context pin stays intact.
|
||||
//
|
||||
// Two pack positions lock that EVERY pack arg is stamped, not just the first.
|
||||
// i64 values only — accepted by print at runtime today (no 0107/0108 coupling).
|
||||
std :: #import "modules/std.sx";
|
||||
|
||||
caller_num :: () -> i64 { return 42; }
|
||||
caller_two :: () -> i64 { return 7; }
|
||||
|
||||
main :: () -> i32 {
|
||||
std.print("{} {}\n", caller_num(), caller_two());
|
||||
return 0;
|
||||
}
|
||||
17
examples/modules/0740-modules-flat-same-name-ufcs-typing.sx
Normal file
17
examples/modules/0740-modules-flat-same-name-ufcs-typing.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// Regression (issue 0102, Phase C): value-receiver free-function UFCS under a
|
||||
// flat same-name collision must be TYPED as the author lowering dispatches.
|
||||
// a.sx (imported first → first-wins winner) authors `tag -> string`; b.sx
|
||||
// authors its OWN `tag -> i64`. In b.sx, `v.tag()` dispatches b.tag (i64), but
|
||||
// before the fix the call PLAN typed it as a.tag (string, first-wins) — so the
|
||||
// pack-fn `print` boxed the raw i64 110 as a string pointer and dereferenced
|
||||
// 0x6e → segfault. `CallResolver.plan` now selects the SAME author the lowering
|
||||
// call-path binds, so plan-typing and dispatch can't disagree (fix-0102 F2).
|
||||
#import "modules/std.sx";
|
||||
#import "0740-modules-flat-same-name-ufcs-typing/a.sx";
|
||||
#import "0740-modules-flat-same-name-ufcs-typing/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
show_a(); // a-side: own == winner → string, byte-for-byte unchanged
|
||||
show_b(); // b-side: shadow author → i64, typed + dispatched as b.tag
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#import "modules/std.sx";
|
||||
// a.sx authors `tag` returning a string; imported first → first-wins winner.
|
||||
// `show_a`'s `v.tag()` is the caller's OWN author (own == winner → existing UFCS
|
||||
// path, byte-for-byte unchanged): typed AND dispatched as a.tag (string).
|
||||
tag :: ufcs (x: i64) -> string { return "a-string"; }
|
||||
show_a :: () { v : i64 = 10; print("a: v.tag() = {}\n", v.tag()); }
|
||||
@@ -0,0 +1,7 @@
|
||||
#import "modules/std.sx";
|
||||
// b.sx authors its OWN `tag` returning i64. `show_b`'s `v.tag()` must be both
|
||||
// dispatched AND typed as b.tag (i64 = 110), not the first-wins winner from a.sx
|
||||
// (string). `print` types each arg from the call plan, so a mistype here boxes
|
||||
// the i64 as a string pointer → segfault before the fix.
|
||||
tag :: ufcs (x: i64) -> i64 { return x + 100; }
|
||||
show_b :: () { v : i64 = 10; print("b: v.tag() = {}\n", v.tag()); }
|
||||
@@ -0,0 +1,18 @@
|
||||
// Regression (issue 0102, Phase C): a BARE call whose own author is a plain free
|
||||
// fn must DISPATCH to that author, not the first-wins winner — even when the
|
||||
// winner is a comptime PACK (`..$args`) of the same name. a.sx (imported first)
|
||||
// authors `f` as a pack → first-wins winner; b.sx authors its OWN plain `f`. In
|
||||
// b.sx, `f()` must reach b.f (returns 2). Before the fix, lowerCall's early
|
||||
// pack/comptime/generic dispatch keyed off the first-wins winner (a's pack) and
|
||||
// invoked it (returns 1) BEFORE consuming the selected author — so plan-selected
|
||||
// author and lowered+dispatched author disagreed. The early dispatch now reads
|
||||
// the SAME `SelectedFunc` the main dispatch binds (fix-0102 F2).
|
||||
#import "modules/std.sx";
|
||||
#import "0741-modules-flat-same-name-bare-pack-winner/a.sx";
|
||||
#import "0741-modules-flat-same-name-bare-pack-winner/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
show_a(); // a-side: own == winner (the pack) → returns 1, byte-for-byte unchanged
|
||||
show_b(); // b-side: selected own plain author → returns 2, not the pack winner
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#import "modules/std.sx";
|
||||
// a.sx authors `f` as a comptime pack; imported first → first-wins winner.
|
||||
// `show_a`'s bare `f()` is the caller's OWN author (own == winner → existing
|
||||
// pack path, byte-for-byte unchanged): dispatched as a.f (the pack → 1).
|
||||
f :: (..$args) -> i64 { return 1; }
|
||||
show_a :: () { print("a: f() = {}\n", f()); }
|
||||
@@ -0,0 +1,6 @@
|
||||
#import "modules/std.sx";
|
||||
// b.sx authors its OWN plain `f`. `show_b`'s bare `f()` must dispatch b.f (2),
|
||||
// not the first-wins pack winner from a.sx (1). The selector picks b.f; the
|
||||
// early pack/comptime dispatch must NOT hijack it with the winner.
|
||||
f :: () -> i64 { return 2; }
|
||||
show_b :: () { print("b: f() = {}\n", f()); }
|
||||
@@ -0,0 +1,15 @@
|
||||
// Bare module-const visibility under a NAMESPACED-only import — the const
|
||||
// sibling of 0736 (folded req #1 of the source-aware resolver, Phase E1).
|
||||
// `dep.sx` is imported only as `dep :: #import`, so its top-level `DEP_LEN`
|
||||
// is reachable ONLY as `dep.DEP_LEN`. A BARE `DEP_LEN` in a comptime
|
||||
// array-dimension position must NOT resolve: bare module-const visibility
|
||||
// joins over the FLAT import edges (`flat_import_graph`), and a namespaced
|
||||
// alias is not a flat edge. Before the fix the bare const leaked through the
|
||||
// global `module_const_map` first-match (and its float-fold fallback) straight
|
||||
// into the `[VAL]i32` dimension, so the array silently got length 3.
|
||||
dep :: #import "0742-modules-namespaced-only-bare-const-not-visible/dep.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
arr : [DEP_LEN]i32 = ---;
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
DEP_LEN :: 3;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Bare TYPE visibility under a NAMESPACED-only import — the struct sibling of
|
||||
// 0736 (bare call) and 0742 (bare const), and the core of the source-aware
|
||||
// nominal leaf (Phase E1). `dep.sx` is imported only as `dep :: #import`, so its
|
||||
// top-level `Secret` struct is reachable ONLY as `dep.Secret`. A BARE `Secret`
|
||||
// in a type position must NOT resolve: bare-TYPE visibility joins over the FLAT
|
||||
// import edges (`flat_import_graph`, transitively), and a namespaced alias is not
|
||||
// a flat edge. Before the fix the bare type leaked through the global
|
||||
// `findByName` first-match (the leaf returned it ahead of the visibility check),
|
||||
// so `s.x` compiled and ran. The qualified form `dep.Secret` stays the supported
|
||||
// spelling (its member resolution lands fully in Phase F).
|
||||
dep :: #import "0743-modules-namespaced-only-bare-type-not-visible/dep.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
s : Secret = .{ x = 5, y = 6 };
|
||||
s.x
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Secret :: struct {
|
||||
x: i32;
|
||||
y: i32;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Bare TYPE visibility under a NAMESPACED-only import — the ENUM sibling of 0743
|
||||
// (struct), covering the second registered nominal kind for the source-aware
|
||||
// nominal leaf (Phase E1). `dep.sx` is imported only as `dep :: #import`, so its
|
||||
// top-level `Color` enum is reachable ONLY as `dep.Color`. A BARE `Color` in a
|
||||
// type position must NOT resolve — bare-TYPE visibility joins over the FLAT
|
||||
// import edges, and a namespaced alias is not a flat edge. Before the fix the
|
||||
// bare enum leaked through the global `findByName` first-match.
|
||||
dep :: #import "0744-modules-namespaced-only-bare-enum-not-visible/dep.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
c : Color = .green;
|
||||
if c == .green { 0 } else { 9 }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Color :: enum {
|
||||
red;
|
||||
green;
|
||||
blue;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Type-author-aware bare-TYPE visibility gate (Phase E1, R1). `flatval.sx` is
|
||||
// flat-imported and authors a VALUE/FUNCTION `Secret`; `nstype.sx` is namespaced
|
||||
// (`nst :: #import`) and authors a TYPE `Secret`. A bare `Secret` in a type
|
||||
// position must NOT resolve: the only flat-visible `Secret` author is a FUNCTION,
|
||||
// and a same-name flat value does NOT make the namespaced-only TYPE bare-visible.
|
||||
// The leak this closes: a name-only gate would see the flat function and let the
|
||||
// global `findByName` first-match return the namespaced-only struct. The type is
|
||||
// reachable only as `nst.Secret`.
|
||||
#import "0745-modules-flat-value-shadows-ns-only-type/flatval.sx";
|
||||
nst :: #import "0745-modules-flat-value-shadows-ns-only-type/nstype.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
s : Secret = .{ x = 5, y = 6 };
|
||||
s.x
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// A flat-visible VALUE/FUNCTION named `Secret` (not a type).
|
||||
Secret :: () -> i32 { 0 }
|
||||
@@ -0,0 +1,4 @@
|
||||
Secret :: struct {
|
||||
x: i32;
|
||||
y: i32;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user