Language Specification

Detailed specification of the Pebble programming language

Comments

Comments help you document your code and make it easier to understand. In Pebble, you can add comments using the single quote character '. Everything after the ' on a line is ignored by the compiler.

' This is a comment
count = 0 ' This is also a comment

Variables and Constants

Variables store data that can change during program execution. In Pebble, all variables are accessible throughout your entire program — there’s no scoping or shadowing. A variable is defined the first time it’s encountered.

Type Inference

Most of the time, Pebble can figure out what type your variable should be:

count = 0        ' Inferred as int
speed = 2.5      ' Inferred as float
active = true    ' Inferred as bool
name = "Alice"    ' Inferred as string

Type Annotations

You can explicitly specify a variable’s type using a colon : followed by the type name:

score: int = 0
position: float = 10.5
data: byte[4] = [1, 2, 3, 4]
data2: byte[] = [1, 2, 3, 4]  ' Size inferred from initializer

Type Stability

Once a variable’s type is set (either through inference or annotation), it can never change type. However, you can mutate the variable’s value:

count = 0      ' count is now an int
count = 5      ' OK: same type
count = 2.5    ' ERROR: can't change type from int to float

Constants

Constants are defined using const and must be initialized with a value. Once defined, constants cannot be changed:

const MAX_SPEED = 100
const PI = 3.14159

MAX_SPEED = 150  ' ERROR: cannot mutate a constant

Identifiers (variable and constant names) can contain letters, numbers, and underscores (a-zA-Z0-9_). By convention, constants are written in SCREAMING_SNAKE_CASE, but any valid identifier is allowed.

Keywords and Identifiers: Keywords (like if, while, type, const) are case-insensitive, so IF, If, and if are all treated the same. However, identifiers (variable names, type names, etc.) are case-sensitive: score, Score, and SCORE are three different variables.

Data Types

Pebble has several built-in data types for different kinds of values.

Primitive Types

  • int - Whole numbers (e.g., 0, 42, -10)
  • float - Decimal numbers (e.g., 3.14, -0.5, 2.0)
  • byte - Small integers 0-255, useful for raw data. (8-bit unsigned)
  • bool - Boolean values: true or false
  • string - Text enclosed in double quotes (e.g., “Hello”)

Numeric Literals: Numbers can be written in decimal (e.g., 42, 3.14) or hexadecimal with 0x prefix (e.g., 0xFF, 0x1A). Hexadecimal literals are useful for byte values and colors.

health: int = 100
temperature: float = 98.6
pixel: byte = 255
gameOver: bool = false
message: string = "Ready!"

Custom Types

Custom types let you group related data together, making your code more organized and easier to understand.

Defining Types

Define a type using type:

type Position
    x: int
    y: int
end

type Player
    pos: Position
    health: int
    score: float
end

Using Custom Types

Create variables of your custom type and access their fields:

player: Player
player.pos.x = 100
player.pos.y = 50
player.health = 100
player.score = 0.0

Custom types can contain other custom types and arrays:

type Game
    player: Player
    enemies: Player[10]
    level: int
end

Enumerations

Enumerations (enums) define a set of named constants, useful for representing states or categories.

Defining Enums

enum Direction
    North
    South
    East
    West
end

enum GameState
    Menu
    Playing
    Paused
    GameOver
end

Using Enums

Access enum values using the double colon :: operator:

currentDirection = Direction::North
state = GameState::Playing

if state == GameState::GameOver then
    print "Game Over!"
end

Arrays

Arrays store multiple values of the same type. All arrays in Pebble have a fixed size that cannot change. Declare them with square brackets:

scores: int[5]                   ' Array of 5 integers
colors: byte[3] = [255, 0, 128]  ' Initialized array
colors: Player[3]                ' Initialized array of Player (custom type)

You can omit the size in the declaration, and it will be inferred from the number of elements in the initializer:

SPRITE_DATA: byte[] = [
    0x00, 0x01, 0x02, 0x03,
    0x04, 0x05, 0x06, 0x07
]  ' Size is 8, inferred from initializer

Access array elements using square brackets with an index (starting from 0). All array elements are automatically initialized to 0 (or false for bool arrays), so it’s safe to read any element:

colors[0] = 255     ' Set first element
value = colors[1]   ' Get second element (returns 0 if not set)

Bounds Checking: Accessing an array index outside its valid range (0 to size-1) causes a runtime error and halts the program. Always ensure your indices are within bounds.

Strings

Strings represent text and are enclosed in double quotes:

name = "Player One"
message = "Hello, world!"

Note: String manipulation functions like substring or concatenation are not yet available. Use string interpolation to combine values.

Expressions and Operators

Expressions combine values and operators to produce new values.

Arithmetic Operators

result = 10 + 5     ' Addition
result = 10 - 5     ' Subtraction
result = 10 * 5     ' Multiplication
result = 10 / 5     ' Division
result = 10 % 3     ' Modulo (remainder)

Compound Assignment

You can combine arithmetic operators with assignment for concise updates:

count = 0
count += 1          ' Same as: count = count + 1
score -= 10         ' Same as: score = score - 10
speed *= 2          ' Same as: speed = speed * 2
lives /= 2          ' Same as: lives = lives / 2

Compound assignments work with member access and array indexing:

player.x += velocity
game.sprites[42].y -= 5
health[index] *= 1.1

Comparison Operators

x = 10
y = 20

x == y      ' Equal to
x != y      ' Not equal to
x < y       ' Less than
x > y       ' Greater than
x <= y      ' Less than or equal to
x >= y      ' Greater than or equal to

Logical Operators

active = true
ready = false

active and ready    ' Logical AND
active or ready     ' Logical OR
not active          ' Logical NOT

Member Access

Use the dot . to access fields in custom types:

player.x = 10
player.position.y = 20

Control Flow

Control flow statements let you make decisions and repeat actions.

Conditional Statements

The if statement executes code when a condition is true. It comes in two forms:

Single-line if: Use then for a single statement on the same line:

if health <= 0 then gameOver = true
if score > 100 then bonus = 50

Block if: For multiple statements, use if/end (optionally with else):

if score > highScore then
    highScore = score
    print "New high score!"
end

if health <= 0 then
    gameOver = true
    print "Game Over!"
else
    print "Keep playing!"
end

While Loops

The while loop repeats code as long as a condition is true:

count = 0
while count < 10
    print "Count: {count}"
    count = count + 1
end

You can create an infinite loop with while true:

while true
    ' Game loop runs forever
    update()
    render()
end

For Loops

The for loop iterates over a range of numbers. The range is inclusive, meaning both the start and end values are included in the iteration. The default step is 1:

for i = 0 to 10
    print "i: {i}"  ' Prints 0, 1, 2, ... 10 (11 times)
end

for i = 10 to 0 step -1
    print "i: {i}"  ' Counts down: 10, 9, ... 0
end

for i = 0 to 20 step 2
    print "i: {i}"  ' Only even numbers: 0, 2, 4, ... 20
end

Subroutines

Subroutines let you organize your code into reusable sections. Use gosub to jump to a label and return to come back.

Labels and Gosub

Define a label by putting a name followed by a colon. Jump to it with gosub:

gosub initialize
gosub update
gosub render

initialize:
    score = 0
    level = 1
return

update:
    score = score + 1
return

render:
    print "Score: {score}"
return

The program will execute the subroutine and return to the line after the gosub call.

String Interpolation

String interpolation lets you embed expressions directly in strings using curly braces {}:

name = "Alice"
score = 1000

print "Player: {name}"
print "Score: {score}"
print "Total: {score * 2}"

You can interpolate variables, expressions, and even member access:

print "Position: ({player.x}, {player.y})"
print "Active: {gamepad.a}"

Select Statements

The select statement (also known as switch/case in other languages) lets you match a value against multiple options. It works with integers and enums (and may support strings in the future).

Each case can have either a single statement on the same line, or multiple statements as a block on following lines.

Execution Rules:

  • The first matching case is executed, then control exits the select (no fallthrough)
  • Only one case ever executes per select statement
  • The else clause is required unless all possible values are covered

Select with Integers

level = 2

select level
    1:   print "Easy mode"
    2:   print "Normal mode"
    3:   print "Hard mode"
    else: print "Unknown level"
end

Select with Enums

When matching enum values, use the variant name directly without the enum type prefix:

enum Direction
    North
    South
    East
    West
end

currentDir = Direction::North

select currentDir
    North: y = y - 1
    South: y = y + 1
    East:
        x = x + 1
        y = 2
    West:
        x = x - 1
end

With statements

If you want to operate on the same custom type multiple times, you can use a with block.

Inside a with block, field access to the selected value is written using the . prefix. All normal statements are allowed inside the block; with only affects how field names are resolved.

with game.sprites[2]
    .pos.x = 3
    print "hello"
    .color = Color::Green
end