Skip to content

Language Guide

Dimitri edited this page Aug 28, 2026 · 3 revisions

Language Guide

This page covers PotScript's syntax and core semantics: lexical structure, values and types, variables, operators, control flow and functions.

Lexical structure

Comments

# everything after a hash, to the end of the line

There are no block comments.

Statement terminators

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

Identifiers

Letters (az, AZ), _, and digits after the first character. Case sensitive.

Keywords

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.

Numbers

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

Strings

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

Operators and punctuation

+  -  *  /  %        ( )  [ ]  { }  ,
=  ==  !=  <  <=  >  >=

A bare ! is a compile error: use not for negation and != for inequality.


Values and types

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

Truthiness

Only nil and false are falsy. 0 and "" are truthy.

if 0 { print("this prints") }

Number printing

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

Equality

== and != compare by value. Lists compare element-by-element (deeply). Types never coerce: 1 == "1" is false.

print([1, [2]] == [1, [2]])   # true

Lists

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.


Variables

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') — always let first.
  • 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.


Operators

Arithmetic

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.

Comparison

<, <=, >, >= 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.

Logic

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.

Indexing

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"

Precedence

Lowest to highest:

=                       (assignment)
or
and
==  !=
<  <=  >  >=
+  -
*  /  %
-x  not x               (unary)
f(...)   x[...]         (call, index)

All binary operators are left-associative. Parentheses group as usual.


Control flow

if / else

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.

while

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
}

Blocks

A bare { } is a statement and introduces a scope:

{
    let temp = expensive()
    print(temp)
}   # temp is gone here

Functions

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 fn inside 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, returns nil.

  • Recursion works, up to 64 call frames deep (call stack overflow past 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 fn declaration 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

Clone this wiki locally