Plan9Basic
Language Reference
Complete reference for the Plan9Basic programming language — syntax, data types, variables, operators, control structures, functions, arrays, debugging, and more.
v1.0 — Complete Reference📄 Introduction
Plan9Basic is a small and simple programming language inspired by classic BASIC, designed for creating small applications called applets. It combines the simplicity and accessibility of traditional BASIC with modern structured programming features and a powerful cross-platform graphical interface.
Plan9Basic is ideal for:
- Learning programming concepts
- Creating simple utilities, games, and tools
- Building interactive graphical applications
- Rapid prototyping
Design Philosophy
Plan9Basic stays true to the spirit of classic 8-bit BASIC interpreters while adding modern structured programming capabilities. Key design decisions include:
- No boolean type — conditional logic is handled directly by control structures
- Case-insensitive —
COUNT,Count, andcountall refer to the same variable - Simple and intuitive — Easy to learn, yet powerful enough for simple and useful applications
- Modern expressions — Full support for grouping logical expressions in complex conditions
How It Works
Plan9Basic is a compiler, not a simple interpreter. When you run an applet, your source code goes through a multi-stage pipeline:
- Lexer — breaks your source code into tokens (keywords, identifiers, numbers, strings, operators)
- Parser / Compiler — analyzes the token stream and compiles it into a compact intermediate representation (P-Code)
- Virtual Machine — executes the P-Code on a stack-based engine
This compiled P-Code runs on the same virtual machine across all supported platforms — Windows, Linux, and Android — regardless of the CPU architecture (x86, x64, or ARM). Write once, run everywhere.
🚀 Getting Started
Your First Applet
' My first Plan9Basic Applet PRINTLN "Hello, World!"
This simple applet displays "Hello, World!" on the output area. Let's break it down:
- The apostrophe (
') starts a comment — text that the interpreter ignores PRINTLNdisplays text and moves to the next line- Text inside double quotes is called a string
A More Interactive Example
' A simple greeting applet name$ = "User" PRINTLN "Welcome to Plan9Basic, " + name$ + "!" PRINTLN "Let's do some math:" PRINTLN "2 + 2 = "; 2 + 2 PRINTLN "10 * 5 = "; 10 * 5
📋 Applet Structure
Lines and Statements
Plan9Basic applets are composed of statements. You can write one statement per line, or multiple statements on the same line separated by colons (:):
x = 10 y = 20 PRINTLN x + y ' Or on one line: x = 10 : y = 20 : PRINTLN x + y
Comments
Comments help document your code. They are ignored during execution.
' This is a comment (single quote) REM This is also a comment (REM keyword) x = 10 ' Comments can appear after statements
Labels
Labels mark positions in your code for use with GOTO and GOSUB:
' Numeric labels (traditional BASIC style) 10 PRINTLN "Line 10" 20 PRINTLN "Line 20" ' Named labels (more readable) start: PRINTLN "Beginning of applet" finish: PRINTLN "End of applet"
Applet Termination
Use END to explicitly terminate your applet:
PRINTLN "This will be displayed" END PRINTLN "This will never be displayed"
📊 Data Types
Plan9Basic supports three fundamental data types. Like classic 8-bit BASIC interpreters, there is no boolean type — conditional logic is handled directly by control structures.
Numbers
Numbers can be integers or floating-point values:
count = 42 ' Integer pi = 3.14159 ' Floating-point big = 1.5e10 ' Scientific notation (1.5 × 10¹&sup0;) small = .5 ' Can omit leading zero (equals 0.5) negative = -273.15 ' Negative numbers
Values are stored internally as extended precision floating-point (80-bit on Windows/Linux x86/x64, giving ~18–19 significant digits). On Android ARM, the runtime maps to double precision (64-bit, ~15 significant digits). Integer values up to 2,147,483,647 are preserved exactly on all platforms.
Strings
Strings are sequences of characters, identified by the $ suffix. A string can hold a single line or multi-line text, with a maximum theoretical size of 2 GB.
name$ = "Alice" greeting$ = "Hello, World!" empty$ = ""
In Plan9Basic, strings are mutable. You can directly assign to individual lines of a multi-line string using $[n] notation:
B$ = "First line" / "Second line" / "Third line" B$[1] = "New content" PRINTLN B$ ' Output: ' First line ' New content ' Third line
Escape Sequences
| Sequence | Meaning |
|---|---|
\" | Double quote |
\\ | Backslash |
\n | New line (LF) |
\r | Carriage return (CR) |
\t | Horizontal tab |
\0 | Null character |
\b | Backspace |
\f | Form feed |
\v | Vertical tab |
\a | Alert/bell |
quote$ = "She said \"Hello!\"" path$ = "C:\\Users\\Documents" multiline$ = "Line 1\nLine 2"
Pointers
Pointers reference complex objects like arrays, GUI controls, dictionaries, and JSON structures. They are identified by the # suffix. Pointers are platform-native (64-bit on 64-bit systems, 32-bit on 32-bit systems) and are typically returned by native functions that interface with the operating system or create internal data structures.
myArray# = dim#(10) ' Create a numeric array of 10 elements myDict# = dict_new#(0) ' Create a dictionary
📄 Variables
Naming Rules
Variable names must:
- Start with a letter (A-Z, a-z) or underscore (_)
- Contain only letters, numbers, and underscores
- End with
$for strings or#for pointers - Not be a reserved word
Variable Types
| Suffix | Type | Example |
|---|---|---|
| (none) | Number | count, total, x |
$ | String | name$, text$, msg$ |
# | Pointer | arr#, obj#, data# |
Case Insensitivity
Plan9Basic does not differentiate between uppercase and lowercase characters. Count, count, and COUNT all refer to the same variable.
Global vs Local Variables
By default, all variables are global (accessible everywhere). Use LOCAL to declare local variables inside functions:
FUNCTION calculate(a, b) LOCAL temp, result temp = a + b result = temp * 2 RETURN result END FUNCTION
🔧 Operators
Arithmetic Operators
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Addition | 5 + 3 | 8 |
- | Subtraction | 5 - 3 | 2 |
* | Multiplication | 5 * 3 | 15 |
/ | Division | 15 / 3 | 5 |
MOD | Modulo (remainder) | 17 MOD 5 | 2 |
^ | Power | 2 ^ 3 | 8 |
?> | Maximum | 5 ?> 3 | 5 |
?< | Minimum | 5 ?< 3 | 3 |
Numeric Operator Precedence
Operators with higher precedence are evaluated first. Operators with the same precedence are evaluated left to right:
| Precedence | Operators | Description |
|---|---|---|
| Highest | ( ) | Parentheses |
| ↓ | - (unary) | Negation |
| ↓ | ^ | Power |
| ↓ | * / MOD | Multiplication, division, modulus |
| Lowest | + - ?> ?< | Addition, subtraction, max, min |
String operators (+, /, -) all have the same precedence and are evaluated left to right.
Comparison Operators
| Operator | Description | Example |
|---|---|---|
= | Equal to | IF x = 5 THEN |
<> | Not equal to | IF x <> 5 THEN |
< | Less than | IF x < 5 THEN |
> | Greater than | IF x > 5 THEN |
<= | Less than or equal | IF x <= 5 THEN |
>= | Greater than or equal | IF x >= 5 THEN |
Comparison operators can only be used within conditional statements (IF, WHILE, UNTIL, etc.). You cannot use comparisons as standalone expressions like result = (5 = 5).
Logical Operators
| Operator | Description | Example |
|---|---|---|
AND | Logical AND | IF a > 0 AND b > 0 THEN |
OR | Logical OR | IF a = 0 OR b = 0 THEN |
NOT | Logical NOT | IF NOT (x = 0) THEN |
Operator Precedence: NOT has the highest precedence, followed by AND, then OR. Use parentheses to control evaluation order.
String Operators
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Concatenation | "Hello" + " World" | "Hello World" |
/ | Concatenate with newline | "Line1" / "Line2" | "Line1\nLine2" |
- | Remove chars from end | "Hello" - 2 | "Hel" |
🔄 Control Structures
IF...THEN...ELSE...ENDIF
' Single line IF IF x > 0 THEN PRINTLN "Positive" ' Multi-line IF...ELSE IF...ELSE IF x > 0 THEN PRINTLN "Positive" ELSE IF x < 0 THEN PRINTLN "Negative" ELSE PRINTLN "Zero" END IF
FOR...NEXT
' Basic FOR loop FOR i = 1 TO 10 PRINTLN i NEXT ' With STEP FOR i = 10 TO 1 STEP -1 PRINTLN i NEXT
Also supports ENDFOR and END FOR as alternatives to NEXT.
WHILE...END WHILE
x = 0 WHILE x < 10 PRINTLN x x = x + 1 END WHILE
Also supports ENDWHILE and WEND.
REPEAT...UNTIL
x = 0 REPEAT PRINTLN x x = x + 1 UNTIL x >= 10
DO...LOOP
' DO WHILE...LOOP (pre-test) x = 0 DO WHILE x < 10 PRINTLN x x = x + 1 LOOP ' DO...LOOP UNTIL (post-test) x = 0 DO PRINTLN x x = x + 1 LOOP UNTIL x >= 10 ' Infinite loop with BREAK x = 0 DO PRINTLN x x = x + 1 IF x >= 10 THEN BREAK LOOP
SELECT CASE
SELECT CASE score CASE 10 PRINTLN "Perfect!" CASE 7, 8, 9 PRINTLN "Good" CASE ELSE PRINTLN "Keep practicing" END SELECT
BREAK and CONTINUE
' BREAK exits the loop immediately FOR i = 1 TO 100 IF i > 10 THEN BREAK PRINTLN i NEXT ' CONTINUE skips to the next iteration FOR i = 1 TO 10 IF i MOD 2 = 0 THEN CONTINUE PRINTLN i ' Only prints odd numbers NEXT
GOTO and GOSUB
GOTO transfers execution unconditionally to a label (line number or named label). GOSUB does the same but first saves a return address, so RETURN brings execution back to the line after the GOSUB:
' GOTO — unconditional jump (no return) GOTO skip PRINTLN "This is never reached" skip: PRINTLN "Jumped here" ' GOSUB/RETURN — subroutine call (returns to caller) PRINTLN "Before subroutine" GOSUB mySubroutine PRINTLN "After subroutine" END mySubroutine: PRINTLN "Inside subroutine" RETURN
ON...GOTO — Computed Jump
ON...GOTO evaluates a numeric expression and jumps to the corresponding label from a list, using 1-based indexing. If the value is 1, execution jumps to the first label; if 2, to the second; and so on. If the value does not match any index, execution falls through to the next line.
ON expression GOTO label1, label2, label3, ...
Labels can be line numbers or named labels:
' Menu selection with named labels choice = 2 ON choice GOTO newGame, loadGame, settings PRINTLN "Invalid choice" ' Only reached if choice is not 1, 2, or 3 END newGame: PRINTLN "Starting new game..." END loadGame: PRINTLN "Loading saved game..." END settings: PRINTLN "Opening settings..." END
ON...GOTO is a one-way jump. It does not save a return address. Execution continues from the target label and never comes back to the line after the ON statement.
ON...GOSUB — Computed Subroutine Call
ON...GOSUB works like ON...GOTO but as a subroutine call: it pushes a return address on the stack, then jumps to the matching label. When the subroutine reaches RETURN, execution resumes at the line after the ON...GOSUB. This makes it ideal for menus and dispatch tables where you want execution to continue after the subroutine finishes.
ON expression GOSUB label1, label2, label3, ...
' Display a status message based on level level = 2 ON level GOSUB showLow, showMedium, showHigh PRINTLN "(execution continues here after RETURN)" END showLow: PRINTLN "Level: LOW" RETURN showMedium: PRINTLN "Level: MEDIUM" RETURN showHigh: PRINTLN "Level: HIGH" RETURN
Like ON...GOTO, labels can be line numbers or named labels, and indexing is 1-based. If the expression value doesn’t match any index, no subroutine is called and execution falls through.
ON...CALL — Computed Function Call
ON...CALL is the most modern of the three variants. Instead of jumping to labels, it calls a named function based on the expression value. The key difference: the function receives the expression value as its parameter, so the called function knows which index triggered it.
ON expression CALL function1, function2, function3, ...
' Process a command based on user choice action = 3 ON action CALL doSave, doLoad, doExport PRINTLN "Done." END FUNCTION doSave(n) PRINTLN "Saving... (triggered by option "; n; ")" RETURN 0 END FUNCTION FUNCTION doLoad(n) PRINTLN "Loading... (triggered by option "; n; ")" RETURN 0 END FUNCTION FUNCTION doExport(n) PRINTLN "Exporting... (triggered by option "; n; ")" RETURN 0 END FUNCTION
ON...CALL works exclusively with numeric functions (no $ suffix). Each function must accept exactly one numeric parameter (the expression value) and return a number. String functions cannot be used here.
Practical Example: Dispatch Table
ON...CALL is particularly well suited for dispatch patterns where each option requires real logic with local variables, error handling, or return values — things that label-based subroutines cannot provide:
' Shape area calculator PRINTLN "1=Circle 2=Rectangle 3=Triangle" shape = 1 ON shape CALL calcCircle, calcRect, calcTriangle END FUNCTION calcCircle(n) LOCAL radius, area radius = 5 area = 3.14159 * radius * radius PRINTLN "Circle area: "; area RETURN area END FUNCTION FUNCTION calcRect(n) LOCAL w, h w = 10 : h = 5 PRINTLN "Rectangle area: "; w * h RETURN w * h END FUNCTION FUNCTION calcTriangle(n) LOCAL base, height base = 8 : height = 6 PRINTLN "Triangle area: "; (base * height) / 2 RETURN (base * height) / 2 END FUNCTION
ON... Variants Comparison
| ON...GOTO | ON...GOSUB | ON...CALL | |
|---|---|---|---|
| Targets | Labels (line numbers or named) | Labels (line numbers or named) | Function names |
| Returns? | No — one-way jump | Yes — RETURN comes back | Yes — ENDFUNCTION returns |
| Passes value? | No | No | Yes — expression value as parameter |
| Local variables? | No (labels use global scope) | No (labels use global scope) | Yes — functions support LOCAL |
| Indexing | 1-based | 1-based | 1-based |
| No match | Falls through to next line | Falls through to next line | Falls through to next line |
| Best for | Simple unconditional branching | Reusable subroutines in classic style | Modern dispatch with scoped logic |
⚙️ Functions
Defining Functions
' Numeric function (no suffix) FUNCTION square(x) RETURN x * x END FUNCTION ' String function ($ suffix) FUNCTION greeting$(name$) RETURN "Hello, " + name$ + "!" END FUNCTION ' Pointer function (# suffix) FUNCTION createArray#(size) RETURN dim#(size) END FUNCTION ' Function with local variables FUNCTION calculate(x, y) LOCAL temp, result temp = x * y result = temp + 10 RETURN result END FUNCTION
Limitations
- Functions cannot be nested (no function inside another function)
GOTOandGOSUBcannot be used inside functions- Maximum of 256 parameters and local variables combined per function
Parameter Passing
Numeric and string parameters are always passed by value — the function receives a copy, so changes inside the function do not affect the original variable. Pointer parameters are always passed by reference — the function receives the actual pointer, so operations on the referenced object are visible outside the function.
FUNCTION addToArray(arr#, index, value) arr#[index] = value ' Modifies the original array (pointer = by reference) END FUNCTION data# = dim#(5) addToArray(data#, 1, 42) PRINTLN data#[1] ' 42 — the original array was modified
Function Overloading
Plan9Basic supports function overloading — you can define multiple functions with the same name as long as their parameter types differ. The compiler uses the function's signature (name + parameter types) to determine which version to call.
' No parameters — signature: hello$@ FUNCTION hello$() RETURN "Hello world." END FUNCTION ' One string param — signature: hello$@$ FUNCTION hello$(name$) RETURN "Hello to you " + name$ END FUNCTION ' One string + one number — signature: hello$@$n FUNCTION hello$(name$, age) RETURN "Hello " + name$ + ", age " + str$(age) END FUNCTION ' The compiler picks the right version automatically: PRINTLN hello$() ' "Hello world." PRINTLN hello$("Alice") ' "Hello to you Alice" PRINTLN hello$("Alice", 30) ' "Hello Alice, age 30"
Each function is stored in a data dictionary with a unique signature of the form name@params, where parameter types are encoded as: n = number, $ = string, # = pointer. This mechanism also applies to native library functions.
📊 Arrays
Numeric Arrays
' One-dimensional array (10 elements, indices 1-10) arr# = dim#(10) arr#[1] = 100 arr#[5] = 500 ' Multi-dimensional array (3x3 matrix) matrix# = dim#(3, 3) matrix#[1, 1] = 1 matrix#[2, 2] = 5
String and Pointer Arrays
' String array names# = sdim#(5) names#$[1] = "Alice" names#$[2] = "Bob" ' Pointer array (for nested structures) containers# = pdim#(3) containers##[1] = dim#(10)
Array Indexing Rules
| Type | Base | Syntax |
|---|---|---|
Numeric arrays (dim#) | 1-based | arr#[1] |
String arrays (sdim#) | 1-based | names#$[1] |
Pointer arrays (pdim#) | 1-based | ptrs##[1] |
String lines ($[n]) | 0-based | text$[0] |
String chars ($[[n]]) | 0-based | word$[[0]] |
Array Utility Functions
| Function | Description |
|---|---|
ndims(arr#) | Get number of dimensions |
ubound(arr#, dim) | Get upper bound of dimension |
lbound(arr#, dim) | Get lower bound (always 1) |
arraysize(arr#) | Get total number of elements |
arraytype(arr#) | Get type (0=numeric, 1=string, 2=pointer) |
arraytypename$(arr#) | Get type name as string |
Chained access like arr##[1][2] is NOT supported. Use intermediate pointers instead: temp# = arr##[1] then temp#[2].
Array Functions (Underlying API)
The bracket syntax (arr#[i]) is syntactic sugar over a set of native functions. Both forms are fully interchangeable:
| Operation | Bracket Syntax | Function Syntax |
|---|---|---|
| Set numeric | arr#[1,2] = 10 | narr_set#(arr#, 1, 2, 10) |
| Get numeric | x = arr#[1,2] | x = narr_get(arr#, 1, 2) |
| Set string | arr#$[1] = "Hi" | sarr_set#(arr#, 1, "Hi") |
| Get string | s$ = arr#$[1] | s$ = sarr_get$(arr#, 1) |
| Free memory | — | arr_free(arr#) |
It is not strictly necessary to call arr_free(). The Plan9Basic environment automatically frees array memory when the applet finishes execution. However, it can be useful for managing memory in long-running codes.
' Both styles produce identical results: ' Function style n# = dim#(5, 5, 5) narr_set#(n#, 1, 1, 1, 10.7) PRINTLN narr_get(n#, 1, 1, 1) ' Bracket style (preferred) n# = dim#(5, 5, 5) n#[1, 1, 1] = 10.7 PRINTLN n#[1, 1, 1]
📝 String Operations
String Indexing
Strings support both line-based and character-based indexing (both 0-based). Since strings are mutable, you can both read and write using index notation:
text$ = "Line 1\nLine 2\nLine 3" ' Read line indexing with $[n] (0-based) PRINTLN text$[0] ' Line 1 PRINTLN text$[1] ' Line 2 ' Write line indexing — replace a specific line text$[1] = "Modified" PRINTLN text$[1] ' Modified ' Character indexing with $[[n]] (0-based) word$ = "Hello" PRINTLN word$[[0]] ' H PRINTLN word$[[4]] ' o
Built-in String Functions
| Function | Description | Example |
|---|---|---|
len(s$) | Length of string | len("Hello") → 5 |
left$(s$, n) | First n characters | left$("Hello", 3) → "Hel" |
right$(s$, n) | Last n characters | right$("Hello", 2) → "lo" |
mid$(s$, start, len) | Substring (1-based) | mid$("Hello", 2, 3) → "ell" |
ucase$(s$) | Uppercase | ucase$("Hello") → "HELLO" |
lcase$(s$) | Lowercase | lcase$("Hello") → "hello" |
trim$(s$) | Remove leading/trailing spaces | trim$(" Hi ") → "Hi" |
instr(s$, find$) | Find substring position | instr("Hello", "ll") → 3 |
str$(n) | Number to string | str$(42) → "42" |
val(s$) | String to number | val("42") → 42 |
chr$(n) | ASCII code to character | chr$(65) → "A" |
asc(s$) | Character to ASCII code | asc("A") → 65 |
space$(n) | String of n spaces | space$(5) |
string$(n, s$) | Repeat string n times | string$(3, "ab") → "ababab" |
replace$(s$, old$, new$) | Replace occurrences | replace$("Hello", "l", "L") → "HeLLo" |
🖨 Input & Output
PRINT and PRINTLN
PRINTLN "Hello, World!" ' Adds newline PRINT "Enter your name: " ' No newline ' Multiple items with semicolon (no space) PRINTLN "Value: "; 42 ' Multiple items with comma (adds separator) PRINTLN "A", "B", "C" ' A,B,C
CLS
CLS clears the screen output area.
INPUT — Asynchronous User Input
Why INPUT is different in Plan9Basic
In classic BASIC dialects like GW-BASIC or QBasic, INPUT halts the applet and waits at the cursor for the user to type something. This works in console environments where blocking the single thread of execution is acceptable.
Plan9Basic cannot work this way. Because the same applet runs on desktop and mobile platforms (Windows, Linux, Android), blocking the main thread would freeze the user interface. On Android, this triggers an Application Not Responding (ANR) dialog and the OS may kill the app.
Plan9Basic solves this with an asynchronous callback model: INPUT opens a native dialog and returns immediately so the UI stays responsive. When the user confirms the dialog, a callback function you specify is invoked with the entered value.
INPUT does not pause execution. It shows a dialog and moves on. Your callback function receives the result later. Any code placed after the INPUT line runs before the user has typed anything.
Syntax
INPUT caption$, label$, defaultValue, callbackFunction| Parameter | Type | Description |
|---|---|---|
caption$ | String (required) | Title displayed at the top of the dialog window. |
label$ | String (required) | Prompt text shown above the input field, telling the user what to type. |
defaultValue | String or Number | Pre-filled value in the input field. The type of this parameter determines the input mode (see below). |
callbackFunction | Function name (no parentheses) | Name of the function to call when the user presses OK. |
All four parameters are mandatory and separated by commas. The parser validates each one at compile time and raises a descriptive error if any is missing or of the wrong type.
Two Modes: String and Numeric
The type of the defaultValue determines everything about how INPUT works — the mode, the internal bytecode instruction emitted, and the required callback signature:
| Default Value | Mode | Bytecode | Callback Receives | Required Callback Signature |
|---|---|---|---|---|
String ("", "hello") | String | INPUT$ | A string parameter | FUNCTION name$(param$) |
Number (0, 42, 3.14) | Numeric | INPUT | A number parameter | FUNCTION name(param) |
The mode and callback must agree. A string default requires a string callback (name ending with $). A numeric default requires a numeric callback (no $). Mismatching them produces a compile-time Syntax error. This is enforced by the parser: it checks that btkStrIdentifier pairs with ekString, and btkIdentifier pairs with ekNumber.
String INPUT
When the default value is a string, the callback must be a string function — its name ends with $, it takes one string parameter, and it returns a string:
' Ask for the user's name (string mode) INPUT "Welcome", "What is your name?", "", onName$ FUNCTION onName$(name$) IF name$ = "" THEN PRINTLN "No name entered." ELSE PRINTLN "Hello, " + name$ + "!" END IF RETURN "" END FUNCTION
The callback receives exactly the string the user typed. The dialog pre-fills the field with the default value (empty string "" in this example).
Numeric INPUT
When the default value is a number, the callback must be a numeric function — no $ suffix, takes one number parameter, returns a number:
' Ask for age (numeric mode) INPUT "Profile", "Enter your age:", 0, onAge FUNCTION onAge(value) IF value < 1 OR value > 120 THEN PRINTLN "Invalid age." ELSE PRINTLN "Age: "; value END IF RETURN 0 END FUNCTION
In numeric mode, the VM attempts to convert the user’s text to a floating-point number. If the conversion fails (the user typed letters, left it blank, etc.), the callback receives 0.0. No error is raised.
Execution Flow — What Actually Happens
Understanding the timeline is the single most important thing when working with INPUT. Here is exactly what happens, step by step:
PRINTLN "Step 1" ' Runs immediately INPUT "Title", "Prompt:", "", onResult$ ' Shows dialog, RETURNS IMMEDIATELY PRINTLN "Step 2 - dialog is still open!" ' Runs RIGHT AWAY ' ... user types and clicks OK ... ' onResult$ is called NOW
When the VM encounters an INPUT instruction, it:
- Pops the four parameters from the stack (callback signature, default value, label, caption).
- Looks up the callback name in the applet’s function dictionary to verify it exists.
- Calls the platform’s asynchronous dialog service, which shows a native OS dialog without blocking.
- Returns immediately. The next instruction in your applet executes right away.
Later, when the user presses OK, the dialog service fires a completion callback inside the VM. At that point:
- The VM saves its complete state — instruction pointer (
PRG_IP), stack pointer (STKP), and base pointer (BASEP). - A clean, isolated stack environment is created (pointers reset to 0) so the callback cannot corrupt the main applet’s stack.
- The user’s input is pushed as the function parameter.
- The callback function body executes in this sandboxed context.
- The VM state is fully restored after the callback completes, guaranteed by a
try/finallyblock even if the callback raises an exception.
When the User Cancels
If the user presses Cancel or dismisses the dialog, the callback is never called. No error occurs; execution simply continues. The internal completion handler checks for AResult = mrOk and only invokes the callback on confirmation.
If you need to detect whether the user responded, use a global flag:
answered = 0 INPUT "Confirm", "Type YES to proceed:", "", onConfirm$ ' If user cancels, answered stays 0 FUNCTION onConfirm$(reply$) IF ucase$(reply$) = "YES" THEN answered = 1 PRINTLN "Proceeding..." ELSE PRINTLN "Declined." END IF RETURN "" END FUNCTION
Chaining Multiple INPUTs
Since INPUT is asynchronous, you cannot place two INPUT statements one after another and expect them to run sequentially — both dialogs would appear simultaneously. Instead, chain them by calling the next INPUT from inside the previous callback:
' Collect multiple values step by step ' Use global variables to share data between callbacks LET gName$ = "" LET gAge = 0 ' Step 1: ask for name INPUT "Registration", "Your name:", "", onName$ FUNCTION onName$(name$) gName$ = name$ ' Step 2: ask for age (chained from inside this callback) INPUT "Registration", "Your age:", 0, onAge RETURN "" END FUNCTION FUNCTION onAge(age) gAge = age ' Both values now available PRINTLN "Name: " + gName$ PRINTLN "Age: "; gAge RETURN 0 END FUNCTION
Use global variables (declared outside functions with LET) to pass data between chained callbacks. Local variables exist only inside their own function and are not visible to other callbacks.
Self-Referencing Callbacks (Loops)
A callback function can call INPUT with itself as the callback, creating a loop. This is the idiomatic way to repeatedly ask for input until a condition is met — analogous to a WHILE loop around a classic INPUT:
' Keep asking until the user enters a valid password INPUT "Login", "Password:", "", onPassword$ FUNCTION onPassword$(pw$) IF pw$ = "secret123" THEN PRINTLN "Access granted!" ELSE PRINTLN "Wrong password. Try again." ' Re-call INPUT with the same callback = loop INPUT "Login", "Password:", "", onPassword$ END IF RETURN "" END FUNCTION
This pattern can also pass the previous answer as the new default value, so the user sees what they last typed. The guessing game example in this document uses exactly this technique — the callback checkGuess calls INPUT again with checkGuess as the callback and the last guess as the default.
Common Pitfall: Code After INPUT
The most frequent mistake for programmers coming from classic BASIC is placing logic after INPUT that depends on the user’s answer:
result$ = "" INPUT "Test", "Enter value:", "", onValue$ ' BUG: result$ is still "" here — the callback hasn't fired yet! PRINTLN "You entered: " + result$ FUNCTION onValue$(v$) result$ = v$ RETURN "" END FUNCTION
INPUT "Test", "Enter value:", "", onValue$ FUNCTION onValue$(result$) ' ALL processing of the answer goes here, inside the callback PRINTLN "You entered: " + result$ RETURN "" END FUNCTION
The rule is simple: everything that depends on the user’s answer must be inside the callback (or in a function called from the callback). Code after the INPUT line in the main flow runs before the dialog is even displayed.
Complete Example: Mini Calculator
This example chains three INPUTs (number → operator → number) and uses global variables to carry state between callbacks:
' Mini Calculator — demonstrates INPUT chaining LET gNum1 = 0 LET gOp$ = "" PRINTLN "=== Calculator ===" INPUT "Calculator", "First number:", 0, onFirst FUNCTION onFirst(num) gNum1 = num INPUT "Calculator", "Operator (+, -, *, /):", "", onOp$ RETURN 0 END FUNCTION FUNCTION onOp$(op$) gOp$ = op$ INPUT "Calculator", "Second number:", 0, onSecond RETURN "" END FUNCTION FUNCTION onSecond(num2) LOCAL result IF gOp$ = "+" THEN result = gNum1 + num2 ELSE IF gOp$ = "-" THEN result = gNum1 - num2 ELSE IF gOp$ = "*" THEN result = gNum1 * num2 ELSE IF gOp$ = "/" THEN IF num2 = 0 THEN PRINTLN "Error: division by zero" RETURN 0 END IF result = gNum1 / num2 ELSE PRINTLN "Unknown operator: " + gOp$ RETURN 0 END IF PRINTLN str$(gNum1) + " " + gOp$ + " " + str$(num2) + " = " + str$(result) RETURN 0 END FUNCTION
Comparison with Classic BASIC
| Classic BASIC | Plan9Basic | |
|---|---|---|
| Behavior | Blocks execution until user types | Shows dialog and returns immediately |
| Syntax | INPUT "Prompt: "; x$ | INPUT "Title", "Prompt", "", cb$ |
| Result | Stored in the variable directly | Passed as parameter to callback function |
| Dialog | Text cursor in the console | Native OS dialog box |
| Sequential | Write multiple INPUT lines | Chain callbacks (next INPUT inside previous callback) |
| Loops | WHILE around INPUT | Callback calls INPUT with itself = self-referencing loop |
| Cancel | Not possible (must type something) | User can cancel; callback is not called |
| Mobile | Would freeze/crash the app | Works on all 6 platforms without blocking |
INPUT Quick Reference
| Rule | Details |
|---|---|
| String mode | Default is a string → callback must be funcname$(param$), returns string |
| Numeric mode | Default is a number → callback must be funcname(param), returns number |
| Execution | Non-blocking. Code after INPUT runs before the user interacts. |
| Cancel | Callback is not called. No error is raised. |
| Invalid numeric input | Silently converts to 0. |
| Chaining | Call the next INPUT inside the current callback. |
| Looping | A callback can call INPUT with itself as the callback. |
| Global variables | Use globals (declared with LET outside functions) to share state between callbacks. |
| Callback isolation | Each callback runs in a sandboxed VM context with its own stack. Cannot corrupt the main applet. |
| Function not found | If the callback name doesn’t exist, a runtime error is raised: “There is no function with such arguments.” |
💾 DATA, READ, and RESTORE
' Store student data DATA "Alice", 85 DATA "Bob", 92 DATA "Carol", 78 ' Read and display FOR i = 1 TO 3 READ name$ READ score PRINTLN name$ + ": " + str$(score) NEXT ' RESTORE resets the data pointer RESTORE READ name$ ' Back to "Alice"
DATA statements cannot appear inside functions.
🐛 Debugging Features
Plan9Basic provides a comprehensive set of debugging tools. All features are controlled by a "master switch" — when tracing is disabled, all debug commands are ignored with zero overhead.
TRACE — The Master Switch
TRACEON ' Turn on debugging (level 1) TRACEOFF ' Turn off debugging TRACE 0 ' Off - no debugging output TRACE 1 ' Basic - show line numbers TRACE 2 ' Standard - lines + function names TRACE 3 ' Verbose - lines + functions + watches
ASSERT, DUMP, WATCH, BREAKPOINT
TRACEON ' ASSERT - validates a condition x = 10 ASSERT x > 0, "x must be positive" ' DUMP - show all variables DUMP "Current state" ' WATCH - monitor specific variables TRACE 3 WATCH x, y, total x = 10 : y = 20 : total = x + y UNWATCH ' BREAKPOINT - pause and inspect BREAKPOINT "Before calc", x, y TRACEOFF
Debugging Quick Reference
| Command | Description |
|---|---|
TRACEON | Enable debugging (level 1) |
TRACEOFF | Disable debugging |
TRACE 0/1/2/3 | Set trace level |
ASSERT cond, msg$ | Stop if condition is false |
DUMP "label" | Show all global variables |
WATCH var1, var2 | Monitor specific variables |
UNWATCH | Stop monitoring |
BREAKPOINT "msg", vars | Pause and show values |
💫 Advanced Features
Indirect Function Calls
Every function in Plan9Basic — whether native or user-defined — is stored in a data dictionary with a unique signature. The & operator lets you call any function dynamically by its signature string at runtime.
This is powerful for building dynamic dispatch, plugin systems, or when user interaction determines which function to call.
' Numeric result: use &(signature, params...) operation$ = "sin" result = &(operation$ + "@n", 90) ' String result: use &$(signature, params...) func$ = "ucase$" result$ = &$(func$ + "@$", "hello") ' HELLO ' Pointer result: use &#(signature, params...) result# = &#("dim#@n", 10)
The return type operator matches the function's return type: & for numeric, &$ for string, &# for pointer.
Example: Dynamic Dispatch with Overloaded Functions
' Define overloaded functions FUNCTION greet$() RETURN "Hello world." END FUNCTION FUNCTION greet$(name$) RETURN "Hello, " + name$ + "!" END FUNCTION FUNCTION greet$(name$, age) RETURN "Hello " + name$ + ", age " + str$(age) END FUNCTION ' Call each overload by its signature: PRINTLN &$("greet$@") ' Hello world. PRINTLN &$("greet$@$", "Alice") ' Hello, Alice! PRINTLN &$("greet$@$n", "Alice", 30) ' Hello Alice, age 30
Function Signatures
The signature format is: name@parameters
n= numeric parameter$= string parameter#= pointer parameter
Examples: sin@n, left$@$n, dim#@nnn
The THIS# Pointer
Inside callbacks and event handlers, THIS# refers to the host application object.
LET Statement
LET is optional for assignments (classic BASIC compatibility): LET x = 10 is the same as x = 10.
JSON Literals
' JSON array literal myArray# = [1, 2, 3, 4, 5] ' JSON object literal myObject# = {"name": "Alice", "age": 25} ' Nested structures data# = { "users": [ {"name": "Alice", "score": 100}, {"name": "Bob", "score": 85} ] }
💡 Complete Examples
Number Guessing Game
' Number Guessing Game CLS PRINTLN "=== Number Guessing Game ===" PRINTLN "" randomize() secretNumber = int(rnd() * 100) + 1 attempts = 0 maxAttempts = 7 PRINTLN "I'm thinking of a number between 1 and 100." PRINTLN "You have " + str$(maxAttempts) + " attempts." INPUT "Guess", "Enter your guess:", 50, checkGuess FUNCTION checkGuess(guess) attempts = attempts + 1 IF guess = secretNumber THEN PRINTLN "Congratulations! Got it in " + str$(attempts) + " attempts!" ELSE IF guess < secretNumber THEN PRINTLN "Too low!" IF attempts < maxAttempts THEN INPUT "Guess", "Try again:", guess, checkGuess ELSE PRINTLN "Game over! The number was " + str$(secretNumber) END IF ELSE PRINTLN "Too high!" IF attempts < maxAttempts THEN INPUT "Guess", "Try again:", guess, checkGuess ELSE PRINTLN "Game over! The number was " + str$(secretNumber) END IF END IF END FUNCTION
Bubble Sort
' Bubble Sort Example CLS size = 10 arr# = dim#(size) randomize() FOR i = 1 TO size arr#[i] = int(rnd() * 100) PRINT arr#[i]; " "; NEXT PRINTLN "" FOR i = 1 TO size - 1 FOR j = 1 TO size - i IF arr#[j] > arr#[j + 1] THEN temp = arr#[j] arr#[j] = arr#[j + 1] arr#[j + 1] = temp END IF NEXT j NEXT i PRINTLN "Sorted:" FOR i = 1 TO size PRINT arr#[i]; " "; NEXT
⚡ Quick Reference
Keywords
| Category | Keywords |
|---|---|
| Control | IF, THEN, ELSE, ENDIF, END IF, FOR, TO, STEP, NEXT, ENDFOR, WHILE, ENDWHILE, WEND, END WHILE, REPEAT, UNTIL, DO, LOOP, SELECT, CASE, END SELECT |
| Branching | GOTO, GOSUB, RETURN, ON...GOTO, ON...GOSUB, ON...CALL, BREAK, CONTINUE, END |
| Functions | FUNCTION, ENDFUNCTION, END FUNCTION, LOCAL |
| I/O | PRINT, PRINTLN, INPUT, CLS |
| Data | DATA, READ, RESTORE, LET |
| Operators | AND, OR, NOT, MOD |
| Debugging | TRACE, TRACEON, TRACEOFF, ASSERT, DUMP, WATCH, UNWATCH, BREAKPOINT |
| Comments | REM, ' |
Operators Quick Reference
| Type | Operators |
|---|---|
| Arithmetic | +, -, *, /, MOD, ^, ?>, ?< |
| Comparison | =, <>, <, >, <=, >= |
| Logical | AND, OR, NOT |
| String | + (concat), / (with newline), - (truncate) |
Type Suffixes
| Suffix | Type | Example |
|---|---|---|
| (none) | Number | x, count |
$ | String | name$, text$ |
# | Pointer | arr#, obj# |
🏆 Tips & Best Practices
- Use meaningful variable names —
playerScoreis better thanx - Comment your code — explain complex logic for future reference
- Use functions — break complex applets into smaller, reusable pieces
- Initialize variables — set initial values before using variables
- Avoid GOTO when possible — use structured loops and functions instead
- Test incrementally — build and test your applet in small steps
- Handle edge cases — check for division by zero, empty strings, etc.
- Comparisons only in conditionals — they only work inside
IF,WHILE,UNTIL, etc. - Use intermediate pointers for nested arrays — chained access like
arr##[1][2]is not supported - Use TRACE for debugging — enable to find problems, disable for production
- Use ASSERT liberally — validate assumptions at the start of functions
- Remember case insensitivity —
CountandCOUNTare the same variable - NEXT can include the control variable —
NEXT ihelps document which loop is ending
🚨 Error Codes
When using library functions, errors are typically indicated by return values:
| Code | Meaning |
|---|---|
0 | No error (success) |
1 | Index out of bounds |
2 | Invalid argument |
3 | Empty string |
4 | File error |
5 | Clipboard error |
Check specific library documentation for additional error codes.