add "X in [1,2,3]" expression (efficient containment check)

This commit is contained in:
Irmen de Jong 2021-12-29 16:21:37 +01:00
parent 7a9e5afb93
commit de6ce4a46e
31 changed files with 615 additions and 150 deletions

View File

@ -37,9 +37,11 @@ internal class ExpressionsAsmGen(private val program: Program, private val asmge
is NumericLiteralValue -> translateExpression(expression)
is IdentifierReference -> translateExpression(expression)
is FunctionCallExpr -> translateFunctionCallResultOntoStack(expression)
is ContainmentCheck -> throw AssemblyError("containment check as complex expression value is not supported")
is ArrayLiteralValue, is StringLiteralValue -> throw AssemblyError("no asm gen for string/array literal value assignment - should have been replaced by a variable")
is RangeExpr -> throw AssemblyError("range expression should have been changed into array values")
is CharLiteral -> throw AssemblyError("charliteral should have been replaced by ubyte using certain encoding")
else -> TODO("missing expression asmgen for $expression")
}
}

View File

@ -9,7 +9,6 @@ import prog8.ast.expressions.RangeExpr
import prog8.ast.statements.ForLoop
import prog8.ast.toHex
import prog8.codegen.target.AssemblyError
import prog8.compilerinterface.toConstantIntegerRange
import kotlin.math.absoluteValue
internal class ForLoopsAsmGen(private val program: Program, private val asmgen: AsmGen) {

View File

@ -271,6 +271,10 @@ internal class AssignmentAsmGen(private val program: Program, private val asmgen
else -> throw AssemblyError("invalid prefix operator")
}
}
is ContainmentCheck -> {
containmentCheckIntoA(value)
assignRegisterByte(assign.target, CpuRegister.A)
}
else -> {
// Everything else just evaluate via the stack.
// (we can't use the assignment helper functions (assignExpressionTo...) to do it via registers here,
@ -294,6 +298,124 @@ internal class AssignmentAsmGen(private val program: Program, private val asmgen
}
}
private fun containmentCheckIntoA(containment: ContainmentCheck) {
val elementDt = containment.element.inferType(program)
val range = containment.iterable as? RangeExpr
if(range!=null) {
val constRange = range.toConstantIntegerRange()
if(constRange!=null)
return containmentCheckIntoA(containment.element, elementDt.getOr(DataType.UNDEFINED), constRange.toList())
TODO("non-const range containment check ${containment.position}")
}
val variable = (containment.iterable as? IdentifierReference)?.targetVarDecl(program)
if(variable!=null) {
if(elementDt istype DataType.FLOAT)
throw AssemblyError("containment check of floats not supported")
if(variable.autogeneratedDontRemove) {
when(variable.datatype) {
DataType.STR -> {
require(elementDt.isBytes)
val stringVal = variable.value as StringLiteralValue
val encoded = program.encoding.encodeString(stringVal.value, stringVal.altEncoding)
return containmentCheckIntoA(containment.element, elementDt.getOr(DataType.UNDEFINED), encoded.map { it.toInt() })
}
DataType.ARRAY_F -> {
// require(elementDt istype DataType.FLOAT)
throw AssemblyError("containment check of floats not supported")
}
in ArrayDatatypes -> {
require(elementDt.isInteger)
val arrayVal = variable.value as ArrayLiteralValue
val values = arrayVal.value.map { it.constValue(program)!!.number.toInt() }
return containmentCheckIntoA(containment.element, elementDt.getOr(DataType.UNDEFINED), values)
}
else -> throw AssemblyError("invalid dt")
}
}
val varname = asmgen.asmVariableName(containment.iterable as IdentifierReference)
when(variable.datatype) {
DataType.STR -> {
assignAddressOf(AsmAssignTarget(TargetStorageKind.VARIABLE, program, asmgen, DataType.UWORD, containment.definingSubroutine, "P8ZP_SCRATCH_W1"), varname)
assignExpressionToRegister(containment.element, RegisterOrPair.A, elementDt istype DataType.BYTE)
val stringVal = variable.value as StringLiteralValue
asmgen.out(" ldy #${stringVal.value.length}")
asmgen.out(" jsr prog8_lib.containment_bytearray")
return
}
DataType.ARRAY_F -> throw AssemblyError("containment check of floats not supported")
DataType.ARRAY_B, DataType.ARRAY_UB -> {
val arrayVal = variable.value as ArrayLiteralValue
assignAddressOf(AsmAssignTarget(TargetStorageKind.VARIABLE, program, asmgen, DataType.UWORD, containment.definingSubroutine, "P8ZP_SCRATCH_W1"), varname)
assignExpressionToRegister(containment.element, RegisterOrPair.A, elementDt istype DataType.BYTE)
asmgen.out(" ldy #${arrayVal.value.size}")
asmgen.out(" jsr prog8_lib.containment_bytearray")
return
}
DataType.ARRAY_W, DataType.ARRAY_UW -> {
val arrayVal = variable.value as ArrayLiteralValue
assignExpressionToVariable(containment.element, "P8ZP_SCRATCH_W1", elementDt.getOr(DataType.UNDEFINED), containment.definingSubroutine)
assignAddressOf(AsmAssignTarget(TargetStorageKind.VARIABLE, program, asmgen, DataType.UWORD, containment.definingSubroutine, "P8ZP_SCRATCH_W2"), varname)
asmgen.out(" ldy #${arrayVal.value.size}")
asmgen.out(" jsr prog8_lib.containment_wordarray")
return
}
else -> throw AssemblyError("invalid dt")
}
}
val stringVal = containment.iterable as? StringLiteralValue
if(stringVal!=null) {
require(elementDt.isBytes)
val encoded = program.encoding.encodeString(stringVal.value, stringVal.altEncoding)
return containmentCheckIntoA(containment.element, elementDt.getOr(DataType.UNDEFINED), encoded.map { it.toInt() })
}
val arrayVal = containment.iterable as? ArrayLiteralValue
if(arrayVal!=null) {
require(elementDt.isInteger)
val values = arrayVal.value.map { it.constValue(program)!!.number.toInt() }
return containmentCheckIntoA(containment.element, elementDt.getOr(DataType.UNDEFINED), values)
}
throw AssemblyError("invalid containment iterable type")
}
private fun containmentCheckIntoA(element: Expression, dt: DataType, values: List<Number>) {
if(values.size<2)
throw AssemblyError("containment check against 0 or 1 values should have been optimized away")
// TODO don't generate a huge cmp-list when we go over a certain number of values
val containsLabel = asmgen.makeLabel("contains")
when(dt) {
in ByteDatatypes -> {
asmgen.assignExpressionToRegister(element, RegisterOrPair.A, dt==DataType.BYTE)
for (value in values) {
asmgen.out(" cmp #$value | beq +")
}
asmgen.out("""
lda #0
beq ++
+ lda #1
+""")
}
in WordDatatypes -> {
asmgen.assignExpressionToRegister(element, RegisterOrPair.AY, dt==DataType.WORD)
for (value in values) {
asmgen.out("""
cmp #<$value
bne +
cpy #>$value
beq $containsLabel
+""")
}
asmgen.out("""
lda #0
beq +
$containsLabel lda #1
+""")
}
else -> throw AssemblyError("invalid dt")
}
}
private fun assignStatusFlagByte(target: AsmAssignTarget, statusflag: Statusflag) {
when(statusflag) {
Statusflag.Pc -> {

View File

@ -22,7 +22,7 @@ class BinExprSplitter(private val program: Program, private val options: Compila
override fun after(assignment: Assignment, parent: Node): Iterable<IAstModification> {
if(assignment.value.inferType(program).istype(DataType.FLOAT) && !options.optimizeFloatExpressions)
if(assignment.value.inferType(program) istype DataType.FLOAT && !options.optimizeFloatExpressions)
return noModifications
val binExpr = assignment.value as? BinaryExpression

View File

@ -23,6 +23,13 @@ class ConstantFoldingOptimizer(private val program: Program) : AstWalker() {
noModifications
}
override fun after(containment: ContainmentCheck, parent: Node): Iterable<IAstModification> {
val result = containment.constValue(program)
if(result!=null)
return listOf(IAstModification.ReplaceNode(containment, result, parent))
return noModifications
}
override fun after(expr: PrefixExpression, parent: Node): Iterable<IAstModification> {
// Try to turn a unary prefix expression into a single constant value.
// Compile-time constant sub expressions will be evaluated on the spot.

View File

@ -9,8 +9,6 @@ import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter
import prog8.compilerinterface.size
import prog8.compilerinterface.toConstantIntegerRange
// Fix up the literal value's type to match that of the vardecl
// (also check range literal operands types before they get expanded into arrays for instance)

View File

@ -344,6 +344,25 @@ class ExpressionSimplifier(private val program: Program) : AstWalker() {
return noModifications
}
override fun after(containment: ContainmentCheck, parent: Node): Iterable<IAstModification> {
val range = containment.iterable as? RangeExpr
if(range!=null && range.step.constValue(program)?.number==1.0) {
val from = range.from.constValue(program)
val to = range.to.constValue(program)
val value = containment.element
if(from!=null && to!=null && value.isSimple) {
if(to.number-from.number>6.0) {
// replace containment test with X>=from and X<=to
val left = BinaryExpression(value, ">=", from, containment.position)
val right = BinaryExpression(value.copy(), "<=", to, containment.position)
val comparison = BinaryExpression(left, "and", right, containment.position)
return listOf(IAstModification.ReplaceNode(containment, comparison, parent))
}
}
}
return noModifications
}
private fun determineY(x: Expression, subBinExpr: BinaryExpression): Expression? {
return when {
subBinExpr.left isSameAs x -> subBinExpr.right

View File

@ -72,10 +72,10 @@ fun Program.splitBinaryExpressions(options: CompilationOptions, compTarget: ICom
fun getTempVarName(dt: InferredTypes.InferredType): List<String> {
return when {
// TODO assume (hope) cx16.r9 isn't used for anything else during the use of this temporary variable...
dt.istype(DataType.UBYTE) -> listOf("cx16", "r9L")
dt.istype(DataType.BYTE) -> listOf("cx16", "r9sL")
dt.istype(DataType.UWORD) -> listOf("cx16", "r9")
dt.istype(DataType.WORD) -> listOf("cx16", "r9s")
dt istype DataType.UBYTE -> listOf("cx16", "r9L")
dt istype DataType.BYTE -> listOf("cx16", "r9sL")
dt istype DataType.UWORD -> listOf("cx16", "r9")
dt istype DataType.WORD -> listOf("cx16", "r9s")
dt.isPassByReference -> listOf("cx16", "r9")
else -> throw FatalAstException("invalid dt $dt")
}

View File

@ -8,7 +8,6 @@ import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
import prog8.compilerinterface.ICompilationTarget
import prog8.compilerinterface.IErrorReporter
import prog8.compilerinterface.size
import kotlin.math.floor
@ -154,11 +153,11 @@ class StatementOptimizer(private val program: Program,
if(constvalue!=null) {
return if(constvalue.asBooleanValue){
// always true -> keep only if-part
errors.warn("condition is always true", ifElse.position)
errors.warn("condition is always true", ifElse.condition.position)
listOf(IAstModification.ReplaceNode(ifElse, ifElse.truepart, parent))
} else {
// always false -> keep only else-part
errors.warn("condition is always false", ifElse.position)
errors.warn("condition is always false", ifElse.condition.position)
listOf(IAstModification.ReplaceNode(ifElse, ifElse.elsepart, parent))
}
}

View File

@ -1083,3 +1083,45 @@ strlen .proc
bne -
+ rts
.pend
containment_bytearray .proc
; -- check if a value exists in a byte array.
; parameters: P8ZP_SCRATCH_W1: address of the byte array, A = byte to check, Y = length of array (>=1).
; returns boolean 0/1 in A.
dey
- cmp (P8ZP_SCRATCH_W1),y
beq +
dey
cpy #255
bne -
lda #0
rts
+ lda #1
rts
.pend
containment_wordarray .proc
; -- check if a value exists in a word array.
; parameters: P8ZP_SCRATCH_W1: value to check, P8ZP_SCRATCH_W2: address of the word array, Y = length of array (>=1).
; returns boolean 0/1 in A.
dey
tya
asl a
tay
- lda P8ZP_SCRATCH_W1
cmp (P8ZP_SCRATCH_W2),y
bne +
lda P8ZP_SCRATCH_W1+1
iny
cmp (P8ZP_SCRATCH_W2),y
beq _found
+ dey
dey
cpy #254
bne -
lda #0
rts
_found lda #1
rts
.pend

View File

@ -67,7 +67,7 @@ internal class BeforeAsmGenerationAstChanger(val program: Program, private val o
&& !assignment.target.isIOAddress(options.compTarget.machine)) {
val binExpr = assignment.value as? BinaryExpression
if(binExpr!=null && binExpr.inferType(program).istype(DataType.FLOAT) && !options.optimizeFloatExpressions)
if(binExpr!=null && binExpr.inferType(program) istype DataType.FLOAT && !options.optimizeFloatExpressions)
return noModifications
if (binExpr != null && binExpr.operator !in ComparisonOperators) {
@ -270,10 +270,10 @@ internal class BeforeAsmGenerationAstChanger(val program: Program, private val o
}
if(separateRightExpr) {
val name = when {
rightDt.istype(DataType.UBYTE) -> listOf("prog8_lib","retval_interm_ub")
rightDt.istype(DataType.UWORD) -> listOf("prog8_lib","retval_interm_uw")
rightDt.istype(DataType.BYTE) -> listOf("prog8_lib","retval_interm_b2")
rightDt.istype(DataType.WORD) -> listOf("prog8_lib","retval_interm_w2")
rightDt istype DataType.UBYTE -> listOf("prog8_lib","retval_interm_ub")
rightDt istype DataType.UWORD -> listOf("prog8_lib","retval_interm_uw")
rightDt istype DataType.BYTE -> listOf("prog8_lib","retval_interm_b2")
rightDt istype DataType.WORD -> listOf("prog8_lib","retval_interm_w2")
else -> throw AssemblyError("invalid dt")
}
rightOperandReplacement = IdentifierReference(name, expr.position)

View File

@ -255,7 +255,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
private fun processAst(program: Program, errors: IErrorReporter, compilerOptions: CompilationOptions) {
// perform initial syntax checks and processings
println("Processing for target ${compilerOptions.compTarget.name}...")
program.preprocessAst(program)
program.preprocessAst(program, errors)
program.checkIdentifiers(errors, program, compilerOptions)
errors.report()
// TODO: turning char literals into UBYTEs via an encoding should really happen in code gen - but for that we'd need DataType.CHAR

View File

@ -874,6 +874,7 @@ internal class AstChecker(private val program: Program,
if(leftDt !in IntegerDatatypes || rightDt !in IntegerDatatypes)
errors.err("bitwise operator can only be used on integer operands", expr.right.position)
}
"in" -> throw FatalAstException("in expression should have been replaced by containmentcheck")
}
if(leftDt !in NumericDatatypes && leftDt != DataType.STR)
@ -1206,6 +1207,17 @@ internal class AstChecker(private val program: Program,
super.visit(whenChoice)
}
override fun visit(containment: ContainmentCheck) {
if(!containment.iterable.inferType(program).isIterable)
errors.err("value set for containment check must be an iterable type", containment.iterable.position)
if(containment.parent is BinaryExpression)
errors.err("containment check is currently not supported in complex expressions", containment.position)
// TODO check that iterable contains the same types as the element that is searched
super.visit(containment)
}
private fun checkFunctionOrLabelExists(target: IdentifierReference, statement: Statement): Statement? {
when (val targetStatement = target.targetStatement(program)) {
is Label, is Subroutine, is BuiltinFunctionPlaceholder -> return targetStatement

View File

@ -72,8 +72,8 @@ internal fun Program.verifyFunctionArgTypes() {
fixer.visit(this)
}
internal fun Program.preprocessAst(program: Program) {
val transforms = AstPreprocessor(program)
internal fun Program.preprocessAst(program: Program, errors: IErrorReporter) {
val transforms = AstPreprocessor(program, errors)
transforms.visit(this)
var mods = transforms.applyModifications()
while(mods>0)

View File

@ -2,21 +2,15 @@ package prog8.compiler.astprocessing
import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.base.NumericDatatypes
import prog8.ast.base.SyntaxError
import prog8.ast.base.VarDeclType
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.NumericLiteralValue
import prog8.ast.expressions.RangeExpr
import prog8.ast.statements.AnonymousScope
import prog8.ast.statements.AssignTarget
import prog8.ast.statements.Assignment
import prog8.ast.statements.VarDecl
import prog8.ast.base.*
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
import prog8.compilerinterface.IErrorReporter
class AstPreprocessor(val program: Program) : AstWalker() {
class AstPreprocessor(val program: Program, val errors: IErrorReporter) : AstWalker() {
override fun after(range: RangeExpr, parent: Node): Iterable<IAstModification> {
// has to be done before the constant folding, otherwise certain checks there will fail on invalid range sizes
@ -82,4 +76,13 @@ class AstPreprocessor(val program: Program) : AstWalker() {
}
return noModifications
}
override fun after(expr: BinaryExpression, parent: Node): Iterable<IAstModification> {
// this has to be done here becuse otherwise the string / range literal values will have been replaced by variables
if(expr.operator=="in") {
val containment = ContainmentCheck(expr.left, expr.right, expr.position)
return listOf(IAstModification.ReplaceNode(expr, containment, parent))
}
return noModifications
}
}

View File

@ -6,10 +6,7 @@ import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.base.ParentSentinel
import prog8.ast.base.Position
import prog8.ast.expressions.DirectMemoryRead
import prog8.ast.expressions.FunctionCallExpr
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.PrefixExpression
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification
@ -25,6 +22,8 @@ internal class CodeDesugarer(val program: Program, private val errors: IErrorRep
//
// List of modifications:
// - replace 'break' statements by a goto + generated after label.
// - replace while and do-until loops by just jumps.
// - replace peek() and poke() by direct memory accesses.
private var generatedLabelSequenceNumber: Int = 0
@ -135,4 +134,11 @@ _after:
}
return noModifications
}
override fun after(expr: BinaryExpression, parent: Node): Iterable<IAstModification> {
if(expr.operator=="in") {
println("IN-TEST: $expr\n in: $parent")
}
return noModifications
}
}

View File

@ -4,6 +4,7 @@ import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.base.DataType
import prog8.ast.expressions.ArrayLiteralValue
import prog8.ast.expressions.ContainmentCheck
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.StringLiteralValue
import prog8.ast.statements.VarDecl
@ -15,7 +16,7 @@ import prog8.ast.walk.IAstModification
internal class LiteralsToAutoVars(private val program: Program) : AstWalker() {
override fun after(string: StringLiteralValue, parent: Node): Iterable<IAstModification> {
if(string.parent !is VarDecl && string.parent !is WhenChoice) {
if(string.parent !is VarDecl && string.parent !is WhenChoice && string.parent !is ContainmentCheck) {
// replace the literal string by an identifier reference to the interned string
val scopedName = program.internString(string)
val identifier = IdentifierReference(scopedName, string.position)
@ -35,6 +36,9 @@ internal class LiteralsToAutoVars(private val program: Program) : AstWalker() {
return listOf(IAstModification.ReplaceNode(vardecl.value!!, cast, vardecl))
}
} else {
if(array.parent is ContainmentCheck)
return noModifications
val arrayDt = array.guessDatatype(program)
if(arrayDt.isKnown) {
// turn the array literal it into an identifier reference

View File

@ -3,6 +3,8 @@ package prog8.compiler.astprocessing
import prog8.ast.IStatementContainer
import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.base.ArrayDatatypes
import prog8.ast.base.DataType
import prog8.ast.base.FatalAstException
import prog8.ast.expressions.*
import prog8.ast.statements.*
@ -97,7 +99,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter)
val rightBinExpr = expr.right as? BinaryExpression
if(leftBinExpr!=null && leftBinExpr.operator=="==" && rightBinExpr!=null && rightBinExpr.operator=="==") {
if(leftBinExpr.right is NumericLiteralValue && rightBinExpr.right is NumericLiteralValue) {
errors.warn("consider using when statement to test for multiple values", expr.position)
errors.warn("consider using 'in' or 'when' to test for multiple values", expr.position)
}
}
}
@ -124,4 +126,76 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter)
}
return noModifications
}
override fun after(containment: ContainmentCheck, parent: Node): Iterable<IAstModification> {
// replace trivial containment checks with just false or a single comparison
fun replaceWithEquals(value: NumericLiteralValue): Iterable<IAstModification> {
errors.warn("containment could be written as just a single comparison", containment.position)
val equals = BinaryExpression(containment.element, "==", value, containment.position)
return listOf(IAstModification.ReplaceNode(containment, equals, parent))
}
fun replaceWithFalse(): Iterable<IAstModification> {
errors.warn("condition is always false", containment.position)
return listOf(IAstModification.ReplaceNode(containment, NumericLiteralValue.fromBoolean(false, containment.position), parent))
}
fun checkArray(array: Array<Expression>): Iterable<IAstModification> {
if(array.isEmpty())
return replaceWithFalse()
if(array.size==1) {
val constVal = array[0].constValue(program)
if(constVal!=null)
return replaceWithEquals(constVal)
}
return noModifications
}
fun checkString(stringVal: StringLiteralValue): Iterable<IAstModification> {
if(stringVal.value.isEmpty())
return replaceWithFalse()
if(stringVal.value.length==1) {
val string = program.encoding.encodeString(stringVal.value, stringVal.altEncoding)
return replaceWithEquals(NumericLiteralValue(DataType.UBYTE, string[0].toDouble(), stringVal.position))
}
return noModifications
}
when(containment.iterable) {
is ArrayLiteralValue -> {
val array = (containment.iterable as ArrayLiteralValue).value
return checkArray(array)
}
is IdentifierReference -> {
val variable = (containment.iterable as IdentifierReference).targetVarDecl(program)!!
when(variable.datatype) {
DataType.STR -> {
val stringVal = (variable.value as StringLiteralValue)
return checkString(stringVal)
}
in ArrayDatatypes -> {
val array = (variable.value as ArrayLiteralValue).value
return checkArray(array)
}
else -> {}
}
}
is RangeExpr -> {
val constValues = (containment.iterable as RangeExpr).toConstantIntegerRange()
if(constValues!=null) {
if (constValues.isEmpty())
return replaceWithFalse()
if (constValues.count()==1)
return replaceWithEquals(NumericLiteralValue.optimalNumeric(constValues.first, containment.position))
}
}
is StringLiteralValue -> {
val stringVal = containment.iterable as StringLiteralValue
return checkString(stringVal)
}
else -> {}
}
return noModifications
}
}

View File

@ -12,8 +12,6 @@ import prog8.ast.statements.ForLoop
import prog8.ast.statements.VarDecl
import prog8.codegen.target.C64Target
import prog8.codegen.target.Cx16Target
import prog8.compilerinterface.size
import prog8.compilerinterface.toConstantIntegerRange
import prog8tests.helpers.*
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.assertFailure
@ -308,8 +306,10 @@ class TestCompilerOnRanges: FunSpec({
main {
sub start() {
ubyte xx
uword ww
str name = "irmen"
ubyte[] values = [1,2,3,4,5,6,7]
uword[] wvalues = [1000,2000,3000]
for xx in name {
xx++
@ -330,19 +330,48 @@ class TestCompilerOnRanges: FunSpec({
for xx in [2,4,6,8] {
xx++
}
for ww in [9999,8888,7777] {
xx++
}
for ww in wvalues {
xx++
}
}
}""").assertSuccess()
}""", writeAssembly = true).assertSuccess()
}
// TODO enable this after this if-syntax is implemented
xtest("if containment check on all possible iterable expressions") {
test("if containment check on all possible iterable expressions") {
compileText(C64Target, false, """
main {
sub start() {
ubyte xx
uword ww
str name = "irmen"
ubyte[] values = [1,2,3,4,5,6,7]
uword[] wvalues = [1000,2000,3000]
if 'm' in name {
xx++
}
if 5 in values {
xx++
}
if 16 in 10 to 20 step 3 {
xx++
}
if 'b' in "abcdef" {
xx++
}
if 8 in [2,4,6,8] {
xx++
}
if xx in name {
xx++
}
@ -362,7 +391,41 @@ class TestCompilerOnRanges: FunSpec({
if xx in [2,4,6,8] {
xx++
}
if ww in [9999,8888,7777] {
xx++
}
if ww in wvalues {
xx++
}
}
}""").assertSuccess()
}""", writeAssembly = true).assertSuccess()
}
test("containment check in expressions") {
compileText(C64Target, false, """
main {
sub start() {
ubyte xx
uword ww
str name = "irmen"
ubyte[] values = [1,2,3,4,5,6,7]
uword[] wvalues = [1000,2000,3000]
xx = 'm' in name
xx = 5 in values
xx = 16 in 10 to 20 step 3
xx = 'b' in "abcdef"
xx = 8 in [2,4,6,8]
xx = xx in name
xx = xx in values
xx = xx in 10 to 20 step 3
xx = xx in "abcdef"
xx = xx in [2,4,6,8]
xx = ww in [9000,8000,7000]
xx = ww in wvalues
}
}""", writeAssembly = true).assertSuccess()
}
})

View File

@ -278,7 +278,7 @@ class TestOptimization: FunSpec({
wwAssign.target.identifier?.nameInSource shouldBe listOf("ww")
expr.type shouldBe DataType.UWORD
expr.expression.inferType(result.program).istype(DataType.UBYTE) shouldBe true
expr.expression.inferType(result.program) istype DataType.UBYTE shouldBe true
}
test("intermediate assignment steps have correct types for codegen phase (BeforeAsmGenerationAstChanger)") {

View File

@ -48,6 +48,12 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
outputln("}\n")
}
override fun visit(containment: ContainmentCheck) {
containment.element.accept(this)
output(" in ")
containment.iterable.accept(this)
}
override fun visit(expr: PrefixExpression) {
if(expr.operator.any { it.isLetter() })
output(" ${expr.operator} ")

View File

@ -4,6 +4,7 @@ import prog8.ast.base.DataType
import prog8.ast.base.FatalAstException
import prog8.ast.base.Position
import prog8.ast.base.VarDeclType
import prog8.ast.expressions.ContainmentCheck
import prog8.ast.expressions.StringLiteralValue
import prog8.ast.statements.Block
import prog8.ast.statements.Subroutine
@ -82,7 +83,7 @@ class Program(val name: String,
// Move a string literal into the internal, deduplicated, string pool
// replace it with a variable declaration that points to the entry in the pool.
if(string.parent is VarDecl) {
if(string.parent is VarDecl || string.parent is ContainmentCheck) {
// deduplication can only be performed safely for known-const strings (=string literals OUTSIDE OF A VARDECL)!
throw FatalAstException("cannot intern a string literal that's part of a vardecl")
}

View File

@ -7,6 +7,7 @@ import prog8.ast.statements.*
import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstVisitor
import java.util.*
import kotlin.math.abs
import kotlin.math.round
val AssociativeOperators = setOf("+", "*", "&", "|", "^", "or", "and", "xor", "==", "!=")
@ -219,6 +220,7 @@ class BinaryExpression(var left: Expression, var operator: String, var right: Ex
"<=", ">=",
"==", "!=" -> InferredTypes.knownFor(DataType.UBYTE)
"<<", ">>" -> leftDt
"in" -> InferredTypes.knownFor(DataType.UBYTE)
else -> throw FatalAstException("resulting datatype check for invalid operator $operator")
}
}
@ -801,6 +803,42 @@ class RangeExpr(var from: Expression,
return "RangeExpr(from $from, to $to, step $step, pos=$position)"
}
fun toConstantIntegerRange(): IntProgression? {
fun makeRange(fromVal: Int, toVal: Int, stepVal: Int): IntProgression {
return when {
fromVal <= toVal -> when {
stepVal <= 0 -> IntRange.EMPTY
stepVal == 1 -> fromVal..toVal
else -> fromVal..toVal step stepVal
}
else -> when {
stepVal >= 0 -> IntRange.EMPTY
stepVal == -1 -> fromVal downTo toVal
else -> fromVal downTo toVal step abs(stepVal)
}
}
}
val fromLv = from as? NumericLiteralValue
val toLv = to as? NumericLiteralValue
val stepLv = step as? NumericLiteralValue
if(fromLv==null || toLv==null || stepLv==null)
return null
val fromVal = fromLv.number.toInt()
val toVal = toLv.number.toInt()
val stepVal = stepLv.number.toInt()
return makeRange(fromVal, toVal, stepVal)
}
fun size(): Int? {
val fromLv = (from as? NumericLiteralValue)
val toLv = (to as? NumericLiteralValue)
if(fromLv==null || toLv==null)
return null
return toConstantIntegerRange()?.count()
}
}
@ -951,6 +989,90 @@ class FunctionCallExpr(override var target: IdentifierReference,
}
class ContainmentCheck(var element: Expression,
var iterable: Expression,
override val position: Position): Expression() {
override lateinit var parent: Node
override fun linkParents(parent: Node) {
this.parent = parent
element.parent = this
iterable.linkParents(this)
}
override val isSimple: Boolean = false
override fun copy() = ContainmentCheck(element.copy(), iterable.copy(), position)
override fun constValue(program: Program): NumericLiteralValue? {
val elementConst = element.constValue(program)
if(elementConst!=null) {
when(iterable){
is ArrayLiteralValue -> {
val exists = (iterable as ArrayLiteralValue).value.any { it.constValue(program)==elementConst }
return NumericLiteralValue.fromBoolean(exists, position)
}
is RangeExpr -> {
val intRange = (iterable as RangeExpr).toConstantIntegerRange()
if(intRange!=null && elementConst.type in IntegerDatatypes) {
val exists = elementConst.number.toInt() in intRange
return NumericLiteralValue.fromBoolean(exists, position)
}
}
is StringLiteralValue -> {
if(elementConst.type in ByteDatatypes) {
val stringval = iterable as StringLiteralValue
val exists = program.encoding.encodeString(stringval.value, stringval.altEncoding).contains(elementConst.number.toInt().toUByte() )
return NumericLiteralValue.fromBoolean(exists, position)
}
}
else -> {}
}
}
when(iterable){
is ArrayLiteralValue -> {
val array= iterable as ArrayLiteralValue
if(array.value.isEmpty())
return NumericLiteralValue.fromBoolean(false, position)
}
is RangeExpr -> {
val size = (iterable as RangeExpr).size()
if(size!=null && size==0)
return NumericLiteralValue.fromBoolean(false, position)
}
is StringLiteralValue -> {
if((iterable as StringLiteralValue).value.isEmpty())
return NumericLiteralValue.fromBoolean(false, position)
}
else -> {}
}
return null
}
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
override fun referencesIdentifier(nameInSource: List<String>): Boolean {
if(element is IdentifierReference)
return element.referencesIdentifier(nameInSource)
return iterable?.referencesIdentifier(nameInSource) ?: false
}
override fun inferType(program: Program) = InferredTypes.knownFor(DataType.UBYTE)
override fun replaceChildNode(node: Node, replacement: Node) {
if(replacement !is Expression)
throw FatalAstException("invalid replace")
if(node===element)
element=replacement
else if(node===iterable)
iterable=replacement
else
throw FatalAstException("invalid replace")
}
}
fun invertCondition(cond: Expression): BinaryExpression? {
if(cond is BinaryExpression) {
val invertedOperator = invertedComparisonOperator(cond.operator)

View File

@ -87,6 +87,7 @@ abstract class AstWalker {
open fun before(block: Block, parent: Node): Iterable<IAstModification> = noModifications
open fun before(branch: Branch, parent: Node): Iterable<IAstModification> = noModifications
open fun before(breakStmt: Break, parent: Node): Iterable<IAstModification> = noModifications
open fun before(containment: ContainmentCheck, parent: Node): Iterable<IAstModification> = noModifications
open fun before(decl: VarDecl, parent: Node): Iterable<IAstModification> = noModifications
open fun before(directive: Directive, parent: Node): Iterable<IAstModification> = noModifications
open fun before(expr: BinaryExpression, parent: Node): Iterable<IAstModification> = noModifications
@ -128,6 +129,7 @@ abstract class AstWalker {
open fun after(block: Block, parent: Node): Iterable<IAstModification> = noModifications
open fun after(branch: Branch, parent: Node): Iterable<IAstModification> = noModifications
open fun after(breakStmt: Break, parent: Node): Iterable<IAstModification> = noModifications
open fun after(containment: ContainmentCheck, parent: Node): Iterable<IAstModification> = noModifications
open fun after(builtinFunctionPlaceholder: BuiltinFunctionPlaceholder, parent: Node): Iterable<IAstModification> = noModifications
open fun after(decl: VarDecl, parent: Node): Iterable<IAstModification> = noModifications
open fun after(directive: Directive, parent: Node): Iterable<IAstModification> = noModifications
@ -228,6 +230,13 @@ abstract class AstWalker {
track(after(directive, parent), directive, parent)
}
fun visit(containment: ContainmentCheck, parent: Node) {
track(before(containment, parent), containment, parent)
containment.element.accept(this, containment)
containment.iterable?.accept(this, containment)
track(after(containment, parent), containment, parent)
}
fun visit(block: Block, parent: Node) {
track(before(block, parent), block, parent)
block.statements.forEach { it.accept(this, block) }

View File

@ -26,6 +26,11 @@ interface IAstVisitor {
fun visit(directive: Directive) {
}
fun visit(containment: ContainmentCheck) {
containment.element.accept(this)
containment.iterable?.accept(this)
}
fun visit(block: Block) {
block.statements.forEach { it.accept(this) }
}

View File

@ -4,9 +4,7 @@ import prog8.ast.base.FatalAstException
import prog8.ast.base.VarDeclType
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.NumericLiteralValue
import prog8.ast.expressions.RangeExpr
import prog8.ast.statements.AssignTarget
import kotlin.math.abs
fun AssignTarget.isIOAddress(machine: IMachineDefinition): Boolean {
val memAddr = memoryAddress
@ -49,39 +47,3 @@ fun AssignTarget.isIOAddress(machine: IMachineDefinition): Boolean {
else -> return false
}
}
fun RangeExpr.toConstantIntegerRange(): IntProgression? {
fun makeRange(fromVal: Int, toVal: Int, stepVal: Int): IntProgression {
return when {
fromVal <= toVal -> when {
stepVal <= 0 -> IntRange.EMPTY
stepVal == 1 -> fromVal..toVal
else -> fromVal..toVal step stepVal
}
else -> when {
stepVal >= 0 -> IntRange.EMPTY
stepVal == -1 -> fromVal downTo toVal
else -> fromVal downTo toVal step abs(stepVal)
}
}
}
val fromLv = from as? NumericLiteralValue
val toLv = to as? NumericLiteralValue
val stepLv = step as? NumericLiteralValue
if(fromLv==null || toLv==null || stepLv==null)
return null
val fromVal = fromLv.number.toInt()
val toVal = toLv.number.toInt()
val stepVal = stepLv.number.toInt()
return makeRange(fromVal, toVal, stepVal)
}
fun RangeExpr.size(): Int? {
val fromLv = (from as? NumericLiteralValue)
val toLv = (to as? NumericLiteralValue)
if(fromLv==null || toLv==null)
return null
return toConstantIntegerRange()?.count()
}

View File

@ -283,6 +283,8 @@ It's possible to assign a new array to another array, this will overwrite all el
array with those in the value array. The number and types of elements have to match.
For large arrays this is a slow operation because every element is copied over. It should probably be avoided.
Using the ``in`` operator you can easily check if a value is present in an array,
example: ``if choice in [1,2,3,4] {....}``
**Arrays at a specific memory location:**
Using the memory-mapped syntax it is possible to define an array to be located at a specific memory location.
@ -332,6 +334,11 @@ as newlines, quote characters themselves, and so on. The ones used most often ar
``\\``, ``\"``, ``\n``, ``\r``. For a detailed description of all of them and what they mean,
read the syntax reference on strings.
Using the ``in`` operator you can easily check if a characater is present in a string,
example: ``if '@' in email_address {....}`` (however this gives no clue about the location
in the string where the character is present, if you need that, use the ``string.find()``
library function instead)
.. hint::
Strings/arrays and uwords (=memory address) can often be interchanged.
An array of strings is actually an array of uwords where every element is the memory
@ -542,6 +549,9 @@ The when-*value* can be any expression but the choice values have to evaluate to
compile-time constant integers (bytes or words). They also have to be the same
datatype as the when-value, otherwise no efficient comparison can be done.
.. note::
Instead of chaining several value equality checks together using ``or`` (ex.: ``if x==1 or xx==5 or xx==9``),
consider using a ``when`` statement or ``in`` containment check instead. These are more efficient.
Assignments
-----------
@ -587,6 +597,9 @@ Expressions can contain procedure and function calls.
There are various built-in functions such as sin(), cos(), min(), max() that can be used in expressions (see :ref:`builtinfunctions`).
You can also reference idendifiers defined elsewhere in your code.
Read the :ref:`syntaxreference` chapter for all details on the available operators and kinds of expressions you can write.
.. attention::
**Floating points used in expressions:**

View File

@ -497,6 +497,24 @@ range creation: ``to``
; i loops 0, 1, 2, ... 127
}
containment check: ``in``
Tests if a value is present in a list of values, which can be a string or an array.
The result is a simple boolean ``true`` or ``false``.
Consider using this instead of chaining multiple value tests with ``or``, because the
containment check is more efficient.
Examples::
ubyte cc
if cc in [' ', '@', 0] {
txt.print("cc is one of the values")
}
str email_address = "?????????"
if '@' in email_address {
txt.print("email address seems ok")
}
address of: ``&``
This is a prefix operator that can be applied to a string or array variable or literal value.
It results in the memory address (UWORD) of that string or array in memory: ``uword a = &stringvar``
@ -799,4 +817,3 @@ case you have to use { } to enclose them::
}
else -> txt.print("don't know")
}

View File

@ -3,11 +3,7 @@ TODO
For next compiler release (7.6)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
add "if X in [1,2,3] {...}" syntax , as an alternative to when X { 1,2,3-> {...} }
if the array is not a literal, do a normal containment test instead in an array or string or range
change "consider using when statement..." to "consider using if X in [..] or when statement..."
also add to the docs!
...
Blocked by an official Commander-x16 v39 release
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -20,6 +16,7 @@ Future
- make it possible to use cpu opcodes such as 'nop' as variable names by prefixing all asm vars with something such as ``v_``
then we can get rid of the instruction lists in the machinedefinitions as well?
- fix the asm-labels problem (github issue #62)
- make (an option) to let 64tass produce a listing file as well as output.
- simplifyConditionalExpression() should not split expression if it still results in stack-based evaluation
- get rid of all TODO's in the code
- improve testability further, add more tests

View File

@ -6,75 +6,57 @@ main {
sub start() {
ubyte @shared xx
str name = "irmen"
ubyte[] values = [1,2,3,4,5,6,7]
ubyte[] values = [1,2,3,4,5]
for xx in name {
txt.chrout(xx)
txt.spc()
if 1 in values {
txt.print("1 ok\n")
} else {
txt.print("1 err\n")
}
txt.nl()
for xx in values {
txt.print_ub(xx)
txt.spc()
if 5 in values {
txt.print("7 ok\n")
} else {
txt.print("7 err\n")
}
txt.nl()
for xx in 10 to 20 step 3 {
txt.print_ub(xx)
txt.spc()
if not(8 in values) {
txt.print("8 ok\n")
} else {
txt.print("8 err\n")
}
txt.nl()
for xx in "abcdef" {
txt.print_ub(xx)
txt.spc()
xx = 1
if xx in values {
txt.print("xx1 ok\n")
} else {
txt.print("xx1 err\n")
}
txt.nl()
for xx in [2,4,6,8] {
txt.print_ub(xx)
txt.spc()
if xx in [1,3,5] {
txt.print("xx1b ok\n")
} else {
txt.print("xx1b err\n")
}
xx=5
if xx in values {
txt.print("xx7 ok\n")
} else {
txt.print("xx7 err\n")
}
if xx in [1,3,5] {
txt.print("xx7b ok\n")
} else {
txt.print("xx7b err\n")
}
xx=8
if not(xx in values) {
txt.print("xx8 ok\n")
} else {
txt.print("xx8 err\n")
}
if not(xx in [1,3,5]) {
txt.print("xx8b ok\n")
} else {
txt.print("xx8b err\n")
}
txt.nl()
; if xx in 100 { ; TODO error
; xx++
; }
;
; if xx in 'a' { ; TODO error
; xx++
; }
;
; if xx in "abc" { ; TODO containment test via when
; xx++
; }
;
; if xx in [1,2,3,4,5] { ; TODO containment test via when
; xx++
; }
;
; if xx in [1,2,3,4,5,6,7,8,9,10] { ; TODO containment test via loop?
; xx++
; }
;
; if xx in name { ; TODO containment test via loop
; xx++
; }
;
; if xx in values { ; TODO containment test via loop
; xx++
; }
;
; if xx in 10 to 20 step 2 { ; TODO
;
; }
; TODO const optimizing of the containment tests
; TODO also with (u)word and floats
if xx==9 or xx==10 or xx==11 or xx==12 or xx==13 {
txt.print("9 10 11\n")

View File

@ -167,6 +167,7 @@ expression :
| left = expression EOL? bop = '|' EOL? right = expression
| left = expression EOL? bop = ('==' | '!=') EOL? right = expression
| rangefrom = expression rto = ('to'|'downto') rangeto = expression ('step' rangestep = expression)? // can't create separate rule due to mutual left-recursion
| left = expression EOL? bop = 'in' EOL? right = expression
| prefix = 'not' expression
| left = expression EOL? bop = 'and' EOL? right = expression
| left = expression EOL? bop = 'or' EOL? right = expression