Frothy in Y Minutes

The complete Frothy language in one runnable, example-first page: values, words, state, control flow, records, errors, events, persistence, and the prompt.


Frothy is a small live language for programmable devices. You send it source, the device installs that source into its current image, and you can inspect, replace, save, and run the result without rebuilding firmware.

This is the whole language, from the first expression to the edges of the current ESP32 profile. Read it top to bottom once, then use the word catalog when you only need a signature.

The Thirty-Second Version

counter is 0

to blink-and-count with pin, wait [
  gpio.high: pin
  ms: wait
  gpio.low: pin
  set counter to counter + 1
  counter
]

blink-and-count: $led_builtin, 75
save

That sample contains most of the model:

  • counter is 0 binds a top-level name to a value.
  • to ... [ ... ] defines a word. Other languages usually say “function.”
  • with pin, wait declares parameters.
  • gpio.high: pin calls a word with :.
  • Newlines separate expressions inside a block.
  • The last expression is the block’s result.
  • set changes an existing place.
  • save persists the user image.

Comments

A line comment begins with two hyphens and ends at the newline.

led.on: -- the rest of this line is ignored

A block comment begins with -* and ends with *-.

-*
This can span
several lines.
*-
led.off:

Comments are recognized after whitespace or punctuation that can precede a comment. Hyphens may also be part of names, so this remains one name:

read-next-byte

Integers And Arithmetic

Frothy has integers, not floating-point numbers. Decimal, hexadecimal, and binary literals are accepted.

42
-7
0x2a
0b101010

On the current 32-bit ESP32 profile, integers range from -1073741824 through 1073741823. An out-of-range literal or arithmetic result is an error; values do not silently wrap.

The arithmetic operators are +, -, *, /, and %.

2 * 3 + 4        -- 10
(2 + 3) * 4      -- 20
37 % 10          -- 7

*, /, and % bind more tightly than + and -. Operators at the same precedence associate from left to right. Division by zero is an error.

% and the word mod use C-style truncating remainder semantics. The math module also provides abs, min, max, clamp, map, and integer sqrt.

Comparisons And Boolean Logic

Integer comparisons use <, >, <=, >=, =, and <>.

2 < 3            -- true
10 >= 10         -- true
4 <> 5           -- true

not, and, and or use Frothy truthiness and produce Bool results. and and or short-circuit, so the right side only runs when needed.

not false                       -- true
2 > 1 and 3 < 4                 -- true
ready or attempt-connect:       -- skips the call when ready is truthy

Precedence, from tightest to loosest, is:

  1. not
  2. *, /, %
  3. +, -
  4. <, >, <=, >=, =, <>
  5. and
  6. or

Use parentheses when the grouping is not immediately obvious.

Values

The current ESP32 profile exposes these value families:

ValueExampleLifetime
Nilnilpersistable
Booltrue, falsepersistable
Int42, 0xffpersistable
Text"ready"persistable
Codefn [ 1 + 2 ]persistable
Cellscells(4)persistable mutable storage
record shape/valuerecord Point [ x, y ]persistable
Bytesbytes.from-text: "AT"transient
Handlei2c.open: ...transient

Only nil, false, and integer 0 are falsy. Every other value is truthy, including empty text.

if 5 [ "yes" ] else [ "no" ]       -- "yes"
if 0 [ "yes" ] else [ "no" ]       -- "no"
if nil [ "yes" ] else [ "no" ]     -- "no"

0x10 is the integer 16; it is not a byte buffer. Create Bytes explicitly with a bytes.* word or an I/O operation that returns bytes.

Square brackets delimit bodies used by language forms. A standalone [ 1 + 2 ] is not a Code literal; use fn [ 1 + 2 ].

Text

Text is immutable and byte-oriented. A literal is enclosed in double quotes.

greeting is "hello"
print: greeting
text.length: greeting       -- 5
text.at: greeting, 1        -- 101, the byte for e

The supported escapes are:

EscapeMeaning
\nnewline
\rcarriage return
\ttab
\"double quote
\\backslash
\xNNone byte written as two hexadecimal digits
print: "first\nsecond\n"
packet is "\x02\x01\x06"

Text cannot contain a literal source newline. Use \n. The current ESP32 profile allows an individual text value up to 4096 bytes and an 8192-byte text pool across the live image.

Use Text, Bytes & PAD for conversion and transient-buffer rules.

Names

Names are intentionally permissive. Dots, hyphens, question marks, underscores, and exclamation marks are ordinary name characters.

sensor.ready?
read-next-byte
all_my.friends!

Whitespace and syntax punctuation end a name. A name cannot contain (, ), [, ], ,, :, ;, +, *, /, %, comparison operators, or ->. A hyphen stays inside a name unless it is clearly subtraction or the start of a negative integer.

x - y       -- subtraction; spaces make the intent clear
x-y         -- one name

Names are case-sensitive. The current profile permits up to 48 bytes per name. Language keywords cannot be used in positions where they would be ambiguous.

Top-Level Bindings

Bind a top-level name with is.

count is 10
message is "ready"

Running another is definition for the same name rebinds its stable slot.

count is 20

Use set when the intent is mutation rather than replacement of a definition.

set count to count + 1

set only changes an existing name or place. It does not create one.

A top-level is definition accepts literals, an existing name, fn, cells, a record value, or the result of a word call. An operator or control-flow expression is evaluated with set after the place exists:

total is 0
set total to 20 + 22

This boundary keeps image installation explicit. A call result may be bound when its value kind is slot-safe; Bytes is the exception and must be packed to Text first.

Words And Calls

Frothy calls functions “words.” Define a word with to and call it with a colon.

to add-three [ 1 + 2 ]
add-three:                       -- 3

to add with a, b [ a + b ]
add: 5, 9                        -- 14

A bare name reads its value. It does not call it.

add          -- displays a Code value at the prompt
add: 5, 9    -- runs it

The last expression in a word body is its result.

to describe with n [
  print: "value: "
  n
]

Arguments are comma-separated. Parenthesize a nested call when it makes the argument boundary explicit.

print: (text.from-int: 42)
total is add: 1, (add: 2, 3)

Calls are checked against the word’s arity. Too few or too many arguments are errors.

Words may call themselves recursively. The ESP32 profile currently allows 24 nested calls.

to fib with n [
  if n < 2 [ n ] else [ fib: n - 1 + fib: n - 2 ]
]

Code Values

fn creates a Code value. to name ... is the convenient top-level spelling for binding that code to a name.

double is fn with n [ n * 2 ]

to double with n [ n * 2 ]

Code can be rebound to another top-level name, and either name can call the same code object.

double is fn with n [ n * 2 ]
also-double is double
also-double: 21                 -- 42

Code is non-capturing. It can read its own parameters, locals declared in its own body, and top-level names. It cannot capture a local from another word.

wait is 50

-- This word can read the top-level wait.
to blink-once with pin [
  gpio.high: pin
  ms: wait
  gpio.low: pin
]

When a word needs a value that is not top-level, pass that value as an argument. fn values themselves are currently installed from top-level definitions; Frothy does not build closures inside a running word. Calls also resolve a top-level word name: a Code value held only in a parameter, local, Cell, or record field is not dynamically callable, and there is no separate call word.

Parameters And Locals

Parameters are immutable. Declare a local with here, then mutate that local with set.

to countdown with start [
  here n is start
  while n > 0 [
    set n to n - 1
  ]
  n
]

Locals use lexical block scope. Lookup chooses the nearest local, then an outer local, then a parameter, then a top-level name.

speed is 100

to demo [
  here speed is 20
  if true [
    here speed is 5
    speed                 -- 5
  ]
  speed                   -- 20
]

The local declared inside the if block stops existing at its closing ]. The current profile allows 8 parameters and 16 locals in one parsed definition.

Blocks And Expression Separation

A body is enclosed in [ ... ]. It must contain at least one expression. Newlines separate expressions naturally.

to pulse [
  led.on:
  ms: 50
  led.off:
]

A semicolon separates expressions on the same line.

to pulse [ led.on:; ms: 50; led.off: ]

A trailing semicolon is allowed. In normal source, prefer newlines and use semicolons only when a compact one-line form is clearer.

Conditional Expressions

if chooses a block and returns that block’s value.

if temperature > 30 [ "hot" ] else [ "comfortable" ]

else if can be repeated.

if percent > 80 [
  "high"
] else if percent > 50 [
  "medium"
] else [
  "low"
]

Without else, a false if yields nil. when is the one-sided positive form; unless is the one-sided negative form.

when wifi.ready?: [ print: "online\n" ]
unless wifi.ready?: [ print: "offline\n" ]

Loops

while checks its condition before each pass.

while count > 0 [
  set count to count - 1
]

repeat evaluates a count and runs a block that many times. A negative count is an error. Add as name for a zero-based loop index.

repeat 3 [ print: "tick\n" ]

repeat 3 as i [
  print: (text.from-int: i)
  print: "\n"
]

The index is a local scoped to the repeat body.

forever runs until the body errors or the evaluation is interrupted.

forever [
  led.toggle:
  ms: 100
]

while, repeat, and forever yield nil when they finish normally.

Cells

Cells are fixed-size mutable indexed storage. Create them at the top level with a positive literal length.

readings is cells(3)

New cells begin as nil. Indexes are zero-based.

set readings[0] to 11
set readings[1] to 22
readings[0] + readings[1]       -- 33

An out-of-bounds index is an error. The current ESP32 profile allows up to 32 elements in one Cells value and 128 cell elements across the live image.

Cells and their persistable contents are included by save. Transient Bytes and Handle values are rejected as cell contents.

Records

Records give names to small fixed shapes. Declare a shape at the top level.

record Point [ x, y ]

The declaration creates a constructor word with the same name. Construct a record at the top level by calling it.

point is Point: 3, 9

Read and write fields with ->.

point -> x                       -- 3
set point -> y to 12

Field names cannot contain dots. The current ESP32 profile permits 4 fields per shape. Record construction inside a word body is not currently supported; construct records at the top level and pass them into words.

Record shapes and values are persistable. Transient Bytes and Handle values cannot be stored in record fields.

Errors And Rescue

An error returns control to the prompt instead of wedging the device. A diagnostic names the error and shows its numeric code. Reader and parser diagnostics can also point at the offending source span.

error: bad value: 0 (3)

Type, domain, and range failures include the value that was rejected. Extra lines may identify the expected kind, native argument position, source span, or valid bound. Values are escaped and bounded for the transcript, and secret arguments are redacted. See Error and notice codes for the full wire shape and recovery table.

attempt [ ... ] rescue [ ... ] catches runtime errors.

attempt [ 10 / divisor ] rescue [ 0 ]

If the attempt succeeds, the entire expression yields its value. If it fails, Frothy restores the value stack to the beginning of the attempt, runs the rescue block, and yields the rescue value.

Inside the rescue block, error.code and error.name describe the caught error.

to safe-divide with a, b [
  attempt [ a / b ] rescue [
    print: error.name
    error.code
  ]
]

Errors raised by called words can be caught by the caller’s attempt. Parse and compile errors happen before execution and cannot be caught. An interrupt is also never catchable.

Events

Event forms register code that the device runs at safe points. Registration belongs inside a word body, not as a bare prompt expression.

Register a repeating or one-shot timer:

to start-ticking [
  every 1000 [ print: "tick\n" ]
]

to stop-later [
  after 5000 [ led.off: ]
]

Register a GPIO edge handler:

to watch-button [
  on $boot_button falling debounce 25 [
    led.toggle:
  ]
]

The edge is rising, falling, or changes. debounce <ms> is optional.

Wi-Fi exposes two event sources:

to watch-network-down [
  on wifi.disconnected [ led.off: ]
]

to watch-network-up [
  on wifi.reconnected [ led.on: ]
]

The current profile permits one event body per definition. Use one small registration word for each event source, then call those words during setup.

Cancel a registration with the same source identity:

cancel every 1000
cancel after 5000
cancel $boot_button
cancel wifi.disconnected

For GPIO, cancellation uses the pin regardless of edge or debounce settings. For timers, every and after are distinct and the millisecond value must match. Event output appears as asynchronous ! lines at the prompt.

See the events module for lifecycle and capacity details.

Bytes And Handles

Bytes and Handle are real Frothy values, but they represent live working state rather than saved project data.

Bytes is a transient byte buffer returned by words such as i2c.read, tcp.read, http.get, and BLE reads.

bytes.length: (http.get: "http://example.com/")

Bytes cannot be installed in top-level names, Cells, or record fields. A here local can hold them during one word call. Copy bytes into persistent Text when the content must survive the current evaluation or loop iteration:

reply-text is text.pack: (http.get: "http://example.com/")

A Handle refers to an open platform resource such as I2C, UART, PWM, TCP, trace, pulse, or BLE state.

bus is i2c.open: 0, $sda, $scl, 400000
i2c.close: bus
set bus to nil

Handles are volatile. Close them and remove them from top-level project slots before save. Reopen required resources from boot after restore.

The Image, Save, And Boot

Frothy starts with a firmware-owned base image and adds your top-level bindings to an overlay image.

threshold is 500
to alarm [ when adc.read: $a0 > threshold [ led.on: ] ]

save writes the overlay to persistent storage. restore replaces the live overlay with that saved overlay. clear removes the live overlay but leaves the saved copy available. dangerous.wipe removes both.

save
clear
restore

If the top-level boot slot holds Code after restore, Frothy runs it before returning to the prompt.

boot is fn [
  led.off:
  wifi.connect:
]
save

Safe boot lets a human interrupt restore/startup and recover a bad saved program. The image and persistence reference covers the full lifecycle.

The Prompt Is Part Of The Language Experience

The prompt evaluates expressions and installs definitions. It also provides inspection commands without colons.

words
see blink
status
events
mem
  • words lists visible names.
  • see name reconstructs a definition or describes a value.
  • status reports the current session and runtime profile.
  • events lists active event bindings.
  • mem reports heap, slots, objects, and event capacity. Use mem heap, mem slots, mem objects, or mem events for one group.
  • clear removes the live overlay and returns to the base image.

Host tooling also uses apply HEX, run HEX, install-library, install-user, and wipe-user. They are prompt protocol or installation-tier commands rather than source-language forms; their exact contracts are in the word catalog .

Press Ctrl-C to abandon incomplete multiline input or interrupt running code. Frothy checks interrupts at safe points and returns the prompt to a usable state.

Source Files And Includes

A .fr file contains the same top-level definitions and expressions you type at the prompt.

to main [
  led.blink: 3, 75
]

main:

include "helper.fr" is a host-side frothy tool directive. The CLI resolves it before sending source to the device. It is not device language syntax and does not create a runtime word.

What To Open Next

  • Word catalog — every current form, command, constant, helper, and ESP32 word in a searchable list.
  • Modules — broad usage guides for GPIO, Wi-Fi, I2C, text, signals, BLE, and the rest of the built-in surface.
  • Device & sessions — prompt behavior, persistence, diagnostics, recovery, and tooling sessions.
  • Toolchain — projects, source files, CLI commands, builds, flashing, and editor support.