diff options
| author | Cliff Click <cliffc@acm.org> | 2026-08-10 11:11:18 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-08 14:10:02 -0800 |
| commit | 44505aa5b75b1046f13f2bc2a52fe0b16d975838 (patch) | |
| tree | cc558485c807ac3842cd73943cf5e294c4a307de /src/main/java | |
| parent | 2560096fb4636263798ae56c88687973c72cdf6e (diff) | |
| download | Simple-linear-chapter30.tar.gz Simple-linear-chapter30.zip | |
Chapter 30: Text Formatting and Diagnosticslinear-chapter30
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com>
Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com>
Co-authored-by: Symious <14933944+symious@users.noreply.github.com>
Co-authored-by: Jun Kawasaki <jun784@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/main/java')
37 files changed, 503 insertions, 151 deletions
diff --git a/src/main/java/com/seaofnodes/simple/README.md b/src/main/java/com/seaofnodes/simple/README.md index 613f66b4..af86f21e 100644 --- a/src/main/java/com/seaofnodes/simple/README.md +++ b/src/main/java/com/seaofnodes/simple/README.md @@ -2,7 +2,7 @@ | Directory | Contents | | --- | --- | -| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter29/29-grammar.md). | +| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter30/30-grammar.md). | | `node/` | Sea-of-Nodes IR nodes: graph edges, type computation, and local peephole rewrites. | | `node/cpus/` | Machine-specific nodes and instruction selection support for `arm`, `riscv`, and `x86_64_v2`. | | `codegen/` | Compilation driver and passes: module loading, optimization, scheduling, register allocation, serialization, and machine-code emission. `IterPeeps` runs the peephole worklist. | diff --git a/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java index 0b314978..8d0237ea 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java +++ b/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java @@ -16,7 +16,7 @@ abstract public class BuildLRG { // true if no hard-conflicts, although we still might not color. public static boolean run(int round, RegAlloc alloc) { FunNode fun = null; - for( Node bb : alloc._code._cfg ) { + for( Node bb : alloc._cfg ) { if( bb instanceof FunNode fun2 ) fun = fun2; for( Node n : bb.outs() ) { diff --git a/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java b/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java index 689ceceb..dbde2f8b 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java +++ b/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java @@ -13,7 +13,7 @@ abstract public class Coalesce { // Walk all the splits, looking for coalesce chances boolean progress = false; - for( CFGNode bb : alloc._code._cfg ) { + for( CFGNode bb : alloc._cfg ) { outer: for( int j=0; j<bb.nOuts(); j++ ) { Node n = bb.out(j); diff --git a/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/src/main/java/com/seaofnodes/simple/codegen/IFG.java index d15e66da..6ad7c0ad 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/IFG.java +++ b/src/main/java/com/seaofnodes/simple/codegen/IFG.java @@ -72,7 +72,7 @@ abstract public class IFG { WORK.clear(); // Last block has nothing live out - for( CFGNode bb : alloc._code._cfg ) + for( CFGNode bb : alloc._cfg ) if( bb.blockHead()) WORK.push( bb ); @@ -164,6 +164,10 @@ abstract public class IFG { // Also record use-side spills for biased coloring. // Then make all inputs live. for( int i=1; i<n.nIns(); i++ ) { + // Metadata and scheduling dependencies do not consume registers. + // In particular, a static call's hidden class argument has no ABI + // location and must not keep its constant live across other calls. + if( ((MachNode)n).regmap(i)==null ) continue; Node def = n.in(i); if( def==null ) continue; LRG lrg1 = alloc.lrg(def); @@ -480,7 +484,7 @@ abstract public class IFG { private static int functionArea(FunNode fun, RegAlloc alloc) { int area=0; - for( CFGNode bb : alloc._code._cfg ) + for( CFGNode bb : alloc._cfg ) if( bb.fun()==fun ) area += bb.nOuts(); return area; diff --git a/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java b/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java index 5fc002a1..2595b4ad 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java +++ b/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java @@ -85,13 +85,7 @@ class InstSelect { } // 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)); - } + if( n instanceof MemOpNode mem ) Machine.externalAddress(mem); map.put(n, x=_code._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/src/main/java/com/seaofnodes/simple/codegen/Machine.java b/src/main/java/com/seaofnodes/simple/codegen/Machine.java index 3eee8fc3..6ccf6b19 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/Machine.java +++ b/src/main/java/com/seaofnodes/simple/codegen/Machine.java @@ -3,6 +3,7 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; abstract public class Machine { // Human readable machine name. Something like "x86-64" or "arm" or "risc5" @@ -46,6 +47,17 @@ abstract public class Machine { // Instruction select from ideal nodes public abstract Node instSelect( Node n ); + // Lower external fields before selecting a memory operation, including + // loads absorbed into another instruction rather than selected separately. + public static void externalAddress(MemOpNode mem) { + if( mem.off() instanceof ExternOffsetNode off ) { + // Retain the declaring type for load/store width selection. + TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type; + mem._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern)); + mem._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO)); + } + } + /** Stack Slot Numbering RA gives all registers a number, starting at 0. Stack slots continue this diff --git a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java index 5f8a0ec0..3306ff8f 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java +++ b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java @@ -140,7 +140,7 @@ public abstract class ParseAll { fref._name = TypeStruct.addClzPrefix(xcunit._cname); cunit.addDep(xcunit); } else { - throw new RuntimeException("Undefined name '" + fref._name +"'"); + throw fref.err(); } } catch( IOException ioe ) { throw new RuntimeException(ioe); @@ -297,14 +297,6 @@ public abstract class ParseAll { Parser.TYPES.put(cunit._cname,ptr._obj); } - // Symbol not found in source code (or we would not be here). - // Search the module for the fref - public static CompUnit findCompUnitOrThrow(CodeGen code, CompUnit cu, String symbol) { - CompUnit cu2 = findCompUnit(code,cu,symbol); - if( cu2 != null ) return cu2; - throw new RuntimeException("Undefined name '" + symbol +"'"); - } - public static CompUnit findCompUnit(CodeGen code, CompUnit cu, String symbol) { // Symbol not found in source code (or we would not be here). // Search the module for the fref @@ -328,8 +320,15 @@ public abstract class ParseAll { // However if A/B contains "Y", the search would be for A.B.Y then A.Y both of // which do not exist. private static CompUnit findCUnitModule( CodeGen code, CompUnit cunit, String symbol ) throws IOException { - if( cunit == null ) // Ran off top of module scope? - return null; // Failed search + if( cunit == null ) { + // The module root is also a namespace. In particular, a root + // client can resolve sys.text from source, without installed .o's. + String fname=symbol.replace('.','/'); + if( !new File(code._modDir+"/"+fname+".smp").exists() ) return null; + CompUnit root=makeCUnit(code,null,fname); + if( root._clz==null && WORK.find(root)==-1 ) enqueue(root); + return root; + } // Check self if( cunit._name.equals(symbol) ) diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java index f1aa18ef..3991cd48 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java +++ b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java @@ -54,6 +54,8 @@ public class RegAlloc { // Top-level program graph structure final CodeGen _code; + // Scheduled blocks of the function currently being allocated. + Ary<CFGNode> _cfg; public int _spills, _spillScaled; @@ -161,7 +163,7 @@ public class RegAlloc { } // ----------------------- - RegAlloc( CodeGen code ) { _code = code; } + RegAlloc( CodeGen code ) { _code = code; _cfg = code._cfg; } public void regAlloc() { // Insert callee-save registers @@ -186,18 +188,41 @@ public class RegAlloc { // Expensive assert: useful when chasing cross-function allocator edges. //assert verifyFunctionLocalEdges(); - // Top driver: repeated rounds of coloring and splitting. - byte round=0; - while( !graphColor(round) ) { - split(round); - if( round >= 7 ) // Really expect to be done soon - throw new IllegalStateException("Register allocation made no progress after eight rounds"); - round++; + // Each function owns its retry budget. Keep completed assignments while + // reusing the live-range and interference scratch space for the next. + IdentityHashMap<Node,LRG> colored = new IdentityHashMap<>(); + _cfg = new Ary<>(CFGNode.class); + for( CFGNode bb : _code._cfg ) { + if( bb instanceof FunNode && !_cfg.isEmpty() ) { + allocateFunction(); + colored.putAll(_lrgs); + _cfg.clear(); + } + _cfg.add(bb); } + if( !_cfg.isEmpty() ) { + allocateFunction(); + colored.putAll(_lrgs); + } + _lrgs.clear(); + _lrgs.putAll(colored); + _cfg = _code._cfg; postColor(); // Remove no-op spills _done = true; } + private void allocateFunction() { + _deferredLoopSelf.clear(); + for( byte round=0; !graphColor(round); round++ ) { + split(round); + if( round >= 7 ) { + FunNode fun = _cfg.at(0).fun(); + throw new IllegalStateException("Register allocation failed after eight rounds in "+ + (fun==null ? "<root>" : fun.label())); + } + } + } + boolean verifyFunctionLocalEdges() { for( CFGNode bb : _code._cfg ) for( Node use : bb._outputs ) { @@ -477,7 +502,10 @@ public class RegAlloc { split.killOrdered(); } for( int i=1; i<(deferred.containsKey(phi) ? 2 : phi.nIns()); i++ ) - if( i==1 || sharedPhiInput(phi,i) ) + // Backedges may be joined indirectly through two-address + // chains, even when their input nodes are distinct. After + // the cold-only attempt, isolate those inputs as well. + if( i==1 || phi.region() instanceof LoopNode || sharedPhiInput(phi,i) ) insertBefore(phi,i,"use/self/phi",round,lrg); } // Split before two-address ops which extend the live range @@ -653,9 +681,8 @@ public class RegAlloc { return; } makeSplit(def,kind,round,lrg,umask).insertBefore(n, i); - // Skip split-of-split same block - if( skip && def instanceof SplitNode && cfg==def.cfg0() ) - n.in(i).setDefOrdered(1,def.in(1)); + // Keep the def-side transfer: a kill between the two copies may be + // exactly why this range split. Coalescing can join them when safe. } void insertBefore(Node n, int i, String kind, byte round, LRG lrg) { insertBefore(n,i,kind,round,lrg,true); diff --git a/src/main/java/com/seaofnodes/simple/codegen/SourceError.java b/src/main/java/com/seaofnodes/simple/codegen/SourceError.java index f2f46146..5be0585f 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/SourceError.java +++ b/src/main/java/com/seaofnodes/simple/codegen/SourceError.java @@ -16,6 +16,10 @@ public abstract class SourceError { source=cu._src!=null ? cu._src.getBytes(StandardCharsets.UTF_8) : file==null ? null : Files.readAllBytes(Path.of(file)); } catch( IOException ex ) { source=null; } + return format(name,source,pos,message); + } + + public static String format(String name, byte[] source, int pos, String message) { if( source==null || pos<0 || pos>source.length ) return name+": error: "+message+" (source unavailable at byte offset "+pos+")"; @@ -28,22 +32,24 @@ public abstract class SourceError { int point=Math.min(pos,end); String before=new String(source,start,point-start,StandardCharsets.UTF_8); String text=new String(source,start,end-start,StandardCharsets.UTF_8); - int column=before.codePointCount(0,before.length())+1; - SB sb=new SB().p(name).p(':').p(line).p(':').p(column).p(": error: ").p(message).p("\n "); + SB excerpt=new SB(); int width=0, caret=0; for( int i=0; i<text.length(); ) { if( i==before.length() ) caret=width; int ch=text.codePointAt(i); if( ch=='\t' ) { int spaces=8-(width%8); - for( int j=0; j<spaces; j++ ) sb.p(' '); + for( int j=0; j<spaces; j++ ) excerpt.p(' '); width+=spaces; - } else { sb.p(new String(Character.toChars(ch))); width++; } + } else { excerpt.p(new String(Character.toChars(ch))); width++; } i+=Character.charCount(ch); } if( before.length()==text.length() ) caret=width; + // Emacs compilation mode defaults to display columns, with 8-column tabs. + SB sb=new SB().p(name).p(':').p(line).p(':').p(caret+1).p(": error: ").p(message) + .p("\n ").p(excerpt.toString()); sb.p("\n "); - for( int i=0; i<caret; i++ ) sb.p('-'); + for( int i=0; i<caret; i++ ) sb.p('_'); return sb.p('^').toString(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/CallNode.java b/src/main/java/com/seaofnodes/simple/node/CallNode.java index 91d27330..ac5e4eee 100644 --- a/src/main/java/com/seaofnodes/simple/node/CallNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -242,7 +242,7 @@ public class CallNode extends CFGNode { if( !tfp.notNull() ) return Parser.error( "Might be null calling "+tfp, _loc); if( nargs() != tfp.nargs() ) - return Parser.error( "Expecting "+tfp.nargs()+" arguments, but found "+nargs(), _loc); + return Parser.error( "Expecting "+(tfp.nargs()-1)+" arguments, but found "+(nargs()-1), _loc); // Check for args for( int i=0; i<tfp.nargs(); i++ ) diff --git a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java index 09e5eac8..e49162f4 100644 --- a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java +++ b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java @@ -49,7 +49,7 @@ public class FldOffNode extends TypeNode { @Override public Parser.ParseException err() { if( in(1)._type instanceof TypeMemPtr tmp && tmp._obj._fref ) - return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null); + return Parser.error("Unknown struct type '"+tmp._obj._name+"'",_loc); return null; } } diff --git a/src/main/java/com/seaofnodes/simple/node/FormatNode.java b/src/main/java/com/seaofnodes/simple/node/FormatNode.java new file mode 100644 index 00000000..d34b342d --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/FormatNode.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.parser.Lexer; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.*; + +/** Resolve default formatting through normal type flow, including forward + * references. Valid programs fold this selector before serialization/codegen. */ +public final class FormatNode extends Node { + private final Lexer _site; + public FormatNode(Node value, Node ints, Node floats, Node strings, Lexer site) { + super(null,value,ints,floats,strings); + _site=site; + } + @Override public String label() { return "Format"; } + private int choice() { + Type t=in(1)._type; + if( t instanceof TypeInteger ) return 2; + if( t instanceof TypeFloat ) return 3; + if( t instanceof TypeMemPtr p && !p.nullable() && p._obj.isAry() && + p._obj._name.equals("[]u8") ) return 4; + return 0; + } + @Override public Type compute() { + if( in(1)._type.isHigh() ) return Type.TOP; + int c=choice(); + return c==0 ? in(2)._type.meet(in(3)._type).meet(in(4)._type) : in(c)._type; + } + @Override public Node idealize() { int c=choice(); return c==0 ? null : in(c); } + @Override public Parser.ParseException err() { + return Parser.error("Cannot format "+in(1)._type+"; expected an integer, float, or byte string",_site); + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java index 85ab29f9..eb78ed1f 100644 --- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java +++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -395,6 +395,9 @@ public class LoadNode extends MemOpNode { // load will need to zero/sign-extend. private Node extend(Node val) { if( !(declaredType() instanceof TypeInteger ti) ) return val; + // A typed load can already know its width while the stored value is + // still unresolved. Do not replace it with an untyped shift/and chain. + if( !(val._type instanceof TypeInteger) ) { addDep(val); return null; } if( ti._min==0 ) // Unsigned return new AndNode(null,val,con(ti._max)); // Signed extension diff --git a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 1130cf44..80d2aa0a 100644 --- a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -638,6 +638,7 @@ public class ScopeNode extends MemMergeNode { int i = _guards._len; while( i > 0 ) { Node cast = _guards.at(--i); + if( cast instanceof FunNode ) break; // Function boundary if( cast instanceof CFGNode ) continue; // Marker between guard sets Node xpred = _guards.at(--i); if( xpred == pred ) diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java index 814a82c2..da9afe71 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java @@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.SB; -// Convert a narrow C return register to Simple's full-width integer value. +// Normalize narrow parameters and C return values to full-width integers. public class ExtendARM extends MachConcreteNode { final int _bits; final boolean _signed; ExtendARM(ProjNode prj, TypeInteger ret) { - super(prj); - _inputs.set(0,null); - _inputs.add(new ProjARM(prj)); + this(prj,new ProjARM(prj),ret); + } + ExtendARM(ParmNode parm, TypeInteger decl) { + this(parm,new ParmARM(parm),decl); + } + private ExtendARM(Node ideal, Node value, TypeInteger ret) { + super(ideal); + _inputs.clear(); + _inputs.add(null); + _inputs.add(value); _bits = 8 << ret.log_size(); _signed = ret._min < 0; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java index 3c965da5..ee7b0fc3 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java @@ -332,7 +332,8 @@ public class arm extends Machine { case NewNode nnn -> new NewARM(nnn); case NotNode not -> new NotARM(not); case OrNode or -> or(or); - case ParmNode parm -> new ParmARM(parm); + case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3 + ? new ExtendARM(parm,ti) : new ParmARM(parm); case BulkMemPhiNode phi -> new BulkMemPhiNode(phi); case MemPhiNode phi -> new MemPhiNode(phi); case PhiNode phi -> new PhiNode(phi); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java index 5bd3ec1c..11182133 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java @@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.SB; -// Convert a narrow C return register to Simple's full-width integer value. +// Normalize narrow parameters and C return values to full-width integers. public class ExtendRISC extends MachConcreteNode { final int _bits; final boolean _signed; ExtendRISC(ProjNode prj, TypeInteger ret) { - super(prj); - _inputs.set(0,null); - _inputs.add(new ProjRISC(prj)); + this(prj,new ProjRISC(prj),ret); + } + ExtendRISC(ParmNode parm, TypeInteger decl) { + this(parm,new ParmRISC(parm),decl); + } + private ExtendRISC(Node ideal, Node value, TypeInteger ret) { + super(ideal); + _inputs.clear(); + _inputs.add(null); + _inputs.add(value); _bits = 8 << ret.log_size(); _signed = ret._min < 0; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java index 0195542c..378955a5 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java @@ -236,7 +236,8 @@ public class riscv extends Machine { case NewNode nnn -> nnn(nnn); case NotNode not -> new NotRISC(not); case OrNode or -> or(or); - case ParmNode parm -> new ParmRISC(parm); + case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3 + ? new ExtendRISC(parm,ti) : new ParmRISC(parm); case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); case MemPhiNode phi -> new MemPhiNode(phi); case PhiNode phi -> new PhiNode(phi); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java index 1a6ae123..9f4da6b8 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java @@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.SB; -// Convert a narrow C return register to Simple's full-width integer value. +// Normalize narrow parameters and C return values to full-width integers. public class ExtendX86 extends MachConcreteNode { final int _bits; final boolean _signed; ExtendX86(ProjNode prj, TypeInteger ret) { - super(prj); - _inputs.set(0,null); - _inputs.add(new ProjX86(prj)); + this(prj,new ProjX86(prj),ret); + } + ExtendX86(ParmNode parm, TypeInteger decl) { + this(parm,new ParmX86(parm),decl); + } + private ExtendX86(Node ideal, Node value, TypeInteger ret) { + super(ideal); + _inputs.clear(); + _inputs.add(null); + _inputs.add(value); _bits = 8 << ret.log_size(); _signed = ret._min < 0; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java index 3dc93bf6..3d71b4ea 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java @@ -306,7 +306,8 @@ public class x86_64_v2 extends Machine { case NewNode nnn -> new NewX86(nnn); case NotNode not -> new NotX86(not); case OrNode or -> or(or); - case ParmNode parm -> new ParmX86(parm); + case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3 + ? new ExtendX86(parm,ti) : new ParmX86(parm); case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); case MemPhiNode phi -> new MemPhiNode(phi); case PhiNode phi -> new PhiNode(phi); @@ -555,6 +556,7 @@ public class x86_64_v2 extends Machine { private static int off, scale, imm; private static Node idx, val; private <N extends MemOpNode > N address(N mop) { + externalAddress(mop); off = scale = imm = 0; // Reset idx = val = null; Node base = mop.ptr(); diff --git a/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/src/main/java/com/seaofnodes/simple/parser/Allocations.java index be68e10e..861f95b6 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Allocations.java +++ b/src/main/java/com/seaofnodes/simple/parser/Allocations.java @@ -14,28 +14,30 @@ final class Allocations { Parse an allocation */ Node parseNew() { + P._lexer.skipWhiteSpace(); + Lexer site=P.loc(); Type t = P.types.parseType(); if( t==null ) { String tname = P._lexer.matchId(); if( tname==null || Parser.KEYWORDS.contains(tname) ) - throw P.error("Expected a type"); + throw Parser.error("Expected a type after 'new'",site); t = P.types.forwardRefType(tname); } // Parse ary[ length_expr ] if( P.match("[") ) { if( !t.makeZero().isa(t) ) - throw P.error("Cannot allocate a non-nullable, since arrays are always zero/null filled"); + throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site); Node len = P.expressions.parseAssignment().keep(); P.require("]"); return allocArray(P.types.typeAry(t,false),len); } else { if( !(t instanceof TypeMemPtr tmp) ) - throw P.error("Cannot allocate a "+t.str()); - return allocStruct(tmp._obj); + throw Parser.error("Cannot allocate a "+t.str(),site); + return allocStruct(tmp._obj,site); } } - private Node allocStruct(TypeStruct ts) { + private Node allocStruct(TypeStruct ts, Lexer site) { Ary<Node> ctorArgs = constructorArgs(); // A field declaration can leave a short forward type name in TYPES. // Allocation happens in lexical context, so resolve that short name @@ -45,7 +47,7 @@ final class Allocations { if( resolved instanceof TypeStruct rts ) ts = rts; TypeStruct clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(typeName)); if( clz==null ) - throw P.error("Unknown struct type '" + ts._name + "'"); + throw Parser.error("Unknown struct type '" + ts._name + "'",site); Field afld = clz.field("<new>"); Node alloc; if( afld != null ) { @@ -56,7 +58,7 @@ final class Allocations { : new FunPtrNode(tfp,P._code._start,fun.ret()).peephole(); } else { if( !ts._fref ) - throw P.error("Constructor arguments for '" + ts._name + "' but no constructor is defined"); + throw Parser.error("Constructor arguments for '" + ts._name + "' but no constructor is defined",site); Type[] sig = new Type[ctorArgs._len]; for( int i = 0; i<ctorArgs._len; i++ ) sig[i] = ctorArgs.at(i)._type; @@ -111,12 +113,12 @@ final class Allocations { return self.unkeep(); } - Node newString(String s) { + Node newString(byte[] s) { TypeStruct ts = P.types.typeAry(TypeInteger.U8,true); int lenAlias = ts.field("#" )._alias; int elemAlias = ts.field("[]")._alias; TypeConAryB body = TypeConAryB.make(s); - TypeInteger slen = TypeInteger.constant(s.length()); + TypeInteger slen = TypeInteger.constant(s.length); // Make a TMP, not-null (byte)2, singleton (true) (requires text // strings are hash-interned), with a TypeStruct having a constant // array body. diff --git a/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java b/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java index caa73076..6c626669 100644 --- a/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java +++ b/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java @@ -14,6 +14,8 @@ final class ControlFlow { // Parse a conditional expression, merging results. Node conditional( Node pred, String fside ) { + Lexer site=P.loc(); + if( !fside.equals("else") ) site._position-=fside.equals(":") ? 1 : 2; // IfNode takes current control and predicate pred.keep(); Node ifNode = new IfNode(P.ctrl(), pred).peephole(); @@ -88,7 +90,7 @@ final class ControlFlow { Node ret = P.peep(new PhiNode("",r,lhs.unkeep(),rhs.unkeep())); // Immediately fail e.g. `arg ? 7 : ptr` ParseException err; - if( !fside.equals("else") && (err = ret.err()) !=null ) throw err; + if( !fside.equals("else") && (err = ret.err()) !=null ) throw Parser.error(err.getMessage(),site); ret.keep(); r.peephole(); return ret.unkeep(); diff --git a/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/src/main/java/com/seaofnodes/simple/parser/Declarations.java index 91955197..13fa59b8 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Declarations.java +++ b/src/main/java/com/seaofnodes/simple/parser/Declarations.java @@ -47,15 +47,17 @@ final class Declarations { boolean inferType = t==Type.TOP || t==Type.BOTTOM; boolean xfinal = bindingFinal(t); Lexer loc = P.loc(); - String name = P.requireId(); + String name = P.requireId(P._scope.klast() instanceof Kind.Func fun && FunNode.isInit(fun._name)); // Optional initializing expression follows boolean fld_final = false; // Field is final, but not deeply final boolean uninit = false; // Required non-null constructor field Node expr; if( P.match("=") ) { + P._lexer.skipWhiteSpace(); + Lexer initializer=P.loc(); if( isExternDecl() ) { if( !(t instanceof TypeFunPtr) ) { - Node ptr = externData(name,t); + Node ptr = externData(name,t,loc); // The lexical binding is a fixed address; its contents are mutable. if( !P._scope.define(name,t,true,ptr,loc) ) throw Parser.error("Redefining name '"+name+"'",loc); @@ -72,8 +74,12 @@ final class Declarations { // var/val, then type comes from expression if( inferType ) { if( expr._type==Type.NIL ) - throw P.error("a not-null/non-zero expression"); + throw Parser.error("Cannot infer a type from null; declare a nullable type explicitly",initializer); t = expr._type; + // A typed expression may still await a forward callee's flow + // type (e.g. interpolation while sys.text is parsed). Its + // checked conversion already supplies a binding/storage type. + if( t==Type.BOTTOM && expr instanceof ConvertNode convert ) t=convert.dst(); if( !xfinal ) t = Conversions.mutableInferredType(t); // Instance fields may change until the constructor finishes. // Keep the declared family/access, not the default's constant @@ -133,8 +139,7 @@ final class Declarations { // Just after parsing "type foo = " can parse `"C"` private boolean isExternDecl( ) { int old = P.pos(); - String s = P._lexer.parseString(); - if( "C".equals(s) ) return true; + if( P.match("\"C\"") ) return true; P.pos(old); return false; } @@ -151,14 +156,14 @@ final class Declarations { // 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) { + private Node externData(String name, Type t, Lexer site) { if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) ) - throw P.error("C data binding requires an integer or float type"); + throw Parser.error("C data binding requires an integer or float type",site); String cname = ("C:"+name).intern(); TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,P._code.externAlias(name),false,true)); Type old = Parser.TYPES.putIfAbsent(cname,ts); if( old!=null && old!=ts ) - throw P.error("Conflicting types for C data '"+name+"'"); + throw Parser.error("Conflicting types for C data '"+name+"'",site); return P.con(TypeMemPtr.make((byte)2,ts,true)); } } diff --git a/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/src/main/java/com/seaofnodes/simple/parser/Expressions.java index 93c07d8a..247d974e 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Expressions.java +++ b/src/main/java/com/seaofnodes/simple/parser/Expressions.java @@ -15,6 +15,8 @@ final class Expressions { /** Parse: [name '='] expr */ Node parseAssignment() { + P._lexer.skipWhiteSpace(); + Lexer site=P.loc(); int old = P.pos(); String name = P._lexer.matchId(); // Just a plain expression, no assignment. @@ -24,13 +26,16 @@ final class Expressions { // Find variable to update Var def = P._scope.lookup(name); - if( def==null ) + if( def==null ) { + P.pos(old); + P._lexer.skipWhiteSpace(); throw P.error("Undefined name '" + name + "'"); + } if( def._extern!=0 ) // Parse the C binding as a memory lvalue { P.pos(old); return parseExpression(); } - P.references.checkAssignment(def); + P.references.checkAssignment(def,site); return P.references.assign(def,parseAssignment()); } @@ -117,6 +122,8 @@ final class Expressions { Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool // Stacked compares? + P._lexer.skipWhiteSpace(); + Lexer operator=P.loc(); int dir0 = parseCompDir(); if( dir0 == 0 ) { rhs.unkill(); @@ -137,7 +144,7 @@ final class Expressions { // compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc. while( dir0 != 0 ) { if( Math.abs(dir) != Math.abs(dir0) ) - throw P.error("Mixing relational directions in a chained relational test"); + throw Parser.error("Mixing relational directions in a chained relational test",operator); // True side parses next arm of test P.ctrl(ifT.unkeep()); lhs = rhs; // Reuse the previous RHS exactly once @@ -150,6 +157,8 @@ final class Expressions { P.ctrl(ifF); fail.mergeScopes(P._scope.dup(), P.loc()).peephole(); // Check next stacked compare + P._lexer.skipWhiteSpace(); + operator=P.loc(); dir0 = parseCompDir(); } rhs.unkill(); @@ -185,9 +194,11 @@ final class Expressions { private Node parseShift() { Node lhs = parseAddition(); while( true ) { - if( P.match("<<") ) lhs = new ShlNode(P.loc(),lhs,null); - else if( P.match(">>>")) lhs = new ShrNode(P.loc(),lhs,null); - else if( P.match(">>") ) lhs = new SarNode(P.loc(),lhs,null); + P._lexer.skipWhiteSpace(); + Lexer site=P.loc(); + if( P.match("<<") ) lhs = new ShlNode(site,lhs,null); + else if( P.match(">>>")) lhs = new ShrNode(site,lhs,null); + else if( P.match(">>") ) lhs = new SarNode(site,lhs,null); else break; lhs.setDef(2,parseAddition()); ParseException err; @@ -251,12 +262,14 @@ final class Expressions { int old = P.pos(); if( P.match("--") || P.match("++") ) { int delta = P._lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec + P._lexer.skipWhiteSpace(); int start = P.pos(); + Lexer site=P.loc(); String name = P._lexer.matchId(); Var var = name==null ? null : P._scope.lookup(name); if( var!=null && !(var.type() instanceof TypeMemPtr) ) { if( var._extern==0 && var._final && !P._scope.canInit(var) ) - throw P.error("Cannot reassign final '"+var._name+"'"); + throw Parser.error("Cannot reassign final '"+var._name+"'",site); // Resolve fields just as a read or postfix update does. Updating // their declaration's local slot would silently lose the Store. P.pos(start); @@ -286,7 +299,7 @@ final class Expressions { // pre is +/-1 for a prefix update of a known name, zero otherwise. private Node parsePrimary(int pre) { if( P._lexer.isNumber(P._lexer.peek()) ) return parseLiteral(); - if( P._lexer.peek('"') ) return parsePostfix(P.allocations.newString(P._lexer.parseString())); + if( P._lexer.peek('"') ) return parsePostfix(P.strings.parse()); if( P.matchx("true" ) ) return P.con(1); if( P.matchx("false") ) return P._code.ZERO; if( P.matchx("null" ) ) return P.con(Type.NIL); @@ -298,8 +311,10 @@ final class Expressions { int pos = P.pos(); // Expect an identifier now String id = P._lexer.matchId(); - if( id == null || Parser.KEYWORDS.contains(id) ) + if( id == null || Parser.KEYWORDS.contains(id) ) { + P.pos(pos); throw P.errorSyntax("an identifier or expression"); + } int pos2 = P.pos(); return P.references.parseName(id,pre,pos,pos2); } @@ -366,7 +381,11 @@ final class Expressions { P._lexer.skipWhiteSpace(); Lexer site = P.loc(); String name; - if( P.match(".") ) name = P.requireId(); + if( P.match(".") ) { + P._lexer.skipWhiteSpace(); + site=P.loc(); + name = P.requireId(true); + } else if( P.match("#") ) name = "#"; else if( P.match("[") ) name = "[]"; // Can get here without an explicit receiver: "f()" or @@ -389,6 +408,6 @@ final class Expressions { // Already parsed "'" Node parseChar() { - return P.require(P.con(TypeInteger.constant(P._lexer.parseChar())),"'"); + return P.con(TypeInteger.constant(P._lexer.parseChar())); } } diff --git a/src/main/java/com/seaofnodes/simple/parser/Functions.java b/src/main/java/com/seaofnodes/simple/parser/Functions.java index 14dee33b..5175acc6 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Functions.java +++ b/src/main/java/com/seaofnodes/simple/parser/Functions.java @@ -73,14 +73,19 @@ final class Functions { while( true ) { Type t = P.types.parseType(); // Arg type if( t==null ) break; + P._lexer.skipWhiteSpace(); + Lexer qualifier=P.loc(); if( P.match("^") ) { if( !(t instanceof TypeMemPtr ptr) || ptr._obj.isAry() ) - throw P.error("'^' requires a struct parameter"); + throw Parser.error("'^' requires a struct parameter",qualifier); t = ptr.withPartial(true); P.construction._borrowed = true; } finals.set(ids.size(),P.declarations.bindingFinal(t)); + P._lexer.skipWhiteSpace(); + Lexer nameLoc=P.loc(); String id = P.requireId(); + if( ids.find(id)!=-1 ) throw Parser.error("Duplicate parameter '"+id+"'",nameLoc); ts .push(t ); // Push type/arg pairs ids.push(id); P.match(","); @@ -98,7 +103,7 @@ final class Functions { Lexer site = P.loc(); site._position--; // Opening '('; arguments can contain nested calls. if( fcn._type == Type.NIL ) - throw P.error("Calling a null function pointer"); + throw Parser.error("Calling a null function pointer",site); fcn.keep(); // Keep while parsing args Ary<Node> args = new Ary<>( Node.class ); @@ -207,6 +212,9 @@ final class Functions { int oldUID = P._code.UID(); // Used to approximate function size FunNode fun = enterFunction(funName,sig,loc,finals,ids); + // Guards belong to this function's control flow. Do not inherit an + // enclosing function's predicates or leak this body's guards on exit. + P._scope.addGuards(fun,null,false); Construction.Frame outerConstruction = P.construction.enter(fun,isCtor && !fun.isClz()); Node rpc = fun.parm(0); if( fun.isClz() ) P.structs.beginClassInit(fun,loc); @@ -260,6 +268,7 @@ final class Functions { P._ref.addFun(P._code,fun); // Unstack parser state + P._scope.removeGuards(fun); if( _returnScope != null ) _returnScope.kill(); _returnScope = returnScope; P.statements._continueScope = continueScope; diff --git a/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/src/main/java/com/seaofnodes/simple/parser/Lexer.java index ec88738b..5e4b7311 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Lexer.java +++ b/src/main/java/com/seaofnodes/simple/parser/Lexer.java @@ -3,10 +3,12 @@ package com.seaofnodes.simple.parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.codegen.CompUnit; import com.seaofnodes.simple.codegen.Serialize; +import com.seaofnodes.simple.codegen.SourceError; import com.seaofnodes.simple.parser.Parser.ParseException; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.BAOS; import java.util.HashMap; +import java.io.ByteArrayOutputStream; // Lexer provides low level access to the raw file bytes, peeks and matches // short strings, parses numbers, skips comments and whitespace, tracks @@ -35,6 +37,11 @@ public class Lexer { public String position() { return _line_number+":"+(_position-_line_start+1); } public int pos() { return _position; } + public String diagnostic(String message) { + return _cu==null ? SourceError.format("<input>",_input,_position,message) + : SourceError.format(_cu,_position,message); + } + /** * Record the source text for lexing */ @@ -143,19 +150,21 @@ public class Lexer { while( true ) { if( isWhiteSpace() ) inc(); // Skip // to end of line - else if( _position+2 < _input.length && + else if( _position+1 < _input.length && _input[_position ] == '/' && _input[_position+1] == '/') { inc(); inc(); while( !isEOF() && _input[_position] != '\n' ) inc(); - } else if( _position+2 < _input.length && + } else if( _position+1 < _input.length && _input[_position ] == '/' && _input[_position+1] == '*') { - // Skip /*comment*/ - while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/')) + Lexer start=new Lexer(this); + inc(); inc(); + while( !isEOF() && !(peek()=='*' && _position+1<_input.length && peek(1)=='/') ) inc(); - inc(); + if( isEOF() ) throw Parser.error("Unclosed block comment",start); + inc(); inc(); } else break; } } @@ -229,20 +238,27 @@ public class Lexer { // Return a constant Type, either TypeInteger or TypeFloat Type parseNumber() { + Lexer start=new Lexer(this); int old = _position; int len = isLongOrDouble(); boolean f32 = peek()=='f'; if( f32 ) nextChar(); int n = Math.abs(len); - if( f32 ) - return TypeFloat.constant(Float.parseFloat(new String(_input,old,n))); - if( len > 0 ) { - if( len > 1 && _input[old]=='0' ) - throw Parser.error("Syntax error: integer values cannot start with '0'",this); - long i = Long.parseLong(new String(_input,old,len)); - return TypeInteger.constant(i); + String literal=new String(_input,old,n); + try { + if( f32 ) + return TypeFloat.constant(Float.parseFloat(literal)); + if( len > 0 ) { + if( len > 1 && _input[old]=='0' ) + throw Parser.error("Syntax error: integer values cannot start with '0'",start); + long i = Long.parseLong(literal); + return TypeInteger.constant(i); + } + return TypeFloat.constant(Double.parseDouble(literal)); + } catch( NumberFormatException ex ) { + throw Parser.error(len>0 && !f32 ? "Integer literal is outside the signed 64-bit range" + : "Invalid floating-point literal '"+literal+(f32 ? "f" : "")+"'",start); } - return TypeFloat.constant(Double.parseDouble(new String(_input,old,n))); } private String parseNumberString() { int old = _position; @@ -319,25 +335,77 @@ public class Lexer { return 0; } - String parseString() { - if( !peek('"') ) return null; - inc(); - StringBuilder sb = new StringBuilder(); + record StringPart(byte[] text, boolean end) {} + + // Opening quote has been consumed. Leave a format's '%' unread so both + // the expression parser and deferred-body scanner use the same grammar. + StringPart stringPart(Lexer start) { + ByteArrayOutputStream sb = new ByteArrayOutputStream(); while( !isEOF() ) { char c = nextChar(); - if( c=='"' ) return sb.toString(); + if( c=='"' ) return new StringPart(sb.toByteArray(),true); + if( c=='%' ) { + if( peek()=='%' ) { inc(); sb.write('%'); continue; } + _position--; + return new StringPart(sb.toByteArray(),false); + } if( c!='\\' ) { - sb.append(c); + sb.write(c); continue; } if( isEOF() ) - throw error("Unclosed string"); - sb.append(escapedChar("string")); + throw Parser.error("Unclosed string",start); + sb.write(escapedChar("string")); } - throw error("Unclosed string"); + throw Parser.error("Unclosed string",start); + } + + record Format(int kind, int flags, int width, int precision, Lexer site) {} + + Format stringFormat() { + Lexer site = new Lexer(this); + assert peek()=='%'; + inc(); + if( peek()=='(' ) { inc(); return new Format(0,0,0,-1,site); } + int flags=0, bit; + while( (bit="-+ #0".indexOf(peek()))>=0 ) { + int mask=1<<bit; + if( (flags&mask)!=0 ) throw Parser.error("Repeated format flag",site); + flags |= mask; + inc(); + } + int width = formatNumber(site); + int precision = -1; + if( peek()=='.' ) { + inc(); + precision = formatNumber(site); + } + char kind=nextChar(); + if( "diuoxXfFeEgGaAcsb".indexOf(kind)<0 ) + throw Parser.error("Expected a string format: %(expression), %d(expression), or %%",site); + boolean number="diuoxXfFeEgGaA".indexOf(kind)>=0; + if( !number && (flags&~1)!=0 || "uoxX".indexOf(kind)>=0 && (flags&6)!=0 || + "diu".indexOf(kind)>=0 && (flags&8)!=0 ) + throw Parser.error("Invalid flags for %"+kind,site); + if( precision>=0 && (kind=='c' || kind=='b') ) + throw Parser.error("Precision is not supported for %"+kind,site); + if( peek()!='(' ) throw Parser.error("Expected '(' after string format %"+kind,site); + inc(); + return new Format(kind,flags,width,precision,site); + } + + private int formatNumber(Lexer site) { + long n=0; + while( peek()>='0' && peek()<='9' ) { + n=n*10+nextChar()-'0'; + if( n>Integer.MAX_VALUE ) throw Parser.error("String format width or precision is too large",site); + } + return (int)n; } private char escapedChar(String kind) { + Lexer start=new Lexer(this); + start._position--; // The backslash introducing the escape. char esc = nextChar(); return switch( esc ) { case '0' -> '\u0000'; @@ -347,17 +415,24 @@ public class Lexer { case '\\' -> '\\'; case '"' -> '"'; case '\'' -> '\''; - default -> throw error("Unknown "+kind+" escape \\\\"+esc); + case '%' -> '%'; + default -> throw Parser.error("Unknown "+kind+" escape \\"+esc,start); }; } char parseChar() { - if( isEOF() ) throw error("Unclosed character"); + Lexer start=new Lexer(this); + start._position--; // Opening quote was consumed by the caller. + if( isEOF() || peek()=='\n' || peek()=='\r' ) throw Parser.error("Unclosed character literal",start); + if( peek()=='\'' ) throw Parser.error("Empty character literal",start); char c = nextChar(); if( c=='\\' ) { - if( isEOF() ) throw error("Unclosed character"); + if( isEOF() ) throw Parser.error("Unclosed character literal",start); c = escapedChar("character"); } + if( isEOF() || peek()=='\n' || peek()=='\r' ) throw Parser.error("Unclosed character literal",start); + if( peek()!='\'' ) throw Parser.error("Character literal must contain exactly one byte",start); + inc(); return c; } @@ -372,16 +447,18 @@ public class Lexer { if( isEOF() ) throw error("Unclosed "+what); char ch = peek(); if( ch==close && depth==0 ) return; + Lexer site=ch=='"' ? new Lexer(this) : null; nextChar(); if( ch==open ) depth++; else if( ch==close ) depth--; - else if( ch=='"' || ch=='\'' ) { - char quote = ch; - while( !isEOF() ) { - ch = nextChar(); - if( ch=='\\' ) nextChar(); - else if( ch==quote ) break; + else if( ch=='"' ) { + while( !stringPart(site).end() ) { + stringFormat(); + skipTo('(',')',"string interpolation"); + nextChar(); } + } else if( ch=='\'' ) { + parseChar(); } } } diff --git a/src/main/java/com/seaofnodes/simple/parser/Parser.java b/src/main/java/com/seaofnodes/simple/parser/Parser.java index 4d140e55..0cf3c0e7 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Parser.java +++ b/src/main/java/com/seaofnodes/simple/parser/Parser.java @@ -24,6 +24,7 @@ public class Parser { final References references = new References(this); final Conversions conversions = new Conversions(this); final Construction construction = new Construction(this); + final Strings strings = new Strings(this); public void checkConstructionSerializable() { construction.checkSerializable(); } @@ -106,7 +107,7 @@ public class Parser { put("i32" ,TypeInteger.I32); put("i64" ,TypeInteger.BOT); put("i8" ,TypeInteger.I8 ); - put("int" ,TypeInteger.BOT); + put("int" ,TypeInteger.I32); put("u1" ,TypeInteger.U1 ); put("u16" ,TypeInteger.U16); put("u32" ,TypeInteger.U32); @@ -117,7 +118,8 @@ public class Parser { public static class ParseException extends RuntimeException { public final Lexer _loc; - ParseException( String msg, Lexer loc ) { super(msg); _loc = 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()); } } public Parser(CodeGen code ) { @@ -186,9 +188,18 @@ public class Parser { // Require and return an identifier String requireId() { + return requireId(false); + } + + // Primitive names are useful member names (sys.text.int), but remain + // reserved for local variables, parameters and type declarations. + String requireId(boolean member) { + _lexer.skipWhiteSpace(); + Lexer site=loc(); String id = _lexer.matchId(); - if (id != null && !KEYWORDS.contains(id) ) return id.intern(); - throw error("Expected an identifier, found '"+id+"'"); + if (id != null && (!KEYWORDS.contains(id) || member && + (INIT_TYPES.get(id) instanceof TypeInteger || INIT_TYPES.get(id) instanceof TypeFloat)) ) return id.intern(); + throw error("Expected an identifier, found '"+new Lexer(site).getAnyNextToken()+"'",site); } String matchId() { @@ -266,7 +277,9 @@ public class Parser { ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); } private ParseException _errorSyntax(String msg) { - return error("Syntax error, "+msg+" but found `" + _lexer.getAnyNextToken()+"`"); + _lexer.skipWhiteSpace(); + // Reading the token for the message must not advance the error location. + return error("Syntax error, "+msg+" but found `" + loc().getAnyNextToken()+"`"); } ParseException error(String msg) { return error(msg,_lexer); } public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); } diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md index ac0d9f29..cee1d64a 100644 --- a/src/main/java/com/seaofnodes/simple/parser/README.md +++ b/src/main/java/com/seaofnodes/simple/parser/README.md @@ -2,7 +2,7 @@ The parser builds Sea-of-Nodes IR directly. Each component shares the cursor, active scope, and compilation state through its `Parser P` reference; there is no intermediate -AST. The [grammar map](../../../../../../../docs/chapter29/29-grammar.md) connects language +AST. The [grammar map](../../../../../../../docs/chapter30/30-grammar.md) connects language productions to their entry points. | File | Responsibility | diff --git a/src/main/java/com/seaofnodes/simple/parser/References.java b/src/main/java/com/seaofnodes/simple/parser/References.java index 483bd7a8..6335bad2 100644 --- a/src/main/java/com/seaofnodes/simple/parser/References.java +++ b/src/main/java/com/seaofnodes/simple/parser/References.java @@ -15,13 +15,36 @@ final class References { References(Parser P) { this.P = P; } + // Compiler-generated library references use ordinary module lookup and + // forward resolution, without a synthetic token stream. + Node classReference(String name, Lexer site) { + CompUnit cu=ParseAll.findCompUnit(P._code,P._ref,name); + if( cu==null ) throw Parser.error("Cannot find standard library '"+name+"'",site); + String clzName=TypeStruct.addClzPrefix(cu._cname); + TypeStruct clz=(TypeStruct)Parser.TYPES.computeIfAbsent(clzName,n -> TypeStruct.make(n,true)); + FRefNode ref=new FRefNode(name,site,TypeMemPtr.make((byte)2,clz,true)); + _frefs.push(ref); + return ref.peephole(); + } + + // Read a member without consuming a postfix from source text. + Node member(Node receiver, String name, Lexer site) { + Field fld=receiver._type instanceof TypeMemPtr ptr ? ptr._obj.field(name) : null; + return P.peep(new LoadNode(site,name,fld==null ? 1 : fld._alias, + P.ctrl(),P.mem(),receiver,P.fldoff(receiver,name))); + } + // Resolve a primary name, retaining its use site before speculative type lookup. Node parseName(String id, int pre, int pos, int pos2) { + Lexer site=P.loc(); + site._position=pos; + site.skipWhiteSpace(); // Attempt a local var lookup first Var var = P._scope.lookup(id); if( var==null ) { // Not a local var, try again as a type name P.pos(pos); + Lexer nameLoc = P.loc(); Type t = P.types.parseType(); if( t!=null ) { // Static <clinit> typename lookup @@ -52,7 +75,7 @@ final class References { t = TypeMemPtr.make((byte)2,clz,true); } // Define the FRef - var = P._scope.defineFRef(id,t,true,P.loc()); + var = P._scope.defineFRef(id,t,true,nameLoc); P.references._frefs.push((FRefNode)P._scope.in(var)); } @@ -73,7 +96,7 @@ final class References { if( kx < fx && !P.structs.inExplicitConstructor() && // Out of scope !(var._final && rvalue._type.isConstant()) ) { // Not a final constant if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke) - throw P.error("Variable '"+var._name+"' is out of function scope and must be a final constant"); + throw Parser.error("Variable '"+var._name+"' is out of function scope and must be a final constant",site); // var.idx==2 is self argument, 3 is args, 4 or more are now static globals if( var._idx >= 4 ) { @@ -98,7 +121,7 @@ final class References { } // Assign-update direct into Scope if( var._final && !P._scope.canInit(var) ) - throw P.error("Cannot reassign final '"+var._name+"'"); + throw Parser.error("Cannot reassign final '"+var._name+"'",site); checkSharedFunction(var); Node op = P.expressions.opAssign(ch,rvalue, var.type() ); P._scope.update(var,op); @@ -106,14 +129,14 @@ final class References { return pre==0 && Lexer.postfix(ch) ? rvalue : op; } - void checkAssignment(Var def) { + void checkAssignment(Var def, Lexer 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 && // Inside an allocation, final assign is OK, outside nope. // The parseNew() call added the allocation scope !P._scope.canInit(def) ) - throw P.error("Cannot reassign final '"+name+"'"); + throw Parser.error("Cannot reassign final '"+name+"'",site); checkSharedFunction(def); } @@ -201,7 +224,7 @@ final class References { if( fld==null && ts!=null ) fld = P.types.fieldOrOpen(ts,name,false); if( fld==null && ts!=null && !P.types.latestStruct(ts)._open ) - throw P.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'"); + throw Parser.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'",site); // We have to record "active touches" of a new class, to track when we // need to force class initialization. Basically, we insert an CLInit, @@ -233,7 +256,7 @@ final class References { // Store to field mem.keep(); if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL ) - throw P.error("Cannot modify final field '"+name+"'"); + throw Parser.error("Cannot modify final field '"+name+"'",site); Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), lift, P.construction.own(expr)).peephole().keep(); P.construction.store(expr,alias,st,mem); st.unkeep(); @@ -250,17 +273,17 @@ final class References { decl = conary.elem(); // Load field - Node load = P.peep(new LoadNode(P.loc(),name,alias,P.ctrl(),mem,expr,off)); + Node load = P.peep(new LoadNode(site,name,alias,P.ctrl(),mem,expr,off)); // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre; if( ch!=0 ) { if( fld!=null && decl==Type.BOTTOM ) - throw P.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" ); + throw Parser.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor",site ); Node op = P.expressions.opAssign(ch,load, decl ).keep(); mem.keep(); if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL ) - throw P.error("Cannot modify final field '"+name+"'"); + throw Parser.error("Cannot modify final field '"+name+"'",site); Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), op, P.construction.own(expr)).peephole().keep(); P.construction.store(expr,alias,st,mem); st.unkeep(); @@ -302,7 +325,8 @@ final class References { Ary<FRefNode> frefs = new Ary<>(FRefNode.class); for( FRefNode fref : _frefs ) { if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue; - CompUnit cu = ParseAll.findCompUnitOrThrow(P._code,P._ref,fref._name); + CompUnit cu = ParseAll.findCompUnit(P._code,P._ref,fref._name); + if( cu==null ) throw fref.err(); // Name lookup may already know this class's fields. Do not replace // that information with an empty placeholder before Iter runs. if( fref._con==FRefNode.FREF_TYPE ) diff --git a/src/main/java/com/seaofnodes/simple/parser/Statements.java b/src/main/java/com/seaofnodes/simple/parser/Statements.java index 4ff4993b..707202ab 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Statements.java +++ b/src/main/java/com/seaofnodes/simple/parser/Statements.java @@ -24,12 +24,14 @@ final class Statements { * @return a {@link Node} or {@code null} */ Node parseStatement() { + P._lexer.skipWhiteSpace(); + Lexer site=P.loc(); if( P.matchx("return") ) return parseReturn(); else if (P.matchx("if") ) return parseIf(); else if (P.matchx("while") ) return parseWhile(); else if (P.matchx("for") ) return parseFor(); - else if (P.matchx("break") ) return parseBreak(); - else if (P.matchx("continue")) return parseContinue(); + else if (P.matchx("break") ) return parseBreak(site); + else if (P.matchx("continue")) return parseContinue(site); else if (P.matchx("struct") ) return P.structs.parseStruct(); else if (P.structs.inStructDeclaration() && P.matchx("new")) return P.structs.parseConstructorDeclaration(); else if (P.matchx(";") ) return P._code.ZERO; // Empty statement @@ -204,7 +206,7 @@ final class Statements { if( doFor ) { int old = P.pos(nextPos); if( !P.peek(')') ) - P.expressions.parseAssignment(); + P.expressions.parseAssignment().isKill(); if( P.pos() != nextEnd ) throw P.errorSyntax( "Unexpected code after expression" ); P.pos(old); @@ -237,8 +239,8 @@ final class Statements { P._scope = exit; return P._code.ZERO; } - private Node parseBreak () { - checkLoopActive(); + private Node parseBreak(Lexer site) { + checkLoopActive(site); // At the time of the break, and loop-exit conditions are only valid if // they are ALSO valid at the break. It is the intersection of // conditions here, not the union. @@ -248,15 +250,20 @@ final class Statements { return P._code.ZERO; } - private Node parseContinue() { checkLoopActive(); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; } + private Node parseContinue(Lexer site) { checkLoopActive(site); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; } private ScopeNode jumpTo(ScopeNode toScope) { ScopeNode cur = P._scope.dup(); P.ctrl(P._code.XCTRL); // Kill current scope // Prune nested lexical scopes that have depth > than the loop head // We use _breakScope as a proxy for the loop head scope to obtain the depth + // The clone shares lexical boundaries and Var records with the active + // parser scope. Promoting on the clone would move those boundaries + // without moving the active scope's bindings. Drop locals first, then + // copy forward references into the jump scope with their own indices. while( cur.depth() > _breakScope.depth() ) - cur.pop(); + cur._pop(); + cur.balanceLoopFRefs(P._scope); // If this is a continue then first time the target is null // So we just use the pruned current scope as the base for the // "continue" @@ -264,9 +271,13 @@ final class Statements { return cur; // toScope is either the break scope, or a scope that was created here assert toScope.depth() <= _breakScope.depth(); + // A later path can discover forward references after an earlier + // break/continue saved its scope. Keep those bindings across the merge. + toScope.balanceLoopFRefs(cur); + cur.balanceLoopFRefs(toScope); toScope.ctrl(toScope.mergeScopes(cur,P.loc()).peephole()); return toScope; } - private void checkLoopActive() { if (_breakScope == null) throw P.error("No active loop for a break or continue"); } + private void checkLoopActive(Lexer site) { if (_breakScope == null) throw Parser.error("No active loop for a break or continue",site); } } diff --git a/src/main/java/com/seaofnodes/simple/parser/Strings.java b/src/main/java/com/seaofnodes/simple/parser/Strings.java new file mode 100644 index 00000000..5bf76585 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/parser/Strings.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.parser; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +/** Interpolation expands to ordinary sys.text calls. The library itself uses + * no interpolation, so it can be compiled from source without staging. */ +final class Strings { + private final Parser P; + Strings(Parser p) { P=p; } + + Node parse() { + Lexer start=P.loc(); + P.require("\""); + Lexer.StringPart part=P._lexer.stringPart(start); + if( part.end() ) return P.allocations.newString(part.text()); + Node text=P.references.classReference("sys.text",P.loc()).keep(); + P.classInit(P.loc(),text); + Node builder=call(text,"begin",P.loc()).keep(); + while( true ) { + if( part.text().length!=0 ) + call(text,"string",P.loc(),builder,P.allocations.newString(part.text()),P.con('s'),P.con(0),P.con(-1)).isKill(); + if( part.end() ) break; + Lexer.Format fmt=P._lexer.stringFormat(); + Node value=P.expressions.parseAssignment().keep(); + P.require(")"); + Node fptr; + if( fmt.kind()==0 ) { + Node ints=P.references.member(text,"integer",fmt.site()).keep(); + Node floats=P.references.member(text,"floating",fmt.site()).keep(); + Node strings=P.references.member(text,"string",fmt.site()).keep(); + fptr=new FormatNode(value,ints.unkeep(),floats.unkeep(),strings.unkeep(),fmt.site()).peephole(); + } else { + String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : "floating"; + fptr=P.references.member(text,family,fmt.site()); + } + call(text,fptr,fmt.site(),builder,value.unkeep(),P.con(fmt.kind() | fmt.flags()<<8), + P.con(fmt.width()),P.con(fmt.precision())).isKill(); + part=P._lexer.stringPart(start); + } + // String syntax promises this result type even when sys.text is still + // awaiting its source parse. Check/convert through the normal IR. + Node result=call(text.unkeep(),"end",P.loc(),builder.unkeep()); + return P.conversions.convertExpr(result,TypeMemPtr.make(P.types.typeAry(TypeInteger.U8,true)).makeRO()); + } + + private Node call(Node text, String name, Lexer site, Node... values) { + return call(text,P.references.member(text,name,site),site,values); + } + + private Node call(Node text, Node fptr, Lexer site, Node... values) { + Ary<Node> args=new Ary<>(Node.class); + args.push(P.ctrl()); args.push(P.mem()); args.push(text); + for( Node value:values ) args.push(value); + args.push(fptr); + return P.functions.functionCall(args,site); + } +} diff --git a/src/main/java/com/seaofnodes/simple/parser/Structs.java b/src/main/java/com/seaofnodes/simple/parser/Structs.java index a5262440..3a3d894c 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Structs.java +++ b/src/main/java/com/seaofnodes/simple/parser/Structs.java @@ -78,12 +78,14 @@ final class Structs { Node parseConstructorDeclaration() { TypeStruct self = P._scope.constructorSelf(); assert self != null; + P._lexer.skipWhiteSpace(); + Lexer site=P.loc(); String name = P.requireId(); String expect = initFieldName(self._name); if( name != expect ) - throw P.error("Constructor name '" + name + "' must match type name '" + expect + "'"); + throw Parser.error("Constructor name '" + name + "' must match type name '" + expect + "'",site); P.require("="); - if( _pendingCtor!=null ) throw P.error("Redefining constructor '"+name+"'"); + if( _pendingCtor!=null ) throw Parser.error("Redefining constructor '"+name+"'",site); _pendingCtor = new Constructor(name,P.loc()); // Parse the body after all field declarations are known, so self.x // and x resolve the same field even when x is declared below this body. diff --git a/src/main/java/com/seaofnodes/simple/parser/Types.java b/src/main/java/com/seaofnodes/simple/parser/Types.java index ffd3a0f4..827518fd 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Types.java +++ b/src/main/java/com/seaofnodes/simple/parser/Types.java @@ -54,12 +54,14 @@ final class Types { // Nest arrays and '?' as needed Type t2 = t1; while( true ) { + P._lexer.skipWhiteSpace(); + Lexer site=P.loc(); if( P.match("?") ) { if( t2 instanceof TypeMemPtr tmp ) { - if( tmp.nullable() ) throw P.error("Type "+t2+" already allows null"); + if( tmp.nullable() ) throw Parser.error("Type "+t2+" already allows null",site); t2 = tmp.makeNullable(); } else - throw P.error("Type "+t0+" cannot be null"); + throw Parser.error("Type "+t0+" cannot be null",site); } else if( P.match("[~]") ) { t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO(); } else if( P.match("[]") ) { diff --git a/src/main/java/com/seaofnodes/simple/parser/package-info.java b/src/main/java/com/seaofnodes/simple/parser/package-info.java index 7212e629..c886f72a 100644 --- a/src/main/java/com/seaofnodes/simple/parser/package-info.java +++ b/src/main/java/com/seaofnodes/simple/parser/package-info.java @@ -7,7 +7,7 @@ * use-side name/field resolution; Allocations, ControlFlow, and Conversions * contain their shared IR construction. Lexer also supplies location snapshots. * - * <p>See {@code chapter29/docs/29-grammar.md} for the production-to-method map. + * <p>See {@code chapter30/docs/30-grammar.md} for the production-to-method map. * Evaluation order and scope lifetimes are explicit: loop updates and declared * constructor bodies are the two deferred source fragments. There is no AST. */ diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java index c9093e3b..9cb23e09 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java @@ -16,6 +16,7 @@ public abstract class TypeConAry<A> extends TypeScalar { TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; } public static void gather(ArrayList<Type> ts) { + ts.add(TypeConAryB.make("")); ts.add(TypeConAryB.ABC); ts.add(TypeConAryB.ABCD); ts.add(TypeConAryI.I123); @@ -36,6 +37,15 @@ public abstract class TypeConAry<A> extends TypeScalar { } Type ymeet( TypeInteger ti ) { + // Empty arrays have no observed elements: use the storage bounds. + // Check both endpoints just as the loop checks a nonempty body; + // retaining high integer inputs here preserves lattice duality. + if( len()==0 ) { + TypeInteger bound=emptyElem(); + if( !ti.isa(TypeInteger.make(bound._min,bound._min,ti._widen)) || + !ti.isa(TypeInteger.make(bound._max,bound._max,ti._widen)) ) + return ti.meet(bound); + } // if i can isa *each* element, then maybe can keep. // no good to i.isa(elem()) because fails the dual for( int j=0; j<len(); j++ ) @@ -64,6 +74,7 @@ public abstract class TypeConAry<A> extends TypeScalar { return _any ? Type.TOP : BOTTOM; int len = len(); long min = Long.MAX_VALUE; + if( len==0 ) return emptyElem(); long max = Long.MIN_VALUE; for( int i=0; i<len; i++ ) { min = Math.min(min,at8(i)); @@ -71,6 +82,10 @@ public abstract class TypeConAry<A> extends TypeScalar { } return TypeInteger.make(min,max,_widen); } + private TypeInteger emptyElem() { + return log_size()==0 ? TypeInteger.make(0,255,_widen) + : TypeInteger.make(Integer.MIN_VALUE,Integer.MAX_VALUE,_widen); + } public abstract long at8(int idx); public abstract int len(); @Override public abstract int log_size(); diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java index 5877c309..84be5fb2 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java @@ -10,7 +10,7 @@ public class TypeConAryB extends TypeConAry<byte[]> { public static TypeConAryB make( String s ) { return make(s.getBytes()); } static final TypeConAryB ABC = make("abc"); static final TypeConAryB ABCD = make("abcd"); - @Override public long at8(int idx) { return _ary[idx]; } + @Override public long at8(int idx) { return _ary[idx]&255; } @Override public int len() { return _ary.length; } @Override public int log_size() { return 0; } @Override public String str() { return "[\""+new String(_ary)+"\"]"; } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java index a0c2266d..6164f721 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -53,7 +53,15 @@ public class TypeInteger extends TypeScalar { } public Type same_but_slightly_wider_than( ) { - return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : BOT; + if( _widen < 3 ) return make(_min,_max, (byte)(_widen+1)); + // Start in the intersection of i32 and u32 for nonnegative ranges. + // Widening those straight to i32 invents negative values that prevent + // a u32 loop from remaining unsigned. Either 32-bit range can then + // grow to its own bound; unbounded arithmetic ultimately reaches BOT. + if( _min >= 0 && _max <= I32._max ) return make(0,I32._max,(byte)3); + if( _min >= I32._min && _max <= I32._max ) return I32; + if( _min >= 0 && _max <= U32._max ) return U32; + return BOT; } // Force max widen @Override public Type widen() { |
