From 06fb5aceeb65a75b1cd643debd667c52fdcafcb9 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sun, 8 Nov 2026 14:10:02 -0800 Subject: Better error messages Better error messages. Buffs to the std lib; getch() to fetch a single character; streaming I/O handles retries, and large sizes, with better error reporting. Formatted strings. Default int moved to i32. TicTacToe, the game. --- chapter30/src/main/smp/sys.smp | 2 + chapter30/src/main/smp/sys/ary.smp | 26 +++++-- chapter30/src/main/smp/sys/aryi64.smp | 25 ++++--- chapter30/src/main/smp/sys/aryu8.smp | 45 +++++------- chapter30/src/main/smp/sys/io.smp | 75 ++++++++++++++------ chapter30/src/main/smp/sys/libc.smp | 25 +++++-- chapter30/src/main/smp/sys/rt.smp | 3 + chapter30/src/main/smp/sys/rt/simple_args.smp | 22 ++++++ chapter30/src/main/smp/sys/text.smp | 98 +++++++++++++++++++++++++++ 9 files changed, 252 insertions(+), 69 deletions(-) create mode 100644 chapter30/src/main/smp/sys/rt.smp create mode 100644 chapter30/src/main/smp/sys/rt/simple_args.smp create mode 100644 chapter30/src/main/smp/sys/text.smp (limited to 'chapter30/src/main/smp') diff --git a/chapter30/src/main/smp/sys.smp b/chapter30/src/main/smp/sys.smp index 7705a187..1201315b 100644 --- a/chapter30/src/main/smp/sys.smp +++ b/chapter30/src/main/smp/sys.smp @@ -7,5 +7,7 @@ val aryi64 = aryi64; // Variable sized array of i64 val aryu8 = aryu8; // Variable sized array of u8 val char = char; // Character utilities val io = io; // IO library +val text = text; // Typed text conversions val libc = libc; // libc FFI bindings +val rt = rt; // Run Time support val scan = scan; // A simple string parser diff --git a/chapter30/src/main/smp/sys/ary.smp b/chapter30/src/main/smp/sys/ary.smp index 8312e94c..c139cd33 100644 --- a/chapter30/src/main/smp/sys/ary.smp +++ b/chapter30/src/main/smp/sys/ary.smp @@ -1,11 +1,25 @@ // Array utilities +// Geometric growth without wrapping a u32 length. Zero means too large. +val capacity = { u32 current, i64 needed -> + if( needed < 0 || needed > 4294967295 ) return 0; + if( needed <= current ) return current; + i64 size = current ? current : 1; + while( size < needed ) { + size *= 2; + if( size > 4294967295 ) size = 4294967295; + } + u32 result = size; + return result; +}; + // Copy byte array contents. Copies the shorter of the buffers and len -val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> +val copy = { u8[] dst, u32 doff, u8[~] src, u32 soff, u32 len -> + if( doff > dst# || soff > src# ) return dst; if( len > dst# - doff ) len = dst# - doff; if( len > src# - soff ) len = src# - soff; - for( int i=0; i // len. Returns <0 or >0 for first non-matching byte. Returns <0 or >0 for // the larger array after that, or 0 if both arrays are fully equal. val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len -> - u32 len1 = len > src1# - off1 ? src1# - off1 : len; - u32 len2 = len > src2# - off2 ? src2# - off2 : len; + u32 left1 = off1 >= src1# ? 0 : src1# - off1; + u32 left2 = off2 >= src2# ? 0 : src2# - off2; + u32 len1 = len > left1 ? left1 : len; + u32 len2 = len > left2 ? left2 : len; u32 max = len1 > len2 ? len2 : len1; - for( int i=0; i - if( len+len2 < buf# ) return self; - int newlen = buf# * 2; - while( len+len2 >= newlen ) newlen *= 2; + val _grow = { u32 len2 -> + if( len+len2 <= buf# ) return self; + u32 newlen = ary.capacity(buf#,len+len2); + if( !newlen ) libc._exit(1); // Length exceeds the array representation. i64[] !buf2 = new i64[newlen]; - for( int i=0; i _grow(1).buf[len++] = c; self; }; - val get = { int index -> return buf[index]; }; + val get = { u32 index -> return buf[index]; }; // Write the bracketed array-list form to aryu8. val str = { -> // TODO: clunky default constructor syntax var str = new aryu8().add('['); - for( int i=0; i + val swap = { u32 i, u32 j -> i64 tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; @@ -75,8 +75,11 @@ val parseAryI64 = { u8[~] str -> return s.x == s.buf# ? nums : null; }; -val copy = { i64[] dst, int doff, i64[~] src, int soff, int len -> - for( int i=0; i + if( doff > dst# || soff > src# ) return dst; + if( len > dst# - doff ) len = dst# - doff; + if( len > src# - soff ) len = src# - soff; + for( u32 i=0; i buf = new u8[1]; }; // Grow as needed - val _grow = { int len2 -> - if( len+len2 < buf# ) return self; - int newlen = buf# * 2; - while( len+len2 >= newlen ) newlen *= 2; + val _grow = { u32 len2 -> + if( len+len2 <= buf# ) return self; + u32 newlen = ary.capacity(buf#,len+len2); + if( !newlen ) libc._exit(1); // Length exceeds the array representation. u8[] !buf2 = new u8[newlen]; - ary.copy(buf2,0,buf,0,buf#); + ary.copy(buf2,0,buf,0,len); buf = buf2; self; }; @@ -27,38 +27,28 @@ struct aryu8 { // Add all of a plain array to the aryu8 val addAry = { u8[~] xs -> - _grow(xs#); - ary.copy(buf,len,xs,0,xs#); - len += xs#; + u32 n=xs#; + _grow(n); + ary.copy(buf,len,xs,0,n); + len += n; self; }; // Remove x bytes from the tail. - val undo = { int x -> + val undo = { u32 x -> len = len >= x ? len-x : 0; self; }; // Print ASCII number into aryu8 val num = { i64 x -> - if( !x ) { add('0'); return self; } - if( x < 0 ) { add('-'); x = -x; } - i64 ten = 1, log = 0; - while( ten <= x ) { ten *= 10; log++; } - for( int i=0; i - i64 ptr = buf; // Cast buffer pointer to i64 for native C call - libc.write(fd,ptr,len); + io.writeOff(fd,buf,0,len); }; }; @@ -66,7 +56,7 @@ struct aryu8 { // Parse one integer, ignore trailing data; returns 0 with an empty string. // If you want a more robust parsing, use the `scan` class val atoi = { u8[~] str -> - int x = 0; + u32 x = 0; while( x < str# && str[x] <= ' ' ) x++; if( x == str# ) return 0; bool neg = false; @@ -77,11 +67,12 @@ val atoi = { u8[~] str -> return neg ? -n : n; }; -// Zero-terminate string for libc strings. No error checking, just copies and -// appends a zero. +// Zero-terminate a string for libc, reserving one array element for the zero. { u8[~] -> u8[] } str0 = { u8[~] str -> + if( str# == 4294967295 ) libc._exit(1); u8[] !strz = new u8[str#+1]; - ary.copy(strz,0,str,0,str#); + u32 len=str#; + ary.copy(strz,0,str,0,len); strz[str#] = '\0'; strz; }; diff --git a/chapter30/src/main/smp/sys/io.smp b/chapter30/src/main/smp/sys/io.smp index d6263ffe..14993429 100644 --- a/chapter30/src/main/smp/sys/io.smp +++ b/chapter30/src/main/smp/sys/io.smp @@ -4,43 +4,74 @@ // Write byte array to stdout val p = { u8[~] str -> write(1,str); }; val p_noInline = { u8[~] str -> write(1,str); }; +// Write to stderr +val err = { u8[~] str -> write(libc.STDERR,str); }; -// Return fd contents as a u8[]. + +// Read until EOF, including after short reads. Return an empty array for an +// empty stream, or null on error with errno preserved (partial data discarded). val read = { i32 fd -> - var buf = new u8[8*1024]; - i64 ptr = buf; - int len = 0; + val chunk = new u8[8*1024]; + i64 ptr = chunk; + val buf = new aryu8(); while( true ) { - i32 rez = libc.read(fd,ptr,buf#); - if( rez > buf# ) libc._exit(libc.errno()); - len += rez; - if( rez < buf# ) - break; - libc._exit(-2); // Need to implement for more than 8K input + i64 rez = libc.read(fd,ptr,chunk#); + if( rez < 0 ) { + if( libc.errno() == libc.EINTR ) continue; + return null; + } + if( rez == 0 ) break; + if( buf.len+rez > 4294967295 ) { + libc.set_errno(libc.EOVERFLOW); + return null; + } + u32 count = rez; + buf._grow(count); + ary.copy(buf.buf,buf.len,chunk,0,count); + buf.len += count; } - var out = new u8[len]; - ary.copy(out,0,buf,0,len); + val out = new u8[buf.len]; + return ary.copy(out,0,buf.buf,0,buf.len); }; -// Write all of str to fd. +// Write all bytes, returning the count or -1 with errno set. An error can +// follow a successfully written prefix; it does not undo that prefix. val write = { i32 fd, u8[~] str -> - i64 ptr = str; // Convert array pointer to a i64 for the native call - libc.write(fd,ptr,str#); + writeOff(fd,str,0,str#); }; -// Write all of str to fd. +// Write a slice, capping len at the remaining bytes. An offset past the end +// is invalid, including for an empty write. A valid empty slice returns zero. +// Invalid arguments return -1; val writeOff = { i32 fd, u8[~] str, u32 off, u32 len -> - i64 ptr = str; // Convert array pointer to a i64 for the native call - ptr += off; // Skip offset - // Cap at available length - if( len > str# - off ) len = str# - off; - // Do the write - libc.write(fd,ptr,len); + if( off > str# ) return libc.set_errno(libc.EINVAL); + if( len > str# - off ) len = str# - off; + i64 ptr = str; + ptr += off; + i64 done = 0; + while( done < len ) { + i64 rez = libc.write(fd,ptr+done,len-done); + if( rez < 0 ) { + if( libc.errno() == libc.EINTR ) continue; + return -1; + } + if( rez == 0 ) { + libc.set_errno(libc.EIO); + return -1; + } + done += rez; + } + return done; }; // Return stdin as a u8[]. val stdin = { -> read(0); }; +// One byte, without Enter or echo. Restores the previous terminal mode before +// returning. Files/pipes also work. Returns 0..255, or -1 with libc.errno() set +// to libc.EOF or an OS error. Ctrl-C is byte 3; escape/UTF-8 sequences are multiple bytes. +val getch = { -> libc.simple_getch(); }; + // Create file for writing. Return fd, or -1 and errno has the error code val create = { u8[~] str -> i64 ptr = sys.aryu8.str0(str); diff --git a/chapter30/src/main/smp/sys/libc.smp b/chapter30/src/main/smp/sys/libc.smp index 17ee65cb..7122bbd8 100644 --- a/chapter30/src/main/smp/sys/libc.smp +++ b/chapter30/src/main/smp/sys/libc.smp @@ -2,17 +2,17 @@ // libc bindings: // https://www.man7.org/linux/man-pages/man2/.2.html -// The buffers are int-versions of zero-terminated strings. -// int buf = aryu8.str0("some_file"); +// The buffers are i64 addresses of zero-terminated strings. +// i64 buf = aryu8.str0("some_file"); // buf mode perm-> fd { i64 i32 i32 -> i32 } open = "C"; // buf perm -> fd { i64 i32 -> i32 } creat = "C"; // fd buf len -> len -{ i32 i64 u32 -> i32 } write = "C"; +{ i32 i64 i64 -> i64 } write = "C"; // fd buf len -> len -{ i32 i64 u32 -> i32 } read = "C"; +{ i32 i64 i64 -> i64 } read = "C"; // fd -> err { i32 -> i32 } close = "C"; // path -> err @@ -27,12 +27,29 @@ i32 O_TRUNC = 1024; // 04000 on x86_64-pc-cygwin i32 STDIN = 0; i32 STDOUT = 1; i32 STDERR = 2; +i32 EOF = -1; // simple_getch sets errno to EOF on end-of-input. // status { i32 -> i32 } _exit = "C"; + // errno is not an ordinary C data symbol; read it through the native runtime. { i32 } simple_errno = "C"; val errno = { -> simple_errno(); }; +{ i32 -> i32 } simple_set_errno = "C"; +// Set errno and return -1, matching the native I/O error convention. +val set_errno = { i32 error -> simple_set_errno(error); }; + +i32 EINTR = "C"; +i32 EIO = "C"; +i32 EINVAL = "C"; +i32 EOVERFLOW = "C"; + +// Read one byte from stdin, without or echo. Restores the previous +// terminal mode before returning. Files and pipes also work. Returns 0..255, +// or -1 with errno set to EOF or an OS error. Ctrl-C is byte 3; escape/UTF-8 +// sequences are multiple bytes. +{ i32 } simple_getch = "C"; + // addr len prot flags fd off -> void* { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; // mmap flags diff --git a/chapter30/src/main/smp/sys/rt.smp b/chapter30/src/main/smp/sys/rt.smp new file mode 100644 index 00000000..d62b4def --- /dev/null +++ b/chapter30/src/main/smp/sys/rt.smp @@ -0,0 +1,3 @@ +// Run Time support: "rt" has the same meaning as Java's rt.jar or C's crt.o. +// Native entry helpers are compiled and linked independently of ordinary sys +// imports, so programs without library imports can use the same startup code. diff --git a/chapter30/src/main/smp/sys/rt/simple_args.smp b/chapter30/src/main/smp/sys/rt/simple_args.smp new file mode 100644 index 00000000..3f630e54 --- /dev/null +++ b/chapter30/src/main/smp/sys/rt/simple_args.smp @@ -0,0 +1,22 @@ +// Native startup support, compiled independently and linked with the CRT. +// Raw native addresses stay i64; only the arrays allocated here are Simple objects. +{ i64 i64 -> i64 } simple_argv = "C"; +{ i64 -> i64 } strlen = "C"; +{ i64 i64 i64 -> i64 } memcpy = "C"; + +val simple_args = { i64 argc, i64 argv -> + if( argc < 0 || argc > 4294967295 ) return null; + // Zero-filled reference arrays require nullable elements. Every slot is + // filled before returning; the native entry bridge publishes u8[~][]. + val args = new u8[~]?[argc]; + for( i64 i=0; i 4294967295 ) return null; + val bytes = new u8[len]; + i64 dst = bytes; + memcpy(dst,src,len); + args[i] = bytes; + } + return args; +}; diff --git a/chapter30/src/main/smp/sys/text.smp b/chapter30/src/main/smp/sys/text.smp new file mode 100644 index 00000000..a414ff45 --- /dev/null +++ b/chapter30/src/main/smp/sys/text.smp @@ -0,0 +1,98 @@ +// Formatting primitives used directly by interpolation and named converters. +// Keep this module free of interpolation: sys can build itself from source. +// spec's low byte is the format kind; bits 8..12 are flags: - + space # 0. + +val begin = { -> new aryu8(); }; + +val end = { aryu8 b -> + val bytes = new u8[b.len]; + ary.copy(bytes,0,b.buf,0,b.len); + u8[~] result = bytes; + result; +}; + +val _pad = { !aryu8 b, i64 n, u8 c -> + for( i64 i=0; i + i64 len=s#; + if( precision>=0 && precision + i64 kind=spec&255; + if( kind=='b' ) return string(b,value ? "true" : "false",spec,width,-1); + if( kind=='c' ) { + if( !(spec&256) ) _pad(b,width-1,' '); + u8 c=value; + b.add(c); + if( spec&256 ) _pad(b,width-1,' '); + return b; + } + bool signed=kind==0 || kind=='d' || kind=='i'; + i64 base=kind=='o' ? 8 : (kind=='x' || kind=='X' ? 16 : 10); + u8[~] alphabet=kind=='X' ? "0123456789ABCDEF" : "0123456789abcdef"; + val digits=new u8[22]; // Maximum: 22 octal digits for a 64-bit integer. + i64 count=0, n=value; + // Signed decimal stays nonpositive, including the minimum i64. + if( signed && n>0 ) n=-n; + if( n==0 && precision!=0 ) digits[count++]='0'; + while( n!=0 ) { + i64 q=0, r=0; + if( signed ) { q=n/base; r=q*base-n; } + else { + // Unsigned division without a u64 type. Halving first makes + // the dividend nonnegative; the remainder needs at most one fix. + q=((n>>>1)/base)*2; + r=n-q*base; + if( r>=base ) { q++; r-=base; } + } + digits[count++]=alphabet[r]; + n=q; + } + u8[~] !prefix=""; + if( signed ) { + if( value<0 ) prefix="-"; + else if( spec&512 ) prefix="+"; + else if( spec&1024 ) prefix=" "; + } else if( (spec&2048) && value!=0 ) { + if( kind=='x' ) prefix="0x"; + if( kind=='X' ) prefix="0X"; + } + i64 zeros=precision>count ? precision-count : 0; + // Alternate octal always begins with a zero, including precision-zero 0. + if( kind=='o' && (spec&2048) && zeros==0 && (count==0 || digits[count-1]!='0') ) zeros=1; + i64 pad=width-prefix#-zeros-count; + bool left=(spec&256)!=0; + bool zero=(spec&4096)!=0 && precision<0 && !left; + if( !left && !zero ) _pad(b,pad,' '); + b.addAry(prefix); + if( zero ) _pad(b,pad,'0'); + _pad(b,zeros,'0'); + while( count>0 ) b.add(digits[--count]); + if( left ) _pad(b,pad,' '); + b; +}; + +// Only conversion of binary floating-point values is delegated to libc. +// The fixed signature accepts a validated format and returns a byte string. +{ f64 i64 i64 i64 -> u8[~] } simple_text_float = "C"; +val floating = { !aryu8 b, f64 value, i64 spec, i64 width, i64 precision -> + b.addAry(simple_text_float(value,spec,width,precision)); +}; + +// Explicitly named, typed conversions share the same formatting routines. +val int = { i64 x -> end(integer (begin(),x, 0 ,0,-1)); }; +val flt = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); }; +val f32 = { f32 x -> end(floating(begin(),x,'g',0,-1)); }; +val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); }; +val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); }; +val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); }; +val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); }; -- cgit v1.2.3