unit test for defer, describe defer and if expression in docs

This commit is contained in:
Irmen de Jong 2024-10-22 22:19:49 +02:00
parent 6da1f7eb4c
commit 326eab3dd1
5 changed files with 142 additions and 9 deletions

View File

@ -9,6 +9,7 @@ import io.kotest.matchers.types.instanceOf
import prog8.ast.IFunctionCall import prog8.ast.IFunctionCall
import prog8.ast.expressions.* import prog8.ast.expressions.*
import prog8.ast.statements.* import prog8.ast.statements.*
import prog8.code.ast.*
import prog8.code.core.DataType import prog8.code.core.DataType
import prog8.code.core.Position import prog8.code.core.Position
import prog8.code.target.C64Target import prog8.code.target.C64Target
@ -686,5 +687,45 @@ main {
}""" }"""
compileText(C64Target(), optimize=false, src, writeAssembly=false) shouldNotBe null compileText(C64Target(), optimize=false, src, writeAssembly=false) shouldNotBe null
} }
test("defer syntactic sugaring") {
val src="""
main {
sub start() {
void test()
}
sub test() -> uword {
defer {
cx16.r0++
cx16.r1++
}
if cx16.r0==0
return cx16.r0+cx16.r1
defer cx16.r2++
}
}"""
val result = compileText(Cx16Target(), optimize=true, src, writeAssembly=true)!!
val main = result.codegenAst!!.allBlocks().single {it.name=="p8b_main"}
val sub = main.children[1] as PtSub
sub.scopedName shouldBe "p8b_main.p8s_test"
// check the desugaring of the defer statements
(sub.children[0] as PtVariable).name shouldBe "p8v_prog8_defers_mask"
val ifelse = sub.children[3] as PtIfElse
val ifscope = ifelse.ifScope.children[0] as PtNodeGroup
val ifscope_push = ifscope.children[0] as PtFunctionCall
val ifscope_defer = ifscope.children[1] as PtFunctionCall
val ifscope_return = ifscope.children[2] as PtReturn
ifscope_defer.name shouldBe "p8b_main.p8s_test.p8s_prog8_invoke_defers"
ifscope_push.name shouldBe "sys.pushw"
(ifscope_return.value as PtFunctionCall).name shouldBe "sys.popw"
val ending = sub.children[5] as PtFunctionCall
ending.name shouldBe "p8b_main.p8s_test.p8s_prog8_invoke_defers"
sub.children[6] shouldBe instanceOf<PtReturn>()
val handler = sub.children[7] as PtSub
handler.name shouldBe "p8s_prog8_invoke_defers"
}
}) })

View File

@ -18,6 +18,7 @@ The language
- Identifiers and string literals can contain non-ASCII characters so for example ``knäckebröd`` and ``見せしめ`` are valid identifiers. - Identifiers and string literals can contain non-ASCII characters so for example ``knäckebröd`` and ``見せしめ`` are valid identifiers.
- There's usually a single statement per line. There is no statement separator. - There's usually a single statement per line. There is no statement separator.
- Semicolon ``;`` is used to start a line comment. Multi-line comments are also possible by enclosing it all in ``/*`` and ``*/``. - Semicolon ``;`` is used to start a line comment. Multi-line comments are also possible by enclosing it all in ``/*`` and ``*/``.
- Ternary operator ``x ? value1 : value2`` is available in the form of an *if-expression*: ``if x value1 else value2``
No linker No linker

View File

@ -551,8 +551,8 @@ Only simple statements are allowed to be inside an unroll loop (assignments, fun
Conditional Execution Conditional Execution
--------------------- ---------------------
if statements if statement
^^^^^^^^^^^^^ ^^^^^^^^^^^^
Conditional execution means that the flow of execution changes based on certain conditions, Conditional execution means that the flow of execution changes based on certain conditions,
rather than having fixed gotos or subroutine calls:: rather than having fixed gotos or subroutine calls::
@ -619,6 +619,25 @@ So ``if_cc goto target`` will directly translate into the single CPU instruction
Maybe in the future this will be a separate nested scope, but for now, that is Maybe in the future this will be a separate nested scope, but for now, that is
only possible when defining a subroutine. only possible when defining a subroutine.
if expression
^^^^^^^^^^^^^
You can also use if..else as an *expression* instead of a statement. This expression selects one of two
different values depending of the condition. Sometimes it may be more legible if you surround the condition expression with parentheses.
An example, to select the number of cards to use depending on what game is played::
ubyte numcards = if game_is_piquet 32 else 52
; it's more verbose with an if statement:
ubyte numcards
if game_is_piquet
numcards = 32
else
numcards = 52
when statement ('jump table') when statement ('jump table')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -798,6 +817,48 @@ Otherwise the compiler will issue a warning about discarding a result value.
then undefined because the variables will get overwritten. then undefined because the variables will get overwritten.
Deferred ("cleanup") code
^^^^^^^^^^^^^^^^^^^^^^^^^
Usually when a subroutine exits, it has to clean up things that it worked on. For example, it has to close
a file that it opened before to read data from, or it has to free a piece of memory that it allocated via
a dynamic memory allocation library, etc.
Every spot where the subroutine exits (return statement, jump, or the end of the routine) you have to take care
of doing the cleanups required. This can get tedious, and the cleanup code is separated from the place where
the resource allocation was done at the start.
To help make this easier and less error prone, you can ``defer`` code to be executed automatically,
immediately before any moment the subroutine exits. So for example to make sure a file is closed
regardless of what happens later in the routine, you can write something along these lines::
sub example() -> bool {
ubyte file = open_file()
defer close_file(file) ; "close it when we exit from here"
uword memory = allocate(1000)
if memory==0
return false
defer deallocate(memory) ; "deallocate when we exit from here"
process(file, memory)
return true
}
In this example, the two deferred statements are not immediately executed. Instead, they are executed when the
subroutine exits at any point. So for example the ``return false`` after the memory check will automatically also close
the file that was opened earlier because the close_file() call was scheduled there.
At the bottom when the ``return true`` appears, *both* deferred cleanup calls are executed: first the deallocation of
the memory, and then the file close. As you can see this saves you from duplicating the cleanup logic,
and the logic is declared very close to the spot where the allocation of the resource happens, so it's easier to read and understand.
It's possible to write a defer for a block of statements, but the advice is to keep such cleanup code as simple and short as possible.
.. caution::
Defers only work for subroutines that are written as regular Prog8 code.
If a piece of inlined assembly somehow causes the routine to exit, the compiler cannot detect this.
Defers will not be handled in such cases.
Library routines and builtin functions Library routines and builtin functions
-------------------------------------- --------------------------------------

View File

@ -648,6 +648,10 @@ address of: ``&``
This operator can also be used as a prefix to a variable's data type keyword to indicate that This operator can also be used as a prefix to a variable's data type keyword to indicate that
it is a memory mapped variable (for instance: ``&ubyte screencolor = $d021``) it is a memory mapped variable (for instance: ``&ubyte screencolor = $d021``)
ternary:
Prog8 doesn't have a ternary operator to choose one of two values (``x? y : z`` in many other languages)
instead it provides this feature in the form of an *if expression*. See below under "Conditional Execution".
precedence grouping in expressions, or subroutine parameter list: ``(`` *expression* ``)`` precedence grouping in expressions, or subroutine parameter list: ``(`` *expression* ``)``
Parentheses are used to group parts of an expression to change the order of evaluation. Parentheses are used to group parts of an expression to change the order of evaluation.
(the subexpression inside the parentheses will be evaluated first): (the subexpression inside the parentheses will be evaluated first):
@ -945,8 +949,8 @@ If you jump to an address variable (uword), it is doing an 'indirect' jump: the
to the address that's currently in the variable. to the address that's currently in the variable.
if statements if statement
^^^^^^^^^^^^^ ^^^^^^^^^^^^
With the 'if' / 'else' statement you can execute code depending on the value of a condition:: With the 'if' / 'else' statement you can execute code depending on the value of a condition::
@ -991,6 +995,16 @@ It can also be one of the four aliases that are easier to read: ``if_z``, ``if_n
This is not always the case after a function call or other operations! This is not always the case after a function call or other operations!
If in doubt, check the generated assembly code! If in doubt, check the generated assembly code!
if expression
^^^^^^^^^^^^^
Similar to the if statement, but this time selects one of two possible values as the outcome of the expression,
depending on the condition. You write it as ``if <condition> <value1> else <value2>`` and it can be
used anywhere an expression is used to assign or pass a value.
The first value will be used if the condition is true, otherwise the second value is used.
Sometimes it may be more legible if you surround the condition expression with parentheses so it is better
separated visually from the first value following it.
You must always provide two alternatives to choose from, and they can only be values (expressions).
when statement ('jump table') when statement ('jump table')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -1018,3 +1032,24 @@ case you have to use { } to enclose them::
else -> txt.print("don't know") else -> txt.print("don't know")
} }
Deferred code ("cleanups")
--------------------------
The ``defer`` keyword can be used to schedule a statement (or block of statements) to be executed
just before exiting of the current subroutine. That can be via a return statement or a jump to somewhere else,
or just the normal ending of the subroutine. This is often useful to "not forget" to clean up stuff,
and if the subroutine has multiple ways or places where it can exit, it saves you from repeating
the cleanup code at every exit spot. Multiple defers can be scheduled in a single subroutine (up to a maximum of 8).
They are handled in reversed order. Return values are evaluated before any deferred code is executed.
You write defers like so::
defer diskio.f_close()
; or multiple statements:
defer {
diskio.f_close()
memory.deallocate()
}

View File

@ -1,11 +1,6 @@
TODO TODO
==== ====
- unit test for defer
- describe defer in the manual
- unit test for ifexpression
- describe ifexpression in the manual
- Optimize the IfExpression code generation to be more like regular if-else code. (both 6502 and IR) - Optimize the IfExpression code generation to be more like regular if-else code. (both 6502 and IR)