diff options
35 files changed, 469 insertions, 90 deletions
diff --git a/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java index 860e5eaf..2f7ffe0d 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java @@ -28,9 +28,9 @@ public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { // Delta is from opcode start @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] + // Round the high part because ADDI's low 12 bits are signed. + enc.patch4(opStart , riscv.u_type(riscv.OP_AUIPC, dst, (delta+0x800)>>12)); + // ADDI dst,dst,#low12_constant_pool enc.patch4(opStart+4, riscv.i_type(riscv.OP_IMM, dst, 0, dst, delta & 0xFFF)); } diff --git a/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java index 8d0f4c65..60917773 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -13,6 +13,38 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter22Test { + + @Test public void testRiscvRightShifts() { + // SRAI, SRLI, SRA, SRL: a0 = -32 shifted by 4. + int[] ops = {0x40455513,0x00455513,0x40b55533,0x00b55533}; + for( int i=0; i<ops.length; i++ ) { + EvalRisc5 r5 = new EvalRisc5(new byte[1<<16],1<<15); + r5.st4(0,ops[i]); + r5.regs[riscv.A0] = -32; + r5.regs[riscv.A0+1] = 4; + assertEquals(0,r5.step(1)); + assertEquals((i&1)==0 ? -2L : 0x0fff_ffff_ffff_fffeL,r5.regs[riscv.A0]); + } + } + + @Test public void testRiscvPointerRelocation() { + CodeGen code = new CheckedCodeGen("return \"x\";").driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + var enc = code._encoding; + assertEquals(1,enc._bigCons.size()); + var ptr = (com.seaofnodes.simple.node.cpus.riscv.TMPRISC)enc._bigCons.keySet().iterator().next(); + int start = enc._opStart[ptr._nid]; + for( int delta : new int[]{0x7ff,0x800,0xfff,0x1000,0x1800,-1,-0x800,-0x801} ) { + ptr.patch(enc,start,enc._opLen[ptr._nid],delta); + // Place the program above zero so backward targets are valid addresses. + byte[] image = new byte[1<<16]; + System.arraycopy(enc.bits(),0,image,0x4000,enc._bits.size()); + EvalRisc5 r5 = new EvalRisc5(image,1<<15); + r5._pc = 0x4000; + assertEquals(0,r5.step(100)); + assertEquals(0x4000+start+delta,r5.regs[riscv.A0]); + } + } + @Test public void testSubZeroTypeError() { try { new CodeGen("return null-0;").parse().opto().typeCheck(); diff --git a/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java index 7b918615..00711f4e 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/EvalRisc5.java @@ -251,7 +251,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/chapter23/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/chapter23/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java index 44e73d1d..94f0da0e 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java +++ b/chapter23/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java @@ -28,9 +28,9 @@ public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { // Delta is from opcode start @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] + // Round the high part because ADDI's low 12 bits are signed. + enc.patch4(opStart , riscv.u_type(riscv.OP_AUIPC, dst, (delta+0x800)>>12)); + // ADDI dst,dst,#low12_constant_pool enc.patch4(opStart+4, riscv.i_type(riscv.OP_IMM, dst, 0, dst, delta & 0xFFF)); } diff --git a/chapter23/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter23/src/test/java/com/seaofnodes/simple/Chapter22Test.java index bca06f0c..6f488c85 100644 --- a/chapter23/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/chapter23/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -13,6 +13,38 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter22Test { + + @Test public void testRiscvRightShifts() { + // SRAI, SRLI, SRA, SRL: a0 = -32 shifted by 4. + int[] ops = {0x40455513,0x00455513,0x40b55533,0x00b55533}; + for( int i=0; i<ops.length; i++ ) { + EvalRisc5 r5 = new EvalRisc5(new byte[1<<16],1<<15); + r5.st4(0,ops[i]); + r5.regs[riscv.A0] = -32; + r5.regs[riscv.A0+1] = 4; + assertEquals(0,r5.step(1)); + assertEquals((i&1)==0 ? -2L : 0x0fff_ffff_ffff_fffeL,r5.regs[riscv.A0]); + } + } + + @Test public void testRiscvPointerRelocation() { + CodeGen code = new CheckedCodeGen("return \"x\";").driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + var enc = code._encoding; + assertEquals(1,enc._bigCons.size()); + var ptr = (com.seaofnodes.simple.node.cpus.riscv.TMPRISC)enc._bigCons.keySet().iterator().next(); + int start = enc._opStart[ptr._nid]; + for( int delta : new int[]{0x7ff,0x800,0xfff,0x1000,0x1800,-1,-0x800,-0x801} ) { + ptr.patch(enc,start,enc._opLen[ptr._nid],delta); + // Place the program above zero so backward targets are valid addresses. + byte[] image = new byte[1<<16]; + System.arraycopy(enc.bits(),0,image,0x4000,enc._bits.size()); + EvalRisc5 r5 = new EvalRisc5(image,1<<15); + r5._pc = 0x4000; + assertEquals(0,r5.step(100)); + assertEquals(0x4000+start+delta,r5.regs[riscv.A0]); + } + } + @Test public void testSubZeroTypeError() { try { new CodeGen("return null-0;").parse().opto().typeCheck(); diff --git a/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java index a4cc7044..633e931e 100644 --- a/chapter23/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter23/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/chapter24/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/chapter24/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java index 44e73d1d..94f0da0e 100644 --- a/chapter24/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java +++ b/chapter24/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java @@ -28,9 +28,9 @@ public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { // Delta is from opcode start @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] + // Round the high part because ADDI's low 12 bits are signed. + enc.patch4(opStart , riscv.u_type(riscv.OP_AUIPC, dst, (delta+0x800)>>12)); + // ADDI dst,dst,#low12_constant_pool enc.patch4(opStart+4, riscv.i_type(riscv.OP_IMM, dst, 0, dst, delta & 0xFFF)); } diff --git a/chapter24/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter24/src/test/java/com/seaofnodes/simple/Chapter22Test.java index e4eb03c0..cf92f830 100644 --- a/chapter24/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/chapter24/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -13,6 +13,38 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter22Test { + + @Test public void testRiscvRightShifts() { + // SRAI, SRLI, SRA, SRL: a0 = -32 shifted by 4. + int[] ops = {0x40455513,0x00455513,0x40b55533,0x00b55533}; + for( int i=0; i<ops.length; i++ ) { + EvalRisc5 r5 = new EvalRisc5(new byte[1<<16],1<<15); + r5.st4(0,ops[i]); + r5.regs[riscv.A0] = -32; + r5.regs[riscv.A0+1] = 4; + assertEquals(0,r5.step(1)); + assertEquals((i&1)==0 ? -2L : 0x0fff_ffff_ffff_fffeL,r5.regs[riscv.A0]); + } + } + + @Test public void testRiscvPointerRelocation() { + CodeGen code = new CheckedCodeGen("return \"x\";").driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + var enc = code._encoding; + assertEquals(1,enc._bigCons.size()); + var ptr = (com.seaofnodes.simple.node.cpus.riscv.TMPRISC)enc._bigCons.keySet().iterator().next(); + int start = enc._opStart[ptr._nid]; + for( int delta : new int[]{0x7ff,0x800,0xfff,0x1000,0x1800,-1,-0x800,-0x801} ) { + ptr.patch(enc,start,enc._opLen[ptr._nid],delta); + // Place the program above zero so backward targets are valid addresses. + byte[] image = new byte[1<<16]; + System.arraycopy(enc.bits(),0,image,0x4000,enc._bits.size()); + EvalRisc5 r5 = new EvalRisc5(image,1<<15); + r5._pc = 0x4000; + assertEquals(0,r5.step(100)); + assertEquals(0x4000+start+delta,r5.regs[riscv.A0]); + } + } + @Test public void testSubZeroTypeError() { try { new CodeGen("return null-0;").parse().opto().typeCheck(); diff --git a/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java b/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java index a4cc7044..633e931e 100644 --- a/chapter24/src/test/java/com/seaofnodes/simple/EvalRisc5.java +++ b/chapter24/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/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 diff --git a/docs/chapter-backports.md b/docs/chapter-backports.md index 304e7472..6e74572b 100644 --- a/docs/chapter-backports.md +++ b/docs/chapter-backports.md @@ -13,6 +13,40 @@ renumbering chapters is deferred. ## Pending corrections +- **Chapter 24 Load BOTTOM-on-error backport, unwound/deferred (2026-09-28).** + Cliff requested starting with 24 and analyzing failures before proceeding. + Literal global BOTTOM memory/pointer inputs already propagate BOTTOM. The + missing behavior is `LoadNode.compute()` returning BOTTOM instead of the + declared field type when `err()!=null`; Cliff unwound that one-line experiment. + Chapter 25's Convert nodes supply the surrounding expression/loop's typed + result while retaining the erroneous BOTTOM-producing Load for TypeCheck. + Chapter 24 relies on the Load itself supplying that typed result. Defer this + backport with the conversion split rather than importing it alone. + The experimental full Make suite had four failures out of 477 + tests (`build/load-bottom-24.log`); the fuzzer recipe was not reached. + - `BrainFuckTest.testBrainFuck`: during parsing, the unfinished loop Phi for + mutable `program` has its declared nullable type. The element load becomes + BOTTOM, and reading the initialized `command` local triggers Parser's + BOTTOM-means-uninitialized check before the loop can finish. Reduced valid + source: `var !program="a"; for(int pc=0; pc<program#; pc++) { var command=program[pc]; return command; } return 0;`. + - `Chapter10Test.testWhileWithNullInside`: the loop's result Phi starts at its + declared `i64` while incomplete. Closing the loop exposes a BOTTOM Load on + the backedge; Phi.compute explicitly propagates BOTTOM, violating the + pessimistic `setType` narrowing assertion. Trace: Phi#1458 goes from i64 to + BOTTOM with Load#1462 on its backedge. This occurs during parsing, before SCCP. + - `Chapter10Test.testNullGuardErrors`: ReturnNode sees BOTTOM without any + recorded scalar return kind, so its `No defined return type` error wins + before the Load's `Might be null` diagnostic. + - `Chapter13Test.testLinkedList0`: `head.next` becomes BOTTOM, and parsing the + following `.i` reports `Expected reference but found Bot` first. + Scratch instrumentation lives entirely under `build/load-bottom-probe`. + Reduced sources pass or report the intended null error with the HEAD Load + compiled separately, and reproduce the failures with the one-line change + (`build/load-bottom-reduced-baseline.log`, `build/load-bottom-reduced.log`, + `build/load-bottom-trace.log`). No earlier chapter was changed. Fixing these + requires coordinating parse-time incomplete types, typed loop-Phi bounds, + and diagnostics; the assertion and expected errors have not been weakened. + - **Chapter 25 BOTTOM-on-error backport boundary (2026-09-28).** Cliff requested investigating the earlier arithmetic contracts. In 24, changing ArithNode's non-integer fallback to global BOTTOM breaks the valid `testCoRecur`: the @@ -24,18 +58,10 @@ renumbering chapters is deferred. (`build/arithmetic-top-24.log`). The Java return signature itself has no typed caller dependency. Further BOTTOM work is paused for Cliff's decision about separating forward/unresolved expressions from initialization errors in the - earlier parser. Earlier loads also retain declared types on errors; do not - silently change that contract. The known-null regression is already rejected + earlier parser. The independent Load experiment is recorded above. The + known-null regression is already rejected during parsing in 10-24. Keep this separate from the constructor/memory fixes. -- **Chapter 25 Load TODO, seed `-4628356252269023530`.** The regression wrapper - reaches the default TODO in `LoadNode.idealize`'s memory walk with worklist - seed 456. Reproduced on 2026-09-28 both with the constructor-check changes - and with the original HEAD Parser compiled separately against the current - node edits. This is independent of the missing-constructor diagnostic; - it remains in both the regression and open-failure seed lists. Logs: - `build/ctor-baseline-seed.log` and `build/ctor-check-tests.log`. - - **FunPtrNode lifetime backport, now next after the allocator review.** `return {->42;};` and `return sys.io.p;` fail during relocation in Chapter 22 on all three targets at seed 123. Opto's name-based pruning equates no linked @@ -51,13 +77,14 @@ renumbering chapters is deferred. addresses. Do not enlarge relocation arrays or add an interim address scan. Validate returned pointers by calling them, including library pointers, while still deleting genuinely unused helpers. Keep module/escape machinery in 25. -- **Chapter 25 ARM extern data.** `i32 errno="C"; return 0;`, compiled through - Encoding with ARM/SystemV, reaches `arm.load_str_imm` with register -1 for the - extern-data value stored by `<clinit>`. Both the saved original and final - compiler reproduce it. This is an extern-data lowering/encoding issue, not - the repaired float-store allocation conflict. The historical Bubble Sort - replay allocates successfully but also exposes this failure if ARM encoding - is requested. Do not describe its allocation statistics as runtime coverage. +- **Chapter 25 narrow C return values need ABI extension.** During the direct + data-binding checks, `sys.libc.close(-1)==-1` evaluated false on x86/Cygwin. + Disassembly compares the full RAX to -1 immediately after `close`, without + sign-extending its declared i32 result. C can return -1 in EAX with the high + half zero. This is independent of C data loads (which do sign-extend). The + errno accessor regression calls `close(-1)` and checks errno directly; it + does not claim to validate negative C return values. Investigate the ABI + return normalization separately. ## AOT class initialization: larger independent work @@ -211,6 +238,9 @@ and its separate validation are recorded independently. | Completed work | Scope and evidence retained | |---|---| +| Chapter 25 direct C data bindings | On 2026-09-28, full `make tests` passed after the Field/ExternOffset refactor: 476 ordinary tests plus the fuzzer wrapper (17 seeds). Coverage checks zero layout space for external fields, native shared integer/float storage across C calls, compound updates, source/precompiled imports sharing aliases and preserving storage metadata, ARM/RISC-V emulated address relocation and signed loads, and the errno accessor after a failed native call. Direct bindings remain in 25: 22-24 have different singleton-pointer constant and instruction-selection contracts. Serialization retains magic C0DE; the changed layout requires rebuilding all objects. Only the independent RISC-V fixes below were selected for backport. | +| RISC-V right shifts and pointer relocation | Backported from 25 to 22-24 on 2026-09-28. EvalRisc5 selects arithmetic/logical right shifts correctly; TMPRISC rounds the AUIPC high part to account for signed ADDI low bits. New regressions cover immediate/register shifts and forward/backward relocation boundaries. Both fail before the Chapter 22 fixes (arithmetic shift becomes logical; delta 0x800 lands 4 KB low). Full Make suites passed: 22 (427), 23 (449), 24 (479), each plus its fuzzer wrapper. Baseline suites also passed (425/447/477). | +| Chapter 25 Load fuzzer recheck | At `8212a66f`, seed `-4628356252269023530` passes its direct fuzzPeepsRegression replay (`build/load-seed-recheck.log`). The complete `make fuzzer` wrapper also passes all 17 seeds on 2026-09-28 (one JUnit wrapper test). The seed remains a regression; OPEN_FAILING_SEEDS is empty. No new compiler fix was needed. The older Load TODO report is superseded. | | Arithmetic high results at the SCCP boundary | Chapter 24 integer and float arithmetic now return global TOP for high operands, matching 25. Widened ArithNode.compute's Java return signature to Type; no caller requires TypeInteger. Retained the typed integer fallback for unresolved/error operands, for the parser dependency in the pending queue. The TOP change starts with SCCP in 24; earlier snapshots retain their typed contracts. Full Chapter 24 Make suite passed: 477 tests plus its fuzzer wrapper (`build/arithmetic-top-full-24.log`). | | Required-field and memory-check backport | Chapters 16-24 retain allocation-time checks for inline `new S { ... }` initialization. Chapter 17 had its TOP/unset-field check commented out; restored it. Expanded the existing missing-initializer regression through 24 (introduced its array-field cases in 16), and forwarded the known-null load regression to 10-24 using their existing parse-time diagnostic. Added the fresh-allocation zero-fold monotonicity assertion in 19-24 and the TOP-pointer store-diagnostic guard in 18-23 (already present in 24). Before 18, TOP pointers already produce a diagnostic before the store's pointer cast. Chapter 25's constructor-exit snapshots and implicit-constructor declaration check stay with its separate allocation/constructor model. Its CallEnd return and MemMerge load-dependency corrections already have equivalent behavior in the applicable earlier implementations; storage-type nullability machinery stays with 25. Global error-result type changes are paused in the queue above. | | Chapter 25 required constructor fields | Parser records each explicit constructor's merged exit field types by Var identity and checks them against the complete field list when the struct closes. With no user constructor, declaration defaults are checked as an implicit empty constructor. Never-returning constructors have no completion obligation; nested blocks retain constructor context. Declaration-body assignments to required fields now emit their initializing stores. Load's fresh-allocation zero fold is restored with a monotonicity guard. Existing constructor tests cover partial/early returns, shadowing, later fields, valid defaults and non-returning constructors; older unused-struct/String fixtures now provide constructors, with measured spill expectations updated. Seed `6506797708065910879` is rejected during parsing and passes its regression. On 2026-09-28, all 472 non-fuzzer tests passed across `build/ctor-check-tests.log` and `build/ctor-remaining-tests.log`; the full Make run remains red solely on the independent Load TODO seed above. Allocation-time initialization checking was lost in the August 10 rewrite (`51f1f8f4`); this restores the obligation in the parser without depending on optimized private memory. | |
