Skip to content

Latest commit

 

History

History
1363 lines (1089 loc) · 43.1 KB

File metadata and controls

1363 lines (1089 loc) · 43.1 KB

Contents

Language Guide

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! 🤘

Comments

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!
REMEND

Variables

This is the most complete version of a variable declaration and assignment.

LOCAL x AS INTEGER = 27

It has 4 main parts,

  1. 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.
  2. x - the name of the variable
  3. AS INTEGER - this is the explicit type of the variable.
  4. = 27 - the assignment that sets the value of x.

That is a lot of typing! There is a shorter way to write the same statement,

x = 27

In 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 = 2

Single-Line Assignment

It is possible to declare or assign multiple variables on a single line, using commas as a separator.

x = 3, y = 2

Sigils

Variable 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,

  1. $ - implies the variable is a string,
  2. # - 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.23

Casting

Be 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.0

It 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 42

Primitive Types

Fade 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 -

Implicit Casts

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

Strings are a special type of primitive that represent text.

x$ = "hello" + " world"
PRINT x$

Functions

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 sum

Elsewhere in your program, you can invoke this function like this,

x = add(1, 2)

Function Scopes

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 result

Return Values

Functions 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 sum

And the following function does not return any result,

FUNCTION nothing()
    ` do nothing
ENDFUNCTION

It 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 0

The 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.


Parameters

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
ENDFUNCTION

Parameters may declare explicit types, or use #sigils. For example, the following function accepts a STRING as an input parameter,

FUNCTION tunafish(s AS STRING)
ENDFUNCTION

And this function also accepts a STRING, but denoted through a sigil.

FUNCTION tunafish(s$)
ENDFUNCTION

No Nested Functions

Functions cannot be declared within another function. For example, the following program is invalid.

FUNCTION outer()
    FUNCTION inner()
    ENDFUNCTION
ENDFUNCTION

No Lambdas or Clojures

Fade does not support function pointers or the ability to create a closure.

Scopes

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.

User Defined Types

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 = 12

The fields declared in a UDT can explicitly declare their types.

TYPE FISH
    name$ AS STRING
ENDTYPE

Fields can also use #sigils to imply their type.

TYPE FISH
    name$ `the $ symbol implies this is a string
ENDTYPE

UDTs 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
ENDTYPE

Be 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.


UDT Default Value

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 0

The default keyword can only be used in simple assignments and declarations.


UDT Initializer

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 = 2

Object 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 1

UDT Assignment

It 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 FISH

In the example above, y receives a copy of the data. Any modification to y.size will have no effect on x.size.


No Methods

Fade does not support coupling functions with UDTs.

Arrays

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 INTEGER

Array 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 42

Multidimensional Arrays

An 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)

Arrays of UDT

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 element

Resize an Array

It 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 zero

Array Out Of Bounds

It 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 crash

Cannot Return Arrays From Functions

It is acceptable to create an array within the Local scope of a function, but it not valid to return the array from the function.


Cannot Assign an Array

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 valid

Literals

There 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 16

Operations

You 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 12

Numeric Operations

Most 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

Commands

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, 2

Commands 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,

  1. Reference the project or Nuget package in your .csproj file,
  2. 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.


Short Circuiting

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 1

In 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 1

Control Statements

Control statements specify the flow of execution in the program.


Conditionals

The IF statement allows the program to optionally run statements.

x = 3
IF x > 1
    PRINT "true"
ELSE
    PRINT "false"
ENDIF

The 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"

Single Line Statements

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"

For Loops

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. 
NEXT

The FOR statement has 4 main components,

  1. The variable declaration, t = 1,
  2. The exit condition, TO 10,
  3. The STEP amount, which is 1. This clause is optional, and defaults to 1.
  4. 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
NEXT

It 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
NEXT

Every 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 1

It 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

While Loops

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
ENDWHILE

The 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
ENDWHILE

It 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
` 1

Repeat Loops

The 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 > 0

Every 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 1

It 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
` 1

Do Loops

The DO loop will execute a block of code forever.

DO
    `whatever code is here will run forever
LOOP

Every 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
LOOP

It 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
` ...

End

The END statement immediately stops the program.

END
PRINT "Where did everyone go?" `this line will never be printed

This 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.


Goto

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 tuna

GOTO 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.


GoSub

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
RETURN

Select Statements

The 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
ENDSELECT

The 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.


Defer Statements

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
` b

When 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
` a

When 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
` a

If 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
` a

It 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
` b

If 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
` b

Deferred 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
` 12

If 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 
` a

Compile Time Execution

Fade Basic allows compile time code generation.


Program Structure

#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
` 2

The 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:
` a

Order 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 zero

Tokenization 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.


Substitutions

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:
` 12

Any 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:
` 12

It 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:
` abc

When 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:
` 12

It is invalid to place stringified substitutions adjacent to existing tokens.


Shorthands

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:
` 12

Outside 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:
` 12

Haunted Code

Any 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
#ENDMACRO

It 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
#ENDMACRO

When 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

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 42

The source code literally becomes the following,

print 42

Compile time constants cannot access any runtime data in their initializer, and cannot be modified at runtime. Compile time constants cannot be multi-line segments.

Memory

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.