- Inspiration
- Language features
Fade Basic is a variant of BASIC. The language is fairly limited in its scope and it is intended to capture the essence of what Dark Basic Pro was able to do in 2003. If you are familiar with Dark Basic, then read about the Differences between Fade Basic and Dark Basic. It is worth glancing over this document with an open mind, as some of the language decisions may raise an eyebrow in 2025.
Fade Basic is an odd duck of a language. Ultimately, it is an interpreted scripting language and runs inside a dotnet process. However, the source code, .fbasic files, are compiled into a byte-code, and that byte-code is what is being interpreted at runtime. Fade is debuggable and using an IDE like Visual Studio Code, you can attach a debugger to the program and use breakpoints, see state, and use watch expressions.
In the current development of Fade, I have been focusing on standalone application architectures, where a dotnet process boots up and immediately runs pre-compiled Fade byte-code. In this way, the program is a dotnet program, and uses a .csproj file to coordinate the build. Read more about that in the Project Guide. Technically, it is possible to construct a Fade program, compile it, and run it, all from within a running dotnet process. Read about that in the SDK Guide.
The rest of this document mostly focuses on the syntax of the language itself. Enjoy! 🤘
On any given line of code, the ` character turns everything to the right of the character into a code comment. Comments are ignored by the compiler, and allow you to mark up your code with prose.
` this is a comment!Block comments are possible as well, using the REMSTART and REMEND keywords. Any text between those two keywords are treated as comments and ignored by the compiler. REMSTART and REMEND keywords may be nested, but it does not make much sense to do so. A REMSTART keyword must have a closing REMEND keyword, or the entire program after REMSTART will be treated as a comment.
REMSTART
this is a multi
line comment!
REMENDThis is the most complete version of a variable declaration and assignment.
LOCAL x AS INTEGER = 27It has 4 main parts,
LOCAL- this is the scope of the variable. See the the #scope section for more detail, but the gist is that in Fade, there are only 2 valid scope values, LOCAL, or GLOBAL.x- the name of the variableAS INTEGER- this is the explicit type of the variable.= 27- the assignment that sets the value ofx.
That is a lot of typing! There is a shorter way to write the same statement,
x = 27In the shorter version, the variable x is implicitly locally scoped, and its type is inferred to be an integer.
The scope and type declaration are optional parts. Therefor, the following examples are also valid,
LOCAL x = 1
LOCAL y
z AS INTEGER
w AS INTEGER = 2It is possible to declare or assign multiple variables on a single line, using commas as a separator.
x = 3, y = 2Variable type inference is not as automatic as you might think.
Fade variable names use sigils as a way to imply their types. Sigils are appended directly to a variable name. There are only 2 types of sigils,
$- implies the variable is a string,#- implies the variable is a float.
For example, the string sigil is used like this,
message$ = "tunafish"You are allowed to ignore the sigil if you explicitly include the type in the declaration. However, this is not idiomatic.
LOCAL message AS STRING = "tunafish"Otherwise, the sigil is used to infer the type, not the value. For example, the following statement is invalid.
message$ = 27 `this is invalid, because 27 is an integer, not a string. Floating point numbers also use a sigil, (#), and follow the same semantics as the string sigil. For example, the following statement would declare a float.
num# = 1.23Be careful! Sigils are a tool intended to help you understand the types of your variables further away from their declaration. However, they can also be places where bugs appear in your logic. The float sigil particularly so, because integers and floats are implicitly cast between each other. The following statements may be surprisingly valid.
x = 3.2 `x is an integer, and holds the value, 3
y# = 2 `y is a float, and holds the value 2.0It is possible to explicitly cast between values by using commands.
The str$() command takes a number, and produces a string.
x$ = str$(42) `x$ is "42"The val() command takes a string, and produces a number.
x = val("42") `x is 42Fade Basic supports the following primitives. The classic name is inspired from the BASIC-era, and the C# equivalent is the mapped type. At the moment, not all valid C# primitive types are available. However, it is valid to use either type name in your declarations. The default value of all primtives is zero, except for STRING, which is an empty string.
| classic name | C# equivalent | byte-size | description | range |
|---|---|---|---|---|
INTEGER |
int |
4 | 32 bit signed int | -2147483648 to 2147483647 |
DOUBLE INTEGER |
long |
8 | 64 bit signed int | -9223372036854775808 to 9223372036854775807 |
BYTE |
byte |
1 | 8 bit unsigned int | 0 to 255 |
WORD |
ushort |
2 | 16 bit unsigned int | 0 to 65536 |
DWORD |
uint |
4 | 32 bit unsigned int | 0 to 4294967295 |
BOOLEAN |
bool |
1 | 8 bit unsigned bit | 0 to 255 |
FLOAT |
float |
4 | 32 bit single-precision floating point type | 3.4E +/- 38 (7 digits) |
DOUBLE FLOAT |
double |
8 | 64 bit double-precision floating point type | 1.7E +/- 308 (15 digits) |
STRING |
string |
4 | a pointer to string memory | - |
Values of primitive types can usually be implicitly cast amongst each other. For example, a FLOAT can be implicitly cast to an INTEGER, or vice versa. When the sizing of the primitive types are different, the cast value is capped at the max size of the primitive value. For example, assigning the INTEGER value of 300 to a BYTE would exceed the byte's max range, and the cast value would be 255.
Strings are a special type of primitive that represent text.
x$ = "hello" + " world"
PRINT x$Functions allow you to re-use sections of code. Here is an example of a function that adds two numbers together. This is the function's declaration.
FUNCTION add(a, b)
sum = a + b
ENDFUNCTION sumElsewhere in your program, you can invoke this function like this,
x = add(1, 2)A Function cannot access variables defined outside of the Function, unless those variables were declared as GLOBAL scoped variables. For example, the following Function is able to access GLOBAL variables,
GLOBAL x = 3
LOCAL y = tunafish()
FUNCTION tunafish()
result = x * 2
ENDFUNCTION resultFunctions can optionally return a value, or not. The following function adds two numbers together and returns the result,
FUNCTION add(a, b)
sum = a + b
ENDFUNCTION sumAnd the following function does not return any result,
FUNCTION nothing()
` do nothing
ENDFUNCTIONIt is possible for a function to have multiple return statements. For example, the following function can return different values based on the logic of the function,
FUNCTION stepFunction(x)
IF x > 0
EXITFUNCTION 1
` this line would never be executed, because it is immediately after an EXITFUNCTION
ENDIF
ENDFUNCTION 0The EXITFUNCTION keyword causes the function to terminate, and return the value, 1.
Functions must return the same type of value on all their terminating statements. If a function returns an INTEGER in one spot, then it must also return an INTEGER is all other spots. Otherwise, the compiler cannot understand the inferred type of function invocations.
All function parameters are always passed by value. In a simple example, the following program attempts to mutate a given parameter. However, the variable is unchanged when the execution returns from the function.
x = 3
tunafish(x)
PRINT x `prints 3
FUNCTION tunafish(x)
x = 10000
PRINT x `prints 10000
ENDFUNCTIONParameters may declare explicit types, or use #sigils. For example, the following function accepts a STRING as an input parameter,
FUNCTION tunafish(s AS STRING)
ENDFUNCTIONAnd this function also accepts a STRING, but denoted through a sigil.
FUNCTION tunafish(s$)
ENDFUNCTIONFunctions cannot be declared within another function. For example, the following program is invalid.
FUNCTION outer()
FUNCTION inner()
ENDFUNCTION
ENDFUNCTIONFade does not support function pointers or the ability to create a closure.
A Scope is a collection of variables that are available to be accessed within the runtime of a Fade program. There are only two valid scopes, LOCAL, and GLOBAL. Variables in the GLOBAL scope are available everywhere, all the time. The LOCAL scope changes based on which function is being executed. When a function invocation starts, a new LOCAL scope is created for all of the function's parameters and variables. The previous LOCAL scope is saved onto a stack. When the function invocation completes, the new scope is discarded and the old LOCAL scope is rescued from the stack.
In addition to primitives, Fade supports the declaration of custom type structures, referred to as User Defined Types (UDT). The following example declares a new type and the creates a variable of the new type.
`declare a type with two fields
TYPE TOAST
crustyness#
size
ENDTYPE
`declare a variable using the User Defined Type
LOCAL myToast AS TOAST
myToast.crustyness = .8
myToast.size = 12The fields declared in a UDT can explicitly declare their types.
TYPE FISH
name$ AS STRING
ENDTYPEFields can also use #sigils to imply their type.
TYPE FISH
name$ `the $ symbol implies this is a string
ENDTYPEUDTs fields can be other UDTs. This allows you to create more complex structures. In this example, the EGG type depends on the CHICKEN type.
TYPE CHICKEN
name$
ENDTYPE
TYPE EGG
size
chicken AS CHICKEN
ENDTYPEBe careful! It is not valid to have a recursive type dependency. In the example above, it would be incorrect to add an EGG field to the CHICKEN type.
An instance of a UDT can be reset back to an empty object using the default keyword.
TYPE VECTOR
x,
y
ENDTYPE
v AS VECTOR
v.x = 4
v.y = 2
` this line resets the object and clears all field values
v = default
PRINT v.x + v.y `prints 0The default keyword can only be used in simple assignments and declarations.
It is possible to set many fields at once by using object initializers.
TYPE VECTOR
x,
y
ENDTYPE
v AS VECTOR = {
x = 1,
y = 2
}This syntax is equivalent to writing the assignments out one by one. The declaration above is equivalent to the following code snippet,
v AS VECTOR
v.x = 1
v.y = 2Object initializers can be used in a nested construction as well.
TYPE CHICKEN
name$
ENDTYPE
TYPE EGG
size
chicken AS CHICKEN
ENDTYPE
e AS EGG = {
size = 1,
chicken = {
name$ = "Albert"
}
}It is also valid to use field accessors within the assignments of an object initializer.
TYPE CHICKEN
name$
ENDTYPE
TYPE EGG
size
chicken AS CHICKEN
ENDTYPE
e AS EGG = {
size = 1,
chicken.name$ = "Albert"
}When object initializers are used after the initial declaration of an instance, they also reset the instance data to default before applying the initializer assignments. For example, the following example only prints "1".
TYPE VECTOR
x,
y
ENDTYPE
v AS VECTOR = {
x = 4,
y = 7
}
v = {
y = 1
}
PRINT v.y + v.x `prints 1It is possible to assign a variable the value of a UDT.
TYPE FISH
size
ENDTYPE
x AS FISH
x.size = 3
y = x `y is implicitly a FISHIn the example above, y receives a copy of the data. Any modification to y.size will have no effect on x.size.
Fade does not support coupling functions with UDTs.
Arrays are structures that hold groups of primitives or UDT data. Here is an example of an group of 10 numbers,
GLOBAL DIM numbers(10) AS INTEGERArray declarations are similar to #variable declarations, except that they include the DIM keyword and a set of parenthesis. The expression between the parenthesis define how many elements are in the array. Unlike variables which are Locally scoped by default, Arrays are Globally scoped by default. Arrays may also use #sigils to imply their element type.
To read or write the value of a specific item in an array, use parenthesis and then the index of the value.
DIM numbers(10)
numbers(0) = 42 `sets the first element to the value 42An array can have more than one element expression. In the example below, the numbers array has a total of 12 elements, but laid out into 3 groups of 4. This means the array is "multidimensional". The first dimension is 3, and the second is 4.
DIM numbers(3, 4)When accessing the elements of a multidimensional array, you must provide an index value for each dimension.
DIM numbers(3, 4)
numbers(0, 3) = 42 `sets the 4rd element in the entire array.Arrays cannot have more than 5 dimensions. The following declaration is invalid. This constraint is inspired from the implementation of Dark Basic Pro.
DIM numbers(1, 2, 3, 4, 5, 6)It is acceptable to declare an array that uses a UDT as the element type. In the example, vecs is an array that has 5 elements of the VECTOR UDT.
TYPE VECTOR
x#, y#
ENDTYPE
DIM vecs(5) AS VECTOR
vecs(0).x# = 1.2 `sets the x# field on the first elementIt is possible to re-size an array. The REDIM keyword can be used to re-size an array once it has been declared. This also clears all elements in the array.
DIM nums(4) `there are 4 elements
nums(2) = 3
REDIM nums(10) `now there are 10 elements
x = nums(2) `x is zeroIt is invalid to access an array with an index that is less than 0, or equal-to-or-greater-than than the length of the array's dimension. If this occurs, the Fade Basic program will crash! There is no way to recover this error during the execution of the program, so it is required that your program avoids accessing an array with an invalid index.
DIM numbers(5)
numbers(5) = 42 `this assignment causes the program to crashIt is acceptable to create an array within the Local scope of a function, but it not valid to return the array from the function.
It is not valid to implicitly assign an entire array. For example, this program is not valid.
DIM fish(5)
DIM sticks(5)
fish = sticks `this assignment is not validThere are 4 ways to type numbers in Fade Basic. Other than the default base-10 decimal system, numbers may be prefixed with a symbol that denotes their base.
| Name | Base | Symbol |
|---|---|---|
| Decimal | 10 | |
| Binary | 2 | % |
| Octal | 8 | 0c |
| Hexadecimal | 16 | 0x |
All of these assignments create the same value, but use different methods to express the value.
x = 52 `decimal, base 10
y = %110100 `binary, base 2
z = 0c64 `octal, base 8
w = 0x34 `hex, base 16You can perform mathematical operations between variables. Operations can be grouped using parenthesis.
x = (3 + 1) * 4 `results in 16
y = 3 + (1 * 4) `results in 12Most numeric operations require two expressions, a left and right. The follow table shows the available numeric operations between two expressions.
| Operation | Description |
|---|---|
| + | adds two numbers 1 + 2 `3 |
| - | subtracts the right number from the left 4 - 1 `3 |
| * | multiplies two numbers 2 * 3 `6 |
| / | divides the left number by the right 6 / 3 `2 |
| mod | takes the modulo of the left number by the right 4 mod 3 `1 |
| ^ | raises the left number to the power of the right 2 ^ 3 `8 |
| AND | results in 1 if both left and right values are positive. Otherwise, the result is 0 1 AND 2 `1 |
| OR | results in 1 if either the left or right values are positive. Otherwise, the result is 0 1 OR 0 `1 |
| XOR | results in 1 if either the left or right values are positive, but not both. Otherwise, the result is 0 1 XOR 1 `0 |
| > | results in 1 if the left number is greater than the right number. Otherwise, the result is 0 2 > 1 `1 |
| < | results in 1 if the right number is greater than the left number. Otherwise, the result is 0 1 < 2 `1 |
| >= | results in 1 if the left number is greater than or equal to the right number. Otherwise, the result is 0 2 >= 2 `1 |
| <= | results in 1 if the right number is greater than or equal to the left number. Otherwise, the result is 0 2 <= 2 `1 |
| = | results in 1 if the left number is equal to the right number. Otherwise, the result is 0 2 = 2 `1 |
| <> | results in 1 if the left number is not equal to the right number. Otherwise, the result is 0 2 <> 2 `0 |
| >> | Bitwise; right shifts the left by the right 4 >> 1 `2 |
| << | Bitwise; left shifts the left by the right 1 << 2 `4 |
| ~~ | Bitwise; results in the XOR between the left and right 4 ~~ 2 `6 |
| .. | Bitwise; results in a number the bitwise opposite of the left 4..0 `-5 |
| && | Bitwise; results in the AND between the left and right 5 && 3 `1 |
When performing operations with numeric variable, variables will be implicitly cast if needed.
Warning
The / operation will crash the program if the denominator is 0. There is no way to recover from this, so the best policy is to simply not. 😅
There is also a few unary numeric operations that only require a single number.
| Operation | Description |
|---|---|
| NOT | if the number is not 0, the result is 0. Otherwise, the result is 1 |
| - | negate the numeric value |
| .. | Bitwise; results in a number the bitwise opposite of the numeric value |
Fade Basic uses Commands to do most of the interesting work a program will do. Commands are like #functions, except that they are declared in C#. All Fade programs need to specify which Commands they will use ahead of time. You can write your own Commands, or use the standard off the shelf ones for now while Fade is still in development.
Commands can pretty much do anything, and they take the place of any built in language standard library.
The PRINT command is a very common one to use, available in the FadeBasic.Lib.Standard.ConsoleCommands collection.
PRINT "hello world"Commands may accept parameters, and may return a value. For example, the CONSOLE WIDTH() command returns a value.
width = CONSOLE WIDTH()When a command has a return value, it must be invoked using parenthesis. Notice that the PRINT command is invocable without parenthesis, because it does not return a value. However, the CONSOLE WIDTH() command returns a number, and therefore requires parenthesis.
When a command requires multiple parameters, they may be optionally separated with commas. Both of these statements are valid.
SET CURSOR 1 2
SET CURSOR 1, 2Commands are very specific to your given project, based on which are configured in the .csproj file. Commands are grouped into collections. To use a command collection, you need to do two things,
- Reference the project or Nuget package in your
.csprojfile, - Include a
<FadeCommand>reference.
Here is an example of a .csproj file that uses two command collections,
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FadeBasic.Build" Version="0.0.18.1" />
<PackageReference Include="FadeBasic.Lang.Core" Version="0.0.18.1" />
<PackageReference Include="FadeBasic.Lib.Standard" Version="0.0.18.1" />
<FadeCommand Include="FadeBasic.Lib.Standard" FullName="FadeBasic.Lib.Standard.ConsoleCommands" />
<FadeCommand Include="FadeBasic.Lib.Standard" FullName="FadeBasic.Lib.Standard.StandardCommands" />
<FadeSource Include="main.fbasic" />
</ItemGroup>
</Project>Commands provide their own documentation, so look for the documentation from the author of the command collection. The FadeBasic.Lib.Standard collection has documentation available here, Standard Library.
To build your own command collection, check the Custom Commands Documentation.
The two logical operators, AND and OR have a special property called short circuiting.
If it possible to know the result of the binary operation from the first term, then the second term
is never evaluated.
In an AND binary operation, both terms must be truthy (aka, a positive number) for the operation to be truthy. If the first term is not truthy, then it would be
impossible for the binary operation to be true, regardless of the second term. The second term will not be evaluated
and the operator is said to have "short circuited". In the example below, the function call is never executed.
x = 0 AND never()
FUNCTION never()
PRINT "this will not be seen"
ENDFUNCTION 1In an OR binary operation, either term may be truthy for the entire operation to be truthy. If the first term is truthy,
then it does not matter if the second term is truthy or not, because the entire operation will be truthy regardless. In this way,
the OR operation is said to "short circuit". In the example below, the function call is never executed.
x = 1 OR never()
FUNCTION never()
PRINT "this will not be seen"
ENDFUNCTION 1Control statements specify the flow of execution in the program.
The IF statement allows the program to optionally run statements.
x = 3
IF x > 1
PRINT "true"
ELSE
PRINT "false"
ENDIFThe IF keyword must be followed by a numeric expression. When the expression results in a positive number, then the expression is truthy and the conditional's truthy statements will execute.
Optionally, the ELSE keyword may be used to define statements that should run with the expression is not truthy. Only one set of statements will execute per conditional.
Once the statements are done executing, the program will resume at the ENDIF keyword. The ENDIF keyword is required for multi-line conditionals.
Conditionals have a single line variant that uses the THEN keyword. When the THEN keyword is used, the ENDIF keyword must not be used.
x = 3
IF x > 0 THEN PRINT "true" ELSE PRINT "false"Although unadvisable, the spirit of Dark Basic Pro empowers Fade Basic to run multiple statements on a single "line" of code.
x = 3
IF x > 0 THEN PRINT "tuna" : PRINT "fish"The FOR statement allows the program to repeat a block of code a specific number of times.
FOR t = 1 TO 10 STEP 1
`whatever code is in here will run 10 times.
NEXTThe FOR statement has 4 main components,
- The variable declaration,
t = 1, - The exit condition,
TO 10, - The
STEPamount, which is1. This clause is optional, and defaults to 1. - The looping statement,
NEXT.
The declaration sets a new variable, or an existing variable to the given value. This is called the control variable. The statements within the FOR statement are executed. Then the control variable is incremented by the STEP value. If the variable's value is less than or equal to the TO value, the FOR statements are executed again. This process repeats until the value in the control variable is greater than the TO value.
The following code will print "1", "2", and "3".
FOR t = 1 TO 3
PRINT t
NEXTIt is possible to create a STEP value that is negative. This example will print "3", "2", and "1".
FOR t = 3 TO 1 STEP -1
PRINT t
NEXTEvery FOR statement must include the TO expression, and must close with a NEXT keyword.
It is possible to exit early from a FOR loop using the EXIT statement. The EXIT statement will skip any modifications to the control variable and move the program execution to the end of the loop. The control variable will be modified, and the loop condition checked. If the loop is still valid, then it will cycle again.
FOR t = 1 TO 10
EXIT
NEXT
PRINT t `prints 1It is possible skip an iteration of a FOR loop using the SKIP statement. The SKIP statement will move the program execution back to the start of the loop.
FOR t = 1 to 3
IF t = 2
SKIP
ENDIF
PRINT t
NEXT
` prints
` 1
` 3
The WHILE statement allows the program to repeat a block of code until a certain condition is truthy.
x = 3
WHILE x > 0
`whatever code is in here will run until x is not greater than zero
ENDWHILEThe WHILE keyword must be followed by a numeric expression. When the expression results in a positive number, then the expression is truthy and the inner statements will execute. Every WHILE statement needs a closing ENDWHILE. When the ENDWHILE keyword is reached, the program execution re-runs the conditional expression, and if it is truthy, then execution loops back to the start of the while loop.
If the conditional expression is never truthy, then the inner statements are never executed.
It is possible to exit early from a WHILE loop using the EXIT statement. The EXIT statement ignores the conditional expression, and moves the program to the end of the loop.
WHILE 1
EXIT `causes the code to not run forever
ENDWHILEIt is possible skip an iteration of a WHILE loop using the SKIP statement. The SKIP statement will move the program execution back to the start of the loop.
n = 3
WHILE n > 0
IF n = 2
SKIP
ENDIF
PRINT n
n = n - 1
ENDWHILE
` prints
` 3
` 1The REPEAT statement is similar to the #While statement, except that the conditional evaluation happens at the end of the loop instead of the beginning.
REPEAT
`whatever code is in here will run until x is not greater than zero
UNTIL x > 0Every REPEAT keyword needs a closing UNTIL keyword. The UNTIL keyword must be followed by a numeric expression. When the expression results in a positive number, then the expression is truthy and the inner statements will execute again.
If the conditional expression is never truthy, then the inner statements will not be executed a second time.
It is possible to exit early from a REPEAT loop using the EXIT statement. The EXIT statement ignores the conditional expression, and moves the program to the end of the loop.
REPEAT
EXIT `causes the code to not run forever
UNTIL 1It is possible skip an iteration of a REPEAT loop using the SKIP statement. The SKIP statement will move the program execution to the UNTIL conditional check. If the loop condition is still valid, then the loop will cycle again.
n = 3
REPEAT
n = n - 1
IF n = 2
SKIP
ENDIF
PRINT n
UNTIL n = 0
` prints
` 3
` 1The DO loop will execute a block of code forever.
DO
`whatever code is here will run forever
LOOPEvery DO keyword must have a closing LOOP keyword. When the program reaches the LOOP keyword, the program returns execution to the start of the loop.
The usual way to exit a DO loop is to use the EXIT keyword, which will move the program's execution to the end of the loop.
DO
EXIT
LOOPIt is possible skip an iteration of a DO loop using the SKIP statement. The SKIP statement will move the program execution to the start of the DO loop
n = 0
DO
n = n + 1
IF n = 2
SKIP
ENDIF
PRINT n
LOOP
` prints
` 1
` 3
` 4
` ...The END statement immediately stops the program.
END
PRINT "Where did everyone go?" `this line will never be printedThis statement is a powerful tool to stop your program before running into labels or functions that should not be executed. However, there is no need to use the command if your program would naturally exit.
The GOTO statement will jump the execution of the program to a specific label in your source code.
x = 5
tuna:
x = x - 1
IF x > 0 THEN GOTO tunaGOTO statements can be used to escape from looping control structures, or indeed from any control statements. However, GOTO statements cannot be used to jump the code between #scopes.
Labels are defined as any valid variable name, with a : symbol immediately following the name. Labels cannot be redeclared.
The GOSUB statement is similar to #Goto, except that execution can be returned to the GOSUB statement.
LOCAL x
GOSUB tuna:
PRINT x `prints 1
END
tuna:
x = 1
RETURNThe SELECT statement can branch code execution into several paths based on a numeric expression.
x = 1
SELECT x
CASE 0
PRINT "zero"
ENDCASE
CASE 1
PRINT "one"
ENDCASE
CASE DEFAULT
PRINT "default"
ENDCASE
ENDSELECTThe SELECT keyword must be followed by a numeric expression. This expression is called the control value.
A SELECT statement is made up of many CASE statements. The CASE keyword must be followed by a constant numeric literal. Each CASE statement has a set of inner statements that will only be executed if it is equal to the control value. Each SELECT statement is allowed one special CASE statement that has the keyword, DEFAULT instead of a numeric literal. If none of the cases' values match the control value, then the DEFAULT case is selected. If no case is selected, the program execution moves onto the closing ENDSELECT keyword.
The DEFER statement allows a statement to be executed at the end of the current scope. Fade Basic only has a single GLOBAL scope, and a LOCAL scope per function invocation. The DEFER keyword may be followed with a statement on the same line, or be followed by the ENDDEFER statement on a later line.
DEFER PRINT "b" `this line executes at the end of the scope
PRINT "a"
`output is
` a
` bWhen multiple DEFER statements are used in the same scope, they execute in a Last In First Out (LIFO) stack order.
DEFER PRINT "a"
DEFER PRINT "b"
DEFER PRINT "c"
`output is
` c
` b
` aWhen a DEFER is used inside a function, the deferred statements will run immediately before the function exits.
FUNCTION example()
DEFER PRINT "a"
PRINT "b"
ENDFUNCTION
example()
`output is
` b
` aIf the function has an early EXITFUNCTION statement, the deferred statements are still run before the function exits.
FUNCTION example(a)
DEFER PRINT "a"
IF a > 0
EXITFUNCTION
ENDIF
PRINT "b"
ENDFUNCTION
example(1)
`output is
` aIt is possible to group multiple statements into a single DEFER block by using the ENDDEFER keyword.
DEFER
PRINT "a"
PRINT "b"
ENDDEFER
`output is
` a
` bIf a DEFER statement appears within a DEFER and ENDDEFER block, then the nested deferred statements execute after all of the statements in the DEFER block.
DEFER
print "a"
DEFER print "b"
print "c"
ENDDEFER
`output is
` a
` c
` bDeferred statements do not capture any lexical or scope state. If a deferred statement references a variable, the variable's state at the end of the scope will be used in the deferred statement.
a = 5
DEFER PRINT a
a = 12
`output is
` 12If a fatal error occurs (such as an invalid array index operation), then deferred statements are not executed, as the program exits immediately.
DEFER PRINT "a"
DIM x(3)
a = x(4)
`a fatal exception is thrown, and "a" is not printed. Deferred statements are executed when the program is explicitly terminated with the END keyword.
DEFER PRINT "a"
END
`output is
` aFade Basic allows compile time code generation.
#MACRO blocks define parts of the program that will be executed at compile time. The #MACRO keyword must be followed by a closing #ENDMACRO keyword. It is invalid to have nested #MACRO blocks.
#MACRO
PRINT "compile time"
#ENDMACRO
PRINT "run time"
`compile time output:
` "compile time"
`
`runtime output:
` "run time"Tip
When using dotnet 9 or above, be sure to disable terminal logger by passing the --tl:off flag. Otherwise, your compile time PRINT statements may be erased by msbuild.
It is possible to have several #MACRO blocks throughout the program. Their contents are concatenated and executed as a singular program during compile time.
#MACRO
PRINT "a"
#ENDMACRO
PRINT "1"
#MACRO
PRINT "b"
#ENDMACRO
PRINT "2"
`compile time output:
` a
` b
`
`runtime output:
` 1
` 2The compile time program has a completely independent lifetime from the runtime program. None of the scope, state, or functions are shared between the compile time and runtime programs.
#MACRO
a = 12
#ENDMACRO
PRINT a `invalid, because the variable does not exist in the runtime program. However, it is possible to emit code from within a compile time block that will be included in the regular program. This is called tokenization. The #TOKENIZE statement may appear within a #MACRO block. The #TOKENIZE keyword must be followed with an #ENDTOKENIZE statement on a later line. It is invalid to have nested #TOKENIZE blocks. Any tokens within a #TOKENIZE block will be inserted into the runtime program's code model. Tokens emitted by a #TOKENIZE block are inserted at the location of the enclosing #MACRO block in the original source.
#MACRO
#TOKENIZE
PRINT "a"
#ENDTOKENIZE
#ENDMACRO
`there is no compile time output.
`
`runtime output:
` aOrder is kept when multiple #TOKENIZE blocks appear in a #MACRO block.
#MACRO
#TOKENIZE
PRINT "a"
#ENDTOKENIZE
#TOKENIZE
PRINT "b"
#ENDTOKENIZE
#ENDMACRO
`there is no compile time output.
`
`runtime output:
` a
` b#TOKENIZE blocks only insert tokens into the runtime program if they are evaluated during the compile time execution.
#MACRO
a = 12
IF a > 0
#TOKENIZE
PRINT "a was greater than zero"
#ENDTOKENIZE
ELSE
#TOKENIZE
PRINT "a was not greater than zero"
#ENDTOKENIZE
ENDIF
#ENDMACRO
`there is no compile time output.
`
`runtime output:
` a was greater than zeroTokenization can occur within any of Fade Basic's regular control flows. It is valid to put #TOKENIZE after a DEFER, or in a loop, or within a function.
Tokenization blocks are able to reference compile time state by using substitution syntax. A substitution uses the square brackets, [ and ]. A valid compile time expression is required between the square brackets. The expression will be evaluated and the resulting value will be used in place of the substitution.
#MACRO
a = 12
#TOKENIZE
PRINT [a]
#ENDTOKENIZE
#ENDMACRO
`there is no compile time output.
`
`runtime output:
` 12Any value from a substitution is inserted as a token.
#MACRO
x$ = "tuna"
#TOKENIZE
[x$] = 12
#ENDTOKENIZE
#ENDMACRO
print tuna
`there is no compile time output.
`
`runtime output:
` 12It is possible to modify the substitution syntax to output strings instead of tokens. Immediately after the opening square bracket, if a $ symbol is used, the output value of the substitution will be added as a string literal. This is called a stringified substitution.
#MACRO
x$ = "abc"
#TOKENIZE
print [$ x$ ]
#ENDTOKENIZE
#ENDMACRO
`there is no compile time output.
`
`runtime output:
` abcWhen a substitution is placed exactly adjacent to an existing token, then the output of the substitution will be added to the existing token.
#MACRO
a = 12
#TOKENIZE
x[a] = 12
#ENDTOKENIZE
#ENDMACRO
PRINT x12
`there is no compile time output.
`
`runtime output:
` 12It is invalid to place stringified substitutions adjacent to existing tokens.
There are several short hands to be aware of.
Within a #MACRO block, starting a line with the # token will create a single line #TOKENIZE block.
#MACRO
# a = 12
#ENDMACRO
PRINT a
`there is no compile time output.
`
`runtime output:
` 12Outside of a #MACRO block, starting a line with the # token will create a single line #MACRO block.
# PRINT "12"
`compile time output:
` 12
`
`there is no runtime output.Outside of a #MACRO block, substitution syntax can be used to access compile time state.
#MACRO
a = 12
#ENDMACRO
PRINT [a]
`there is no compile time output.
`
`runtime output:
` 12Any code that the compile time program emits must be done so deterministically. Non-determinism may appear when the compile time program makes calls to commands. Any variable that is the output of a command is considered to have a non deterministic value. These variables are called "haunted".
Any variable that is assigned from an expression including haunted variables will become haunted.
It is invalid to create tokenization statements within a haunted statement.
#MACRO
x = rnd(10) `the rnd command returns a random number up to 10.
if x
#TOKENIZE
`this tokenization block is invalid.
#ENDTOKENIZE
endif
#ENDMACROIt is invalid to use haunted variables to create tokens that are part of an assignment statement.
#MACRO
x = rnd(10) `the rnd command returns a random number up to 10.
#TOKENIZE
a[x] = 12 `this is invalid
#ENDTOKENIZE
#ENDMACROWhen a haunted variable is re-assigned to an non-haunted value, the variable becomes un-haunted. However, once an array variable becomes haunted, it cannot be un-haunted.
Any part of an array or a UDT that becomes haunted causes the entire structure to become haunted.
Compile time constants are a way to do text replacement in your source code before the text is compiled.
In the example below, the constant, x is not an actual variable, but symbolizes the text, "42".
#CONSTANT x 42
print x `prints 42The source code literally becomes the following,
print 42Compile time constants cannot access any runtime data in their initializer, and cannot be modified at runtime. Compile time constants cannot be multi-line segments.
Fade Basic has a garbage collection system. When arrays UDTs, or strings are allocated, they will automatically be removed from Fade's memory when no more variables reference the data.
TYPE TUNA
size
ENDTYPE
redFish as TUNA `a TUNA is allocated
blueFish as TUNA `a second TUNA is allocated
redFish = blueFish `the first TUNA is no longer being reference, and is garbage collected.