summaryrefslogtreecommitdiff
path: root/src/main/smp
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
commit44505aa5b75b1046f13f2bc2a52fe0b16d975838 (patch)
treecc558485c807ac3842cd73943cf5e294c4a307de /src/main/smp
parent2560096fb4636263798ae56c88687973c72cdf6e (diff)
downloadSimple-linear-chapter30.tar.gz
Simple-linear-chapter30.zip
Chapter 30: Text Formatting and Diagnosticslinear-chapter30
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/main/smp')
-rw-r--r--src/main/smp/sys.smp2
-rw-r--r--src/main/smp/sys/ary.smp26
-rw-r--r--src/main/smp/sys/aryi64.smp25
-rw-r--r--src/main/smp/sys/aryu8.smp45
-rw-r--r--src/main/smp/sys/io.smp75
-rw-r--r--src/main/smp/sys/libc.smp25
-rw-r--r--src/main/smp/sys/rt.smp3
-rw-r--r--src/main/smp/sys/rt/simple_args.smp22
-rw-r--r--src/main/smp/sys/text.smp98
9 files changed, 252 insertions, 69 deletions
diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp
index 7705a187..1201315b 100644
--- a/src/main/smp/sys.smp
+++ b/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/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp
index 8312e94c..c139cd33 100644
--- a/src/main/smp/sys/ary.smp
+++ b/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; i++ )
+ for( u32 i=0; i<len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
@@ -14,10 +28,12 @@ val copy = { u8[] dst, int doff, u8[~] src, int soff, int len ->
// 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<max; i++ )
+ for( u32 i=0; i<max; i++ )
if( src1[off1+i] != src2[off2+i] )
return src1[off1+i] - src2[off2+i]; // Bytes determine compare
return len1 - len2; // Length determines compare
diff --git a/src/main/smp/sys/aryi64.smp b/src/main/smp/sys/aryi64.smp
index 98b72605..b9f1fbed 100644
--- a/src/main/smp/sys/aryi64.smp
+++ b/src/main/smp/sys/aryi64.smp
@@ -9,36 +9,36 @@ struct aryi64 {
};
// 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.
i64[] !buf2 = new i64[newlen];
- for( int i=0; i<buf#; i++ )
+ for( u32 i=0; i<len; i++ )
buf2[i] = buf[i];
buf = buf2;
self;
};
- // Add a i64 to aryu8
+ // Add an i64 to aryi64
val add = { i64 c ->
_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<len; i++ )
+ for( u32 i=0; i<len; i++ )
str.num(buf[i]).add(',').add(' ');
if( len ) str.undo(2);
return str.add(']');
};
- val swap = { int i, int j ->
+ 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<len; i++ )
+val copy = { i64[] dst, u32 doff, i64[~] 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( u32 i=0; i<len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp
index 21cf920b..18543f13 100644
--- a/src/main/smp/sys/aryu8.smp
+++ b/src/main/smp/sys/aryu8.smp
@@ -7,12 +7,12 @@ struct aryu8 {
new aryu8 = { -> 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<log; i++ ) {
- ten /= 10;
- i64 q = x/ten;
- u8 digit = q+'0';
- add(digit);
- x -= q*ten;
- }
+ text.integer(self,x,0,0,-1);
self;
};
- // Write to fd, returns the number of bytes written (len).
+ // Write all bytes to fd; return len, or -1 with errno set on failure.
val write = { i32 fd ->
- 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/src/main/smp/sys/io.smp b/src/main/smp/sys/io.smp
index d6263ffe..14993429 100644
--- a/src/main/smp/sys/io.smp
+++ b/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/src/main/smp/sys/libc.smp b/src/main/smp/sys/libc.smp
index 17ee65cb..7122bbd8 100644
--- a/src/main/smp/sys/libc.smp
+++ b/src/main/smp/sys/libc.smp
@@ -2,17 +2,17 @@
// libc bindings:
// https://www.man7.org/linux/man-pages/man2/<libc call>.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 <enter> 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/src/main/smp/sys/rt.smp b/src/main/smp/sys/rt.smp
new file mode 100644
index 00000000..d62b4def
--- /dev/null
+++ b/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/src/main/smp/sys/rt/simple_args.smp b/src/main/smp/sys/rt/simple_args.smp
new file mode 100644
index 00000000..3f630e54
--- /dev/null
+++ b/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<argc; i++ ) {
+ i64 src = simple_argv(argv,i);
+ i64 len = strlen(src);
+ if( len < 0 || len > 4294967295 ) return null;
+ val bytes = new u8[len];
+ i64 dst = bytes;
+ memcpy(dst,src,len);
+ args[i] = bytes;
+ }
+ return args;
+};
diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp
new file mode 100644
index 00000000..a414ff45
--- /dev/null
+++ b/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<n; i++ ) b.add(c);
+ b;
+};
+
+val string = { !aryu8 b, u8[~] s, i64 spec, i64 width, i64 precision ->
+ i64 len=s#;
+ if( precision>=0 && precision<len ) len=precision;
+ i64 pad=width-len;
+ if( !(spec&256) ) _pad(b,pad,' ');
+ for( i64 i=0; i<len; i++ ) b.add(s[i]);
+ if( spec&256 ) _pad(b,pad,' ');
+ b;
+};
+
+val integer = { !aryu8 b, i64 value, i64 spec, i64 width, i64 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)); };