Statements ========== A function consists of a "list of statements", which is also called a **compound statement**. A compound statement looks like this: { statement1 statement2 statement3 ... } Statements are typically terminated by a semicolon, but no semicolon should be placed at the end of a compound statement. Compound statements may also be nested, and may appear anywhere where a regular statement is allowed. In a compund statement, each sub-statement makes up a nested namespace which lasts until the end of the statement. So declaring a variable is statement2 in the example above, would make it visible in both statement2 and statement3, but not in statement1. Declarations inside compound statements are also hidden to outside statements, which can be used to e.g. declare variables with the same name: { int x; { int y; // "x" and "y" can be accessed } // "y" is no longer visible { int y; // this is a new variable, also called "y" } // new "y" is not longer visible. } **TODO** where break/continue/return is allowed, goto/skipto/repeatfrom is also allowed. Expression statements --------------------- Syntax: expr; Examples: n = 1 + n*2; arr#[0] = 123; func(); See the file **expressions.md** for descriptions of the available expressions. Data declaration statements --------------------------- Syntax: type name = expr; The initial expression is optional. Examples: int a; int b = 123; (struct, If the expression is omitted, the initial value will be undefined. It is an error to attempt to read from an undefined value, and what will happen is undefined and depends on the backend. Type declaration statements --------------------------- Syntax: typedef flags name = type; The flags are optional. Examples: typedef color = enum (red, yellow, green, blue); typedef alias length = count; The typedef statement works exactly like at the top level (see **toplevels.md**), but declares the identifier in the current and following statements. assert ------ Syntax: assert expr; Produces an error if expr is false. Since the behavior of errors is undefined, the compiler is free to choose if it should e.g. print an error and exit, or to assume that the expr is never false and use that information to optimize the program. **NOTE** should "assert :false;" be allowed or not? perhaps it's better to use "undefined;" for cases where you want to indicate that a part of code is unreachable. or perhaps a new keyword? break ----- Syntax: break; Jumps out of the containing loop (for, while or do/while). If there are several nested loops, it breaks out of the innermost one. Example: for int i in [1,1,2,3,5] { for int j in [2,3,5,7,11] { if i == j break; } // the break jumps here } **TODO:** Fix the compiler so it can detect the type of the arrays. continue -------- Syntax: continue; Skips the rest of the current step in the containing loop (for, while or do/while). If there are several nested loops, it does this in the innermost one. Example: for int i in [1,1,2,3,5] { for int j in [2,3,5,7,11] { if i == j continue; // this line is not reached when i==j // (but the loop will continue at j+1) } } **TODO:** Fix the compiler so it can detect the type of the arrays. do -- Syntax: do { statements } while expression; Executes the statements until the expression is false, but at least once (the check is done after each loop iteration). **TODO** "end" and "empty" statements The expression must be of **bool** type. for --- Syntax: for type name in expression loopstatement end endstatement empty emptystatement **FIXME** make the example more readable Iterates over the expression and executes the loop statement for each value from the iteration. What "iteration" means depends on the type of the expression. For expressions for which there's an identifier called **next\_element** in the inside the scope of the type of the expression, that function will be called for each iteration to obtain the next value and determine when to stop iterating. In this case the value of the expression is called an _iterator_. **TODO:** will the compiler handle this for non-struct types? **TODO:** shorten these names to next\_elem and get\_iter **TODO** example For expressions for which there's an identifier called **get\_iterator** in the inside the scope of the type of the expression, that function will be called first to obtain an _iterator_. Then **next\_element** will be called on the iterator like above. **TODO** example For array expressions, iteration means to go over all elements in the array. For example: int#[3] arr = [11,22,33]; for int i in arr { // do someting } The "end" and "empty" statements are executed depending on what the loop executed. If no "break" statement was executed in the loop (so the loop actually reached is end), then the **end** statement will be executed. If the loop statement was not executed at all (due to an empty array or iterator), then the **empty** expression will be executed. Just like in **if** statements, the loop statement must be either a compound statement, or a break, continue or return statement. Usually it's a compound statement. goto ---- Syntax (for goto, skipto and repeatfrom): goto labelname; skipto labelname; repeatfrom labelname; These statements all jump to the given label. There's no difference between them, except that the keyword describes how they are used. A **skipto** jumps forward in the order of statements, a **repeatform** jumps backwards and a goto is used to jump when there's no forward/backwards relation between the jump and it's label, for instance between if/else statements. For example: () test() { skipto one; x(); label one: if 1 == 2 { a(); label inside: c(); } else { b(); if 2 == 2 goto inside; y(); } repeatfrom one; } It's only possible to jump within a function, not between functions. **TODO:** actually check that they are used correctly. **TODO:** change label syntax to not use : and use ; instead? if -- Syntax: if expression statement1 else statement2 If expression is true, then statement1 is executed. Otherwise statement2 is executed. The else part is optional. The else statement may be another if statement, so "else if"s can be written like this: if i == 1 return "one"; else if i == 2 return "two"; else return "other"; Each of the statements must be either a compound statement, or break, continue or return statement. In other words, you'll need to write the statement inside { } brackets, unless the statemet is a break, continue or return. Example: if i == 1 { // {} are required becuase this is an expression statement // and not a break, continue or return. do_something(); } else if i == 2 { // here we have a break, but when there are multiple statements // we must also wrap them in {} do_something_else(); break; } else break; // no {} needed for break, continue or return The expression after the **if** keyword must be of **bool** type. label ----- Syntax: label name: Defines a label for goto/skipto/repeatfrom. Note that it ends with a colon. Usually it is de-indented two spaces compared to the surrounding lines. Example: () test() { int x = 5; if x == 5 skipto dont_work; work(); label dont_work: fun(); } **TODO:** change label syntax to not use : and use ; instead? repeatfrom -- See **goto** return ------ Syntax: return expression; Evalutes the expression and returns from the current function with the value of the expression. If no expression is given it returns an empty struct. The type of the expression must be compatible (assignable to) the return type of the function. skipto ------ See **goto** switch ------ Syntax: switch expression { case value1 casestatement1 case value21 with withstatement21 case value22 with withstatement22 ... case value23 with withstatement23 casestatement2 case value3 casestatement3 default defaultstatement } Evaluates the expression once and then compares it with the case values. The expression can have any type, including array or struct type, but the case values must be compile-time constants. When a matching case value is found, the following statement is executed. If no matching case value is found then the default statement is executed. When the end of a case statement is reached, then the switch statement is exited. Unlike some other languages there is no need to "break" out of the switch statement, and the "break" keyword does not apply to the switch statement. A single case statement may have multiple case values before it. Here's an example: switch i { case 1 case 2 case 3 { // value is 1, 2 or 3 } case 9 return; // value is 9 default { // if neither 1,2,3 or 9 } } When you have multiple case expressions, it can be useful to know which one was matched. This can be done with the "with" syntax: var int type; switch compute_value() { case 111 with type = 10 case 222 with type = 20 { // type is set to either 10 or 20. } case 333 with type = 30 case 444 with type = 40 { // type is set to either 30 or 40. } } Just like with the **if** statement, the case statements must be either compound statements, or break, continue or return statements. typeassert ---------- Syntax: typeassert name is type in range do statement else statement Checks if the identifier (which must be a constant data declaration) called name is within the given range and may be converted to the given type. **FIXME** doesn't just check the type, it also casts it. Strictly speaking it create a new variable with the same name, but the new type, that shadows the old variable. **TODO:** decide how the range part should work and implement it. **TODO:** mutable data is currently allowed. it should probably be forbidden. The "is" and "in" parts are optional, but one of them must be specified. If one of them is omitted, then the other one is determined automatically. The "do" and "else" parts are both optional. If the do part is specified it will be executed if the type check succeeds, otherwise the following statements are executed. If there an "else" part it is executed if the type check fails. If there's no "do" part and no "else" part, a type failure will be considered an error. If there's no "do" part but there's an "else" part, then only reaching the end of the "else" statement will be considered an error. What happens on an error is undefined and depends on the backend. Here's an example: () test() { int i = 10; typeassert i is byte; // type check is OK, execution continues typeassert i is byte do { // type check is OK // this will execute } else { // this will NOT execute } // Now test with an value that's out of range int j = 1000; typeassert j is byte do { // will NOT execute } else { // will execute } typeassert j is byte else { // will execute skipto dont_crash; // if we reach the end, then that's an error! } label dont_crash: typeassert j is byte; // the statement above causes an error. // depending on the backend, anything can happen now. } **TODO:** change label syntax to not use : and use ; instead? Just like with the **if** statement, the case statements must be either compound statements, or break, continue or return statements. unreachable ----------- Syntax: unreachable; Tells the compiler that the statement will never be executed, which can prevent compiler warnings or allow compiler optimizations. If the unreachable statement is actually executed, then that's an error and has undefined behavior. while ----- Syntax: while expression { statements } Executes the statements until the expression is false. The check is done before each loop iteration, so the statements inside the loop might not be executed at all. **TODO** "end" and "empty" statements The expression must be of **bool** type. Special rules ------------- **FIXME** none of the above statements require a compound statement Where the syntaxes above sometimes require a compound statement, it is also _allowed_ but _not recommended_ to use a break, continue, or return statement. The compiler is allowed to generate a warning in that case (but the current version of **lrlc** does not show any warnings at all).