CrashBASIC Variable Scoping

CrashBASIC Variable Scoping

This guide explains how variable scoping works in CrashBASIC, based on the actual interpreter behavior.

Overview

CrashBASIC uses a hybrid scoping model:

  • Shared globals - Variables created at the top level are accessible by all threads
  • Thread-local scopes - Variables created inside functions/subs are isolated per-thread with nested scope stacks

Scope Levels

Global Scope (Top Level)

Variables assigned at the top level of your program (outside any FUNCTION or SUB) are shared globals:

' These are shared globals - visible everywhere
score = 0
playerName$ = "Player 1"
DIM inventory(10) AS STRING

SUB AddScore(points)
    score = score + points   ' Accesses the global "score"
END SUB

Key behavior:

  • Created when first assigned at depth 1 (no active function)
  • Accessible from any function, sub, or thread
  • Updates to these variables are visible to all threads immediately

Local Scope (Inside Functions/Subs)

Variables created inside a function or sub are thread-local:

FUNCTION Calculate(x, y)
    temp = x * 2        ' Local to this function call
    result = temp + y   ' Also local
    Calculate = result
END FUNCTION

Key behavior:

  • Created when first assigned inside a function (depth > 1)
  • Destroyed when the function returns
  • Each thread has its own independent copy
  • Cannot be accessed from outside the function

Variable Resolution

When you access a variable, CrashBASIC searches in this order:

  1. Pseudo-variables (getters like TIME$, MOUSEX, RND)
  2. Thread-local scopes (innermost to outermost)
  3. Shared globals
x = 100                   ' Shared global

SUB Test()
    PRINT x               ' Prints 100 (finds global)
    x = 200               ' Updates the global (found in globals)
    DIM x AS INTEGER      ' Creates LOCAL x, shadows global
    x = 50                ' Updates local x
    PRINT x               ' Prints 50 (local shadows global)
END SUB

CALL Test()
PRINT x                   ' Prints 200 (global was updated before DIM)

Variable Assignment Rules

When you assign to a variable (x = value):

  1. Check shared globals first - If the variable exists in globals, update it there
  2. Check thread-local scopes - If found in local scopes, update it there
  3. Create new variable - If not found anywhere:
    • At global scope (depth 1): Create in shared globals
    • Inside a function (depth > 1): Create in current local scope
score = 0                 ' Creates shared global (at top level)

SUB IncrementScore()
    score = score + 1     ' Updates existing global
    bonus = 10            ' Creates LOCAL variable (inside function)
END SUB

Explicit Local Variables

Use DIM inside a function to explicitly create a local variable:

DIM x AS INTEGER          ' Global x

SUB Process()
    DIM x AS INTEGER      ' Local x - shadows global
    x = 42                ' Assigns to local x
END SUB

CALL Process()
PRINT x                   ' Prints 0 (global x unchanged)

Parameter Passing

BYREF (the default — in a SUB)

A SUB parameter is passed by reference unless you say otherwise, so changes to it affect the caller's variable:

SUB Double(n)
    n = n * 2
END SUB

x = 5
CALL Double(x)
PRINT x             ' Prints 10

FUNCTION parameters are always BYVAL

None of the above applies to a FUNCTION. Its parameters are always passed
by value, and writing BYREF in one is an error rather than a preference:

FUNCTION Doubled(n)       ' n is BYVAL, always
    Doubled = n * 2
END FUNCTION

FUNCTION Bad(BYREF n)     ' Error: BYREF is not allowed in a FUNCTION

The rule exists so a function call is safe to use inside an expression: reading
total = Doubled(x) + Doubled(y) should not leave you wondering whether x
changed. If you need to modify the caller's variable, that is what a SUB is
for.

BYVAL

Use BYVAL to pass a copy:

SUB Double(BYVAL n)
    n = n * 2
END SUB

x = 5
CALL Double(x)
PRINT x             ' Prints 5 (unchanged)

When BYREF quietly becomes BYVAL

BYREF needs a caller's variable to write back into, so it applies only when the
argument is a plain variable
. Anything else has no home to write to and is
passed by value no matter how the parameter was declared — a literal, an
expression, an array element (rooms(1)), or the result of a function:

SUB Modify(n)
    n = 999
END SUB

CALL Modify(42)           ' A literal has no variable to write back to

Reference Types: Arrays, Objects, and Slices

Scalars (INTEGER, FLOAT, STRING, COORDINATE) are value types — copying one
copies its contents. Arrays and objects are reference types — a variable
holds a handle to shared storage, not the data itself. This changes what
assignment and parameter passing mean for them.

Assignment aliases, it doesn't copy

DIM a(3) AS INTEGER
a(0) = 10
b = a              ' b is a second NAME for the same array
b(0) = 99
PRINT a(0)         ' 99 — one array, two names

To get an independent copy, use CLONE (a deep copy — nested arrays/objects
included):

b = CLONE(a)       ' a separate array
b(0) = 99
PRINT a(0)         ' 10 — untouched

BYREF vs BYVAL for reference types

The default (BYREF) and explicit BYVAL behave the way their names promise —
but it's worth being precise about element writes versus reassigning the
whole parameter:

SUB FillByRef(arr)            ' BYREF (default)
    arr(0) = 99               ' reaches the caller's array
END SUB

SUB FillByVal(BYVAL arr)      ' explicit BYVAL
    arr(0) = 99               ' local only — caller's array is untouched
END SUB

DIM data(3) AS INTEGER
data(0) = 1

CALL FillByRef(data)
PRINT data(0)                 ' 99 — BYREF shares storage

data(0) = 1
CALL FillByVal(data)
PRINT data(0)                 ' 1  — BYVAL handed the sub its own deep copy
Element write through the parameter Reassigning the whole parameter
BYREF (default) Reaches the caller's array (shared storage) Reaches the caller
BYVAL Local only — the callee got a deep copy Local only

BYVAL means BYVAL. An explicit BYVAL array or object parameter is a
genuinely independent deep copy — mutating its elements cannot leak back to
the caller. (This is true even though plain = assignment aliases; the
copy happens at the call boundary.)

Slices are shared-storage views

Indexing a multi-dimensional array with fewer indices than its rank yields a
slice — a view that shares the parent's storage (see the Data Types guide
for the full rules). A slice behaves like any other array reference:

DIM Rooms(2, 9, 9) AS INTEGER
room = Rooms(1)               ' a 2D view aliasing Rooms[1, :, :]
room(4, 3) = 7                ' writes straight through to Rooms(1, 4, 3)

One subtlety where this meets parameter passing: a slice expression like
Rooms(1) isn't a plain variable, so handing it to a BYREF parameter
still passes the view — and because the view shares the parent's storage,
writes through the parameter reach the parent:

SUB FillCorner(grid)          ' BYREF
    grid(0, 0) = 5
END SUB

CALL FillCorner(Rooms(1))     ' Rooms(1, 0, 0) becomes 5

If you want the sub to work on a private copy of the slice instead, declare
the parameter BYVAL (which deep-copies it) or pass CLONE(Rooms(1)).

Constants

Use CONST to create read-only variables:

CONST PI = 3.14159
CONST APP_NAME$ = "My Game"

PI = 3                    ' ERROR: Cannot assign to constant

Constants follow the same scoping rules as regular variables:

  • At top level: shared global constant
  • Inside function: local constant (destroyed when function returns)

Thread Safety

CrashBASIC is designed for multi-threaded game loops:

score = 0                 ' Shared global

SUB PlayerThread()
    localData = 0         ' Thread-local
    DO
        localData = localData + 1
        score = score + 1  ' Safe: atomic update to shared global
        YIELD
    LOOP
END SUB

CALL THREAD PlayerThread

Shared globals:

  • Protected by RwLock for thread-safe access
  • Read/write operations are atomic at the variable level

Thread-local variables:

  • Each thread has its own scope stack
  • No locking needed - complete isolation

Namespaces

A NAMESPACE ... END NAMESPACE block groups variables, functions, and subs under a named scope. Everything declared inside a namespace is addressable from outside with Namespace.Member syntax — variables, subs, and functions alike.

NAMESPACE Player
    health = 100          ' Stored internally as PLAYER.HEALTH
END NAMESPACE

PRINT Player.health       ' 100  — reads work from anywhere
Player.health = 75        ' Writes work from anywhere too
PRINT Player.health       ' 75

Variable resolution inside a namespace

Inside NAMESPACE Player:

  • A bare name like health resolves to the namespaced variable (PLAYER.HEALTH) if one exists, otherwise falls back to a shared global.
  • Writing health = 75 updates the namespaced variable; it does not leak into globals.
  • An explicit Player.health also works and always refers to the namespaced variable.
globalScore = 0

NAMESPACE Player
    health = 100
    SUB Hurt(amount)
        health = health - amount        ' writes PLAYER.HEALTH
        globalScore = globalScore - 1   ' reads/writes the shared global
    END SUB
END NAMESPACE

Subs and functions in namespaces

Subs and functions declared inside a namespace live under the same prefix. Call them from outside with Namespace.Name(args):

NAMESPACE Player
    health = 100

    SUB TakeDamage(amount)
        health = health - amount
        IF health < 0 THEN health = 0
    END SUB

    FUNCTION IsAlive()
        IsAlive = health > 0
    END FUNCTION
END NAMESPACE

CALL Player.TakeDamage(25)
PRINT Player.health          ' 75
IF Player.IsAlive() THEN PRINT "Still kicking"

Inside the namespace, sibling subs/functions can be called by bare name (TakeDamage resolves to Player.TakeDamage). From outside, the fully qualified name is required.

CALL RENDER, CALL TIMER, CALL THREAD, and CALL EVENT all accept namespaced sub names the same way:

CALL RENDER Player.Draw() AS "player_render"
CALL TIMER Player.Regen() EVERY 2 SECONDS AS "player_regen"

Cross-namespace access

Variables in one namespace can be read and written from inside another namespace using the fully qualified path:

NAMESPACE Player
    hp = 100
END NAMESPACE

NAMESPACE Combat
    SUB Attack()
        Player.hp = Player.hp - 10
    END SUB
END NAMESPACE

CALL Combat.Attack()
PRINT Player.hp              ' 90

This works at the top level of a namespace block too — you don't need to be inside a sub:

NAMESPACE Combat
    Player.hp = 50           ' fine; directly sets PLAYER.HP
END NAMESPACE

Summary of namespace access

Action From inside the namespace From outside / another namespace
Read bare var Resolves to the namespaced variable (falls back to global) N/A (use Namespace.var)
Read Namespace.var Works Works
Write var = value Updates the namespaced variable N/A
Write Namespace.var = value Works Works
Call bare sub/function Resolves to the namespaced sub N/A (use Namespace.Name)
Call Namespace.Name(args) Works Works

Pseudo-Variables

Some variables are computed on access (read-only):

Variable Description
TIME$ Current time as string
DATE$ Current date as string
TIMER Seconds since midnight
RND Random number 0-1
PI Mathematical constant
MOUSEX, MOUSEY Mouse position
INKEY$ Last key pressed

These take priority over any user-defined variables with the same name.

Nested Scopes

Each function call creates a new scope level:

SUB Outer()
    x = 1                 ' Local to Outer
    CALL Inner()
    PRINT x               ' Prints 1
END SUB

SUB Inner()
    x = 2                 ' Local to Inner (different variable)
    PRINT x               ' Prints 2
END SUB

Scope depth increases with each function call and decreases when returning.

Common Patterns

Accessing Globals from Functions

playerHealth = 100

SUB TakeDamage(amount)
    playerHealth = playerHealth - amount  ' Updates global
    IF playerHealth < 0 THEN playerHealth = 0
END SUB

Avoiding Accidental Global Modification

SUB ProcessData(data)
    DIM result AS INTEGER     ' Explicit local
    DIM i AS INTEGER          ' Explicit local

    FOR i = 1 TO 10
        result = result + data(i)
    NEXT i

    ProcessData = result
END SUB

Thread-Local State

SUB EnemyAI()
    DIM state AS INTEGER      ' Each enemy thread has its own state
    DIM targetX AS INTEGER
    DIM targetY AS INTEGER

    DO
        ' AI logic using thread-local variables
        YIELD
    LOOP
END SUB

' Spawn multiple enemy threads - each has isolated state
FOR i = 1 TO 5
    CALL THREAD EnemyAI
NEXT i

Summary

Context Variable Creation Visibility
Top level assignment Shared global All threads
Top level DIM Shared global All threads
Inside function assignment (new var) Thread-local Current function only
Inside function DIM Thread-local Current function only
Function parameter Thread-local Current function only
CONST at top level Shared global (read-only) All threads
CONST in function Thread-local (read-only) Current function only