Docs Language Reference
Documentation

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-insensitiveCOUNT, Count, and count all 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:

  1. Lexer — breaks your source code into tokens (keywords, identifiers, numbers, strings, operators)
  2. Parser / Compiler — analyzes the token stream and compiles it into a compact intermediate representation (P-Code)
  3. 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

╯ hello.bas
' 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
  • PRINTLN displays text and moves to the next line
  • Text inside double quotes is called a string

A More Interactive Example

╯ greeting.bas
' 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 (:):

╯ plan9basic
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.

╯ plan9basic
' 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:

╯ plan9basic
' 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:

╯ plan9basic
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:

╯ plan9basic
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
💡 Note

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.

╯ plan9basic
name$ = "Alice"
greeting$ = "Hello, World!"
empty$ = ""
💡 Mutable Strings

In Plan9Basic, strings are mutable. You can directly assign to individual lines of a multi-line string using $[n] notation:

╯ plan9basic
B$ = "First line" / "Second line" / "Third line"
B$[1] = "New content"
PRINTLN B$
' Output:
' First line
' New content
' Third line

Escape Sequences

SequenceMeaning
\"Double quote
\\Backslash
\nNew line (LF)
\rCarriage return (CR)
\tHorizontal tab
\0Null character
\bBackspace
\fForm feed
\vVertical tab
\aAlert/bell
╯ plan9basic
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.

╯ plan9basic
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

SuffixTypeExample
(none)Numbercount, total, x
$Stringname$, text$, msg$
#Pointerarr#, obj#, data#

Case Insensitivity

⚠️ Important

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:

╯ plan9basic
FUNCTION calculate(a, b) LOCAL temp, result
  temp = a + b
  result = temp * 2
  RETURN result
END FUNCTION

🔧 Operators

Arithmetic Operators

OperatorDescriptionExampleResult
+Addition5 + 38
-Subtraction5 - 32
*Multiplication5 * 315
/Division15 / 35
MODModulo (remainder)17 MOD 52
^Power2 ^ 38
?>Maximum5 ?> 35
?<Minimum5 ?< 33

Numeric Operator Precedence

Operators with higher precedence are evaluated first. Operators with the same precedence are evaluated left to right:

PrecedenceOperatorsDescription
Highest( )Parentheses
- (unary)Negation
^Power
*/MODMultiplication, division, modulus
Lowest+-?>?<Addition, subtraction, max, min
💡 Note

String operators (+, /, -) all have the same precedence and are evaluated left to right.

Comparison Operators

OperatorDescriptionExample
=Equal toIF x = 5 THEN
<>Not equal toIF x <> 5 THEN
<Less thanIF x < 5 THEN
>Greater thanIF x > 5 THEN
<=Less than or equalIF x <= 5 THEN
>=Greater than or equalIF x >= 5 THEN
⚠️ Important

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

OperatorDescriptionExample
ANDLogical ANDIF a > 0 AND b > 0 THEN
ORLogical ORIF a = 0 OR b = 0 THEN
NOTLogical NOTIF NOT (x = 0) THEN

Operator Precedence: NOT has the highest precedence, followed by AND, then OR. Use parentheses to control evaluation order.

String Operators

OperatorDescriptionExampleResult
+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

╯ plan9basic
' 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

╯ plan9basic
' 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

╯ plan9basic
x = 0
WHILE x < 10
  PRINTLN x
  x = x + 1
END WHILE

Also supports ENDWHILE and WEND.

REPEAT...UNTIL

╯ plan9basic
x = 0
REPEAT
  PRINTLN x
  x = x + 1
UNTIL x >= 10

DO...LOOP

╯ plan9basic
' 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

╯ plan9basic
SELECT CASE score
  CASE 10
    PRINTLN "Perfect!"
  CASE 7, 8, 9
    PRINTLN "Good"
  CASE ELSE
    PRINTLN "Keep practicing"
END SELECT

BREAK and CONTINUE

╯ plan9basic
' 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:

╯ plan9basic
' 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.

╯ syntax
ON expression GOTO label1, label2, label3, ...

Labels can be line numbers or named labels:

╯ on_goto.bas
' 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
💡 No return

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.

╯ syntax
ON expression GOSUB label1, label2, label3, ...
╯ on_gosub.bas
' 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.

╯ syntax
ON expression CALL function1, function2, function3, ...
╯ on_call.bas
' 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
⚠ Numeric functions only

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:

╯ dispatch.bas
' 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...GOTOON...GOSUBON...CALL
TargetsLabels (line numbers or named)Labels (line numbers or named)Function names
Returns?No — one-way jumpYes — RETURN comes backYes — ENDFUNCTION returns
Passes value?NoNoYes — expression value as parameter
Local variables?No (labels use global scope)No (labels use global scope)Yes — functions support LOCAL
Indexing1-based1-based1-based
No matchFalls through to next lineFalls through to next lineFalls through to next line
Best forSimple unconditional branchingReusable subroutines in classic styleModern dispatch with scoped logic

⚙️ Functions

Defining Functions

╯ plan9basic
' 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)
  • GOTO and GOSUB cannot 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.

╯ plan9basic
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.

╯ plan9basic
' 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

╯ plan9basic
' 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

╯ plan9basic
' 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

TypeBaseSyntax
Numeric arrays (dim#)1-basedarr#[1]
String arrays (sdim#)1-basednames#$[1]
Pointer arrays (pdim#)1-basedptrs##[1]
String lines ($[n])0-basedtext$[0]
String chars ($[[n]])0-basedword$[[0]]

Array Utility Functions

FunctionDescription
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
⚠️ Important

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:

OperationBracket SyntaxFunction Syntax
Set numericarr#[1,2] = 10narr_set#(arr#, 1, 2, 10)
Get numericx = arr#[1,2]x = narr_get(arr#, 1, 2)
Set stringarr#$[1] = "Hi"sarr_set#(arr#, 1, "Hi")
Get strings$ = arr#$[1]s$ = sarr_get$(arr#, 1)
Free memoryarr_free(arr#)
💡 Note

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.

╯ plan9basic
' 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:

╯ plan9basic
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

FunctionDescriptionExample
len(s$)Length of stringlen("Hello")5
left$(s$, n)First n charactersleft$("Hello", 3)"Hel"
right$(s$, n)Last n charactersright$("Hello", 2)"lo"
mid$(s$, start, len)Substring (1-based)mid$("Hello", 2, 3)"ell"
ucase$(s$)Uppercaseucase$("Hello")"HELLO"
lcase$(s$)Lowercaselcase$("Hello")"hello"
trim$(s$)Remove leading/trailing spacestrim$(" Hi ")"Hi"
instr(s$, find$)Find substring positioninstr("Hello", "ll")3
str$(n)Number to stringstr$(42)"42"
val(s$)String to numberval("42")42
chr$(n)ASCII code to characterchr$(65)"A"
asc(s$)Character to ASCII codeasc("A")65
space$(n)String of n spacesspace$(5)
string$(n, s$)Repeat string n timesstring$(3, "ab")"ababab"
replace$(s$, old$, new$)Replace occurrencesreplace$("Hello", "l", "L")"HeLLo"

🖨 Input & Output

PRINT and PRINTLN

╯ plan9basic
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.

💡 Key Concept

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

╯ syntax
INPUT caption$, label$, defaultValue, callbackFunction
ParameterTypeDescription
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.
defaultValueString or NumberPre-filled value in the input field. The type of this parameter determines the input mode (see below).
callbackFunctionFunction 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 ValueModeBytecodeCallback ReceivesRequired Callback Signature
String ("", "hello")StringINPUT$A string parameterFUNCTION name$(param$)
Number (0, 42, 3.14)NumericINPUTA number parameterFUNCTION name(param)
⚠ Type Matching is Strict

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:

╯ string_input.bas
' 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:

╯ numeric_input.bas
' 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:

╯ timeline
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:

  1. Pops the four parameters from the stack (callback signature, default value, label, caption).
  2. Looks up the callback name in the applet’s function dictionary to verify it exists.
  3. Calls the platform’s asynchronous dialog service, which shows a native OS dialog without blocking.
  4. 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:

  1. The VM saves its complete state — instruction pointer (PRG_IP), stack pointer (STKP), and base pointer (BASEP).
  2. A clean, isolated stack environment is created (pointers reset to 0) so the callback cannot corrupt the main applet’s stack.
  3. The user’s input is pushed as the function parameter.
  4. The callback function body executes in this sandboxed context.
  5. The VM state is fully restored after the callback completes, guaranteed by a try/finally block 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:

╯ plan9basic
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:

╯ chained_inputs.bas
' 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
💡 Pattern

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:

╯ input_loop.bas
' 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:

╯ ❌ WRONG
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
╯ ✅ CORRECT
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:

╯ calculator.bas
' 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 BASICPlan9Basic
BehaviorBlocks execution until user typesShows dialog and returns immediately
SyntaxINPUT "Prompt: "; x$INPUT "Title", "Prompt", "", cb$
ResultStored in the variable directlyPassed as parameter to callback function
DialogText cursor in the consoleNative OS dialog box
SequentialWrite multiple INPUT linesChain callbacks (next INPUT inside previous callback)
LoopsWHILE around INPUTCallback calls INPUT with itself = self-referencing loop
CancelNot possible (must type something)User can cancel; callback is not called
MobileWould freeze/crash the appWorks on all 6 platforms without blocking

INPUT Quick Reference

RuleDetails
String modeDefault is a string → callback must be funcname$(param$), returns string
Numeric modeDefault is a number → callback must be funcname(param), returns number
ExecutionNon-blocking. Code after INPUT runs before the user interacts.
CancelCallback is not called. No error is raised.
Invalid numeric inputSilently converts to 0.
ChainingCall the next INPUT inside the current callback.
LoopingA callback can call INPUT with itself as the callback.
Global variablesUse globals (declared with LET outside functions) to share state between callbacks.
Callback isolationEach callback runs in a sandboxed VM context with its own stack. Cannot corrupt the main applet.
Function not foundIf the callback name doesn’t exist, a runtime error is raised: “There is no function with such arguments.”

💾 DATA, READ, and RESTORE

╯ plan9basic
' 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"
💡 Note

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

╯ plan9basic
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

╯ plan9basic
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

CommandDescription
TRACEONEnable debugging (level 1)
TRACEOFFDisable debugging
TRACE 0/1/2/3Set trace level
ASSERT cond, msg$Stop if condition is false
DUMP "label"Show all global variables
WATCH var1, var2Monitor specific variables
UNWATCHStop monitoring
BREAKPOINT "msg", varsPause 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.

╯ plan9basic
' 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

╯ dynamic_dispatch.bas
' 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

╯ plan9basic
' 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

╯ guessing_game.bas
' 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.bas
' 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

CategoryKeywords
ControlIF, THEN, ELSE, ENDIF, END IF, FOR, TO, STEP, NEXT, ENDFOR, WHILE, ENDWHILE, WEND, END WHILE, REPEAT, UNTIL, DO, LOOP, SELECT, CASE, END SELECT
BranchingGOTO, GOSUB, RETURN, ON...GOTO, ON...GOSUB, ON...CALL, BREAK, CONTINUE, END
FunctionsFUNCTION, ENDFUNCTION, END FUNCTION, LOCAL
I/OPRINT, PRINTLN, INPUT, CLS
DataDATA, READ, RESTORE, LET
OperatorsAND, OR, NOT, MOD
DebuggingTRACE, TRACEON, TRACEOFF, ASSERT, DUMP, WATCH, UNWATCH, BREAKPOINT
CommentsREM, '

Operators Quick Reference

TypeOperators
Arithmetic+, -, *, /, MOD, ^, ?>, ?<
Comparison=, <>, <, >, <=, >=
LogicalAND, OR, NOT
String+ (concat), / (with newline), - (truncate)

Type Suffixes

SuffixTypeExample
(none)Numberx, count
$Stringname$, text$
#Pointerarr#, obj#

🏆 Tips & Best Practices

  1. Use meaningful variable namesplayerScore is better than x
  2. Comment your code — explain complex logic for future reference
  3. Use functions — break complex applets into smaller, reusable pieces
  4. Initialize variables — set initial values before using variables
  5. Avoid GOTO when possible — use structured loops and functions instead
  6. Test incrementally — build and test your applet in small steps
  7. Handle edge cases — check for division by zero, empty strings, etc.
  8. Comparisons only in conditionals — they only work inside IF, WHILE, UNTIL, etc.
  9. Use intermediate pointers for nested arrays — chained access like arr##[1][2] is not supported
  10. Use TRACE for debugging — enable to find problems, disable for production
  11. Use ASSERT liberally — validate assumptions at the start of functions
  12. Remember case insensitivityCount and COUNT are the same variable
  13. NEXT can include the control variableNEXT i helps document which loop is ending

🚨 Error Codes

When using library functions, errors are typically indicated by return values:

CodeMeaning
0No error (success)
1Index out of bounds
2Invalid argument
3Empty string
4File error
5Clipboard error

Check specific library documentation for additional error codes.