package com.seaofnodes.simple; import com.seaofnodes.graph.GraphObserver; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; import java.util.*; /** * The Parser converts a Simple source program to the Sea of Nodes intermediate * representation directly in one pass. There is no intermediate Abstract * Syntax Tree structure. *
* This is a simple recursive descent parser. All lexical analysis is done here as well.
*/
public class Parser {
// Current compilation context; the observer belongs to the parser.
public static Parser PARSER;
public GraphObserver
* To make the compiler multithreaded, this field will have to move into a TLS.
*/
public static StartNode START;
public static ConstantNode ZERO; // Very common node, cached here
public static XCtrlNode XCTRL; // Very common node, cached here
// Next available memory alias number
static int ALIAS;
public StopNode STOP;
// Debugger Printing.
public static boolean SCHEDULED; // True if debug printer can use schedule info
// The Lexer. Thin wrapper over a byte[] buffer with a cursor.
private final Lexer _lexer;
/**
* Current ScopeNode - ScopeNodes change as we parse code, but at any point of time
* there is one current ScopeNode. The reason the current ScopeNode can change is to do with how
* we handle branching. See {@link #parseIf()}.
*
* Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds
* variable names to Nodes. The top of this stack represents current scope.
*
* We keep a list of all ScopeNodes so that we can show them in graphs.
* @see #parseIf()
* @see #_xScopes
*/
public ScopeNode _scope;
/**
* List of keywords disallowed as identifiers
*/
private static final HashSet
* '{' statements '}'
*
* Does not parse the opening or closing '{}'
* @return a {@link Node} or {@code null}
*/
private Node parseBlock() {
// Enter a new scope
_scope.push();
while (!peek('}') && !_lexer.isEOF())
parseStatement();
// Exit scope
_scope.pop();
return null;
}
/**
* Parses a statement
*
*
* returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
*
* @return a {@link Node} or {@code null}
*/
private Node parseStatement() {
if( false ) return null;
else if (matchx("return") ) return parseReturn();
else if (match ("{") ) return require(parseBlock(),"}");
else if (matchx("if") ) return parseIf();
else if (matchx("while") ) return parseWhile();
else if (matchx("break") ) return parseBreak();
else if (matchx("continue")) return parseContinue();
else if (matchx("struct") ) return parseStruct();
else if (matchx(";") ) return null; // Empty statement
// declarations of vars with struct type are handled in parseExpressionStatement due
// to ambiguity
else return parseExpressionStatement();
}
/**
* Parse a struct field.
*
* type IDENTIFIER ;
*
*/
private Field parseField() {
Type t = type();
if( t==null )
throw errorSyntax("Requires a field type, found '"+_lexer.getAnyNextToken()+"'");
return require(Field.make(requireId().intern(),ALIAS++,t),";");
}
/**
* Parse a struct declaration, and return the following statement.
* Only allowed in top level scope.
* Structs cannot be redefined.
*
* @return The statement following the struct
*/
private Node parseStruct() {
if (_xScopes.size() > 1) throw errorSyntax("struct declarations can only appear in top level scope");
String typeName = requireId();
Type t = TYPES.get(typeName);
if( t!=null && !(t instanceof TypeMemPtr tmp && tmp._obj._fields==null) )
throw errorSyntax("struct '" + typeName + "' cannot be redefined");
// Parse a collection of fields
ArrayList
* while ( expression ) statement
*
* @return a {@link Node}, never {@code null}
*/
private Node parseWhile() {
var savedContinueScope = _continueScope;
var savedBreakScope = _breakScope;
require("(");
// Loop region has two control inputs, the first is the entry
// point, and second is back edge that is set after loop is parsed
// (see end_loop() call below). Note that the absence of back edge is
// used as an indicator to switch off peepholes of the region and
// associated phis; see {@code inProgress()}.
ctrl(new LoopNode(ctrl()).peephole()); // Note we set back edge to null here
// At loop head, we clone the current Scope (this includes all
// names in every nesting level within the Scope).
// We create phis eagerly for all the names we find, see dup().
// Save the current scope as the loop head
ScopeNode head = _scope.keep();
// Clone the head Scope to create a new Scope for the body.
// Create phis eagerly as part of cloning
_xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis
// Parse predicate
var pred = require(parseExpression(), ")");
// IfNode takes current control and predicate
Node ifNode = new IfNode(ctrl(), pred).peephole();
// Setup projection nodes
Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole();
// Clone the body Scope to create the break/exit Scope which accounts for any
// side effects in the predicate. The break/exit Scope will be the final
// scope after the loop, and its control input is the False branch of
// the loop predicate. Note that body Scope is still our current scope.
ctrl(ifF);
_xScopes.push(_breakScope = _scope.dup());
// No continues yet
_continueScope = null;
// Parse the true side, which corresponds to loop body
// Our current scope is the body Scope
ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
parseStatement(); // Parse loop body
// Merge the loop bottom into other continue statements
if (_continueScope != null) {
_continueScope = jumpTo(_continueScope);
_scope.kill();
_scope = _continueScope;
}
// The true branch loops back, so whatever is current _scope.ctrl gets
// added to head loop as input. endLoop() updates the head scope, and
// goes through all the phis that were created earlier. For each phi,
// it sets the second input to the corresponding input from the back
// edge. If the phi is redundant, it is replaced by its sole input.
var exit = _breakScope;
head.endLoop(_scope, exit);
head.unkeep().kill();
_xScopes.pop(); // Cleanup
_xScopes.pop(); // Cleanup
_continueScope = savedContinueScope;
_breakScope = savedBreakScope;
// At exit the false control is the current control, and
// the scope is the exit scope after the exit test.
return _scope = exit;
}
private ScopeNode jumpTo(ScopeNode toScope) {
ScopeNode cur = _scope.dup();
ctrl(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
while( cur._idxs.size() > _breakScope._idxs.size() )
cur.pop();
// 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"
if (toScope == null)
return cur;
// toScope is either the break scope, or a scope that was created here
assert toScope._idxs.size() <= _breakScope._idxs.size();
toScope.ctrl(toScope.mergeScopes(cur));
return toScope;
}
private void checkLoopActive() { if (_breakScope == null) throw Parser.error("No active loop for a break or continue"); }
private Node parseBreak () { checkLoopActive(); return ( _breakScope = require(jumpTo( _breakScope ),";")); }
private Node parseContinue() { checkLoopActive(); return (_continueScope = require(jumpTo( _continueScope ),";")); }
/**
* Parses a statement
*
*
* if ( expression ) statement [else statement]
*
* @return a {@link Node}, never {@code null}
*/
private Node parseIf() {
require("(");
// Parse predicate
var pred = require(parseExpression(), ")").keep();
// IfNode takes current control and predicate
Node ifNode = new IfNode(ctrl(), pred).peephole();
// Setup projection nodes
Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole().keep();
// In if true branch, the ifT proj node becomes the ctrl
// But first clone the scope and set it as current
int ndefs = _scope.nIns();
ScopeNode fScope = _scope.dup(); // Duplicate current scope
fScope.ctrl(ifF); // The pending arm already starts at False
_xScopes.push(fScope); // For graph visualization we need all scopes
// Parse the true side
ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
_scope.upcast(ifT,pred,false); // Up-cast predicate
parseStatement(); // Parse true-side
ScopeNode tScope = _scope;
// Parse the false side
_scope = fScope; // Restore scope, then parse else block if any
ifF.unkeep(); // fScope already owns the false control
_scope.upcast(ifF,pred,true); // Up-cast predicate
if (matchx("else")) {
parseStatement();
fScope = _scope;
}
pred.unkeep();
if( tScope.nIns() != ndefs || fScope.nIns() != ndefs )
throw error("Cannot define a new name on one arm of an if");
// Merge results
_scope = tScope;
_xScopes.pop(); // Discard pushed from graph display
return ctrl(tScope.mergeScopes(fScope));
}
/**
* Parses a return statement; "return" already parsed.
* The $ctrl edge is killed.
*
*
* 'return' expr ;
*
* @return an expression {@link Node}, never {@code null}
*/
private Node parseReturn() {
var expr = require(parseExpression(), ";");
Node ret = STOP.addReturn(new ReturnNode(ctrl(), expr, _scope).peephole());
ctrl(XCTRL); // Kill control
return ret;
}
/**
* Parses an expression statement or a declaration statement where type is a struct
*
*
* name; // Error
* type name; // Define name with default initial value
* type name = expr; // Define name with given initial value
* name = expr; // Reassign existing
* expr // Something else
*
* @return an expression {@link Node}, never {@code null}
*/
private Node parseExpressionStatement() {
int old = _lexer._position;
Type t = type();
String name = requireId();
Node expr;
if( match(";") ) { // Assign a default value
// No type and no expr is an error
if( t==null ) throw errorSyntax("expression");
expr = new ConstantNode(t.makeInit()).peephole();
} else if( match("=") ) { // Assign "= expr;"
expr = require(parseExpression(), ";");
} else { // Neither, so just a normal expression parse
_lexer._position = old;
return require(parseExpression(),";");
}
// Defining a new variable vs updating an old one
if( t != null ) {
if( _scope.define(name,t,expr) == null )
throw error("Redefining name '" + name + "'");
} else {
if( _scope.lookup(name)==null )
throw error("Undefined name '" + name + "'");
t = _scope.lookupDeclaredType(name);
}
// Auto-widen int to float
if( expr._type instanceof TypeInteger && t instanceof TypeFloat )
expr = new ToFloatNode(expr).peephole();
// Auto-narrow wide ints to narrow ints
expr = zsMask(expr,t);
// Auto-deepen forward ref types
Type e = expr._type;
if( e instanceof TypeMemPtr tmp && tmp._obj._fields==null )
e = TYPES.get(tmp._obj._name);
// Type is sane
if( !e.isa(t) )
throw error("Type " + e.str() + " is not of declared type " + t.str());
return _scope.update(name,expr);
}
// Parse and return a type or null. Valid types always are followed by an
// 'id' which the caller must parse. This lets us distinguish forward ref
// types (which ARE valid here) from local vars in an (optional) forward
// ref type position.
private Type type() {
int old1 = _lexer._position;
String tname = _lexer.matchId();
if( tname==null ) return null;
// Convert the type name to a type.
Type t0 = TYPES.get(tname);
Type t1 = t0 == null ? TypeMemPtr.make(TypeStruct.make(tname)) : t0; // Null: assume a forward ref type
// Nest arrays and '?' as needed
while( true ) {
assert !(t1 instanceof TypeStruct);
if( match("?") ) {
if( !(t1 instanceof TypeMemPtr tmp) )
throw error("Type "+t0+" cannot be null");
if( tmp._nil ) throw error("Type "+t1+" already allows null");
t1 = TypeMemPtr.make(tmp._obj,true);
continue;
}
if( match("[]") ) {
t1 = typeAry(t1);
continue;
}
break;
}
// Check no forward ref
if( t0 != null ) return t1;
// Check valid forward ref, after parsing all the type extra bits.
// Cannot check earlier, because cannot find required 'id' until after "[]?" syntax
int old2 = _lexer._position;
String id = _lexer.matchId();
_lexer._position = old2; // Reset lexer to reparse
if( id==null ) {
_lexer._position = old1; // Reset lexer to reparse
return null; // Not a type
}
// Yes a forward ref, so declare it
TYPES.put(tname,t1);
return t1;
}
// Make an array type of t
private TypeMemPtr typeAry( Type t ) {
if( t instanceof TypeMemPtr tmp && !tmp._nil )
throw error("Arrays of reference types must always be nullable");
String tname = t.str()+"[]";
Type ta = TYPES.get(tname);
if( ta != null ) return (TypeMemPtr)ta;
// Need make an array type.
TypeStruct ts = TypeStruct.makeAry(TypeInteger.BOT,ALIAS++,t,ALIAS++);
TypeMemPtr tary = TypeMemPtr.make(ts);
TYPES.put(tname,tary);
START.addMemProj(ts, _scope); // Insert memory alias edges
return tary;
}
/**
* Parse an expression of the form:
*
*
* expr : compareExpr
*
* @return an expression {@link Node}, never {@code null}
*/
private Node parseExpression() { return parseBitwise(); }
/**
* Parse an bitwise expression
*
*
* bitwise : compareExpr (('&' | '|' | '^') compareExpr)*
*
* @return a bitwise expression {@link Node}, never {@code null}
*/
private Node parseBitwise() {
Node lhs = parseComparison();
while( true ) {
if( false ) ;
else if( match("&") ) lhs = new AndNode(lhs,null);
else if( match("|") ) lhs = new OrNode(lhs,null);
else if( match("^") ) lhs = new XorNode(lhs,null);
else break;
lhs.setDef(2,parseComparison());
lhs = lhs.peephole();
}
return lhs;
}
/**
* Parse an expression of the form:
*
*
* expr : additiveExpr op additiveExpr
*
* @return an comparator expression {@link Node}, never {@code null}
*/
private Node parseComparison() {
var lhs = parseShift();
while( true ) {
int idx=0; boolean negate=false;
// Test for any local nodes made, and "keep" lhs during peepholes
if( false ) ;
else if( match("==") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); }
else if( match("!=") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); negate=true; }
else if( match("<=") ) { idx=2; lhs = new BoolNode.LE(lhs, null); }
else if( match("<" ) ) { idx=2; lhs = new BoolNode.LT(lhs, null); }
else if( match(">=") ) { idx=1; lhs = new BoolNode.LE(null, lhs); }
else if( match(">" ) ) { idx=1; lhs = new BoolNode.LT(null, lhs); }
else break;
// Peepholes can fire, but lhs is already "hooked", kept alive
lhs.setDef(idx,parseShift());
lhs = lhs.widen().peephole();
if( negate ) // Extra negate for !=
lhs = new NotNode(lhs).peephole();
}
return lhs;
}
/**
* Parse an additive expression
*
*
* shiftExpr : additiveExpr (('<<' | '>>' | '>>>') additiveExpr)*
*
* @return a shift expression {@link Node}, never {@code null}
*/
private Node parseShift() {
Node lhs = parseAddition();
while( true ) {
if( false ) ;
else if( match("<<") ) lhs = new ShlNode(lhs,null);
else if( match(">>>")) lhs = new ShrNode(lhs,null);
else if( match(">>") ) lhs = new SarNode(lhs,null);
else break;
lhs.setDef(2,parseAddition());
lhs = lhs.widen().peephole();
}
return lhs;
}
/**
* Parse an additive expression
*
*
* additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)*
*
* @return an add expression {@link Node}, never {@code null}
*/
private Node parseAddition() {
Node lhs = parseMultiplication();
while( true ) {
if( false ) ;
else if( match("+") ) lhs = new AddNode(lhs,null);
else if( match("-") ) lhs = new SubNode(lhs,null);
else break;
lhs.setDef(2,parseMultiplication());
lhs = lhs.widen().peephole();
}
return lhs;
}
/**
* Parse an multiplicativeExpr expression
*
*
* multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)*
*
* @return a multiply expression {@link Node}, never {@code null}
*/
private Node parseMultiplication() {
var lhs = parseUnary(); boolean mul;
while( true ) {
if( false ) ;
else if( match("*") ) lhs = new MulNode(lhs,null);
else if( match("/") ) lhs = new DivNode(lhs,null);
else break;
lhs.setDef(2,parseUnary());
lhs = lhs.widen().peephole();
}
return lhs;
}
/**
* Parse a unary minus expression.
*
*
* unaryExpr : ('-') | '!') unaryExpr | postfixExpr | primaryExpr
*
* @return a unary expression {@link Node}, never {@code null}
*/
private Node parseUnary() {
if (match("-")) return new MinusNode(parseUnary()).widen().peephole();
if (match("!")) return new NotNode(parseUnary()).peephole();
return parsePostfix(parsePrimary());
}
/**
* Parse a primary expression:
*
*
* primaryExpr : integerLiteral | Identifier | true | false | null | new Type | '(' expression ')'
*
* @return a primary {@link Node}, never {@code null}
*/
private Node parsePrimary() {
if( _lexer.isNumber(_lexer.peek()) ) return parseLiteral();
if( match("(") ) return require(parseExpression(), ")");
if( matchx("true" ) ) return con(1);
if( matchx("false") ) return ZERO;
if( matchx("null" ) ) return new ConstantNode(TypeMemPtr.NULLPTR).peephole();
if( matchx("new") ) {
Type t = type();
if( t==null ) throw error("Expected a type");
if( match("[") ) {
Node len = parseExpression().keep();
if( !(len._type instanceof TypeInteger) )
throw error("Cannot allocate an array with length "+len._type);
require("]");
TypeMemPtr tmp = typeAry(t);
return newArray(tmp._obj,len);
}
if( t instanceof TypeMemPtr tmp ) {
TypeStruct obj = tmp._obj;
if( obj._fields==null )
throw error("Unknown struct type '" + obj._name + "'");
if( obj.isAry() )
throw Utils.TODO(); // Missing array length
return newStruct(obj,con(obj.offset(obj._fields.length)));
}
throw error("Cannot allocate a "+t.str());
}
String name = _lexer.matchId();
if( name == null) throw errorSyntax("an identifier or expression");
Node n = _scope.lookup(name);
if( n!=null ) return n;
throw error("Undefined name '" + name + "'");
}
/**
* Return a NewNode with pre-zeroed memory
*/
private Node newStruct(TypeStruct obj, Node size) {
Field[] fs = obj._fields;
Node[] ns = new Node[2+fs.length];
ns[0] = ctrl();
ns[1] = size;
for( int i = 0; i < fs.length; i++ )
ns[i+2] = memAlias(fs[i]._alias);
Node nnn = new NewNode(TypeMemPtr.make(obj), ns).peephole();
for( int i = 0; i < fs.length; i++ )
memAlias(fs[i]._alias, new ProjNode(nnn,i+2,memName(fs[i]._alias)).peephole());
return new ProjNode(nnn,1,obj._name).peephole();
}
private Node newArray(TypeStruct ary, Node len) {
int base = ary.aryBase ();
int scale= ary.aryScale();
Node size = new AddNode(con(base),new ShlNode(len,con(scale)).peephole()).peephole();
Node ptr = newStruct(ary,size);
int alias = ary._fields[0]._alias; // Length alias
memAlias(alias,new StoreNode("#",alias,TypeInteger.BOT,memAlias(alias),ptr,con( ary.offset(0) ), len.unkeep(), true ).peephole());
return ptr;
}
// We set up memory aliases by inserting special vars in the scope these
// variables are prefixed by $ so they cannot be referenced in Simple code.
// Using vars has the benefit that all the existing machinery of scoping
// and phis work as expected
private Node memAlias(int alias ) { return _scope.lookup(memName(alias) ); }
private Node memAlias(int alias, Node st) { return _scope.update(memName(alias), st); }
public static String memName(int alias) { return ("$"+alias).intern(); }
/**
* Parse postfix expression. For now this is just a field
* expression, but in future could be array index too.
*
*
* expr ('.' IDENTIFIER)* [ = expr ]
* expr #
* expr ('[' expr ']')* = [ = expr ]
*
*/
private Node parsePostfix(Node expr) {
String name = null;
if( match(".") ) name = requireId().intern();
else if( match("#") ) name = "#";
else if( match("[") ) name = "[]";
else return expr; // No postfix
// Sanity check expr for being a reference
if( !(expr._type instanceof TypeMemPtr ptr) )
throw error("Expected reference but got " + expr._type.str());
if( ptr == TypeMemPtr.TOP ) throw error("Accessing field '" + name + "' from null");
if( ptr._obj == null ) throw error("Accessing unknown field '" + name + "' from '" + ptr.str() + "'");
// Sanity check field name for existing
TypeMemPtr tmp = (TypeMemPtr)TYPES.get(ptr._obj._name);
if( tmp == null ) throw error("Accessing unknown field '" + name + "' from '" + ptr.str() + "'");
TypeStruct base = tmp._obj;
int fidx = base.find(name);
if( fidx == -1 ) throw error("Accessing unknown field '" + name + "' from '" + ptr.str() + "'");
expr.keep();
// Get field type and layout offset from base type and field index fidx
Field f = base._fields[fidx]; // Field from field index
Node off;
if( name.equals("[]") ) { // If field is an array body
// Array index math
Node idx = require(parseExpression(),"]");
Node shl = new ShlNode(idx,con(base.aryScale())).peephole();
off = new AddNode(con(base.aryBase()),shl).peephole();
} else { // Else normal struct field
// Hardwired field offset
off = con(base.offset(fidx));
}
if( match("=") ) {
// Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment
if( peek('=') ) _lexer._position--;
else {
off.keep();
Node val = parseExpression();
// Auto-truncate when storing to narrow fields
val = zsMask(val,f._type).keep();
Node st = new StoreNode(name, f._alias, f._type, memAlias(f._alias), expr.unkeep(), off.unkeep(), val, false);
// Arrays include control, as a proxy for a safety range check.
// Structs don't need this; they only need a NPE check which is
// done via the type system.
if( base.isAry() ) st.setDef(0,ctrl());
memAlias(f._alias, st.peephole());
return val.unkeep(); // "obj.a = expr" returns the expression while updating memory
}
}
Node load = new LoadNode(name, f._alias, f._type.glb(), memAlias(f._alias), expr.unkeep(), off);
// Arrays include control, as a proxy for a safety range check
// Structs don't need this; they only need a NPE check which is
// done via the type system.
if( base.isAry() ) load.setDef(0,ctrl());
return parsePostfix(load.peephole());
}
// zero/sign extend. "i" is limited to either classic unsigned (min==0) or
// classic signed (min=minus-power-of-2); max=power-of-2-minus-1.
private Node zsMask(Node val, Type t ) {
if( !(val._type instanceof TypeInteger tval && t instanceof TypeInteger t0 && !tval.isa(t0)) ) {
if( !(val._type instanceof TypeFloat tval && t instanceof TypeFloat t0 && !tval.isa(t0)) )
return val;
// Float rounding
return new RoundF32Node(val).peephole();
}
if( t0._min==0 ) // Unsigned
return new AndNode(val,con(t0._max)).peephole();
// Signed extension
int shift = Long.numberOfLeadingZeros(t0._max)-1;
Node shf = con(shift);
if( shf._type==TypeInteger.ZERO )
return val;
return new SarNode(new ShlNode(val,shf.keep()).peephole(),shf.unkeep()).peephole();
}
/**
* Parse integer literal
*
*
* integerLiteral: [1-9][0-9]* | [0]
* floatLiteral: [digits].[digits]?[e [digits]]?
*
*/
private ConstantNode parseLiteral() {
return (ConstantNode) new ConstantNode(_lexer.parseNumber()).peephole();
}
public static Node con( long con ) {
return new ConstantNode(TypeInteger.constant(con)).peephole();
}
//////////////////////////////////
// Utilities for lexical analysis
// Return true and skip if "syntax" is next in the stream.
private boolean match (String syntax) { return _lexer.match (syntax); }
// Match must be "exact", not be followed by more id letters
private boolean matchx(String syntax) { return _lexer.matchx(syntax); }
// Return true and do NOT skip if 'ch' is next
private boolean peek(char ch) { return _lexer.peek(ch); }
private boolean peekIsId() { return _lexer.peekIsId(); }
// Require and return an identifier
private String requireId() {
String id = _lexer.matchId();
if (id != null && !KEYWORDS.contains(id) ) return id;
throw error("Expected an identifier, found '"+id+"'");
}
// Require an exact match
private Parser require(String syntax) { require(null, syntax); return this; }
private