summaryrefslogtreecommitdiff
path: root/chapter25
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-28 13:30:22 -0700
committerCliff Click <cliffc@acm.org>2026-09-28 13:30:22 -0700
commit586c8f65f2d0c0e1f73aadd0aa8d850aa653bddd (patch)
tree2f539c308452ddf5fa37826da95b17ba557e8352 /chapter25
parent8212a66ff27e56531e0b48807088f1c544d75756 (diff)
downloadSimple-586c8f65f2d0c0e1f73aadd0aa8d850aa653bddd.tar.gz
Simple-586c8f65f2d0c0e1f73aadd0aa8d850aa653bddd.zip
Support extern C ver direct updates
FIx minor RISC encodings.
Diffstat (limited to 'chapter25')
-rw-r--r--chapter25/README.md24
-rw-r--r--chapter25/docs/examples/FileIO.smp2
-rw-r--r--chapter25/src/main/c/simple_crt.c4
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/Parser.java59
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/Var.java1
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java13
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java8
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java11
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java19
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java10
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java2
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/Node.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java4
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java9
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java5
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/type/Field.java46
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/type/TypeStruct.java9
-rw-r--r--chapter25/src/main/smp/sys/io.smp2
-rw-r--r--chapter25/src/main/smp/sys/libc.smp5
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java88
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java2
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data.c22
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c14
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/spill/README.md10
25 files changed, 314 insertions, 61 deletions
diff --git a/chapter25/README.md b/chapter25/README.md
index b6a7ded5..6d1d5e99 100644
--- a/chapter25/README.md
+++ b/chapter25/README.md
@@ -29,6 +29,26 @@ external Simple object search paths, including an object file or a directory.
The compiler also resolves external C symbols for native linking. Names beginning
with `_` control privacy; public reachability depends on enclosing names too.
+Explicit numeric C declarations bind directly to mutable native storage:
+
+```java
+i32 counter = "C";
+counter += 1;
+return counter;
+```
+
+The native linker resolves `counter`. Reads and writes access that C variable;
+the declaration neither copies nor initializes it. Repeated declarations and
+module imports share the same storage alias. Integer and floating-point data
+bindings are supported; pointer and aggregate data bindings are not yet supported.
+External fields carry a storage flag and occupy no bytes in their declaring
+class. Layout resolves their field offsets to `ExternOffset` symbols; instruction
+selection replaces class-base-plus-offset with the native symbol address.
+Native object output uses the existing AMD64 ELF writer; ARM and RISC-V support
+these addresses through the in-memory linker. Thread-local variables and C macros
+need accessor functions: the system library exposes `libc.errno()` through a small
+native runtime accessor.
+
## Object files contain ideal IR
The ELF writer stores native code and a `.simple` section containing canonical
@@ -47,6 +67,10 @@ expensive serialization bijection check is currently disabled, and the larger
incremental-rebuild test `testModule0` is ignored. Neither is an active
validation guarantee.
+The external-field flag and symbolic offsets change the serialized layout.
+The magic remains `C0DE`; there is no separate format version yet. Rebuild all
+Simple object files before importing them with this compiler.
+
## Constructing SSA before types are complete
Simple still parses directly to SSA without an AST. Forward references mean an
diff --git a/chapter25/docs/examples/FileIO.smp b/chapter25/docs/examples/FileIO.smp
index 6966755a..e48d435b 100644
--- a/chapter25/docs/examples/FileIO.smp
+++ b/chapter25/docs/examples/FileIO.smp
@@ -47,4 +47,4 @@ val _error2 = { i32 errno ->
sys.libc._exit(errno);
};
-val _error = { -> _error2(sys.libc.errno); };
+val _error = { -> _error2(sys.libc.errno()); };
diff --git a/chapter25/src/main/c/simple_crt.c b/chapter25/src/main/c/simple_crt.c
index b79e6f88..8656a3cc 100644
--- a/chapter25/src/main/c/simple_crt.c
+++ b/chapter25/src/main/c/simple_crt.c
@@ -1,6 +1,10 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
+#include <errno.h>
+
+// errno may be thread-local or a macro calling a platform accessor.
+int32_t simple_errno(void) { return errno; }
// Native process arguments converted to ordinary Simple array layouts.
// The outer array is mutable; every contained byte array is published as
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
index af5af514..19894357 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
@@ -346,9 +346,11 @@ public class Parser {
Var v = _scope.var(lex + nvar++);
// Field with sharper type
Field old = tself.field(v._name);
- Field fld = old == null
+ Field fld = v._extern!=0
+ ? Field.make(v._name,v.type(),v._extern,false,true)
+ : old == null
? Field.make(v._name,v.type(),_code.alias(_ref._cname),v._final)
- : Field.make(v._name,v.type(), old._alias, old._final);
+ : old.makeFrom(v.type());
tself = tself.addOrUpdate(fld);
}
TYPES.put(typeName,tself);
@@ -426,7 +428,7 @@ public class Parser {
if( vidx == -1 )
continue;
Var v = _scope.var(vidx);
- if( v._fref ) continue; // Forward refs not declared here
+ if( v._fref || v._extern!=0 ) continue; // C storage has no Simple initializer
// NewNode's TOP field is the zero-code poison for a required
// field. Leave it alone unless the declaration body supplied a
// value; otherwise a user constructor must initialize it.
@@ -970,6 +972,9 @@ public class Parser {
if( def==null )
throw error("Undefined name '" + name + "'");
+ if( def._extern!=0 ) // Parse the C binding as a memory lvalue
+ { pos(old); return parseExpression(); }
+
// TOP fields are for late-initialized fields; these have never
// been written to, and this must be the final write. Other writes
// outside the constructor need to check the final bit.
@@ -1101,6 +1106,14 @@ public class Parser {
Node expr;
if( match("=") ) {
if( isExternDecl() ) {
+ if( !(t instanceof TypeFunPtr) ) {
+ Node ptr = externData(name,t);
+ // The lexical binding is a fixed address; its contents are mutable.
+ if( !_scope.define(name,t,true,ptr,loc) )
+ throw error("Redefining name '"+name+"'",loc);
+ _scope.lookup(name)._extern = _code.externAlias(name);
+ return _code.ZERO;
+ }
expr = externDecl(name,t);
t = expr._type; // Upgrade declared type to the exact extern decl type
} else if( isConstructorDecl(name) ) {
@@ -1280,7 +1293,7 @@ public class Parser {
priv.keep();
for( Field field : ts._fields ) {
Type storage = field._t.makeStorage();
- Field escaped = field._final ? field : Field.make(field._fname,storage,field._alias,true);
+ Field escaped = field._final ? field : field.makeFrom(storage).makeFrom(true);
pub.keep();
Node esc = new EscapeNode(escaped,self,priv,pub).peephole();
MemMergeNode merge = new MemMergeNode(null,pub);
@@ -1879,6 +1892,8 @@ public class Parser {
if( name!=null ) {
Var n = _scope.lookup(name);
if( n != null && !(n.type() instanceof TypeMemPtr) ) {
+ if( n._extern!=0 )
+ return parsePostfixName(_scope.in(n),name,delta);
if( n._final )
throw error("Cannot reassign final '"+n._name+"'");
Node expr = zsMask(peep(new AddNode(_scope.in(n),con(delta))),n.type());
@@ -1963,6 +1978,8 @@ public class Parser {
// Load local value
Node rvalue = _scope.in(var);
+ if( var._extern!=0 )
+ return parsePostfixName(rvalue,id,0);
if( var._fref )
return parsePostfix(rvalue);
// Required constructor fields begin as their declared pointer type
@@ -1980,7 +1997,7 @@ public class Parser {
Kind fk = _scope._kinds.at(fx);
// Access instance field from 'self'
if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && fk instanceof Kind.Func method )
- return parsePostfixName(_scope.in(method._lexSize),id);
+ return parsePostfixName(_scope.in(method._lexSize),id,0);
// Check for a function-escaping variable; these require true
// closures. Final constants are OK; final vars require a hidden var
@@ -1998,7 +2015,7 @@ public class Parser {
// Get the most recent sharpen global
TypeStruct clz = (TypeStruct) TYPES.get( ((TypeMemPtr) clzptr.type())._obj._name );
TypeMemPtr clztmp = TypeMemPtr.make( (byte) 2, clz, true );
- return parsePostfixName( con( clztmp ), id );
+ return parsePostfixName( con( clztmp ), id,0 );
//return parsePostfixName(_scope.in(2),id);
}
}
@@ -2206,7 +2223,7 @@ public class Parser {
else if( match("(") ) return parsePostfix(require(functionCall(expr,defaultSelf()),")"));
else return expr; // No postfix
- return parsePostfixName(expr,name);
+ return parsePostfixName(expr,name,0);
}
private Node defaultSelf() {
@@ -2225,7 +2242,8 @@ public class Parser {
* </pre>
*/
- private Node parsePostfixName(Node expr, String name) {
+ // pre is +/-1 for a prefix update, otherwise parse a postfix operation.
+ private Node parsePostfixName(Node expr, String name, int pre) {
// Keep expr across possible updates
expr.keep();
@@ -2278,7 +2296,7 @@ public class Parser {
: fldoff(expr,name)).keep();
// Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment
- if( matchOpx('=','=') ) {
+ if( pre==0 && matchOpx('=','=') ) {
// Field assignment
Node val = parseAsgn().keep();
// Lift value for store
@@ -2306,20 +2324,20 @@ public class Parser {
Node load = peep(new LoadNode(loc(),name, alias, ctrl(), mem, expr, off));
// Check for assign-update, "ptr.fld += expr" or "ary[idx]++"
- char ch = _lexer.matchOperAssign();
+ char ch = pre==0 ? _lexer.matchOperAssign() : (char)pre;
if( ch!=0 ) {
if( fld!=null && decl==Type.BOTTOM )
throw error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" );
- Node op = opAssign(ch,load, decl );
+ Node op = opAssign(ch,load, decl ).keep();
mem.keep();
Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), op, false).peephole().keep();
storeMem(st,alias,mem);
st.unkeep();
mem.unkeep();
- load = postfix(ch) ? load.unkeep() : op;
- // And use the original loaded value as the result
- return load;
+ op.unkeep(); // A narrow Store may have folded away the expression's conversion.
+ if( postfix(ch) ) load.unkeep();
+ return pre==0 && postfix(ch) ? load : op;
}
off.unkill();
// Might be a method call, so pass expr as 'self'
@@ -2487,6 +2505,19 @@ public class Parser {
return (ExternNode)(new ExternNode(t,ex).peephole());
}
+ // A lexical C binding has a singleton namespace with one external field.
+ // The namespace occupies no storage; its field offset supplies the address.
+ private Node externData(String name, Type t) {
+ if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) )
+ throw error("C data binding requires an integer or float type");
+ String cname = ("C:"+name).intern();
+ TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,_code.externAlias(name),false,true));
+ Type old = TYPES.putIfAbsent(cname,ts);
+ if( old!=null && old!=ts )
+ throw error("Conflicting types for C data '"+name+"'");
+ return con(TypeMemPtr.make((byte)2,ts,true));
+ }
+
/**
* Parse integer literal
*
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/Var.java b/chapter25/src/main/java/com/seaofnodes/simple/Var.java
index 1906ce15..af371d21 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/Var.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/Var.java
@@ -19,6 +19,7 @@ public class Var {
// Parser-only definite-initialization marker. The current binding's
// nullable type carries the path-sensitive state through SSA merges.
public boolean _uninit;
+ public int _extern; // C storage alias; zero for ordinary variables
public Parser.Lexer _loc; // Source location
public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc ) {
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
index 0f38d3ff..86e83372 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
@@ -321,6 +321,12 @@ public class CodeGen {
public final GlobalBits _aliases = new GlobalBits();
public int alias(String clz) { return _aliases.next(clz); }
+ // C storage has one global alias per symbol, including across object files.
+ public int externAlias(String name) { return _aliases.next("C:"+name,GlobalBits.RESERVED); }
+
+ // External data addresses supplied by an in-memory linker (image offsets).
+ public final HashMap<String,Integer> _externDataAddresses = new HashMap<>();
+
// Compute local function index (FIDX) from global function info. This is
// called *in order* during parsing, and that order is part of the global
// unique mapping
@@ -740,6 +746,13 @@ public class CodeGen {
}
// Produce a machine node from n; map it to flag as done so stops cycles.
+ if( n instanceof MemOpNode mem && mem.off() instanceof ExternOffsetNode off ) {
+ // External fields replace base+offset with symbol+0. Keep the ideal
+ // declaring type on the address for machine load/store width selection.
+ TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type;
+ n._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern));
+ n._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO));
+ }
map.put(n, x=_mach.instSelect(n) );
// Carry loop-tree and pre-order info across the ideal->mach transition
if( n instanceof CFGNode ncfg && x instanceof CFGNode xcfg ) {
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
index a74303e6..7423edbd 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java
@@ -316,6 +316,14 @@ public class ElfWriter {
relocations.writeReloOffPlus(offset, symidx, (byte)4/*PLT32*/);
}
+ // Native ELF output is AMD64. Data addresses use PC32, not PLT calls.
+ for( Node n : enc._externData.keySet() ) {
+ String extern = enc._externData.get(n);
+ int symidx = symbols.symbol(extern, 0, SYM_BIND_GLOBAL, SYM_TYPE_OBJECT);
+ int offset = enc.opStart(n) + enc.opLen(n) - 4;
+ relocations.writeReloOffPlus(offset, symidx, (byte)2/*PC32*/);
+ }
+
// Write relocations for the constant pool
String cpoolPrefix = _code.entryClinitName().replaceAll("[^A-Za-z0-9_$]", "_") + "$CPOOL$";
for( Encoding.Relo relo : enc._bigCons.values() ) {
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
index 618ec689..215fd694 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
@@ -187,6 +187,9 @@ public class Encoding {
return this;
}
+ public final HashMap<Node,String> _externData = new HashMap<>();
+ public void externalData(Node n, String name) { _externData.put(n,name); }
+
// Store t as a 32/64 bit constant in the code space; generate RIP-relative
// addressing to load it. Type is stored in either the .rodata or .data.
public void largeConstant( Node relo, Type t, int off, int elf ) {
@@ -581,6 +584,7 @@ public class Encoding {
int off=0; // offset in the struct
for( int fn=0; fn<ts._fields.length; fn++ ) {
Field f = ts._fields[layout[fn]];
+ if( f._extern ) continue;
int foff = ts. offset(layout[fn]);
// Pad up to field
while( off < foff ) { bits.write(0); off++; };
@@ -604,6 +608,13 @@ public class Encoding {
public static int SENTINEL_WRITE = -8;
void patchGlobalRelocations() {
+ for( Node src : _externData.keySet() ) {
+ String name = _externData.get(src);
+ Integer target = _code._externDataAddresses.get(name);
+ if( target==null ) throw new IllegalArgumentException("Unresolved C data symbol '"+name+"'");
+ int start = opStart(src);
+ ((RIPRelSize)src).patch(this,start,opLen(src),target-start);
+ }
for( Node src : _externals.keySet() ) {
int start = opStart(src);
String dst = _externals.get(src);
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java
index c7ee1359..468721a4 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java
@@ -52,6 +52,9 @@ public class ConFldOffNode extends ConstantNode {
public Node asOffset() {
int fldx = _fname==" len" ? _ts._fields.length : _ts.find(_fname);
if( fldx == -1 ) return null;
+ // " len" selects the terminal size slot, not a Field.
+ if( fldx < _ts._fields.length && _ts._fields[fldx]._extern )
+ return new ExternOffsetNode(_fname).peephole();
return ConstantNode.make(TypeInteger.constant(_ts.offset(fldx))).peephole();
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java
new file mode 100644
index 00000000..409ce5d1
--- /dev/null
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java
@@ -0,0 +1,19 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.simple.type.Type;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.BAOS;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+
+// A field address supplied by the C linker, not a displacement from the base.
+// The class base remains available for ideal type/alias analysis. Instruction
+// selection replaces it with this symbol's address and a zero displacement.
+public class ExternOffsetNode extends ExternNode {
+ public ExternOffsetNode(String name) { super(TypeInteger.BOT,name); }
+ @Override public Tag serialTag() { return Tag.ExternOffset; }
+ @Override public Node copy() { return new ExternOffsetNode(_extern); }
+ @Override public void packed(BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node,Integer> nodes) {
+ baos.packed2(strs.get(_extern));
+ }
+}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index bd80a74b..193fbe17 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -92,8 +92,7 @@ public class LoadNode extends MemOpNode {
// Loads into structs do not need a ctrl edge, as null-ptr checking is
// baked into the type system. Loads into arrays DO need the ctrl
// edge, at least until proper range-checking is in place.
- if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp &&
- !tmp._obj.isAry() ) {
+ if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp && !tmp._obj.isAry() ) {
setDef(0,null);
return this;
}
@@ -113,6 +112,10 @@ public class LoadNode extends MemOpNode {
if( _alias == 1 )
return null;
+ // External storage is shared even through different class namespaces.
+ boolean external = ptr._type instanceof TypeMemPtr tmp &&
+ (fld=tmp._obj.field(_name)) != null && fld._extern;
+
// Simple Load-after-Store on same address.
if( mem instanceof StoreNode st &&
ptr == st.nnptr() &&
@@ -154,7 +157,7 @@ public class LoadNode extends MemOpNode {
return extend(castRO(st.val())); // Proved equal
// Can we prove unequal? Offsets do not overlap?
if( !off()._type.join(st.off()._type).isHigh() && // Offsets overlap
- !neverAlias(ptr,st.ptr()) ) { // And might alias
+ (external || !neverAlias(ptr,st.ptr())) ) { // And might alias
addDep( off()); // Offsets can fold, proving unequal
addDep(st.off()); // Offsets can fold, proving unequal
break outer; // Cannot tell, stop trying
@@ -175,6 +178,7 @@ public class LoadNode extends MemOpNode {
case ProjNode mproj: // Memory projection
switch( mproj.in(0) ) {
case NewNode nnn1:
+ if( external ) break outer; // Class allocation does not initialize C storage
// Direct load from fresh zero/default-filled allocation.
Type decl = declaredType();
assert decl!=Type.BOTTOM;
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
index 083889c5..441edc3e 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
@@ -99,7 +99,7 @@ public abstract class MemOpNode extends TypeNode {
@Override public StringBuilder _print1( StringBuilder sb, BitSet visited ) { return _printMach(sb,visited); }
public StringBuilder _printMach( StringBuilder sb, BitSet visited ) { throw Utils.TODO(); }
- //public int log_size() { return _con.log_size(); }
+
@Override
public boolean eq(Node n) {
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java
index e77b0c0a..8ca4662b 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java
@@ -84,7 +84,7 @@ public abstract class Node implements Cloneable {
New,Never,Not,Or,Parm,Phi,Proj,
ReadOnly,Return,Region,RoundF32,
Sar,Shl,Shr,Start,Stop,Store,Sub,ToFloat,
- XCtrl,Xor,StartCU,StopCU,Guard,MemPhi,BulkMemPhi,PtrToInt;
+ XCtrl,Xor,StartCU,StopCU,Guard,MemPhi,BulkMemPhi,PtrToInt,ExternOffset;
public static final Tag[] VALS = values();
public Node make( BAOS bais, String[] strs, Type[] types, GlobalBits fileAliases, GlobalBits aliases ) {
return switch(this) {
@@ -124,6 +124,7 @@ public abstract class Node implements Cloneable {
case Con -> ConstantNode.make(bais ,types);
case Escape-> EscapeNode.make(bais ,types);
case Extern-> ExternNode.make(bais,strs,types);
+ case ExternOffset-> new ExternOffsetNode(strs[bais.packed2()]);
case FunPtr-> FunPtrNode.make(bais, types);
case Fun -> FunNode.make(bais,strs,types);
case Load -> new LoadNode(bais,strs,types,fileAliases,aliases);
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java
index f86d85dc..8209eb4b 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java
@@ -17,8 +17,8 @@ public class TMPARM extends ConstantNode implements MachNode, RIPRelSize {
@Override public boolean isClone() { return true; }
@Override public TMPARM copy() { return new TMPARM(this,_ext); }
@Override public void encoding( Encoding enc ) {
- if( _ext!=null ) throw Utils.TODO();
- enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1);
+ if( _ext!=null ) enc.externalData(this,_ext);
+ else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1);
short dst = enc.reg(this);
// adrp x0, 0
enc.add4(arm.adrp(1,0, arm.OP_ADRP, 0,dst));
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java
index aa001e1e..45c3eed2 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java
@@ -6,7 +6,6 @@ import com.seaofnodes.simple.node.MachNode;
import com.seaofnodes.simple.node.Node;
import com.seaofnodes.simple.type.TypeMemPtr;
import com.seaofnodes.simple.util.SB;
-import com.seaofnodes.simple.util.Utils;
public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize {
final String _ext;
@@ -17,8 +16,8 @@ public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize {
@Override public boolean isClone() { return true; }
@Override public TMPRISC copy() { return new TMPRISC(this,_ext); }
@Override public void encoding( Encoding enc ) {
- if( _ext!=null ) throw Utils.TODO();
- enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1);
+ if( _ext!=null ) enc.externalData(this,_ext);
+ else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1);
short dst = enc.reg(this);
// AUIPC dst,#hi20_constant_pool
enc.add4(riscv.u_type(riscv.OP_AUIPC, dst, 0));
@@ -32,8 +31,8 @@ public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize {
@Override public void patch( Encoding enc, int opStart, int opLen, int delta ) {
short dst = enc.reg(this);
// AUIPC dst,#hi20_constant_pool
- enc.patch4(opStart , riscv.u_type(riscv.OP_AUIPC, dst, delta>>12));
- // Load dst,[dst+#low12_constant_pool]
+ enc.patch4(opStart , riscv.u_type(riscv.OP_AUIPC, dst, (delta+0x800)>>12));
+ // ADDI's low 12 bits are signed; round the AUIPC page accordingly.
enc.patch4(opStart+4, riscv.i_type(riscv.OP_IMM, dst, 0, dst, delta & 0xFFF));
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java
index cc8741b3..fbd79dd7 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java
@@ -4,7 +4,6 @@ import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeMemPtr;
import com.seaofnodes.simple.util.SB;
-import com.seaofnodes.simple.util.Utils;
public class TMPX86 extends ConstantNode implements MachNode, RIPRelSize{
private byte _opLen;
@@ -17,12 +16,12 @@ public class TMPX86 extends ConstantNode implements MachNode, RIPRelSize{
@Override public Node copy() { return new TMPX86(this,_ext); }
@Override public void encoding( Encoding enc ) {
- if( _ext!=null ) throw Utils.TODO();
short dst = enc.reg(this);
// 0 or 1 for REX depending on the dst.
_opLen = (byte)(6+ x86_64_v2.rexF(dst, 0, 0, true, enc));
enc.add1(0x8D).add1(x86_64_v2.modrm(x86_64_v2.MOD.INDIRECT, dst, 0b101)).add4(0);
- enc.largeConstant(this,((TypeMemPtr)_con)._obj, _opLen-4, 2/*ELF encoding PC32*/);
+ if( _ext!=null ) enc.externalData(this,_ext);
+ else enc.largeConstant(this,((TypeMemPtr)_con)._obj, _opLen-4, 2/*ELF encoding PC32*/);
}
// Delta is from opcode start
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter25/src/main/java/com/seaofnodes/simple/type/Field.java
index ed505b19..beaa719c 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/type/Field.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/type/Field.java
@@ -1,7 +1,6 @@
package com.seaofnodes.simple.type;
import com.seaofnodes.simple.util.Ary;
-import com.seaofnodes.simple.util.AryInt;
import com.seaofnodes.simple.util.BAOS;
import com.seaofnodes.simple.util.SB;
import java.util.*;
@@ -22,23 +21,26 @@ public class Field extends Type {
public int _alias;
// Field is only written in the constructor
public boolean _final;
+ // Native C storage, outside the containing object's layout.
+ public boolean _extern;
private static final Ary<Field> FREE = new Ary<>(Field.class);
- private Field(String fname, Type type, int alias, boolean xfinal ) { super(TFLD); init(fname,type,alias,xfinal); }
- private Field init(String fname, Type type, int alias, boolean xfinal ) {
+ private Field(String fname, Type type, int alias, boolean xfinal, boolean external ) { super(TFLD); init(fname,type,alias,xfinal,external); }
+ private Field init(String fname, Type type, int alias, boolean xfinal, boolean external ) {
_fname = fname;
_t = type;
_alias = alias;
_final = xfinal;
+ _extern = external;
return this;
}
// Return a filled-in Field; either from free list or alloc new.
- private static Field malloc(String fname, Type type, int alias, boolean xfinal ) {
- return FREE.isEmpty() ? new Field(fname,type,alias,xfinal) : FREE.pop().init(fname,type,alias,xfinal);
+ private static Field malloc(String fname, Type type, int alias, boolean xfinal, boolean external ) {
+ return FREE.isEmpty() ? new Field(fname,type,alias,xfinal,external) : FREE.pop().init(fname,type,alias,xfinal,external);
}
// Malloc-from
- public Field malloc( ) { return malloc(_fname,null,_alias,_final); }
+ public Field malloc( ) { return malloc(_fname,null,_alias,_final,_extern); }
@Override Field free(Type t) {
Field f = (Field)t;
assert !f.isFree() && !f._terned;
@@ -52,16 +54,19 @@ public class Field extends Type {
// Make and intern with listed fields
public static Field make( String fname, Type type, int alias, boolean xfinal ) {
- Field f = malloc(fname,type,alias,xfinal);
+ return make(fname,type,alias,xfinal,false);
+ }
+ public static Field make( String fname, Type type, int alias, boolean xfinal, boolean external ) {
+ Field f = malloc(fname,type,alias,xfinal,external);
Field f2 = f.intern();
if( f2==f ) return f;
return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f);
}
public Field makeFrom( Type type ) {
- return type == _t ? this : make(_fname,type,_alias,_final);
+ return type == _t ? this : make(_fname,type,_alias,_final,_extern);
}
public Field makeFrom( boolean xfinal ) {
- return xfinal == _final ? this : make(_fname,_t,_alias,xfinal);
+ return xfinal == _final ? this : make(_fname,_t,_alias,xfinal,_extern);
}
// Cycle-making-breaking
@@ -71,7 +76,8 @@ public class Field extends Type {
public static final Field TEST = make("test",Type.NIL,-2,false);
public static final Field TEST2 = make("test",Type.NIL,-2,true);
public static final Field FLT = make("flt ",TypeFloat.F32,-2,false);
- public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST2); ts.add(FLT); }
+ public static final Field EXT = make("test",TypeInteger.I32,-2,false,true);
+ public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST2); ts.add(FLT); ts.add(EXT); }
@Override Field xmeet( Type that ) {
Field fld = (Field)that; // Invariant
@@ -80,16 +86,17 @@ public class Field extends Type {
if( fld ==BOT.dual() ) return this;
if( _fname!=fld._fname ) { assert !_fname.equals(fld._fname); return BOT; }
assert _alias==fld._alias;
- return make(_fname, _t.meet(fld._t ), _alias, _final | fld._final);
+ if( _extern!=fld._extern ) return BOT;
+ return make(_fname, _t.meet(fld._t ), _alias, _final | fld._final,_extern);
}
@Override
- Field xdual() { return malloc(_fname, _t.dual(),_alias,!_final); }
+ Field xdual() { return malloc(_fname, _t.dual(),_alias,!_final,_extern); }
@Override Field rdual() {
if( _dual!=null ) return dual();
assert !_terned;
- Field d = malloc(_fname,null,_alias,!_final);
+ Field d = malloc(_fname,null,_alias,!_final,_extern);
(_dual = d)._dual = this; // Cross link duals
d._t = _t._terned ? _t.dual() : _t.rdual();
return d;
@@ -97,7 +104,7 @@ public class Field extends Type {
@Override boolean _isConstant() { return _t._isConstant(); }
@Override boolean _isFinal() { return _final && _t._isFinal(); }
- @Override Field _makeRO() { return _final ? this : make(_fname, _t._makeRO(),_alias,true); }
+ @Override Field _makeRO() { return _final ? this : make(_fname, _t._makeRO(),_alias,true,_extern); }
@Override Field _close( String name, HashMap<String, Type> TYPES ) { return makeFrom( _t._close(name, TYPES )); }
@Override Type _upgradeType(HashMap<String,Type> TYPES) {
@@ -105,10 +112,10 @@ public class Field extends Type {
}
// Override in subclasses
- int hash() { return _fname.hashCode() ^ _t.hashCode() ^ _alias ^ (_final ? 1024 : 0); }
+ int hash() { return _fname.hashCode() ^ _t.hashCode() ^ _alias ^ (_final ? 1024 : 0) ^ (_extern ? 2048 : 0); }
private boolean static_eq( Field f ) {
- return _fname.equals(f._fname) && _alias==f._alias && _final==f._final && _t !=null && f._t !=null;
+ return _fname.equals(f._fname) && _alias==f._alias && _final==f._final && _extern==f._extern && _t !=null && f._t !=null;
}
@Override boolean eq( Type t ) {
Field f = (Field)t;
@@ -123,22 +130,23 @@ public class Field extends Type {
@Override public int nkids() { return 1; }
@Override public Type at( int idx ) { return _t; }
@Override public void set( int idx, Type t ) { _t = t; }
- // Tags: final/!final; +alias+name
+ // Tags: final/!final; +alias+name+external-storage flag
@Override int TAGOFF() { return 2; }
@Override public void packed( BAOS baos, HashMap<String,Integer> strs ) {
baos.write(TAGOFFS[_type] + (_final ? 1 : 0));
baos.packed2(_alias);
baos.packed2(strs.get(_fname));
+ baos.write(_extern ? 1 : 0);
}
static Field packed( int tag, BAOS bais, String[] strs ) {
int alias = bais.packed2();
String fname = strs[bais.packed2()];
- return malloc(fname,null,alias,(tag&1)==1);
+ return malloc(fname,null,alias,(tag&1)==1,bais.read()!=0);
}
@Override
public SB _print( SB sb, BitSet visit, boolean html ) {
- sb.p(_final?"":"!").p(_fname).p(":").p(_alias).p(" : ");
+ sb.p(_extern ? "extern C " : "").p(_final?"":"!").p(_fname).p(":").p(_alias).p(" : ");
return _t ==null ? sb.p("---") : _t.print(sb,visit,html);
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter25/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
index 0d6700b9..82c37a12 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
@@ -154,6 +154,7 @@ public class TypeStruct extends Type {
public TypeStruct makeInit() {
Field[] fs = new Field[_fields.length];
for( int i=0; i<_fields.length; i++ ) {
+ if( _fields[i]._extern ) { fs[i] = _fields[i]; continue; }
Type zero = _fields[i]._t.makeZero();
fs[i] = _fields[i].makeFrom(_fields[i]._t != Type.BOTTOM && zero.isa(_fields[i]._t) ? zero : Type.TOP);
}
@@ -336,7 +337,7 @@ public class TypeStruct extends Type {
int len = Math.min(_fields.length,that._fields.length);
for( int i=0; i<len; i++ ) {
Field f0 = _fields[i], f1 = that._fields[i];
- if( !f0._fname.equals(f1._fname) || f0._alias != f1._alias )
+ if( !f0._fname.equals(f1._fname) || f0._alias != f1._alias || f0._extern != f1._extern )
return BOT;
}
@@ -465,7 +466,7 @@ public class TypeStruct extends Type {
@Override public int alignment() {
int align = 0;
for( Field f : _fields )
- align = Math.max(align, f._t.alignment());
+ if( !f._extern ) align = Math.max(align, f._t.alignment());
return align;
}
@@ -614,7 +615,8 @@ public class TypeStruct extends Type {
int[] cnts = new int[5]; // Count of fields at log field size
int flen = _fields.length;
for( int i=0; i<flen; i++ )
- cnts[_fields[i]._t.log_size()]++; // Log size is 0(byte), 1(i16/u16), 2(i32/f32), 3(i64/dbl)
+ if( !_fields[i]._extern )
+ cnts[_fields[i]._t.log_size()]++; // Log size is 0(byte), 1(i16/u16), 2(i32/f32), 3(i64/dbl)
int off = 0, idx = 0; // Base common struct fields go here, e.g. Mark/Klass
// Compute offsets to the start of each power-of-2 aligned fields.
int[] offs = new int[4];
@@ -625,6 +627,7 @@ public class TypeStruct extends Type {
// Assign offsets to all fields.
// Really a hidden radix sort.
for( int i=0; i<flen; i++ ) {
+ if( _fields[i]._extern ) { _offs[idx++] = -1; continue; }
int log = _fields[i]._t.log_size();
_offs[idx++] = offs[log]; // Field offset
offs[log] += 1<<log; // Next field offset at same alignment
diff --git a/chapter25/src/main/smp/sys/io.smp b/chapter25/src/main/smp/sys/io.smp
index 33fbdb5e..d6263ffe 100644
--- a/chapter25/src/main/smp/sys/io.smp
+++ b/chapter25/src/main/smp/sys/io.smp
@@ -12,7 +12,7 @@ val read = { i32 fd ->
int len = 0;
while( true ) {
i32 rez = libc.read(fd,ptr,buf#);
- if( rez > buf# ) libc._exit(libc.errno);
+ if( rez > buf# ) libc._exit(libc.errno());
len += rez;
if( rez < buf# )
break;
diff --git a/chapter25/src/main/smp/sys/libc.smp b/chapter25/src/main/smp/sys/libc.smp
index 133b5bf7..17ee65cb 100644
--- a/chapter25/src/main/smp/sys/libc.smp
+++ b/chapter25/src/main/smp/sys/libc.smp
@@ -30,8 +30,9 @@ i32 STDERR = 2;
// status
{ i32 -> i32 } _exit = "C";
-// Error return from many native calls
-i32 errno = "C";
+// errno is not an ordinary C data symbol; read it through the native runtime.
+{ i32 } simple_errno = "C";
+val errno = { -> simple_errno(); };
// addr len prot flags fd off -> void*
{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C";
// mmap flags
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 41225097..bde25ab7 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -21,6 +21,87 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class Chapter25Test {
+ @Test
+ public void testExternData() throws IOException {
+ String src = """
+ i32 counter="C";
+ i8 small="C";
+ f64 fraction="C";
+ {int} bump="C";
+ val test_global={ ->
+ val before=counter;
+ counter=before;
+ counter=++counter+1;
+ counter+=1;
+ val mid=bump();
+ small++;
+ fraction=fraction+0.5;
+ return before*10000+mid*100+counter;
+ };
+ """;
+ TestC.runC(src,"extern_data","",-1);
+ }
+
+ @Test
+ public void testExternDataEncoding() {
+ for( String cpu : new String[]{"arm","riscv"} ) {
+ CodeGen code = new CheckedCodeGen("i32 counter=\"C\"; counter=counter+3; return counter;");
+ // Exercise signed low-12 relocation arithmetic, past the first page.
+ int addr = 0x3800;
+ code._externDataAddresses.put("counter",addr);
+ code.driver(cpu,"SystemV",true,false);
+ byte[] image = new byte[1<<20];
+ var bits = code._encoding._bits;
+ System.arraycopy(bits.buf(),0,image,0,bits.size());
+ if( cpu.equals("arm") ) {
+ EvalArm64 arm = new EvalArm64(image,1<<16);
+ arm.st4(addr,-7);
+ assertEquals(0,arm.step(1000));
+ assertEquals(-4,arm.regs[0]);
+ assertEquals(-4,arm.ld4s(addr));
+ } else {
+ EvalRisc5 r5 = new EvalRisc5(image,1<<16);
+ r5.st4(addr,-7);
+ assertEquals(0,r5.step(1000));
+ assertEquals(-4,r5.regs[10]);
+ assertEquals(-4,r5.ld4s(addr));
+ }
+ }
+ }
+
+ @Test
+ public void testExternDataImports() throws IOException {
+ Path dir = Files.createTempDirectory(Path.of("build/objs"),"extern_data_");
+ String globals = "i32 counter=\"C\"; val read={ -> counter; };";
+ CodeGen globalCode = new CodeGen(null,dir.toString(),null,"Globals",globals,126,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var clz = globalCode.compunit()._clz;
+ int counter = clz.find("counter");
+ assertTrue(clz._fields[counter]._extern);
+ assertEquals(-1,clz.offset(counter));
+ assertEquals(clz.remove(counter).size(),clz.size());
+ // Load the serialized declaration and ideal graph from Globals.o.
+ String src = """
+ i32 counter="C";
+ val test_global={ ->
+ val before=Globals.counter;
+ Globals.counter=before+1;
+ counter+=2;
+ return before*100+Globals.read();
+ };
+ """;
+ Ary<String> paths = new Ary<>(new String[]{dir.toString()});
+ for( boolean imported : new boolean[]{true,false} ) {
+ Path root = dir.resolve(imported ? "binary" : "source");
+ Files.createDirectories(root.resolve("Main"));
+ if( !imported ) Files.writeString(root.resolve("Main/Globals.smp"),globals);
+ new CodeGen(root.toString(),root.toString(),imported ? paths : null,"Main",src,126,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ assertEquals("",TestC.gcc(root+"/Main.o","",TestC.C_DRIVERS_DIR+"extern_data_import.c",
+ (String)null,new Ary<>(new String[]{dir+"/Globals.o"}),root+"/main"+(TestC.OS.startsWith("Windows") ? ".exe" : "")));
+ }
+ }
+
@Test public void testPrintingConstantPool() throws Exception {
com.seaofnodes.simple.codegen.PrintRegTestSupport.checkConstantPool();
}
@@ -312,6 +393,13 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
}
@Test
+ public void testErrnoAccessor() throws IOException {
+ String src = "sys.libc.close(-1); return sys.libc.errno()==0;";
+ TestC.run(src,"errnoAccessor",new Ary<>(new String[]{SYS_BLDDIR}),
+ TestC.CALL_CONVENTION,null,null,"",-1);
+ }
+
+ @Test
public void testBubbles() throws IOException {
String src = Files.readString( Path.of("docs/examples/BubbleSort.smp"));
String exe = TestC.compile(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}),
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java
index a4cc7044..633e931e 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/EvalRisc5.java
@@ -252,7 +252,7 @@ public class EvalRisc5 {
case 2 -> (rs1 < rs2) ? 1 : 0;
case 3 -> (rs1 < rs2) ? 1 : 0;
case 4 -> rs1 ^ rs2;
- case 5 -> (ir & 0x40000000) != 0 ? (rs1 >>> (rs2 & 0x3F)) : (rs1 >> (rs2 & 0x3F));
+ case 5 -> (ir & 0x40000000) != 0 ? (rs1 >> (rs2 & 0x3F)) : (rs1 >>> (rs2 & 0x3F));
case 6 -> rs1 | rs2;
case 7 -> rs1 & rs2;
default -> rval;
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data.c b/chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data.c
new file mode 100644
index 00000000..9325f65e
--- /dev/null
+++ b/chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data.c
@@ -0,0 +1,22 @@
+#include <stdint.h>
+#include <stdio.h>
+
+int32_t counter=7;
+int8_t small=-3;
+double fraction=1.25;
+
+int64_t bump(void) { counter+=5; return counter; }
+__attribute__((CALL_CONV)) extern int64_t test_global(void);
+
+int main(void) {
+ int64_t result=test_global();
+ if(result!=71515 || counter!=15 || small!=-2 || fraction!=1.75) {
+ printf("result=%lld counter=%d small=%d fraction=%g\n",
+ (long long)result,counter,small,fraction);
+ return 1;
+ }
+ // A second call must observe C's new values, not a Simple initialization copy.
+ counter=-2;
+ result=test_global();
+ return result!=-19394 || counter!=6 || small!=-1 || fraction!=2.25;
+}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c b/chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c
new file mode 100644
index 00000000..f65e5c84
--- /dev/null
+++ b/chapter25/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c
@@ -0,0 +1,14 @@
+#include <stdint.h>
+#include <stdio.h>
+
+int32_t counter=7;
+__attribute__((CALL_CONV)) extern int64_t test_global(void);
+
+int main(void) {
+ int64_t result=test_global();
+ if(result!=710 || counter!=10) {
+ printf("result=%lld counter=%d\n",(long long)result,counter);
+ return 1;
+ }
+ return 0;
+}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md b/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md
index fae44126..6c78d46f 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md
+++ b/chapter25/src/test/java/com/seaofnodes/simple/spill/README.md
@@ -27,11 +27,11 @@ Chapter 25 cannot parse all earlier sources unchanged. Adaptations are explicit:
All entries replay **through RegAlloc**, checking scheduled register constraints.
The historical phase column is provenance, not a promise to run those old native
-harnesses in Chapter 25. In particular, the adapted Bubble Sort still exposes a
-pre-existing ARM extern-data encoding limitation (`i32 errno="C"; return 0;` is
-sufficient to reproduce it). Allocation succeeds; ARM encoding does not. That
-separate failure remains in the backport queue. Do not count this corpus as
-historical runtime coverage.
+harnesses in Chapter 25. The adapted Bubble Sort retains its historical
+`i32 errno="C"` declaration for allocation comparability. Direct data bindings
+now encode on all three targets, but this declaration cannot link to a libc
+whose errno is thread-local or accessor-based. Current system-library code uses
+`libc.errno()` instead. Do not count this corpus as historical runtime coverage.
`SpillStats` then compiles the current system library through encoding at seed
456, and runs Chapter25Test with its existing seeds and native checks. Together