std: restructure — std/ modules, namespace tail, std/xml.sx
allocators/fs/process/socket/log/trace/test move under modules/std/ (allocators.sx becomes std/mem.sx; the Allocator protocol moves into the std.sx prelude, impls stay in mem.sx). New std/xml.sx holds xml_escape as xml.escape. std.sx gains the carried namespace tail — flat-importing std.sx now also provides mem./xml./log. — with the remaining modules (fs/process/socket/json/cli/hash/test) deferred from the tail until the global last-wins maps are fully own-wins (pulling them into every closure collides bare names corpus-wide; they stay direct imports: modules/std/fs.sx etc.). log.sx's internal emit renamed log_emit (it clobbered consumer fns named emit program-wide). bundle.sx uses xml.escape via the carried alias. Consumer import paths swept mechanically; .ir snapshots recaptured for the larger std closure. m3te + game build unchanged.
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/compiler.sx";
|
||||
proc :: #import "modules/process.sx";
|
||||
proc :: #import "modules/std/process.sx";
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
@@ -54,7 +54,7 @@ ns_get_argc :: () -> *s32 #foreign libc "_NSGetArgc";
|
||||
// value), `EX_UNAVAILABLE` when the platform is unsupported.
|
||||
// 2. TERMINATORS. `exit_ok()` / `exit_usage()` end the process with the
|
||||
// matching code. They route through the canonical
|
||||
// `process.exit(code: u8)` (modules/process.sx) — there is NO second
|
||||
// `process.exit(code: u8)` (modules/std/process.sx) — there is NO second
|
||||
// hand-rolled `_exit` binding in this module; the unsupported-platform
|
||||
// path below goes through `proc.exit(EX_UNAVAILABLE)` too.
|
||||
//
|
||||
@@ -62,7 +62,7 @@ ns_get_argc :: () -> *s32 #foreign libc "_NSGetArgc";
|
||||
// `parsed.json` (true iff `--json` appears in the argv — see `Parsed.json`).
|
||||
// The convention a front-end follows: in json mode stdout carries ONLY the
|
||||
// machine result, and human diagnostics go to stderr (e.g. via
|
||||
// `modules/log.sx`'s `log.err`). Detect json mode by reading `parsed.json`.
|
||||
// `modules/std/log.sx`'s `log.err`). Detect json mode by reading `parsed.json`.
|
||||
// =====================================================================
|
||||
|
||||
EX_OK :u8: 0; // success
|
||||
|
||||
268
library/modules/std/fs.sx
Normal file
268
library/modules/std/fs.sx
Normal file
@@ -0,0 +1,268 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
// =====================================================================
|
||||
// fs.sx — file system stdlib (POSIX backend, macOS values).
|
||||
//
|
||||
// Allocation contract: every returned `string` or slice is allocated
|
||||
// from `context.allocator`. Callers are responsible for releasing it
|
||||
// (typically via an arena reset).
|
||||
//
|
||||
// Handle ownership: `File` is a small value-typed handle wrapping the
|
||||
// POSIX file descriptor. Methods are provided for read/write/close;
|
||||
// the value is invalid (fd == -1) after `close()`.
|
||||
//
|
||||
// Scope (Phase 1A): file I/O + directory creation/deletion + path
|
||||
// helpers needed for `.app` bundling. Recursive walkers, `stat`, and
|
||||
// the full path module land in subsequent phases.
|
||||
// =====================================================================
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
// ── Low-level libc bindings ─────────────────────────────────────────
|
||||
// These declare the actual libc symbols and must use the libc names
|
||||
// verbatim (no prefix), so they live at module top-level. The public
|
||||
// API below wraps them. Users should not call these directly.
|
||||
//
|
||||
// macOS `open` is variadic in C (`int open(const char*, int, ...)`);
|
||||
// declared with `..args: []s32` so the mode is passed via the C
|
||||
// variadic tail. Without that, the mode arg goes to the wrong
|
||||
// register on arm64 and the file ends up with mode 0.
|
||||
|
||||
open :: (path: [:0]u8, flags: s32, ..args: []s32) -> s32 #foreign libc;
|
||||
close :: (fd: s32) -> s32 #foreign libc;
|
||||
read :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
lseek :: (fd: s32, offset: s64, whence: s32) -> s64 #foreign libc;
|
||||
unlink :: (path: [:0]u8) -> s32 #foreign libc;
|
||||
rmdir :: (path: [:0]u8) -> s32 #foreign libc;
|
||||
mkdir :: (path: [:0]u8, mode: u32) -> s32 #foreign libc;
|
||||
access :: (path: [:0]u8, mode: s32) -> s32 #foreign libc;
|
||||
chmod :: (path: [:0]u8, mode: u32) -> s32 #foreign libc;
|
||||
rename :: (oldp: [:0]u8, newp: [:0]u8) -> s32 #foreign libc;
|
||||
|
||||
// macOS POSIX constants. Linux values differ; split into platform-
|
||||
// conditional includes when we gain a Linux host.
|
||||
O_RDONLY :s32: 0x0000;
|
||||
O_WRONLY :s32: 0x0001;
|
||||
O_RDWR :s32: 0x0002;
|
||||
O_APPEND :s32: 0x0008;
|
||||
O_CREAT :s32: 0x0200;
|
||||
O_TRUNC :s32: 0x0400;
|
||||
|
||||
SEEK_SET :s32: 0;
|
||||
SEEK_CUR :s32: 1;
|
||||
SEEK_END :s32: 2;
|
||||
|
||||
F_OK :s32: 0;
|
||||
|
||||
// ── Public types ─────────────────────────────────────────────────────
|
||||
|
||||
OpenMode :: enum {
|
||||
read; // O_RDONLY
|
||||
write; // O_WRONLY | O_CREAT | O_TRUNC
|
||||
append; // O_WRONLY | O_CREAT | O_APPEND
|
||||
read_write; // O_RDWR
|
||||
}
|
||||
|
||||
SeekFrom :: enum { set; current; end; }
|
||||
|
||||
File :: struct {
|
||||
fd: s32 = -1;
|
||||
|
||||
is_valid :: (self: *File) -> bool { self.fd >= 0 }
|
||||
|
||||
close :: (self: *File) -> bool {
|
||||
if self.fd < 0 { return false; }
|
||||
rc := close(self.fd);
|
||||
self.fd = -1;
|
||||
rc == 0
|
||||
}
|
||||
|
||||
read :: (self: *File, buf: string) -> s64 {
|
||||
if self.fd < 0 { return -1; }
|
||||
n := read(self.fd, buf.ptr, xx buf.len);
|
||||
cast(s64) n
|
||||
}
|
||||
|
||||
write :: (self: *File, data: string) -> s64 {
|
||||
if self.fd < 0 { return -1; }
|
||||
n := write(self.fd, data.ptr, xx data.len);
|
||||
cast(s64) n
|
||||
}
|
||||
|
||||
seek :: (self: *File, offset: s64, whence: SeekFrom) -> s64 {
|
||||
if self.fd < 0 { return -1; }
|
||||
w := SEEK_SET;
|
||||
if whence == .current { w = SEEK_CUR; }
|
||||
if whence == .end { w = SEEK_END; }
|
||||
lseek(self.fd, offset, w)
|
||||
}
|
||||
}
|
||||
|
||||
// ── High-level file API ─────────────────────────────────────────────
|
||||
// Named `open_file` (not `open`) so they don't shadow libc's `open`
|
||||
// symbol; the latter is needed for `#foreign libc` to resolve. Same
|
||||
// idea for `delete_file`/`delete_dir` vs libc's `unlink`/`rmdir`,
|
||||
// `set_mode` vs libc's `chmod`, etc.
|
||||
|
||||
mode_to_flags :: (m: OpenMode) -> s32 {
|
||||
if m == .read { return O_RDONLY; }
|
||||
if m == .write { return O_WRONLY | O_CREAT | O_TRUNC; }
|
||||
if m == .append { return O_WRONLY | O_CREAT | O_APPEND; }
|
||||
if m == .read_write { return O_RDWR; }
|
||||
O_RDONLY
|
||||
}
|
||||
|
||||
open_file :: (path: [:0]u8, mode: OpenMode) -> ?File {
|
||||
fd := open(path, mode_to_flags(mode), 420); // 0o644 = 420
|
||||
if fd < 0 { return null; }
|
||||
File.{ fd = fd }
|
||||
}
|
||||
|
||||
// One-shot read: opens, slurps the whole file into a fresh buffer,
|
||||
// closes. Returns null on any failure. Uses libc directly (not File
|
||||
// methods) so it remains callable from the post-link IR interpreter,
|
||||
// which doesn't yet handle `*Self` method dispatch on locally-
|
||||
// unwrapped optionals.
|
||||
read_file :: (path: [:0]u8) -> ?string {
|
||||
fd := open(path, O_RDONLY, 0);
|
||||
if fd < 0 { return null; }
|
||||
size := lseek(fd, 0, SEEK_END);
|
||||
if size < 0 { close(fd); return null; }
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
buf := cstring(size);
|
||||
n := read(fd, buf.ptr, xx size);
|
||||
close(fd);
|
||||
if cast(s64) n != size { return null; }
|
||||
buf
|
||||
}
|
||||
|
||||
// One-shot write: creates / truncates and writes the whole buffer.
|
||||
write_file :: (path: [:0]u8, data: string) -> bool {
|
||||
fd := open(path, O_WRONLY | O_CREAT | O_TRUNC, 420); // 0o644
|
||||
if fd < 0 { return false; }
|
||||
n := write(fd, data.ptr, xx data.len);
|
||||
close(fd);
|
||||
cast(s64) n == cast(s64) data.len
|
||||
}
|
||||
|
||||
append_file :: (path: [:0]u8, data: string) -> bool {
|
||||
fd := open(path, O_WRONLY | O_CREAT | O_APPEND, 420);
|
||||
if fd < 0 { return false; }
|
||||
n := write(fd, data.ptr, xx data.len);
|
||||
close(fd);
|
||||
cast(s64) n == cast(s64) data.len
|
||||
}
|
||||
|
||||
// ── Single-syscall ops ───────────────────────────────────────────────
|
||||
|
||||
exists :: (path: [:0]u8) -> bool {
|
||||
access(path, F_OK) == 0
|
||||
}
|
||||
|
||||
delete_file :: (path: [:0]u8) -> bool {
|
||||
unlink(path) == 0
|
||||
}
|
||||
|
||||
delete_dir :: (path: [:0]u8) -> bool {
|
||||
rmdir(path) == 0
|
||||
}
|
||||
|
||||
create_dir :: (path: [:0]u8) -> bool {
|
||||
mkdir(path, 493) == 0 // 0o755 = 493
|
||||
}
|
||||
|
||||
set_mode :: (path: [:0]u8, mode: u32) -> bool {
|
||||
chmod(path, mode) == 0
|
||||
}
|
||||
|
||||
move :: (oldp: [:0]u8, newp: [:0]u8) -> bool {
|
||||
rename(oldp, newp) == 0
|
||||
}
|
||||
|
||||
// Recursive mkdir -p. Walks the path and creates each missing
|
||||
// segment. Treats existing directories as success.
|
||||
create_dir_all :: (path: [:0]u8) -> bool {
|
||||
if path.len == 0 { return true; }
|
||||
if exists(path) { return true; }
|
||||
last := path.len - 1;
|
||||
while last > 0 {
|
||||
if path[last] == 47 { break; }
|
||||
last -= 1;
|
||||
}
|
||||
if last > 0 {
|
||||
parent := cstring(last);
|
||||
memcpy(parent.ptr, path.ptr, last);
|
||||
if !create_dir_all(parent) { return false; }
|
||||
}
|
||||
create_dir(path)
|
||||
}
|
||||
|
||||
// Copy a file by streaming through a 64KB buffer. Uses libc directly
|
||||
// (not File methods) — same interpreter-compat reason as read_file.
|
||||
// No metadata is preserved beyond what `open` creates (mode 0644).
|
||||
// Caller is responsible for setting executable bits with `set_mode`.
|
||||
copy_file :: (src: [:0]u8, dst: [:0]u8) -> bool {
|
||||
src_fd := open(src, O_RDONLY, 0);
|
||||
if src_fd < 0 { return false; }
|
||||
dst_fd := open(dst, O_WRONLY | O_CREAT | O_TRUNC, 420);
|
||||
if dst_fd < 0 {
|
||||
close(src_fd);
|
||||
return false;
|
||||
}
|
||||
ok := true;
|
||||
buf := cstring(65536);
|
||||
loop := true;
|
||||
while loop {
|
||||
n := read(src_fd, buf.ptr, 65536);
|
||||
if n < 0 { ok = false; loop = false; }
|
||||
if n == 0 { loop = false; }
|
||||
if n > 0 {
|
||||
w := write(dst_fd, buf.ptr, xx n);
|
||||
if w != cast(isize) n { ok = false; loop = false; }
|
||||
}
|
||||
}
|
||||
close(src_fd);
|
||||
close(dst_fd);
|
||||
ok
|
||||
}
|
||||
|
||||
// ── Path helpers ─────────────────────────────────────────────────────
|
||||
// `path_join` is in std.sx (used widely beyond fs). These are the
|
||||
// fs-adjacent helpers — basename/dirname operate purely on text.
|
||||
|
||||
basename :: (p: string) -> string {
|
||||
if p.len == 0 { return ""; }
|
||||
last := p.len - 1;
|
||||
while last > 0 {
|
||||
if p[last] != 47 { break; }
|
||||
last -= 1;
|
||||
}
|
||||
end := last + 1;
|
||||
while last > 0 {
|
||||
if p[last - 1] == 47 { return substr(p, last, end - last); }
|
||||
last -= 1;
|
||||
}
|
||||
substr(p, 0, end)
|
||||
}
|
||||
|
||||
dirname :: (p: string) -> string {
|
||||
if p.len == 0 { return ""; }
|
||||
last := p.len - 1;
|
||||
while last > 0 {
|
||||
if p[last] != 47 { break; }
|
||||
last -= 1;
|
||||
}
|
||||
while last > 0 {
|
||||
if p[last] == 47 {
|
||||
while last > 0 {
|
||||
if p[last - 1] != 47 { break; }
|
||||
last -= 1;
|
||||
}
|
||||
return substr(p, 0, last);
|
||||
}
|
||||
last -= 1;
|
||||
}
|
||||
if p[0] == 47 { return "/"; }
|
||||
"."
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
// =====================================================================
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/fs.sx";
|
||||
#import "modules/std/fs.sx";
|
||||
|
||||
// Low 32 bits. SHA-256 is defined over 32-bit words; every add/rotate
|
||||
// result is masked back through this so the carry never escapes bit 31.
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
#import "modules/std.sx";
|
||||
// `Array`/`Object` methods take an explicit `alloc: Allocator`; bare-import
|
||||
// visibility is non-transitive, so the module that names the type imports it.
|
||||
#import "modules/allocators.sx";
|
||||
#import "modules/fs.sx";
|
||||
#import "modules/std/mem.sx";
|
||||
#import "modules/std/fs.sx";
|
||||
|
||||
// The writer's failure contract: a too-small caller buffer (Overflow) or
|
||||
// a short/failed file write (Io). Surfaced on the error channel — never a
|
||||
|
||||
29
library/modules/std/log.sx
Normal file
29
library/modules/std/log.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
// =====================================================================
|
||||
// log.sx — plain leveled logging (ERR step E4.1), orthogonal to the
|
||||
// error channel. Each entry is written to stderr as `LEVEL: <msg>\n`,
|
||||
// where <msg> is the formatted `fmt` + args (same `{}` interpolation as
|
||||
// `print`). Sink is stderr (fd 2) so log output stays out of a program's
|
||||
// stdout data stream.
|
||||
//
|
||||
// Note: PLAN-ERR §log sketches a `LEVEL ts msg` line with an ISO-8601
|
||||
// UTC timestamp. The timestamp is deferred — it needs a clock binding
|
||||
// and would make golden tests time-dependent; the level + message are
|
||||
// the load-bearing parts. Add `ts` once a pinnable clock lands.
|
||||
// =====================================================================
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
|
||||
// Prefix the level, append a newline, write the whole line to stderr.
|
||||
log_emit :: (level: string, msg: string) {
|
||||
line := concat(concat(level, msg), "\n");
|
||||
write(2, line.ptr, xx line.len);
|
||||
}
|
||||
|
||||
warn :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "log_emit(\"WARN: \", result);"; }
|
||||
info :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "log_emit(\"INFO: \", result);"; }
|
||||
debug :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "log_emit(\"DEBUG: \", result);"; }
|
||||
err :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "log_emit(\"ERROR: \", result);"; }
|
||||
243
library/modules/std/mem.sx
Normal file
243
library/modules/std/mem.sx
Normal file
@@ -0,0 +1,243 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
// --- CAllocator: stateless allocator that delegates directly to libc ---
|
||||
//
|
||||
// Zero-sized struct. Used as the default `context.allocator` at program
|
||||
// start (see `__sx_default_context` in the codegen). The thunks never
|
||||
// dereference `self`, so the protocol value's ctx field is `null`.
|
||||
//
|
||||
// Unlike GPA, no `init()` is needed — there's nothing to allocate.
|
||||
|
||||
CAllocator :: struct {}
|
||||
|
||||
impl Allocator for CAllocator {
|
||||
alloc :: (self: *CAllocator, size: s64) -> *void {
|
||||
return libc_malloc(size);
|
||||
}
|
||||
dealloc :: (self: *CAllocator, ptr: *void) {
|
||||
libc_free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// --- GPA: general purpose allocator (malloc/free wrapper) ---
|
||||
//
|
||||
// `init` returns the GPA by value. Caller binds it to a local; the
|
||||
// local IS the allocator state, no heap-side allocation for the
|
||||
// struct itself. `xx gpa` borrows the local under Option 3, so the
|
||||
// Allocator protocol value's `ctx` points at the local.
|
||||
//
|
||||
// Usage:
|
||||
// gpa := GPA.init(); // GPA
|
||||
// push Context.{ allocator = xx gpa, data = null } { ... }
|
||||
// print("alloc count: {}\n", gpa.alloc_count);
|
||||
|
||||
GPA :: struct {
|
||||
alloc_count: s64;
|
||||
|
||||
init :: () -> GPA {
|
||||
GPA.{ alloc_count = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Allocator for GPA {
|
||||
alloc :: (self: *GPA, size: s64) -> *void {
|
||||
self.alloc_count += 1;
|
||||
return libc_malloc(size);
|
||||
}
|
||||
dealloc :: (self: *GPA, ptr: *void) {
|
||||
self.alloc_count -= 1;
|
||||
libc_free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Arena: multi-chunk bump allocator ---
|
||||
//
|
||||
// `init` returns the Arena by value; the caller's local holds the
|
||||
// state, no heap-side allocation for the struct itself. The arena's
|
||||
// chunks ARE heap-allocated through the parent allocator, but those
|
||||
// are owned by `deinit` (or `reset` for the non-first ones).
|
||||
//
|
||||
// Usage:
|
||||
// gpa := GPA.init();
|
||||
// arena := Arena.init(xx gpa, 4096); // Arena
|
||||
// push Context.{ allocator = xx arena, data = null } { ... }
|
||||
// arena.reset(); // free all chunks except the first
|
||||
// arena.deinit(); // free every chunk
|
||||
|
||||
ArenaChunk :: struct {
|
||||
next: *ArenaChunk;
|
||||
cap: s64;
|
||||
}
|
||||
|
||||
Arena :: struct {
|
||||
first: *ArenaChunk;
|
||||
end_index: s64;
|
||||
parent: Allocator;
|
||||
|
||||
add_chunk :: (a: *Arena, min_size: s64) {
|
||||
prev_cap := if a.first != null then a.first.cap else 0;
|
||||
needed := min_size + 16 + 16;
|
||||
len := (prev_cap + needed) * 3 / 2;
|
||||
raw := a.parent.alloc(len);
|
||||
chunk : *ArenaChunk = xx raw;
|
||||
chunk.next = a.first;
|
||||
chunk.cap = len;
|
||||
a.first = chunk;
|
||||
a.end_index = 0;
|
||||
}
|
||||
|
||||
init :: (parent_alloc: Allocator, size: s64) -> Arena {
|
||||
self : Arena = .{ first = null, end_index = 0, parent = parent_alloc };
|
||||
self.add_chunk(size);
|
||||
self
|
||||
}
|
||||
|
||||
reset :: (a: *Arena) {
|
||||
if a.first != null {
|
||||
it := a.first.next;
|
||||
while it != null {
|
||||
next := it.next;
|
||||
a.parent.dealloc(it);
|
||||
it = next;
|
||||
}
|
||||
a.first.next = null;
|
||||
}
|
||||
a.end_index = 0;
|
||||
}
|
||||
|
||||
deinit :: (a: *Arena) {
|
||||
it := a.first;
|
||||
while it != null {
|
||||
next := it.next;
|
||||
a.parent.dealloc(it);
|
||||
it = next;
|
||||
}
|
||||
a.first = null;
|
||||
a.end_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Allocator for Arena {
|
||||
alloc :: (self: *Arena, size: s64) -> *void {
|
||||
aligned := (size + 7) & (0 - 8);
|
||||
if self.first != null {
|
||||
usable := self.first.cap - 16;
|
||||
if self.end_index + aligned <= usable {
|
||||
buf : [*]u8 = xx self.first;
|
||||
ptr := @buf[16 + self.end_index];
|
||||
self.end_index = self.end_index + aligned;
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
self.add_chunk(aligned);
|
||||
buf : [*]u8 = xx self.first;
|
||||
ptr := @buf[16 + self.end_index];
|
||||
self.end_index = self.end_index + aligned;
|
||||
ptr
|
||||
}
|
||||
dealloc :: (self: *Arena, ptr: *void) {}
|
||||
}
|
||||
|
||||
// --- BufAlloc: bump allocator backed by a user-provided slice ---
|
||||
//
|
||||
// Usage:
|
||||
// stack_buf : [128]u8 = ---;
|
||||
// buf := BufAlloc.init(@stack_buf[0], 128); // *BufAlloc
|
||||
// push Context.{ allocator = xx buf, data = null } { ... }
|
||||
// buf.reset();
|
||||
|
||||
BufAlloc :: struct {
|
||||
buf: [*]u8;
|
||||
len: s64;
|
||||
pos: s64;
|
||||
|
||||
init :: (buf: [*]u8, len: s64) -> *BufAlloc {
|
||||
self_size :: size_of(BufAlloc);
|
||||
if len < self_size { return null; }
|
||||
b : *BufAlloc = xx buf;
|
||||
b.buf = @buf[self_size];
|
||||
b.len = len - self_size;
|
||||
b.pos = 0;
|
||||
b
|
||||
}
|
||||
|
||||
reset :: (b: *BufAlloc) {
|
||||
b.pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Allocator for BufAlloc {
|
||||
alloc :: (self: *BufAlloc, size: s64) -> *void {
|
||||
aligned := (size + 7) & (0 - 8);
|
||||
if self.pos + aligned > self.len {
|
||||
return null;
|
||||
}
|
||||
ptr := @self.buf[self.pos];
|
||||
self.pos = self.pos + aligned;
|
||||
ptr
|
||||
}
|
||||
dealloc :: (self: *BufAlloc, ptr: *void) {}
|
||||
}
|
||||
|
||||
// --- TrackingAllocator: wraps any Allocator, counts allocs/deallocs ---
|
||||
//
|
||||
// Useful for catching leaks during development. Wraps a parent
|
||||
// Allocator; every call delegates to the parent while updating
|
||||
// counters. `report()` prints a summary; `leak_count()` returns
|
||||
// (alloc_count - dealloc_count).
|
||||
//
|
||||
// Manual opt-in pattern (compiler auto-wrap lands in Phase 5):
|
||||
//
|
||||
// tracker := TrackingAllocator.init(context.allocator); // TrackingAllocator
|
||||
// push Context.{ allocator = xx tracker, data = null } {
|
||||
// // ... user code allocates via tracker → delegates to the
|
||||
// // original context.allocator (libc-backed by default) ...
|
||||
// }
|
||||
// tracker.report();
|
||||
// if tracker.leak_count() != 0 { return 1; }
|
||||
//
|
||||
// Limitations under the current 2-method Allocator protocol:
|
||||
// dealloc(ptr) provides no size info, so bytes_outstanding /
|
||||
// peak_bytes cannot be tracked accurately. Only alloc count and
|
||||
// total bytes allocated are recorded. Phase 4's size-aware
|
||||
// dealloc(ptr, size, align) unlocks full byte tracking.
|
||||
|
||||
TrackingAllocator :: struct {
|
||||
parent: Allocator;
|
||||
alloc_count: s64;
|
||||
dealloc_count: s64;
|
||||
total_alloc_bytes: s64;
|
||||
|
||||
init :: (parent_alloc: Allocator) -> TrackingAllocator {
|
||||
TrackingAllocator.{
|
||||
parent = parent_alloc,
|
||||
alloc_count = 0,
|
||||
dealloc_count = 0,
|
||||
total_alloc_bytes = 0,
|
||||
}
|
||||
}
|
||||
|
||||
leak_count :: (t: *TrackingAllocator) -> s64 {
|
||||
t.alloc_count - t.dealloc_count
|
||||
}
|
||||
|
||||
report :: (t: *TrackingAllocator) {
|
||||
print("TrackingAllocator: allocs={} deallocs={} outstanding={} total_alloc_bytes={}\n",
|
||||
t.alloc_count, t.dealloc_count, t.leak_count(), t.total_alloc_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
impl Allocator for TrackingAllocator {
|
||||
alloc :: (self: *TrackingAllocator, size: s64) -> *void {
|
||||
ptr := self.parent.alloc(size);
|
||||
if ptr != null {
|
||||
self.alloc_count += 1;
|
||||
self.total_alloc_bytes += size;
|
||||
}
|
||||
ptr
|
||||
}
|
||||
dealloc :: (self: *TrackingAllocator, ptr: *void) {
|
||||
self.parent.dealloc(ptr);
|
||||
self.dealloc_count += 1;
|
||||
}
|
||||
}
|
||||
153
library/modules/std/process.sx
Normal file
153
library/modules/std/process.sx
Normal file
@@ -0,0 +1,153 @@
|
||||
#import "modules/std.sx";
|
||||
trace :: #import "modules/std/trace.sx";
|
||||
|
||||
// =====================================================================
|
||||
// process.sx — subprocess + environment stdlib (POSIX backend).
|
||||
//
|
||||
// Scope (Phase 1A): one entry point `run(cmd)` that shells out to
|
||||
// /bin/sh, captures stdout, returns exit code + stdout. Plus
|
||||
// `env(name)` / `find_executable(name)`. The bundler uses these to
|
||||
// invoke `codesign`, `plutil`, `security`, `aapt2`, `javac`, `d8`,
|
||||
// `keytool`, `apksigner`.
|
||||
//
|
||||
// Roadmap: phase 1B replaces `popen` with `posix_spawn` + pipes so
|
||||
// we can capture stderr separately and pass argv without shell
|
||||
// quoting. Until then, callers responsible for quoting + use 2>&1
|
||||
// to fold stderr into the captured stream.
|
||||
// =====================================================================
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
// ── Low-level libc bindings ─────────────────────────────────────────
|
||||
|
||||
popen :: (cmd: [:0]u8, mode: [:0]u8) -> *void #foreign libc;
|
||||
pclose :: (stream: *void) -> s32 #foreign libc;
|
||||
fread :: (ptr: [*]u8, size: usize, nmemb: usize, stream: *void) -> usize #foreign libc;
|
||||
feof :: (stream: *void) -> s32 #foreign libc;
|
||||
getenv :: (name: [:0]u8) -> *u8 #foreign libc;
|
||||
strlen :: (s: *u8) -> usize #foreign libc;
|
||||
system :: (cmd: [:0]u8) -> s32 #foreign libc;
|
||||
|
||||
// ── Public types ─────────────────────────────────────────────────────
|
||||
|
||||
ProcessResult :: struct {
|
||||
/// Exit code as reported by `WEXITSTATUS(status)`. 0 = success.
|
||||
/// Note: doesn't distinguish "killed by signal" from "exited
|
||||
/// non-zero"; phase 1B will return a tagged union.
|
||||
exit_code: s32;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────
|
||||
|
||||
// Run a shell command, capture stdout, wait for exit. Returns null if
|
||||
// the shell itself couldn't be spawned. A non-zero exit_code with
|
||||
// valid stdout means the command ran and exited non-zero — distinct
|
||||
// from spawn failure.
|
||||
//
|
||||
// `cmd` is interpreted by /bin/sh — callers MUST quote arguments
|
||||
// containing spaces or shell metacharacters. To capture stderr along
|
||||
// with stdout, append " 2>&1" to the command.
|
||||
run :: (cmd: [:0]u8) -> ?ProcessResult {
|
||||
f := popen(cmd, "r");
|
||||
if cast(s64) f == 0 { return null; }
|
||||
|
||||
out := "";
|
||||
buf := cstring(4096);
|
||||
loop := true;
|
||||
while loop {
|
||||
n := fread(buf.ptr, 1, 4096, f);
|
||||
if n == 0 { loop = false; }
|
||||
if n > 0 {
|
||||
chunk : string = ---;
|
||||
chunk.ptr = buf.ptr;
|
||||
chunk.len = cast(s64) n;
|
||||
out = concat(out, chunk);
|
||||
}
|
||||
}
|
||||
raw_status := pclose(f);
|
||||
if raw_status < 0 { return null; }
|
||||
// POSIX wait(2) status encoding: low byte = signal (if signaled),
|
||||
// next byte = exit code (if normally exited). For our MVP we just
|
||||
// surface the exit-code byte; the signal case is folded into the
|
||||
// non-zero return.
|
||||
exit_code := (raw_status >> 8) & 0xFF;
|
||||
if exit_code == 0 {
|
||||
if (raw_status & 0x7F) != 0 {
|
||||
// Killed by signal — surface as a non-zero exit.
|
||||
exit_code = 128 + (raw_status & 0x7F);
|
||||
}
|
||||
}
|
||||
ProcessResult.{ exit_code = exit_code, stdout = out }
|
||||
}
|
||||
|
||||
// Read an environment variable. Returns null if unset; an empty
|
||||
// string if set to "".
|
||||
env :: (name: [:0]u8) -> ?string {
|
||||
p := getenv(name);
|
||||
addr : s64 = xx p;
|
||||
if addr == 0 { return null; }
|
||||
n := strlen(p);
|
||||
if n == 0 { return ""; }
|
||||
buf := cstring(cast(s64) n);
|
||||
memcpy(buf.ptr, xx p, cast(s64) n);
|
||||
buf
|
||||
}
|
||||
|
||||
// Locate an executable by walking `$PATH`. Returns the absolute path
|
||||
// to the first hit, or null if not found anywhere. Uses `command -v`
|
||||
// under the shell; cheap and matches what the bundler ultimately
|
||||
// shells out to anyway.
|
||||
find_executable :: (name: [:0]u8) -> ?string {
|
||||
// Compose `command -v <name>` — name is assumed shell-safe
|
||||
// (executable names like `codesign`, `javac`, `aapt2`).
|
||||
cmd := concat("command -v ", name);
|
||||
// Need null-terminated for popen.
|
||||
cmd_z := cstring(cmd.len);
|
||||
memcpy(cmd_z.ptr, cmd.ptr, cmd.len);
|
||||
if r := run(cmd_z) {
|
||||
if r.exit_code != 0 { return null; }
|
||||
// Strip the trailing newline that `command -v` emits.
|
||||
out := r.stdout;
|
||||
if out.len > 0 {
|
||||
if out[out.len - 1] == 10 { out = substr(out, 0, out.len - 1); }
|
||||
}
|
||||
if out.len == 0 { return null; }
|
||||
return out;
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
// ── Process termination (ERR step E4.1) ───────────────────────────────
|
||||
|
||||
// Bound to POSIX `_exit(2)` (immediate termination — no atexit, no stdio
|
||||
// flush), NOT libc `exit(3)`. Two reasons: (1) it matches `process.exit`'s
|
||||
// "immediate stop, no cleanup" contract; (2) sx's `print` writes unbuffered
|
||||
// via `write(2)`, so skipping the stdio flush loses nothing. Binding the
|
||||
// symbol `"exit"` would also collide with this module's own `exit` function
|
||||
// at the link level.
|
||||
clib_exit :: (code: s32) -> noreturn #foreign libc "_exit";
|
||||
|
||||
// Stop the process immediately with exit code `code`. Does NOT unwind:
|
||||
// no `defer` / `onfail` cleanup runs, no error-trace frames are pushed —
|
||||
// it's the POSIX `_exit(2)` syscall. At comptime (`#run`) it terminates the
|
||||
// COMPILER with the same code after printing a diagnostic naming the call site
|
||||
// (`loc` defaults to `#caller_location`); in compiled code the `is_comptime()`
|
||||
// branch folds away to just the syscall.
|
||||
exit :: (code: u8, loc: Source_Location = #caller_location) -> noreturn {
|
||||
if is_comptime() {
|
||||
print("\nprocess.exit({}) called from {} at {}:{}\n", code, loc.func, loc.file, loc.line);
|
||||
trace.print_interpreter_frames();
|
||||
}
|
||||
clib_exit(xx code)
|
||||
}
|
||||
|
||||
// Abort with a message when `cond` is false. Prints `ASSERTION FAILED at
|
||||
// <file>:<line>: <msg>` (the caller's location, via `#caller_location`) then
|
||||
// exits 1; a true condition is a no-op.
|
||||
assert :: (cond: bool, msg: string, loc: Source_Location = #caller_location) {
|
||||
if !cond {
|
||||
print("ASSERTION FAILED at {}:{}: {}\n", loc.file, loc.line, msg);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
33
library/modules/std/socket.sx
Normal file
33
library/modules/std/socket.sx
Normal file
@@ -0,0 +1,33 @@
|
||||
// POSIX socket module (macOS only)
|
||||
// sockaddr_in layout and constants are platform-specific.
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
// POSIX socket API
|
||||
socket :: (domain: s32, kind: s32, protocol: s32) -> s32 #foreign libc;
|
||||
setsockopt :: (fd: s32, level: s32, optname: s32, optval: *s32, optlen: u32) -> s32 #foreign libc;
|
||||
bind :: (fd: s32, addr: *SockAddr, addrlen: u32) -> s32 #foreign libc;
|
||||
listen :: (fd: s32, backlog: s32) -> s32 #foreign libc;
|
||||
accept :: (fd: s32, addr: *SockAddr, addrlen: *u32) -> s32 #foreign libc;
|
||||
read :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
close :: (fd: s32) -> s32 #foreign libc;
|
||||
|
||||
// Constants (macOS)
|
||||
AF_INET :s32: 2;
|
||||
SOCK_STREAM :s32: 1;
|
||||
SOL_SOCKET :s32: 0xFFFF;
|
||||
SO_REUSEADDR :s32: 0x4;
|
||||
|
||||
// macOS sockaddr_in (16 bytes, has sin_len field)
|
||||
SockAddr :: struct {
|
||||
sin_len: u8;
|
||||
sin_family: u8;
|
||||
sin_port: u16;
|
||||
sin_addr: u32 = 0;
|
||||
sin_zero: u64 = 0;
|
||||
}
|
||||
|
||||
htons :: (port: s64) -> u16 {
|
||||
cast(u16) (((port & 0xFF) << 8) | ((port >> 8) & 0xFF))
|
||||
}
|
||||
7
library/modules/std/test.sx
Normal file
7
library/modules/std/test.sx
Normal file
@@ -0,0 +1,7 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
assert :: (condition: bool) {
|
||||
if !condition {
|
||||
out("assertion failed\n");
|
||||
}
|
||||
}
|
||||
98
library/modules/std/trace.sx
Normal file
98
library/modules/std/trace.sx
Normal file
@@ -0,0 +1,98 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
// =====================================================================
|
||||
// trace.sx — error return-trace formatting (ERR step E3.3).
|
||||
//
|
||||
// Reads the thread-local return-trace buffer (ERR E3.1, populated by the
|
||||
// push/clear wiring in E3.2) and renders it. A `raise` / propagating `try`
|
||||
// pushes a frame; an absorbing site (`catch` / `or value` / destructure)
|
||||
// clears the buffer. So at format time the buffer holds exactly the frames
|
||||
// of failures that escaped to where you're formatting — typically inside a
|
||||
// `catch` handler (the clear fires when the handler completes, so the body
|
||||
// still sees the chain) or the (future) failable-`main` wrapper.
|
||||
//
|
||||
// Frame resolution (ERR E3.0 slice 3a): in compiled code a frame is a pointer
|
||||
// to an interned `TraceFrame` the compiler stamped in at the push site, so the
|
||||
// location resolves in-process with no DWARF and no symbolizer. (The comptime
|
||||
// path — a packed `(func_id, ir_offset)` resolved via the interpreter's IR
|
||||
// tables — lands with slice 3b.)
|
||||
// =====================================================================
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
// The compiled return-trace frame. Named `TraceFrame` (not `Frame`) so it never
|
||||
// collides with a UI / geometry `Frame` a consumer flat-imports — same-name
|
||||
// types are now distinct nominal identities (issue 0105), so a bare `Frame` must
|
||||
// resolve unambiguously to the consumer's own. Layout MUST match
|
||||
// `getFrameStructType` in src/ir/emit_llvm.zig and `SxFrame` in
|
||||
// library/vendors/sx_trace_runtime/sx_trace.c.
|
||||
TraceFrame :: struct {
|
||||
file: string;
|
||||
line: s32;
|
||||
col: s32;
|
||||
func: string;
|
||||
line_text: string; // the source line, for the snippet + caret
|
||||
}
|
||||
|
||||
// `n` spaces — used to position the `^` caret under a column.
|
||||
spaces :: (n: s32) -> string {
|
||||
s := "";
|
||||
i : s32 = 0;
|
||||
while i < n {
|
||||
s = concat(s, " ");
|
||||
i = i + 1;
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// The error-trace buffer C API (library/vendors/sx_trace_runtime/sx_trace.c),
|
||||
// linked in for the JIT and auto-injected for AOT when traces are used.
|
||||
// `frame_at` returns the raw stored `u64`; `__trace_resolve_frame` turns it
|
||||
// into a `TraceFrame` — by reinterpreting the stamped `*TraceFrame` in compiled code, or
|
||||
// by resolving the packed `(func_id, span.start)` in the comptime interpreter.
|
||||
sx_trace_len :: () -> u32 #foreign;
|
||||
sx_trace_truncated :: () -> u32 #foreign;
|
||||
sx_trace_frame_at :: (i: u32) -> u64 #foreign;
|
||||
|
||||
write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
|
||||
// Render the current trace buffer to a string (allocated from
|
||||
// context.allocator). Empty buffer → "" so callers can cheaply skip output.
|
||||
to_string :: () -> string {
|
||||
n := sx_trace_len();
|
||||
if n == 0 { return ""; }
|
||||
|
||||
result := "error return trace (most recent call last):\n";
|
||||
if sx_trace_truncated() != 0 {
|
||||
result = concat(result, " ... older frames omitted (buffer full)\n");
|
||||
}
|
||||
|
||||
i : u32 = 0;
|
||||
while i < n {
|
||||
f := __trace_resolve_frame(sx_trace_frame_at(i));
|
||||
result = concat(result, format(" {} at {}:{}:{}\n", f.func, f.file, f.line, f.col));
|
||||
if f.line_text.len > 0 {
|
||||
result = concat(result, format(" {}\n", f.line_text));
|
||||
result = concat(result, concat(" ", concat(spaces(f.col - 1), "^\n")));
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Write the current trace to stderr (fd 2). No-op when the buffer is empty.
|
||||
print_current :: () {
|
||||
s := to_string();
|
||||
if s.len > 0 {
|
||||
write(2, s.ptr, xx s.len);
|
||||
}
|
||||
}
|
||||
|
||||
// Dump the comptime (`#run`) interpreter call-frame chain (ERR E4.1). At
|
||||
// comptime the interpreter walks its active sx frames and appends them to the
|
||||
// build output; in compiled code this folds to nothing (there is no
|
||||
// interpreter stack — the only caller is a dead `is_comptime()` branch).
|
||||
// Frame source locations await IR-offset resolution, so only names print today.
|
||||
print_interpreter_frames :: () {
|
||||
__interp_print_frames();
|
||||
}
|
||||
32
library/modules/std/xml.sx
Normal file
32
library/modules/std/xml.sx
Normal file
@@ -0,0 +1,32 @@
|
||||
// XML helpers. `escape` replaces XML special characters with entity
|
||||
// references — used when emitting Info.plist / AndroidManifest content
|
||||
// from sx values that may contain user-supplied text.
|
||||
#import "modules/std.sx";
|
||||
|
||||
escape :: (s: string) -> string {
|
||||
result := "";
|
||||
i := 0;
|
||||
seg_start := 0;
|
||||
while i < s.len {
|
||||
c := s[i];
|
||||
// 38='&', 60='<', 62='>', 34='"', 39='\''
|
||||
ent := "";
|
||||
if c == 38 { ent = "&"; }
|
||||
if c == 60 { ent = "<"; }
|
||||
if c == 62 { ent = ">"; }
|
||||
if c == 34 { ent = """; }
|
||||
if c == 39 { ent = "'"; }
|
||||
if ent.len > 0 {
|
||||
if i > seg_start {
|
||||
result = concat(result, substr(s, seg_start, i - seg_start));
|
||||
}
|
||||
result = concat(result, ent);
|
||||
seg_start = i + 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if seg_start < s.len {
|
||||
result = concat(result, substr(s, seg_start, s.len - seg_start));
|
||||
}
|
||||
result
|
||||
}
|
||||
Reference in New Issue
Block a user