From afcf96444366fb4b7c83f938763013cbc360aade Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Fri, 13 Nov 2026 16:17:13 -0800 Subject: Array init syntax New array init syntax; used in a bunch of places. Allows e.g. read-only arrays with not-null entries. Fix bug x86 encoding of mem/cmp/fp compares --- chapter31/README.md | 25 +++ chapter31/docs/31-grammar.md | 11 +- chapter31/docs/examples/Dijkstra.smp | 9 +- chapter31/docs/examples/TicTacToe.smp | 3 +- .../com/seaofnodes/simple/codegen/TypeCheck.java | 6 +- .../com/seaofnodes/simple/node/ArrayInitNode.java | 44 +++++ .../simple/node/cpus/x86_64_v2/MemOpX86.java | 6 +- .../simple/node/cpus/x86_64_v2/StoreX86.java | 3 + .../com/seaofnodes/simple/parser/Allocations.java | 15 +- .../simple/parser/ArrayInitializers.java | 185 +++++++++++++++++++++ .../com/seaofnodes/simple/parser/Functions.java | 3 + .../java/com/seaofnodes/simple/parser/Parser.java | 6 + .../com/seaofnodes/simple/parser/References.java | 2 + .../com/seaofnodes/simple/parser/Statements.java | 1 + chapter31/src/main/smp/sys/aryu8.smp | 11 +- chapter31/src/main/smp/sys/draw/Canvas.smp | 90 ++-------- chapter31/src/main/smp/sys/text.smp | 5 +- .../java/com/seaofnodes/simple/ArrayInitTest.java | 175 +++++++++++++++++++ .../java/com/seaofnodes/simple/Chapter22Test.java | 4 +- .../java/com/seaofnodes/simple/Chapter25Test.java | 2 +- .../java/com/seaofnodes/simple/EscapeFlowTest.java | 31 ++++ .../src/test/java/com/seaofnodes/simple/Eval2.java | 18 +- .../com/seaofnodes/simple/MemoryEncodingTest.java | 25 +++ .../seaofnodes/simple/ParserDiagnosticTest.java | 18 ++ 24 files changed, 589 insertions(+), 109 deletions(-) create mode 100644 chapter31/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java create mode 100644 chapter31/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java create mode 100644 chapter31/src/test/java/com/seaofnodes/simple/ArrayInitTest.java diff --git a/chapter31/README.md b/chapter31/README.md index b82d9612..356b9cdb 100644 --- a/chapter31/README.md +++ b/chapter31/README.md @@ -24,6 +24,31 @@ animation and the reusable `sys.draw.Button` widget: `make -C chapter31 run-clock CTAGS=`. Click its button or press Space to switch between green seven-segment digits and an analog dial. +## Array initializers + +Arrays can be filled with a list or an index-based initializer: + +```simple +val glyphs = new u16[~]{075557,026227,061247}; // Read-only, inferred length. +val squares = new int[10]{int i -> i*i;}; +val names = new u8[~][~2]{"Alice","Bob"}; // Read-only, non-null strings. +``` + +An explicit list length must be a constant equal to the number of elements. +A generator evaluates its length once and runs in index order, once per element +(zero times for an empty array). Its body supports statements and `return`, or +uses its last expression as the result. It can read surrounding locals but +cannot reassign them. Use `int`, `i64`, or `u32` for the index. + +`[~n]` (or `[~]` with a list) makes the array read-only; `val` alone only fixes +the binding. Initializers can construct arrays of non-null references because +every element is supplied before the array becomes accessible. Read-only access +propagates to referenced elements, following the existing view rules. Initialization runs at runtime; +read-only does not require compile-time evaluation. Canvas's font now uses a +read-only `u16` table, and `sys.text.end` uses a generator for its byte copy. + +## Running the examples + ```sh make -C chapter31 libs CTAGS= # Once while online (also: init or lib). make -C chapter31 run-graphical-tictactoe CTAGS= diff --git a/chapter31/docs/31-grammar.md b/chapter31/docs/31-grammar.md index 3083fcc2..f4ca76f1 100644 --- a/chapter31/docs/31-grammar.md +++ b/chapter31/docs/31-grammar.md @@ -185,7 +185,14 @@ argumentList : assignment (',' assignment)* ','? ; // Allocation invokes the declared constructor; inline initializer blocks from // earlier chapters are replaced by constructor arguments. -newExpression : 'new' type ('[' assignment ']' | callSuffix)? ; +newExpression + : 'new' type ('[' '~'? assignment ']' arrayInitializer? | arrayInitializer | callSuffix)? + ; +// With no explicit length, type must end in [] or [~] and the initializer is a list. +arrayInitializer + : '{' (assignment (',' assignment)* ','?)? '}' + | '{' ('int' | 'i32' | 'i64' | 'u32') IDENTIFIER '->' statement* '}' + ; INTEGER_LITERAL : [1-9] DIGIT* | '0' [0-7]* | '0' [xX] HEX_DIGIT+ ; // The f suffix also accepts an integer-shaped token, e.g. 12f. @@ -231,7 +238,7 @@ there is no inheritance chain, AST, or additional compiler pass. | `assignment` through `primaryExpression` | [`Expressions`](../src/main/java/com/seaofnodes/simple/parser/Expressions.java): `parseAssignment`, `parseExpression`, `parseLogical`, `parseBitwise`, `parseEquality`, `parseComparison`, `parseShift`, `parseAddition`, `parseMultiplication`, `parseUnary`, `parsePrimary` | | `postfixExpression`, member selection and updates | `Expressions.parsePostfix` / `parsePostfixMethod` dispatch to [`References.parseField`](../src/main/java/com/seaofnodes/simple/parser/References.java); `References.parseName` handles bare names, implicit receivers, and forward references | | `functionExpression`, `parameterList`, calls, `argumentList` | [`Functions`](../src/main/java/com/seaofnodes/simple/parser/Functions.java): `parseFunction`, `parseConstructor`, shared `parameters` and `arguments`, then `parseBody` | -| `newExpression` | [`Allocations.parseNew`](../src/main/java/com/seaofnodes/simple/parser/Allocations.java); the allocation-helper graph builder follows the expression routines | +| `newExpression`, `arrayInitializer` | [`Allocations.parseNew`](../src/main/java/com/seaofnodes/simple/parser/Allocations.java), [`ArrayInitializers`](../src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java); private initialization, list checks, and generator loops | | `stringExpression`, `interpolation` | [`Strings.parse`](../src/main/java/com/seaofnodes/simple/parser/Strings.java), using `Lexer.stringPart` / `stringFormat`; normal expression parsing resumes inside each interpolation | | Literals, identifiers, comments, token matching | [`Lexer`](../src/main/java/com/seaofnodes/simple/parser/Lexer.java); it also supplies source-location snapshots used by diagnostics and serialized IR | diff --git a/chapter31/docs/examples/Dijkstra.smp b/chapter31/docs/examples/Dijkstra.smp index 768bdd08..a8fcf3da 100644 --- a/chapter31/docs/examples/Dijkstra.smp +++ b/chapter31/docs/examples/Dijkstra.smp @@ -3,13 +3,10 @@ // https://sampleprograms.io/projects/dijkstra/ val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst -> - i64 infinity = (1 << 63)-1; - var dist = new i64[nodes]; + i64 infinity = sys.integer.MAX_I64; var visited = new sys.adt.bitset(); - // All nodes are infinitely far from each other - for( int i=0; i i==src ? 0 : infinity; }; i64 result = -1; int count = 0; diff --git a/chapter31/docs/examples/TicTacToe.smp b/chapter31/docs/examples/TicTacToe.smp index a710e440..1e0f3ca0 100644 --- a/chapter31/docs/examples/TicTacToe.smp +++ b/chapter31/docs/examples/TicTacToe.smp @@ -3,8 +3,7 @@ val main = { u8[~][] args -> sys.io.p("Tic-tac-toe!\nYou are X; the CPU is O. You go first.\n"); sys.io.p("Press a square's number (no Enter needed), or q/Esc to quit.\n"); sys.io.p("Squares:\n 1 | 2 | 3 \n---+---+---\n 4 | 5 | 6 \n---+---+---\n 7 | 8 | 9 \n\n"); - val board = new u8[9]; - for( int i=0; i ' '; }; // Initialize the board // Draw empty board _draw(board); diff --git a/chapter31/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/chapter31/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java index a86cd18a..418edaf6 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java @@ -21,12 +21,14 @@ abstract class TypeCheck { Ary errs = new Ary<>(Node.class); Ary constructs = new Ary<>(ConstructNode.class); Ary views = new Ary<>(InitViewNode.class); + Ary arrays = new Ary<>(ArrayInitNode.class); code._stop.walk( n -> { if( n.err() != null ) errs.add(n); if( n instanceof CLInitNode ) code.add(n); if( n instanceof ConstructNode con ) constructs.add(con); if( n instanceof InitViewNode view ) views.add(view); + if( n instanceof ArrayInitNode array ) arrays.add(array); return null; }); @@ -57,13 +59,15 @@ abstract class TypeCheck { if( !con.isDead() ) con.subsume(con.in(1)); for( InitViewNode view : views ) if( !view.isDead() ) view.subsume(view.in(1)); + for( ArrayInitNode array : arrays ) + if( !array.isDead() ) array.subsume(array.in(2)); } private static int errorRank(Node n) { // Bad memory access originates the error. Missing construction fields // in turn explain generic-scalar arithmetic further downstream. if( n instanceof MemOpNode && noBottomInputs(n) ) return 0; - if( n instanceof ConstructNode ) return 1; + if( n instanceof ConstructNode || n instanceof ArrayInitNode ) return 1; return noBottomInputs(n) ? 2 : 3; } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java b/chapter31/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java new file mode 100644 index 00000000..30f86ac6 --- /dev/null +++ b/chapter31/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.parser.Lexer; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +/** Source checks for array construction and its yielded elements. Erased by + * TypeCheck before serialization and scheduling. */ +public final class ArrayInitNode extends Node { + private final int _count; // List size, -1 for a generator, -2 for a yielded element. + private final Lexer _loc; + public ArrayInitNode(Node ctrl, Node mem, Node ptr, Node len, int count, Lexer loc, Ary values) { + super(ctrl,mem,ptr,len); + _count=count; _loc=loc; + for( Node value : values ) addDef(value); + } + public ArrayInitNode(Node ctrl, Node value, Lexer loc) { + super(ctrl,null,value); + _count=-2; _loc=loc; + } + @Override public String label() { return "ArrayInit"; } + @Override protected ExprPrinter _print1(ExprPrinter p) { return p.p("arrayInit(").n(in(2)).p(")"); } + @Override public Type compute() { return in(0)._type==Type.CONTROL ? in(2)._type : Type.TOP; } + @Override public Node idealize() { return null; } + @Override public Parser.ParseException err() { + if( in(0)._type==Type.XCONTROL ) return null; + if( _count==-2 ) + return TypeMemPtr.mayBePartial(in(2)._type) + ? Parser.error("Array initializer requires fully initialized elements",_loc) : null; + if( !(in(3)._type instanceof TypeInteger len) ) + return Parser.error("Array length must be an integer",_loc); + if( len.isConstant() && (len.value()<0 || len.value()>TypeInteger.U32._max) ) + return Parser.error("Array length is outside the u32 range",_loc); + if( _count>=0 && (!len.isConstant() || len.value()!=_count) ) + return Parser.error("Array initializer has "+_count+" elements; length must equal that constant",_loc); + for( int i=4; i= '4' - ? x86_64_v2.MEM_MASK // value in GPR or XMM - : x86_64_v2.RMASK; // Bytes and shorts in GPR only + // Integer arithmetic and comparisons require a GPR value operand. + // Only StoreX86 has alternate encodings that accept XMM values. + if( i==4 ) return x86_64_v2.RMASK; return null; // Anti-dependence } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java index 9ffa25fc..8dfd31e7 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java @@ -25,6 +25,9 @@ public class StoreX86 extends MemOpX86 { } // Register mask allowed as a result. 0 for no register. @Override public RegMask outregmap() { return null; } + @Override public RegMask regmap(int i) { + return i==4 && _sz>='4' ? x86_64_v2.MEM_MASK : super.regmap(i); + } @Override public void encoding( Encoding enc ) { short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); if( src==-1 ) X86.storeImm(enc,Integer.numberOfTrailingZeros(_sz-'0'),_imm,ptr,idx,_off,_scale); diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Allocations.java index 861f95b6..5c6b5765 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Allocations.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Allocations.java @@ -25,11 +25,20 @@ final class Allocations { } // Parse ary[ length_expr ] if( P.match("[") ) { - if( !t.makeZero().isa(t) ) - throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site); + boolean readonly = P.match("~"); + if( P.match("]") ) { + P.require("{"); + return P.arrays.parse(t,readonly,null,site); + } Node len = P.expressions.parseAssignment().keep(); P.require("]"); + if( P.match("{") ) return P.arrays.parse(t,readonly,len,site); + if( !t.makeZero().isa(t) ) + throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site); + if( readonly ) return new ReadOnlyNode(allocArray(P.types.typeAry(t,true),len)).peephole(); return allocArray(P.types.typeAry(t,false),len); + } else if( t instanceof TypeMemPtr ary && ary._obj.isAry() && P.match("{") ) { + return P.arrays.parse(ary._obj.field("[]")._t,ary._ro,null,site); } else { if( !(t instanceof TypeMemPtr tmp) ) throw Parser.error("Cannot allocate a "+t.str(),site); @@ -101,8 +110,6 @@ final class Allocations { // TODO: Needs runtime check. Node st = P.peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, ConFldOffNode.make(ts,"#"), len.unkeep(), true)); - // TODO: Allow add-on "constructor" to init the array elements - Node prior = P.mem(); Node esc = P.peep(new EscapeNode(lenFld,self,st,prior)); P.mem(P.mergeAlias(prior,lenFld._alias,esc)); diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java new file mode 100644 index 00000000..70eeae1f --- /dev/null +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java @@ -0,0 +1,185 @@ +package com.seaofnodes.simple.parser; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.ScopeNode.Kind; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +/** Array construction keeps the destination private until all elements exist. + * A generator is a scoped, immediately executed loop body, not an escaping closure. */ +final class ArrayInitializers { + private final Parser P; + Frame _frame; + + static final class Frame { + final Kind.Block _kind; + final int _depth; + final Var _value; + ScopeNode _returns; + Frame(Kind.Block kind, int depth, Var value) { + _kind=kind; _depth=depth; _value=value; + } + } + + ArrayInitializers(Parser P) { this.P=P; } + + // Opening brace consumed; an absent length means a list with inferred size. + Node parse(Type elem, boolean readonly, Node len, Lexer site) { + int pos=P.pos(); + Type indexType=P.types.parseType(); + String index=indexType==null ? null : P._lexer.matchId(); + boolean generator=index!=null && P.match("->"); + if( !generator ) P.pos(pos); + if( generator && Parser.KEYWORDS.contains(index) ) + throw Parser.error("Expected an array index name",site); + if( generator && (indexType!=TypeInteger.BOT && indexType!=TypeInteger.I32 && indexType!=TypeInteger.U32) ) + throw Parser.error("Array generator index must be int, i64, or u32",site); + if( generator && len==null ) + throw Parser.error("Array generator requires a length",site); + + Ary values=new Ary<>(Node.class); + if( !generator ) { + if( !P.peek('}') ) do { + values.add(P.conversions.convertExpr(P.expressions.parseAssignment(),elem).keep()); + } while( P.match(",") && !P.peek('}') ); + P.require("}"); + if( len==null ) len=P.con(values.size()).keep(); + } + + TypeStruct ts=P.types.typeAry(elem,readonly); + Field body=ts.field("[]"), length=ts.field("#"); + Node size=new AddNode(ConFldOffNode.make(ts,"[]"), + new ShlNode(null,len,P.con(ts.aryScale())).peephole()).peephole(); + NewNode alloc=new NewNode(ts,P.ctrl(),size).init(); + Node self=new ProjNode(alloc,0,ts.str()).init().keep(); + Node mem=new ProjNode(alloc,1,"#arrayMem").init().keep(); + Node slen=new StoreNode(site,"#",length._alias,TypeInteger.U32,null,mem,self, + ConFldOffNode.make(ts,"#"),len,true).peephole().keep(); + + if( generator ) { + mem=generator(ts,self,mem,len,index,indexType,site); + } else { + for( int i=0; i_frame._depth ) cur._pop(); + cur.balanceLoopFRefs(P._scope); + cur.update(_frame._value,value.unkeep()); + if( _frame._returns==null ) _frame._returns=cur; + else { + _frame._returns.balanceLoopFRefs(cur); + cur.balanceLoopFRefs(_frame._returns); + _frame._returns.ctrl(_frame._returns.mergeScopes(cur,P.loc()).peephole()); + } + P.ctrl(P._code.XCTRL); + return P._code.ZERO; + } + + void checkAssignment(Var var, Lexer site) { + if( _frame!=null && var._idx<_frame._kind._lexSize ) + throw Parser.error("Cannot assign captured local '"+var._name+"' in an array generator",site); + } +} diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java index 5175acc6..8047fabc 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Functions.java @@ -198,6 +198,8 @@ final class Functions { } ReturnNode parseBody( String funName, TypeFunPtr sig, Lexer loc, BitSet finals, String[] ids) { + ArrayInitializers.Frame outerArray = P.arrays._frame; + P.arrays._frame = null; Structs.Constructor outerCtor = P.structs._pendingCtor; P.structs._pendingCtor = null; boolean isInit = FunNode.isInit(funName); @@ -275,6 +277,7 @@ final class Functions { P.statements._breakScope = breakScope; P._scope.pop(); P.construction._frame = outerConstruction; + P.arrays._frame = outerArray; return ret; } diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java index 0cf3c0e7..3e7bb6b8 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Parser.java @@ -21,6 +21,7 @@ public class Parser { final Functions functions = new Functions(this); final Structs structs = new Structs(this); final Allocations allocations = new Allocations(this); + final ArrayInitializers arrays = new ArrayInitializers(this); final References references = new References(this); final Conversions conversions = new Conversions(this); final Construction construction = new Construction(this); @@ -120,6 +121,11 @@ public class Parser { public final Lexer _loc; ParseException( String msg, Lexer loc ) { super(msg); _loc = loc==null ? null : new Lexer(loc); } public String diagnostic() { return _loc==null ? getMessage() : _loc.diagnostic(getMessage()); } + // JUnit and uncaught-exception reporters use these entry points rather + // than the CLI's diagnostic(). Source errors should retain their source + // excerpt and caret there too. getMessage() stays stable for callers. + @Override public void printStackTrace(java.io.PrintStream out) { out.println(diagnostic()); } + @Override public void printStackTrace(java.io.PrintWriter out) { out.println(diagnostic()); } } public Parser(CodeGen code ) { diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java index 6335bad2..f170cae2 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/References.java @@ -120,6 +120,7 @@ final class References { return P.expressions.parsePostfixMethod(P.construction.view(rvalue),P._scope.in(P._scope.update(P._scope.var(selfx),null))); } // Assign-update direct into Scope + P.arrays.checkAssignment(var,site); if( var._final && !P._scope.canInit(var) ) throw Parser.error("Cannot reassign final '"+var._name+"'",site); checkSharedFunction(var); @@ -130,6 +131,7 @@ final class References { } void checkAssignment(Var def, Lexer site) { + if( fieldSelf(def)==null ) P.arrays.checkAssignment(def,site); String name = def._name; // A final field may receive its first value in its constructor. if( P._scope.in(def._idx)._type!=Type.TOP && def._final && diff --git a/chapter31/src/main/java/com/seaofnodes/simple/parser/Statements.java b/chapter31/src/main/java/com/seaofnodes/simple/parser/Statements.java index 707202ab..0c2a25f7 100644 --- a/chapter31/src/main/java/com/seaofnodes/simple/parser/Statements.java +++ b/chapter31/src/main/java/com/seaofnodes/simple/parser/Statements.java @@ -78,6 +78,7 @@ final class Statements { */ private Node parseReturn() { var expr = P.require(P.expressions.parseAssignment(), ";"); + if( P.arrays._frame!=null ) return P.arrays.addReturn(expr); return P.functions.addReturn(expr); } diff --git a/chapter31/src/main/smp/sys/aryu8.smp b/chapter31/src/main/smp/sys/aryu8.smp index 88574522..76d47ec5 100644 --- a/chapter31/src/main/smp/sys/aryu8.smp +++ b/chapter31/src/main/smp/sys/aryu8.smp @@ -69,10 +69,7 @@ val atoi = { u8[~] str -> // Zero-terminate a string for libc, reserving one array element for the zero. { u8[~] -> u8[] } str0 = { u8[~] str -> - if( str# == sys.integer.MAX_U32 ) libc._exit(1); - u8[] !strz = new u8[str#+1]; - u32 len=str#; - ary.copy(strz,0,str,0,len); - strz[str#] = '\0'; - strz; -}; + if( str# == sys.integer.MAX_U32 ) libc._exit(1); + // Copy, terminate and return + return new u8[str#+1]{i64 i -> i - // Only space for upper-case letters in a 3x5 font - if( 'a' <= c <= 'z' ) - c -= 'a'-'A'; - // Five octal digits: one 3-bit row per digit, top to bottom. - if( c==' ' ) return 000000; - if( c=='0' ) return 075557; - if( c=='1' ) return 026227; - if( c=='2' ) return 061247; - if( c=='3' ) return 061216; - if( c=='4' ) return 055711; - if( c=='5' ) return 074616; - if( c=='6' ) return 034757; - if( c=='7' ) return 071222; - if( c=='8' ) return 075757; - if( c=='9' ) return 075716; - if( c=='A' ) return 025755; - if( c=='B' ) return 065656; - if( c=='C' ) return 034443; - if( c=='D' ) return 065556; - if( c=='E' ) return 074647; - if( c=='F' ) return 074644; - if( c=='G' ) return 034553; - if( c=='H' ) return 055755; - if( c=='I' ) return 072227; - if( c=='J' ) return 011152; - if( c=='K' ) return 055655; - if( c=='L' ) return 044447; - if( c=='M' ) return 057755; - if( c=='N' ) return 057775; - if( c=='O' ) return 025552; - if( c=='P' ) return 065644; - if( c=='Q' ) return 025573; - if( c=='R' ) return 065655; - if( c=='S' ) return 034216; - if( c=='T' ) return 072222; - if( c=='U' ) return 055557; - if( c=='V' ) return 055552; - if( c=='W' ) return 055775; - if( c=='X' ) return 055255; - if( c=='Y' ) return 055222; - if( c=='Z' ) return 071247; - if( c=='-' ) return 000700; - if( c=='.' ) return 000002; - if( c==':' ) return 002020; - if( c==',' ) return 000024; - if( c==';' ) return 002024; - if( c=='\'' ) return 022000; - if( c=='"' ) return 055000; - if( c=='`' ) return 042000; - if( c=='/' ) return 011244; - if( c=='\\' ) return 044211; - if( c=='(' ) return 012221; - if( c==')' ) return 042224; - if( c=='[' ) return 032223; - if( c==']' ) return 062226; - if( c=='{' ) return 032423; - if( c=='}' ) return 062126; - if( c=='+' ) return 002720; - if( c=='=' ) return 007070; - if( c=='_' ) return 000007; - if( c=='<' ) return 012421; - if( c=='>' ) return 042124; - if( c=='|' ) return 022222; - if( c=='^' ) return 025000; - if( c=='~' ) return 003600; - if( c=='*' ) return 005250; - if( c=='#' ) return 057575; - if( c=='%' ) return 051245; - if( c=='!' ) return 022202; - if( c=='?' ) return 061202; - return 061202; + return 32<=c<128 ? _glyphs[c-32] : 061202; }; // Pixel coverage for a round-ended segment, separate from traversal. diff --git a/chapter31/src/main/smp/sys/text.smp b/chapter31/src/main/smp/sys/text.smp index a28edf6d..2c4b44ff 100644 --- a/chapter31/src/main/smp/sys/text.smp +++ b/chapter31/src/main/smp/sys/text.smp @@ -5,10 +5,7 @@ val begin = { -> new aryu8(); }; val end = { aryu8 b -> - val bytes = new u8[b.len]; - ary.copy(bytes,0,b.buf,0,b.len); - u8[~] result = bytes; - result; + new u8[~b.len]{ i64 i -> b.buf[i]; }; }; val _pad = { !aryu8 b, i64 n, u8 c -> diff --git a/chapter31/src/test/java/com/seaofnodes/simple/ArrayInitTest.java b/chapter31/src/test/java/com/seaofnodes/simple/ArrayInitTest.java new file mode 100644 index 00000000..6cba5ee3 --- /dev/null +++ b/chapter31/src/test/java/com/seaofnodes/simple/ArrayInitTest.java @@ -0,0 +1,175 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ArrayInitTest { + private static void value(String src, long expected) { + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals(Long.toString(expected),Eval2.eval(code,0,100000)); + } + + private static void bad(String src, String message) { + try { + new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + fail("Expected: "+message); + } catch( com.seaofnodes.simple.parser.Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains(message)); + } + } + + @Test public void lists() { + value("val a=new u16[3]{025755,1,65538}; return a#*100000+a[0]+a[1]+a[2];",311248); + value("val a=new int[]{4,7,9,}; a[1]=2; return a#+a[0]+a[1]+a[2];",18); + value("val a=new int[]{}; return a#;",0); + bad("val a=new int[2]{1}; return a[0];","initializer has 1 elements"); + } + + @Test public void generators() { + value("int n=6; val a=new int[n]{int i -> i*i;}; return a[5]+a#;",31); + value("int base=7; val a=new int[3]{int i -> return base+i;}; return a[2];",9); + value("val a=new int[3]{int i -> if(i==1) return 9; i+1;}; return a[0]+a[1]+a[2];",13); + value("val a=new int[0]{int i -> 1/i;}; return a#;",0); + bad("val a=new int[]{int i -> i;}; return a#;","requires a length"); + bad("val f={-> int x=1; val a=new int[2]{int i -> x++;}; return a[0];}; return f();","captured local"); + } + + @Test public void referencesAndReadonly() { + value("struct S { int x; new S={i64 v -> x=v;}; }; val a=new S[~3]{int i -> new S(i+4);}; return a[2].x;",6); + value("val a=new u8[~]{1,2,3}; return a[1];",2); + value("val names=new u8[~][~2]{\"Alice\",\"Bob\"}; return names[1][0];",66); + value("val a=new int[ ~ ]{4,5}; return a[1];",5); + value("val a=new {i64 -> i64}[]{ {i64 x -> x+1;}, {i64 x -> x*2;} }; return a[1](7);",14); + value("struct S { int x=1; }; val a=new !S[1]{new S}; a[0].x=8; return a[0].x;",8); + bad("struct S { int x=1; }; val a=new !S[~1]{new S}; a[0].x=8; return a[0].x;","final field"); + value("struct S { int x; }; val a=new S?[~2]{u32 i -> null;}; return a[1]==null;",1); + bad("val a=new int[~2]{1,2}; a[0]=3; return a[0];","final field"); + bad("struct S { int x; }; val a=new S[1]{null}; return a#;","declared type"); + } + + @Test public void nativeInitializers() throws Exception { + TestC.runSF(""" + val main = { -> + int n=8; + val a=new u16[~n]{int i -> i*i+1;}; + val b=new u8[]{257,2,3}; + return a[7]==50 && b[0]==1 && a#==8 ? 0 : 1; + }; + ""","arrayInitializers","",-1); + } + + @Test public void nestedBodies() { + value(""" + val f={ -> + int offset=5; + val a=new int[4]{int i -> + int sum=offset; + for(int j=0;j + new int[2]{int j -> i*10+j;}; + }; + return a[2][1]+a[0][0]; + """,21); + value(""" + val a=new int[2]{int i -> + val twice={i64 x -> return x*2;}; + twice(i); + }; + return a[1]; + """,2); + } + + @Test public void orderAndIdentity() throws Exception { + TestC.runSF(""" + struct Counter { + int calls=0; + val next={->calls++;}; + }; + struct Item { + int x; + new Item={i64 v -> x=v;}; + }; + val build_noInline={u32 n, !Counter c -> + new Item[~n]{u32 i -> new Item(c.next());}; + }; + val main={-> + val c=new Counter; + val empty=build_noInline(0,c); + if(empty#!=0 || c.calls!=0) return 1; + val a=build_noInline(4,c); + if(c.calls!=4 || a[0].x!=0 || a[3].x!=3 || a[0]==a[1]) return 2; + val b=new int[]{c.next(),c.next(),c.next()}; + if(b[0]!=4 || b[2]!=6) return 3; + val d=new int[c.next()]{i64 i -> c.next();}; + if(d#!=7 || d[0]!=8 || d[6]!=14 || c.calls!=15) return 4; + return 0; + }; + ""","arrayInitOrder","",-1); + } + + @Test public void diagnostics() { + bad("val f={u32 n -> new int[n]{1,2};}; return f(arg)#;","length must equal that constant"); + bad("val a=new int[2]{u8 i -> i;}; return a[0];","generator index"); + bad("val a=new int[2]{int return -> 0;}; return a[0];","index name"); + bad("val a=new int[2]{int i -> i++;}; return a[0];","final 'i'"); + bad("val f={->int x=0; return new int[2]{int i -> x=i;};}; return f()#;","captured local"); + bad("val a=new int[2]{int i -> break;}; return a[0];","No active loop"); + bad("val a=new int[-1]{}; return a#;","outside the u32 range"); + bad("val a=new int[1.5]{int i -> i;}; return a#;","integer"); + bad("val a=new int[~]{1,2}; val b=a; b[0]++; return b[0];","final field"); + for( String init : new String[]{"self","int i -> self;"} ) + bad("struct C { int x; new C={-> val a=new C[1]{"+init+"}; x=a#;}; }; return new C().x;", + "fully initialized elements"); + } + + @Test public void forwardReferencesAndLoopScopes() { + value(""" + val f={ -> + int sum=0; + for(int k=0;k<3;k++) { + val a=new int[2]{int i -> + if(i) return later(i); + later(3); + }; + if(k==1) continue; + sum+=a[0]+a[1]; + } + return sum; + }; + val later={i64 i -> i*2;}; + return f(); + """,16); + } + + @Test public void optimizationOrder() { + String src=""" + struct Box { int x; new Box={i64 v -> x=v;}; }; + val f={u32 n -> + int base=7; + val a=new Box[~n]{u32 i -> + int x=base; + for(int j=0;j i*i; }; val _sum = { i32[~] is -> // final array int sum=0; for( int i=0; i x+1;}; + val data=new i32[%s(arg&7)]{int i -> i*i;}; + return data#; + """.formatted(access),seed,true).parse(); + int hidden=named(code,"_hidden_noInline").sig().fidx(); + code.driver(CodeGen.Phase.TypeCheck); + TypeMem outside=((TypeTuple)code._start._type).mem(); + assertFalse(XInt.bit(outside._escFs,hidden)); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("4",Eval2.eval(code,4)); + } + } + + @Test public void testArrayGeneratorPublishesItsCallbacks() { + CodeGen code=new CodeGen(""" + val _callback_noInline={i64 x -> x+1;}; + val callbacks=new {i64 -> i64}[~3]{int i -> _callback_noInline;}; + return callbacks; + """).parse(); + FunNode callback=named(code,"_callback_noInline"); + int fidx=callback.sig().fidx(); + code.driver(CodeGen.Phase.TypeCheck); + assertTrue(XInt.bit(((TypeTuple)code._start._type).mem()._escFs,fidx)); + assertFalse(callback.isDead()); + assertTrue(callback.unknownCallers()); + } + // A cofinite outside set must not revive private allocation helpers. @Test public void testOpaqueRoundTrip() { for( long seed : new long[]{1,123,987654321} ) { diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Eval2.java b/chapter31/src/test/java/com/seaofnodes/simple/Eval2.java index 8d0e3d5b..b6228666 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/Eval2.java @@ -388,7 +388,7 @@ public abstract class Eval2 { TypeMem mem = (TypeMem)con._type; Object val = con(mem._t); TypeMemPtr tmp = (TypeMemPtr)esc.self()._type; - int idx = tmp._obj.findAlias(esc.fld()._alias); + int idx = layout(tmp._obj).findAlias(esc.fld()._alias); Object[] fs = (Object[])val(esc.self()); if( tmp._obj.isAry() ) { assert idx==0; // length field @@ -443,10 +443,20 @@ public abstract class Eval2 { private static Object[] classObject(TypeStruct type) { Object[] ptr = CLASSES.get(type._name); - if( ptr==null ) CLASSES.put(type._name,ptr=alloc(type)); + if( ptr==null ) CLASSES.put(type._name,ptr=alloc(layout(type))); return ptr; } + // Imported pointer views may retain parsing-only forward-reference fields. + // Like native field offsets, evaluator slots use the module's final layout. + private static TypeStruct layout(TypeStruct type) { + if( TypeStruct.startsClzPrefix(type._name) ) { + CompUnit cu=CODE._compunits.get(type._name.substring(6).replace('.','/')); + if( cu!=null ) return cu._clz; + } + return type; + } + private static Object not( NotNode not ) { Object n = val(not.in(1)); return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 ) @@ -465,7 +475,7 @@ public abstract class Eval2 { TypeMemPtr tmp = (TypeMemPtr)ld.ptr()._type; int idx = tmp._obj.isAry() ? offToIdx(x(ld.off()),tmp._obj) - : tmp._obj.find(ld._name); + : layout(tmp._obj).find(ld._name); return fs[idx]; } @@ -476,7 +486,7 @@ public abstract class Eval2 { Object[] fs = (Object[])val(st.ptr()); int idx = tmp._obj.isAry() ? offToIdx(x(st.off()),tmp._obj) - : tmp._obj.find(st._name); + : layout(tmp._obj).find(st._name); Object val = val(st.val()); // Idealization can remove conversions already performed by the store. Type type = st.declaredType(); diff --git a/chapter31/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/chapter31/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java index adeba136..6d81af85 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java @@ -8,6 +8,31 @@ import static org.junit.Assert.*; // Execution regressions are separate from the fixed spill-measurement cohorts. public class MemoryEncodingTest { + @Test public void testIntegerMemoryOperandsAcrossCalls() throws java.io.IOException { + TestC.runSF(""" + val touch_noInline={i64[] a, i64 i -> a[i]+=1;}; + val scan_noInline={i64[] a, i64 limit, i64 delta -> + int below=0, above=0; + for(int i=0;i + val a=new i64[]{0x7ffffffffffffffe,-2,0}; + if(scan_noInline(a,0x7fffffffffffffff,0)!=20) return 1; + if(a[0]!=0x7fffffffffffffff || a[1]!=-1 || a[2]!=1) return 2; + val b=new i64[]{-3,-2,0}; + if(scan_noInline(b,-1,0x100000000)!=11) return 3; + if(b[0]!=0xfffffffe || b[1]!=0xffffffff || b[2]!=0x100000001) return 4; + return 0; + }; + ""","integerMemoryAcrossCalls","",-1); + } + @Test public void testLoopLoadSearch() { String[] sources = { """ diff --git a/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java index ef26a943..a1253d05 100644 --- a/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java +++ b/chapter31/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java @@ -134,6 +134,24 @@ public class ParserDiagnosticTest { +"\n return x;\n ^",error.diagnostic()); } + @Test public void testJUnitReportsSourceDiagnostic() { + var error=rejected("val dist=new i64[3]{int i -> i;}\ndist[0]=0;"); + String expected="Test.smp:2:1: error: Syntax error, expected `;` but found `dist`" + +"\n dist[0]=0;\n ^"+System.lineSeparator(); + var test=org.junit.runner.Description.createTestDescription(getClass(),"missingSemicolon"); + assertEquals(expected,new org.junit.runner.notification.Failure(test,error).getTrace()); + + var bytes=new java.io.ByteArrayOutputStream(); + error.printStackTrace(new java.io.PrintStream(bytes,true,StandardCharsets.UTF_8)); + assertEquals(expected,bytes.toString(StandardCharsets.UTF_8)); + assertEquals("Syntax error, expected `;` but found `dist`",error.getMessage()); + + // Compiler implementation failures must still include Java frames. + String trace=new org.junit.runner.notification.Failure(test,new IllegalStateException("compiler bug")).getTrace(); + assertTrue(trace,trace.contains("java.lang.IllegalStateException: compiler bug")); + assertTrue(trace,trace.contains("ParserDiagnosticTest.testJUnitReportsSourceDiagnostic")); + } + @Test public void testEOF() { var error=rejected("return 1"); assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`" -- cgit v1.2.3