adding short-circuit boolean expression evaluation (in IR codegen) also -noshortcircuit cli option

This commit is contained in:
Irmen de Jong 2023-10-30 00:09:18 +01:00
parent 813007a5d8
commit f790182f0b
13 changed files with 146 additions and 46 deletions

View File

@ -18,6 +18,7 @@ class CompilationOptions(val output: OutputType,
var optimize: Boolean = false, var optimize: Boolean = false,
var asmQuiet: Boolean = false, var asmQuiet: Boolean = false,
var asmListfile: Boolean = false, var asmListfile: Boolean = false,
var shortCircuit: Boolean = true,
var includeSourcelines: Boolean = false, var includeSourcelines: Boolean = false,
var experimentalCodegen: Boolean = false, var experimentalCodegen: Boolean = false,
var varsHighBank: Int? = null, var varsHighBank: Int? = null,

View File

@ -697,35 +697,59 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
private fun operatorAnd(binExpr: PtBinaryExpression, vmDt: IRDataType): ExpressionCodeResult { private fun operatorAnd(binExpr: PtBinaryExpression, vmDt: IRDataType): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>() val result = mutableListOf<IRCodeChunkBase>()
return if(binExpr.right is PtNumber) { if(codeGen.options.shortCircuit && (!binExpr.left.isSimple() && !binExpr.right.isSimple())) {
val tr = translateExpression(binExpr.left) // short-circuit LEFT and RIGHT --> if LEFT then RIGHT else LEFT (== if !LEFT then LEFT else RIGHT)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.AND, vmDt, reg1 = tr.resultReg, immediate = (binExpr.right as PtNumber).number.toInt()), null)
ExpressionCodeResult(result, vmDt, tr.resultReg, -1)
} else {
val leftTr = translateExpression(binExpr.left) val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1) addToResult(result, leftTr, leftTr.resultReg, -1)
val shortcutLabel = codeGen.createLabelName()
addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = shortcutLabel), null)
val rightTr = translateExpression(binExpr.right) val rightTr = translateExpression(binExpr.right)
addToResult(result, rightTr, rightTr.resultReg, -1) addToResult(result, rightTr, leftTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.ANDR, vmDt, reg1 = leftTr.resultReg, reg2 = rightTr.resultReg), null) result += IRCodeChunk(shortcutLabel, null)
ExpressionCodeResult(result, vmDt, leftTr.resultReg, -1) return ExpressionCodeResult(result, vmDt, leftTr.resultReg, -1)
} else {
return if(binExpr.right is PtNumber) {
val tr = translateExpression(binExpr.left)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.AND, vmDt, reg1 = tr.resultReg, immediate = (binExpr.right as PtNumber).number.toInt()), null)
ExpressionCodeResult(result, vmDt, tr.resultReg, -1)
} else {
val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1)
val rightTr = translateExpression(binExpr.right)
addToResult(result, rightTr, rightTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.ANDR, vmDt, reg1 = leftTr.resultReg, reg2 = rightTr.resultReg), null)
ExpressionCodeResult(result, vmDt, leftTr.resultReg, -1)
}
} }
} }
private fun operatorOr(binExpr: PtBinaryExpression, vmDt: IRDataType): ExpressionCodeResult { private fun operatorOr(binExpr: PtBinaryExpression, vmDt: IRDataType): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>() val result = mutableListOf<IRCodeChunkBase>()
return if(binExpr.right is PtNumber) { if(codeGen.options.shortCircuit && (!binExpr.left.isSimple() && !binExpr.right.isSimple())) {
val tr = translateExpression(binExpr.left) // short-circuit LEFT or RIGHT --> if LEFT then LEFT else RIGHT
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.OR, vmDt, reg1 = tr.resultReg, immediate = (binExpr.right as PtNumber).number.toInt()), null)
ExpressionCodeResult(result, vmDt, tr.resultReg, -1)
} else {
val leftTr = translateExpression(binExpr.left) val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1) addToResult(result, leftTr, leftTr.resultReg, -1)
val shortcutLabel = codeGen.createLabelName()
addInstr(result, IRInstruction(Opcode.BSTNE, labelSymbol = shortcutLabel), null)
val rightTr = translateExpression(binExpr.right) val rightTr = translateExpression(binExpr.right)
addToResult(result, rightTr, rightTr.resultReg, -1) addToResult(result, rightTr, leftTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.ORR, vmDt, reg1 = leftTr.resultReg, reg2 = rightTr.resultReg), null) result += IRCodeChunk(shortcutLabel, null)
ExpressionCodeResult(result, vmDt, leftTr.resultReg, -1) return ExpressionCodeResult(result, vmDt, leftTr.resultReg, -1)
} else {
return if(binExpr.right is PtNumber) {
val tr = translateExpression(binExpr.left)
addToResult(result, tr, tr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.OR, vmDt, reg1 = tr.resultReg, immediate = (binExpr.right as PtNumber).number.toInt()), null)
ExpressionCodeResult(result, vmDt, tr.resultReg, -1)
} else {
val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1)
val rightTr = translateExpression(binExpr.right)
addToResult(result, rightTr, rightTr.resultReg, -1)
addInstr(result, IRInstruction(Opcode.ORR, vmDt, reg1 = leftTr.resultReg, reg2 = rightTr.resultReg), null)
ExpressionCodeResult(result, vmDt, leftTr.resultReg, -1)
}
} }
} }

View File

@ -50,6 +50,7 @@ private fun compileMain(args: Array<String>): Boolean {
val sourceDirs by cli.option(ArgType.String, fullName="srcdirs", description = "list of extra paths, separated with ${File.pathSeparator}, to search in for imported modules").multiple().delimiter(File.pathSeparator) val sourceDirs by cli.option(ArgType.String, fullName="srcdirs", description = "list of extra paths, separated with ${File.pathSeparator}, to search in for imported modules").multiple().delimiter(File.pathSeparator)
val includeSourcelines by cli.option(ArgType.Boolean, fullName = "sourcelines", description = "include original Prog8 source lines in generated asm code") val includeSourcelines by cli.option(ArgType.Boolean, fullName = "sourcelines", description = "include original Prog8 source lines in generated asm code")
val splitWordArrays by cli.option(ArgType.Boolean, fullName = "splitarrays", description = "treat all word arrays as tagged with @split to make them lsb/msb split in memory") val splitWordArrays by cli.option(ArgType.Boolean, fullName = "splitarrays", description = "treat all word arrays as tagged with @split to make them lsb/msb split in memory")
val noShortCircuit by cli.option(ArgType.Boolean, fullName = "noshortcircuit", description = "do not apply McCarthy/short-circuit evaluation to boolean expressions")
val breakpointCpuInstruction by cli.option(ArgType.Boolean, fullName = "breakinstr", description = "also use a CPU instruction for %breakpoint") val breakpointCpuInstruction by cli.option(ArgType.Boolean, fullName = "breakinstr", description = "also use a CPU instruction for %breakpoint")
val compilationTarget by cli.option(ArgType.String, fullName = "target", description = "target output of the compiler (one of '${C64Target.NAME}', '${C128Target.NAME}', '${Cx16Target.NAME}', '${AtariTarget.NAME}', '${PETTarget.NAME}', '${VMTarget.NAME}') (required)") val compilationTarget by cli.option(ArgType.String, fullName = "target", description = "target output of the compiler (one of '${C64Target.NAME}', '${C128Target.NAME}', '${Cx16Target.NAME}', '${AtariTarget.NAME}', '${PETTarget.NAME}', '${VMTarget.NAME}') (required)")
val startVm by cli.option(ArgType.Boolean, fullName = "vm", description = "load and run a .p8ir IR source file in the VM") val startVm by cli.option(ArgType.Boolean, fullName = "vm", description = "load and run a .p8ir IR source file in the VM")
@ -148,6 +149,7 @@ private fun compileMain(args: Array<String>): Boolean {
warnSymbolShadowing == true, warnSymbolShadowing == true,
quietAssembler == true, quietAssembler == true,
asmListfile == true, asmListfile == true,
noShortCircuit != true,
includeSourcelines == true, includeSourcelines == true,
experimentalCodegen == true, experimentalCodegen == true,
varsHighBank, varsHighBank,
@ -224,6 +226,7 @@ private fun compileMain(args: Array<String>): Boolean {
warnSymbolShadowing == true, warnSymbolShadowing == true,
quietAssembler == true, quietAssembler == true,
asmListfile == true, asmListfile == true,
noShortCircuit != true,
includeSourcelines == true, includeSourcelines == true,
experimentalCodegen == true, experimentalCodegen == true,
varsHighBank, varsHighBank,

View File

@ -33,6 +33,7 @@ class CompilerArguments(val filepath: Path,
val warnSymbolShadowing: Boolean, val warnSymbolShadowing: Boolean,
val quietAssembler: Boolean, val quietAssembler: Boolean,
val asmListfile: Boolean, val asmListfile: Boolean,
val shortCircuit: Boolean,
val includeSourcelines: Boolean, val includeSourcelines: Boolean,
val experimentalCodegen: Boolean, val experimentalCodegen: Boolean,
val varsHighBank: Int?, val varsHighBank: Int?,
@ -76,6 +77,7 @@ fun compileProgram(args: CompilerArguments): CompilationResult? {
optimize = args.optimize optimize = args.optimize
asmQuiet = args.quietAssembler asmQuiet = args.quietAssembler
asmListfile = args.asmListfile asmListfile = args.asmListfile
shortCircuit = args.shortCircuit
includeSourcelines = args.includeSourcelines includeSourcelines = args.includeSourcelines
experimentalCodegen = args.experimentalCodegen experimentalCodegen = args.experimentalCodegen
breakpointCpuInstruction = args.breakpointCpuInstruction breakpointCpuInstruction = args.breakpointCpuInstruction

View File

@ -30,6 +30,7 @@ private fun compileTheThing(filepath: Path, optimize: Boolean, target: ICompilat
warnSymbolShadowing = false, warnSymbolShadowing = false,
quietAssembler = true, quietAssembler = true,
asmListfile = false, asmListfile = false,
shortCircuit = true,
includeSourcelines = false, includeSourcelines = false,
experimentalCodegen = false, experimentalCodegen = false,
varsHighBank = null, varsHighBank = null,

View File

@ -28,6 +28,7 @@ class TestCompilerOptionSourcedirs: FunSpec({
warnSymbolShadowing = false, warnSymbolShadowing = false,
quietAssembler = true, quietAssembler = true,
asmListfile = false, asmListfile = false,
shortCircuit = true,
includeSourcelines = false, includeSourcelines = false,
experimentalCodegen = false, experimentalCodegen = false,
varsHighBank = null, varsHighBank = null,

View File

@ -27,6 +27,7 @@ internal fun compileFile(
warnSymbolShadowing = false, warnSymbolShadowing = false,
quietAssembler = true, quietAssembler = true,
asmListfile = false, asmListfile = false,
shortCircuit = true,
includeSourcelines = false, includeSourcelines = false,
experimentalCodegen = false, experimentalCodegen = false,
varsHighBank = null, varsHighBank = null,

View File

@ -195,6 +195,11 @@ One or more .p8 module files
This removes the need to add @split yourself but some programs may fail to compile with This removes the need to add @split yourself but some programs may fail to compile with
this option as not all array operations are implemented yet on split arrays. this option as not all array operations are implemented yet on split arrays.
``-noshortcircuit``
Do *not* apply `McCarthy/short-circuit evaluation <https://en.wikipedia.org/wiki/Short-circuit_evaluation>`_ to boolean expressions.
This is a new feature and changes the behavior of existing programs so it can be turned off again for now.
This toggle will disappear eventually.
``-vm`` ``-vm``
load and run a p8-virt or p8-ir listing in the internal VirtualMachine instead of compiling a prog8 program file.. load and run a p8-virt or p8-ir listing in the internal VirtualMachine instead of compiling a prog8 program file..

View File

@ -2,7 +2,11 @@
TODO TODO
==== ====
- [on branch: shortcircuit] investigate McCarthy evaluation again? this may also reduce code size perhaps for things like if a>4 or a<2 .... - [on branch: shortcircuit] complete McCarthy evaluation. This may also reduce code size perhaps for things like if a>4 or a<2 ....
- vm ircodegen (DONE)
- in 6502 codegen (see vm's ExpressionGen operatorAnd / operatorOr)
- IR: reduce amount of CMP/CMPI after instructions that set the status bits correctly (LOADs? INC? Bitwise operations, etc), but only after setting the status bits is verified!
... ...
@ -11,7 +15,6 @@ Future Things and Ideas
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
Compiler: Compiler:
- (after shortcircuit is in:) What happens when we make all subs return a boolean not as ubyte in A, but in the cpu's Carry flag?
- Multidimensional arrays and chained indexing, purely as syntactic sugar over regular arrays. - Multidimensional arrays and chained indexing, purely as syntactic sugar over regular arrays.
- make a form of "manual generics" possible like: varsub routine(T arg)->T where T is expanded to a specific type - make a form of "manual generics" possible like: varsub routine(T arg)->T where T is expanded to a specific type
(this is already done hardcoded for several of the builtin functions) (this is already done hardcoded for several of the builtin functions)
@ -27,7 +30,6 @@ Compiler:
- (need separate step in codegen and IR to write the "golden" variables) - (need separate step in codegen and IR to write the "golden" variables)
- do we need (array)variable alignment tag instead of block alignment tag? You want to align the data, not the code in the block? - do we need (array)variable alignment tag instead of block alignment tag? You want to align the data, not the code in the block?
- ir: proper code gen for the CALLI instruction and that it (optionally) returns a word value that needs to be assigned to a reg
- ir: getting it in shape for code generation - ir: getting it in shape for code generation
- ir: related to the one above: block alignment doesn't translate well to variables in the block (the actual stuff that needs to be aligned in memory) but: need variable alignment tag instead of block alignment tag, really - ir: related to the one above: block alignment doesn't translate well to variables in the block (the actual stuff that needs to be aligned in memory) but: need variable alignment tag instead of block alignment tag, really
- ir: idea: (but LLVM IR simply keeps the variables, so not a good idea then?...): replace all scalar variables by an allocated register. Keep a table of the variable to register mapping (including the datatype) - ir: idea: (but LLVM IR simply keeps the variables, so not a good idea then?...): replace all scalar variables by an allocated register. Keep a table of the variable to register mapping (including the datatype)
@ -48,7 +50,7 @@ Compiler:
Libraries: Libraries:
- once a VAL_1 implementation is merged into the X16 kernal properly, remove all the workarounds in cx16 floats.parse_f() . Prototype parse routine in examples/cx16/floatparse.p8 - once a VAL_1 implementation is merged into the X16 kernal properly, remove all the workarounds in cx16 floats.parse_f()
- fix the problems in atari target, and flesh out its libraries. - fix the problems in atari target, and flesh out its libraries.
- c128 target: make syslib more complete (missing kernal routines)? - c128 target: make syslib more complete (missing kernal routines)?
- pet32 target: make syslib more complete (missing kernal routines)? - pet32 target: make syslib more complete (missing kernal routines)?
@ -56,6 +58,8 @@ Libraries:
Optimizations: Optimizations:
- give a warning for variables that could be a const - or even make them a const (if not @shared)?
- treat every scalar variable decl with initialization value, as const by default, unless the variable gets assigned to somewhere (or has its address taken, or is @shared)
- VariableAllocator: can we think of a smarter strategy for allocating variables into zeropage, rather than first-come-first-served? - VariableAllocator: can we think of a smarter strategy for allocating variables into zeropage, rather than first-come-first-served?
for instance, vars used inside loops first, then loopvars, then uwords used as pointers, then the rest for instance, vars used inside loops first, then loopvars, then uwords used as pointers, then the rest
- various optimizers skip stuff if compTarget.name==VMTarget.NAME. Once 6502-codegen is done from IR code, - various optimizers skip stuff if compTarget.name==VMTarget.NAME. Once 6502-codegen is done from IR code,
@ -78,6 +82,9 @@ What if we were to re-introduce Structs in prog8? Some thoughts:
Other language/syntax features to think about Other language/syntax features to think about
--------------------------------------------- ---------------------------------------------
- add (rom/ram)bank support to romsub. A call will then automatically switch banks, use callfar and something else when in banked ram. - module directive to set the text encoding for that whole file (iso, petscii, etc.)
challenges: how to not make this too X16 specific? How does the compiler know what bank to switch (ram/rom)? - chained assignments `x=y=z=99`
How to make it performant when we want to (i.e. NOT have it use callfar/auto bank switching) ? - declare multiple variables `ubyte x,y,z` (if init value present, all get that init value)
- chained comparisons `10<x<20` , `x==y==z` (desugars to `10<x and x<20`, `x==y and y==z`)
- postincrdecr as expression, preincrdecr expression (`y = x++`, `y = ++x`) .... is this even possible, expression with side effects like this?
- negative array index to refer to an element from the end of the array. Python `[-1]` or Raku syntax `[\*-1]` , `[\*/2]` .... \*=size of the array

View File

@ -1,21 +1,67 @@
%import math
%import string
%import emudbg
%import palette
%import floats
%import textio %import textio
%zeropage dontuse %zeropage dontuse
main { main {
sub start() { sub start () {
emudbg.console_value1(123) ubyte a1 = 10
emudbg.console_value2(99) ubyte a2 = 20
emudbg.console_chrout('@') ubyte x1 = 30
emudbg.console_chrout('h') ubyte x2 = 40
emudbg.console_chrout('e') ubyte zero = 0
emudbg.console_chrout('l')
emudbg.console_chrout('l') txt.print("1a:\n")
emudbg.console_chrout('o') if calc_a1()<calc_x1() and calc_a2()<=calc_x2()
emudbg.console_chrout('\n') txt.print("* 1a and ok\n")
txt.print("\n1b:\n")
if calc_a1()<calc_x1() and calc_a2()>calc_x2()
txt.print("* 1b and fail\n")
txt.print("\n1c:\n")
if calc_a1()>calc_x1() and calc_a2()<=calc_x2()
txt.print("* 1c and fail\n")
txt.print("\n2a:\n")
if calc_a1()<calc_x1() or calc_a2()<=calc_x2()
txt.print("* 2a or ok\n")
txt.print("\n2b:\n")
if calc_a1()<calc_x1() or calc_a2()>calc_x2()
txt.print("* 2b or ok\n")
txt.print("\n3a:\n")
if calc_a1()>calc_x1() or calc_a2()<=calc_x2()
txt.print("* 3a or ok\n")
txt.print("\n3b:\n")
if calc_a1()>calc_x1() or calc_a2()>calc_x2()
txt.print("* 3b or fail\n")
txt.print("\n4a:\n")
bool result = calc_a1()<calc_x1() or calc_a2()>calc_x2()
txt.print_ub(result)
txt.nl()
txt.print("\n4b:\n")
result = calc_a1()>=calc_x1() and calc_a2()>calc_x2()
txt.print_ub(result)
txt.nl()
@($4000) &= 22
sub calc_a1() -> ubyte {
txt.print("calc_a1\n")
return a1+zero
}
sub calc_a2() -> ubyte {
txt.print("calc_a2\n")
return a2+zero
}
sub calc_x1() -> ubyte {
txt.print("calc_x1\n")
return x1+zero
}
sub calc_x2() -> ubyte {
txt.print("calc_x2\n")
return x2+zero
}
} }
} }

View File

@ -40,6 +40,7 @@ class RequestParser : Take {
quietAssembler = false, quietAssembler = false,
includeSourcelines = false, includeSourcelines = false,
asmListfile = false, asmListfile = false,
shortCircuit = true,
experimentalCodegen = false, experimentalCodegen = false,
splitWordArrays = false, splitWordArrays = false,
breakpointCpuInstruction = false, breakpointCpuInstruction = false,

View File

@ -14,10 +14,10 @@ Program to execute is not stored in the system memory, it's just a separate list
65536 virtual floating point registers (64 bits double precision) fr0-fr65535 65536 virtual floating point registers (64 bits double precision) fr0-fr65535
65536 bytes of memory. Thus memory pointers (addresses) are limited to 16 bits. 65536 bytes of memory. Thus memory pointers (addresses) are limited to 16 bits.
Value stack, max 128 entries of 1 byte each. Value stack, max 128 entries of 1 byte each.
Status flags: Carry, Zero, Negative. NOTE: status flags are only affected by the CMP instruction or explicit CLC/SEC!!! Status flags: Carry, Zero, Negative. NOTE: status flags are only affected by the CMP instruction or explicit CLC/SEC,
LOAD instructions DO affect the Z and N flags LOAD instructions DO affect the Z and N flags.
INC/DEC instructions DO affect the Z and N flags INC/DEC instructions DO affect the Z and N flags,
other instructions such as logical or arithmetic operations DO NOT AFFECT THE STATUS FLAGS UNLESS EXPLICITLY NOTED! other instructions only affect Z an N flags if the value in a result register is written.
Instruction set is mostly a load/store architecture, there are few instructions operating on memory directly. Instruction set is mostly a load/store architecture, there are few instructions operating on memory directly.

View File

@ -311,8 +311,16 @@ class VirtualMachine(irProgram: IRProgram) {
private inline fun setResultReg(reg: Int, value: Int, type: IRDataType) { private inline fun setResultReg(reg: Int, value: Int, type: IRDataType) {
when(type) { when(type) {
IRDataType.BYTE -> registers.setUB(reg, value.toUByte()) IRDataType.BYTE -> {
IRDataType.WORD -> registers.setUW(reg, value.toUShort()) registers.setUB(reg, value.toUByte())
statusZero = value==0
statusNegative = value>=0x80
}
IRDataType.WORD -> {
registers.setUW(reg, value.toUShort())
statusZero = value==0
statusNegative = value>=0x8000
}
IRDataType.FLOAT -> throw IllegalArgumentException("attempt to set integer result register but float type") IRDataType.FLOAT -> throw IllegalArgumentException("attempt to set integer result register but float type")
} }
} }