-
Notifications
You must be signed in to change notification settings - Fork 0
Language Guide
This page covers PotScript's syntax and core semantics: lexical structure, values and types, variables, operators, control flow and functions.
# everything after a hash, to the end of the line
There are no block comments.
A statement ends at a newline or a semicolon (;). Newlines are suppressed inside
parentheses and brackets, so calls and list literals may span lines:
let grid = [
1, 2, 3,
4, 5, 6
]
print("a",
"b")
Braces do not suppress newlines — a { } block is a sequence of statements, one per line.
let a = 1; let b = 2 # semicolons work too
Letters (a–z, A–Z), _, and digits after the first character. Case sensitive.
let fn if else while return break continue
and or not true false nil
Builtin function names are not reserved — let print = 3 is legal and shadows the builtin
for the rest of the program.
Decimal only, always 64-bit floating point: 42, 3.14, 0.5. There is no hex, no
exponent notation, and no leading-dot form (.5 is invalid — write 0.5). A trailing dot is
not part of the number (5. lexes as 5 followed by an error).
Double quotes only. Escapes: \n, \t, \", \\. Any other escape is a compile error, and
a raw newline inside a string is an "unterminated string" error.
print("tab\there\nnew line")
+ - * / % ( ) [ ] { } ,
= == != < <= > >=
A bare ! is a compile error: use not for negation and != for inequality.
Six types, reported by type(x):
| Type name | Values |
|---|---|
nil |
nil |
bool |
true, false
|
number |
double-precision float |
string |
UTF-16 text |
list |
mutable, heterogeneous, 0-indexed |
function |
user functions and builtins |
Only nil and false are falsy. 0 and "" are truthy.
if 0 { print("this prints") }
Whole numbers below 1e15 print without a decimal point; everything else prints as a Java double.
print(10 / 4) # 2.5
print(10 / 5) # 2
== and != compare by value. Lists compare element-by-element (deeply). Types never
coerce: 1 == "1" is false.
print([1, [2]] == [1, [2]]) # true
Lists are reference values: assigning one to another variable or passing one to a function does not copy it.
let a = [1, 2]
let b = a
push(b, 3)
print(a) # [1, 2, 3]
The only exception is send/broadcast, which deep-copy the payload so sender and receiver
never share mutable state.
let declares. At the top level it creates a global; inside any { } block it creates a
local.
let count = 0 # global
let empty # declared, initialised to nil
{
let count = 99 # a distinct local, shadows the global
print(count) # 99
}
print(count) # 0
Rules:
- Assigning to a name that was never declared is a runtime error
(
undefined variable 'x') — alwaysletfirst. - Reading an undeclared name is the same error.
- Declaring the same local twice in one scope is a compile error. Re-
leting a global is allowed and simply overwrites it. - Locals are limited to 256 per function.
Assignment is an expression that yields the assigned value, so (x = 5) and
print(x = 5) both work — though the plain statement form is what you normally want.
| Operator | Operands | Notes |
|---|---|---|
+ |
number+number | addition |
+ |
anything with a string | concatenation — the other side is stringified |
+ |
list+list | returns a new joined list |
- * / %
|
numbers only |
/ and % by zero are runtime errors |
-x |
number | negation |
print("count: " + 3) # "count: 3"
print([1] + [2]) # [1, 2]
print(7 % 3) # 1
print(-7 % 3) # -1 (sign follows the left operand)
Adding two values that are neither of the above (e.g. a bool and a number) is a runtime error.
<, <=, >, >= accept numbers only — comparing strings or bools raises
expected a number, got .... There is no lexicographic string comparison.
Comparisons do not chain usefully: 1 < 2 < 3 parses as (1 < 2) < 3 and then fails at
runtime because true is not a number.
and, or short-circuit and return one of their operands, not a coerced bool:
let name = load("name") or "unnamed" # nil-coalescing idiom
let ok = has_msg() and recv() # only receives if a message is waiting
not x always returns a bool.
x[i] reads from a list or a string; x[i] = v writes into a list only.
- Indices must be numbers; fractional indices are floored.
- Negative indices count from the end:
l[-1]is the last element. - Out-of-range indices are runtime errors — there is no silent
nil. - Indexing a string yields a one-character string.
let word = "pot"
print(word[0]) # "p"
print(word[-1]) # "t"
Lowest to highest:
= (assignment)
or
and
== !=
< <= > >=
+ -
* / %
-x not x (unary)
f(...) x[...] (call, index)
All binary operators are left-associative. Parentheses group as usual.
if light() > 10 {
print("day")
} else if light() > 4 {
print("dusk")
} else {
print("night")
}
Braces are mandatory — there are no brace-less bodies. The else may sit on its own line
after the closing brace.
let i = 0
while i < 10 {
if i == 3 { i = i + 1; continue }
if i == 8 { break }
print(i)
i = i + 1
}
while is the only loop. break and continue are compile errors outside a loop; both
correctly discard the locals of the scopes they exit.
There is no for loop — iterate with while, or with range:
let xs = range(5) # [0, 1, 2, 3, 4]
let i = 0
while i < len(xs) {
print(xs[i])
i = i + 1
}
A bare { } is a statement and introduces a scope:
{
let temp = expensive()
print(temp)
} # temp is gone here
fn add(a, b) {
return a + b
}
fn fib(n) {
if n < 2 { return n }
return fib(n - 1) + fib(n - 2)
}
print(add(2, 3)) # 5
Rules and limits:
-
Top level only. Declaring
fninside a block, a loop or another function is a compile error (functions must be declared at top level). -
No closures. A function body sees its own parameters and locals, plus globals. It cannot capture a local from an enclosing scope.
-
Arity is exact. Calling with the wrong number of arguments is a runtime error. Maximum 16 parameters and 16 arguments.
-
A function that falls off the end, or uses bare
return, returnsnil. -
Recursion works, up to 64 call frames deep (
call stack overflowpast that). -
Functions are ordinary values — store them in variables and lists and call them later:
let ops = [add, fib] print(ops[0](1, 2)) -
A
fndeclaration takes effect when execution reaches it. Since top-level code runs top to bottom, mutual recursion is fine as long as both functions are declared before the first call executes.
See also: Execution Model · Standard Library · Wiki Home