Overview

Cob is a corn-themed hybrid language with Python-style, indentation-and-colon block structure. A program is a sequence of statements; blocks (currently only while — there is no if/else) are opened with a trailing : and their body is whatever's indented further than the header line.

set x = 5
while x > 0:
    pop("counting down")
    set x = x - 1
pop("liftoff")

Indentation is spaces only — tabs are a hard error.

The toolchain

ToolWhat it does
cob_interpInterprets a .cob file directly, with a fast-boot .strawberry bytecode cache. Ships in several builds — see below — that add SQLite, a native window, and/or Tcl/Tk on top of the same core language.
popcorn_compCompiles a .strawberry file to a real, standalone native executable. Only the core language on this page (not the SQLite/_cobwindow/Tcl/Tk extensions below) is currently supported.
farmerThe package manager — installs .cob libraries from a static registry.

Language reference

pop("text")

Writes text to stdout followed by a newline.

pop("hello")          # hello
pop("line one\nline two")

Supported escapes inside the string literal: \\, \", \n, \t.

set <name> = <expr>

Assigns a value to a variable, creating it if it doesn't exist yet. A value is either an integer or a string. Expressions support + - * / with standard precedence (*// bind tighter than +/-) on two integers; + also concatenates when either side is a string, converting the other side to text first.

set a = 2 + 3 * 4              # 14
set b = a - 1
set greeting = "count: " + a  # "count: 14"

while <condition>:

Loops while the condition is true. A bare expression is a truthiness test (nonzero); a comparison uses == != < > <= >=, and works the same way on two strings as it does on two integers. Loops nest to any depth and are capped at 10,000,000 iterations as a safety net against runaway/infinite programs.

while a != 0:
    pop("tick")
    set a = a - 1

shuck <library_name>

Loads <library_name>.cob (checking the current directory first, then cob_modules/<library_name>/<library_name>.cob — the layout farmer harvest installs into) and splices its statements in at that point, like a textual include. Circular imports and excessive nesting depth are rejected with a clear error.

shuck greeter
pop("back in the main program")

harvest(<bytes>) / trash(<variable>)

Low-level manual memory: harvest(n) is an expression that allocates n bytes and returns an opaque integer handle; trash(v) frees the handle held in v. Both require the --no-gc flag — a program that uses either without it is refused outright by both cob_interp and popcorn_comp, not silently ignored.

set p = harvest(64)
trash(p)
Handles are plain integers, not real pointers — Cob code can't do pointer arithmetic with them, only pass them to trash(). Double-free and unknown-handle use are reported as warnings, not crashes.

_MakeCache = False

If the literal first line of a .cob file is _MakeCache = False (whitespace/case-insensitive), the .strawberry fast-boot cache is disabled entirely for that file — neither read nor written.

_MakeCache = False
pop("always reparsed fresh")

Extensions 0.0.5

SQLite — sql_open / sql_exec / sql_query / sql_close

A thin binding onto SQLite's C API, one open database handle per call to sql_open().

CallReturns
sql_open(<path>)handle (int), 0 on failure
sql_exec(<handle>, <sql>)rc (int), 0 on success
sql_query(<handle>, <sql>)string — first column of the first row, "" if none
sql_close(<handle>)0
set h = sql_open("scores.db")
set rc = sql_exec(h, "CREATE TABLE IF NOT EXISTS t (name TEXT)")
set rc = sql_exec(h, "INSERT INTO t VALUES ('cob')")
set name = sql_query(h, "SELECT name FROM t LIMIT 1")
pop("got: " + name)
set rc = sql_close(h)
Builds via SQLite's single-file amalgamation — no configure step, no separate library install.

_cobwindow — a native window, backed by raylib + raygui

A small, real (not simulated) desktop window, opened with shuck cobwindow first.

CallReturns
window_open(<title>)handle (int), 0 on failure
window_label(<h>, <text>)0 — sets/replaces the window's text
window_wait(<h>, <seconds>)0 — keeps the window responsive for ~<seconds>
window_close(<h>)0
window_button(<h>, <label>)1 if clicked since the last read of that label, else 0
window_slider(<h>, <label>, <max>)current value, 0..<max>, as an int
window_textbox(<h>, <label>)current string contents of the box
shuck cobwindow
set h = window_open("Cob Window Demo")
set rc = window_label(h, "Hello from Cob!")
set clicked = window_button(h, "OK")
set volume = window_slider(h, "Volume", 100)
set name = window_textbox(h, "Name")
set rc = window_wait(h, 5)
set rc = window_close(h)

Each widget is identified by its own label text, not a separate handle — the first window_button()/ window_slider()/window_textbox() call with a given label creates that widget, auto-stacked on screen below the window_label() text; every later call with the same label (and kind — a button and a slider can share a label without colliding) reads or updates that same widget. Declaring and reading happen in the same call, since Cob has no separate "declare once, poll every frame" mechanism:

  • window_button()'s click flag resets on read, so a click is reported exactly once.
  • window_slider() re-ranges (and clamps the current value into the new range) if called again with a different <max>.
  • window_textbox() is always in an editable state — Cob has no click-to-focus concept to hook a toggle to.
Only one native window per process — calling window_open() again before closing the first warns and returns 0. The window's own close button ([X]) is intentionally ignored; Cob has no callback mechanism to notify a running script that it was clicked, so the window stays open until window_wait()'s timer runs out or window_close() is called explicitly — the same "explicit close only" model harvest()/ trash() use for memory. Running more than one window_textbox() at a time hasn't been verified for keyboard-focus behavior; treat that as unverified rather than unsupported.

Tcl/Tk — tcl_eval / tk_eval

CallReturns
tcl_eval(<script>)string — Tcl's own string result
tk_eval(<script>)string — same interpreter as tcl_eval, with Tk also loaded
pop("answer=" + tcl_eval("expr {6*7}"))  # answer=42
Only in the separate cob_interp_db build — see below. Needs a real X11 $DISPLAY at runtime for tk_eval(); without one it evaluates to a normal Tcl error string, the same as real Tk would report, not a crash.

Tool reference

cob_interp

cob_interp <file.cob> [--no-cache] [--no-gc]
--no-cacheSkip reading/writing the .strawberry cache for this run.
--no-gcUnlock harvest()/trash().

Four build variants, all built from the same src/cob_interp.c:

BuildAdds on top of the core language above
cob_interpNothing — zero vendor dependency, builds in under a second.
cob_interp_window_cobwindow (raylib + raygui).
cob_interp_fullSQLite + _cobwindow — what a plain make builds by default.
cob_interp_dbSQLite + Tcl/Tk (no _cobwindow).
Calling a sql_*/window_*/tcl_eval/tk_eval keyword in a build that doesn't include it prints a one-line warning and evaluates to a harmless default (0 or "") rather than failing to build or crashing.

popcorn_comp

popcorn_comp <file.strawberry> -o <output> [--no-gc] [--cc <compiler>] [--emit-c <path.c>]

Transpiles the bytecode to plain C, then spawns a real C compiler to produce a native executable. Default backend is Zig's zig cc — a Clang-based drop-in C compiler that bundles libc/CRT files for essentially every target in one install, which is what makes CobOS/CobArch cross-compiling a plain -target flag instead of needing a separately installed cross-toolchain per platform.

Compiler is resolved in this order:

--cc <path>Explicit override, always wins.
$CobCCEnvironment variable override.
$CobOS / $CobArchLooked up in a table of Zig target triples.
zig ccDefault: native compile, Zig auto-detects the host.
CobOS=windows CobArch=amd64 popcorn_comp prog.strawberry -o prog.exe
# -> spawns: zig cc -target x86_64-windows-gnu ...
Linux targets default to musl libc rather than glibc, producing fully static binaries with no runtime libc dependency.
The SQLite/_cobwindow/Tcl/Tk extensions above are cob_interp-only for now — popcorn_comp's codegen doesn't implement any of them yet. A .strawberry cache that uses one of those keywords still loads (the format is shared and stays backward-readable), it just fails cleanly at the specific unsupported call instead of at load time.

farmer

farmer harvest <package>

Fetches {registry}/api/v1/packages/<package>.json, verifies the metadata's name field matches what was requested, downloads the package zip, verifies its sha256 if one was provided, and unzips it into cob_modules/<package>/ — ready for shuck <package>.

$COB_FARMER_BASE_URLOverride the registry base URL.

The registry itself lives at pixel-pulse-labs/cpi, served for free via GitHub Pages — no server, just static JSON files.