V7 of the silly compiler is tagged. (EDIT: I’d previously announced this as V8, but I jumped the gun, including having an AI generate a “V8” image for me below. The newly tagged version is V7. There is no V8 yet, and only when I create it, will the V8 image below will be justified.)
This version makes a couple small bug fixes, and a bunch of maintenance changes, mostly related to location information in the builder/parser, which is now considerably simpler. It also adds support for PRINT numeric-literal, and implements an ELIF statement. That last was in the grammar, but had no builder support.
Adding the ELIF feature actually simplifed things. I didn’t try to add my own silly.if MLIR operator, with intrinsic support for this else-if construct that I had in the grammar. Instead I just used scf.if — essentially making a transformation of the following form in the builder:
to
In my original implementation of IF/ELSE, I only generated the ELSE region and block when the user code had an ELSE statement. Then when the ANTRL4 enterElseStatement was called, I’d then generate the else region and block (setting the insertion point at that point, so that the following statements would then populate that block.). Then when exitElseStatement was run, I’d generate the scf::Yield operation to terminate the block.
With the implementation of ELIF, I changed the scf::IfOp generation to include empty then and else regions. When this is done up front by default, the constructor adds the scf::Yield operators automatically — all that’s required is setting the insertion point. I then only have to push/pop a single insertion point, adjusting it (without push) for each ELIF/ELSE callback in the ANTLR4 tree walker.
Here are some examples:
This is an IF without an ELSE, but the MLIR now has an empty else block:
module {
func.func @main() -> i32 {
"silly.scope"() ({
"silly.declare"() <{type = i32}> {sym_name = "y"} : () -> ()
"silly.declare"() <{type = i32}> {sym_name = "x"} : () -> ()
%c3_i64 = arith.constant 3 : i64
silly.assign @x = %c3_i64 : i64
%0 = silly.load @x : i32
%c4_i64 = arith.constant 4 : i64
%1 = "silly.less"(%0, %c4_i64) : (i32, i64) -> i1
scf.if %1 {
%c42_i64 = arith.constant 42 : i64
silly.assign @y = %c42_i64 : i64
%3 = silly.load @y : i32
silly.print %3 : i32
} else {
}
%2 = "silly.string_literal"() <{value = "Done."}> : () -> !llvm.ptr
silly.print %2 : !llvm.ptr
%c0_i32 = arith.constant 0 : i32
"silly.return"(%c0_i32) : (i32) -> ()
}) : () -> ()
"silly.yield"() : () -> ()
}
}
Here's one with an ELIF:
INT32 x; // line 1
x = 3; // line 3
IF ( x < 4 ) // line 5
{
PRINT x; // line 7
}
ELIF ( x > 5 ) // line 9
{
PRINT "Bug if we get here."; // line 11
};
PRINT 42; // line 13
This one has a nested if/else within the else:
module {
func.func @main() -> i32 {
"silly.scope"() ({
"silly.declare"() <{type = i32}> {sym_name = "x"} : () -> ()
%c3_i64 = arith.constant 3 : i64
silly.assign @x = %c3_i64 : i64
%0 = silly.load @x : i32
%c4_i64 = arith.constant 4 : i64
%1 = "silly.less"(%0, %c4_i64) : (i32, i64) -> i1
scf.if %1 {
%2 = silly.load @x : i32
silly.print %2 : i32
} else {
%2 = silly.load @x : i32
%c5_i64 = arith.constant 5 : i64
%3 = "silly.less"(%c5_i64, %2) : (i64, i32) -> i1
scf.if %3 {
%4 = "silly.string_literal"() <{value = "Bug if we get here."}> : () -> !llvm.ptr
silly.print %4 : !llvm.ptr
} else {
}
}
%c42_i64 = arith.constant 42 : i64
silly.print %c42_i64 : i64
%c0_i32 = arith.constant 0 : i32
"silly.return"(%c0_i32) : (i32) -> ()
}) : () -> ()
"silly.yield"() : () -> ()
}
}
Effectively, the existing IF and ELSE implementation has been moved into helper functions, leaving the guts of all the walking functions for IF/ELIF/ELSE almost trivial:
void MLIRListener::enterIfStatement( SillyParser::IfStatementContext *ctx )
try
{
assert( ctx );
mlir::Location loc = getStartLocation( ctx );
SillyParser::BooleanValueContext *booleanValue = ctx->booleanValue();
assert( booleanValue );
createIf( loc, booleanValue, true );
}
CATCH_USER_ERROR
void MLIRListener::enterElseStatement( SillyParser::ElseStatementContext *ctx )
try
{
assert( ctx );
mlir::Location loc = getStartLocation( ctx );
selectElseBlock( loc, ctx->getText() );
}
CATCH_USER_ERROR
void MLIRListener::enterElifStatement( SillyParser::ElifStatementContext *ctx )
try
{
assert( ctx );
mlir::Location loc = getStartLocation( ctx );
selectElseBlock( loc, ctx->getText() );
SillyParser::BooleanValueContext *booleanValue = ctx->booleanValue();
createIf( loc, booleanValue, false );
}
CATCH_USER_ERROR
void MLIRListener::exitIfelifelse( SillyParser::IfelifelseContext *ctx )
try
{
// Restore EXACTLY where we were before creating the scf.if
// This places new ops right AFTER the scf.if
builder.restoreInsertionPoint( insertionPointStack.back() );
insertionPointStack.pop_back();
}
CATCH_USER_ERROR
The selectElseBlock function used to be a createElseBlock. Now it just sets the insertion point, which takes a bit of work to figure out where it is:
void MLIRListener::selectElseBlock( mlir::Location loc, const std::string & errorText )
{
mlir::scf::IfOp ifOp;
// Temporarily restore the insertion point to right after the scf.if, to search for our current IfOp
builder.restoreInsertionPoint( insertionPointStack.back() );
// Now find the scf.if op that is just before the current insertion point
mlir::Block *currentBlock = builder.getInsertionBlock();
assert( currentBlock );
mlir::Block::iterator ip = builder.getInsertionPoint();
// The insertion point is at the position where new ops would be inserted.
// So the operation just before it should be the scf.if
if ( ip != currentBlock->begin() )
{
mlir::Operation *prevOp = &*( --ip ); // the op immediately before the insertion point
ifOp = dyn_cast<mlir::scf::IfOp>( prevOp );
}
if ( !ifOp )
{
throw ExceptionWithContext(
__FILE__, __LINE__, __func__,
std::format( "{}internal error: Could not find scf.if op corresponding to this if statement\n",
formatLocation( loc ), errorText ) );
}
mlir::Region &elseRegion = ifOp.getElseRegion();
mlir::Block &elseBlock = elseRegion.front();
builder.setInsertionPointToStart( &elseBlock );
}
The createIf helper is also pretty trivial:
void MLIRListener::createIf( mlir::Location loc, SillyParser::BooleanValueContext *booleanValue, bool saveIP )
{
mlir::Value conditionPredicate = parsePredicate( loc, booleanValue );
if ( saveIP )
{
insertionPointStack.push_back( builder.saveInsertionPoint() );
}
mlir::scf::IfOp ifOp = builder.create<mlir::scf::IfOp>(
loc, conditionPredicate,
/*withElseRegion=*/true );
mlir::Block &thenBlock = ifOp.getThenRegion().front();
builder.setInsertionPointToStart( &thenBlock );
}
