Unfish is packaged with zero external dependencies. Everything you need โ compiler, virtual machines, interactive debugger, formatter, and language server โ is bundled in a single self-contained executable.
โก One-Line Universal Install
Paste this command into your terminal to install the latest prebuilt Unfish binary to ~/.unfish/bin:
Rich syntax highlighting, bracket matching, indentation rules, code formatting, and Language Server Protocol (LSP) client support for VS Code & VSCodium.
The standalone single-header C99 runtime with open-addressed hash tables, garbage-collected value types, and memory arenas. Embed Unfish AOT compiled code into any C project.
Pure ANSI C99 source code with zero dependencies. Includes 21 unit test suites, stress suites, 104 differential conformance tests, and 13 benchmarks. Builds with make in 2 seconds.
Once installed, the unified unfish binary provides commands across the entire software development lifecycle:
Command
Description
unfish run <file>
Execute script via tree-walking AST interpreter
unfish run --vm <file>
Execute script via 59-opcode Stack Virtual Machine (2xโ6x faster)
unfish run --regvm <file>
Execute script via 256-register computed-goto VM
unfish build [-o bin] <file>
Compile script directly into native standalone C99 binary (25โ60x speedup)
unfish build --wasm <file>
Compile script into WebAssembly binary module
unfish format -i <file>
Format source code to canonical Unfish style in place
unfish debug <file>
Launch interactive step debugger with breakpoints and stack traces
unfish check [--strict] <file>
Validate syntax and gradual type annotations
unfish lsp
Start Language Server Protocol (JSON-RPC 2.0)
unfish repl
Start interactive read-eval-print loop
unfish playground [--port <p>]
Launch local web playground server
unfish studio [--port <p>]
Launch full local browser Studio IDE
๐ ๏ธ Installation & Build Guide
Unfish requires only a standard C99 compiler (such as gcc or clang) and standard POSIX make. It has zero external dependencies beyond libc and -lm.
Building from Source
Terminal Shell (Bash)
git clone https://github.com/ashokwebs/unfish.git
cd unfish
make -j4
./bin/unfish version
Running the Test Suites
Verification Commands
# Run all 21 unit test suites and the 104-test differential conformance suite
make test
# Verify zero memory leaks under AddressSanitizer & UndefinedBehaviorSanitizer
make test-asan
# Run multi-tier performance benchmarks
make bench
๐ก What is Unfish?
Unfish is a real, general-purpose programming language, runtime, compiler, and educational computing environment written in clean ANSI C99 with zero external dependencies.
It is designed to provide an unbroken path of intellectual ascent: from visual block programming, into clean indentation-based text programming, to algorithms, data structures, runtime architecture, garbage collection, bytecode virtual machines, and low-level systems programming.
"Hide complexity until the learner is ready โ never hide it forever."
Five Execution Engines in Lockstep
Unfish implements five distinct execution backends, kept in lockstep by a differential test suite that requires byte-identical output from every one of them:
Tree-Walking AST Interpreter: Immediate startup, direct AST evaluation, ideal for REPL and visual block editing.
Bytecode Virtual Machine (--vm): 59-opcode stack VM with lexical upvalue capture cells, exception unwinding, and 2xโ6x speedup over AST tree-walking.
Register-Based Bytecode VM (--regvm): 256-register, 3-address VM with computed-goto dispatch.
Native C99 AOT Compiler (build): Direct transpilation into standalone ANSI C99 with single-header runtime, measured at 25โ60x faster than AST.
WebAssembly Target (build --wasm): Portable WASM binary running in browsers and edge runtimes.
๐ Your First Program
Unfish source code uses the .unfish or .uf extension. Here is a program demonstrating string interpolation, structs, and list transformations:
first_program.unfish
๐ป REPL & Developer Toolchain
The unfish executable provides a unified interface across the development lifecycle:
Command
Purpose
unfish run <file>
Execute script using tree-walking AST interpreter
unfish run --vm <file>
Execute script using Stack Bytecode VM
unfish run --regvm <file>
Execute script using Register Bytecode VM
unfish build -o <bin> <file>
Compile program to native executable binary
unfish build --wasm <file>
Compile program to WebAssembly module
unfish format -i <file>
Format source code to canonical style in place
unfish debug <file>
Launch interactive step debugger with breakpoints
unfish studio
Launch the browser IDE & visual blocks canvas
unfish lsp
Start Language Server Protocol server for VS Code
unfish repl
Launch interactive Read-Eval-Print Loop
๐ฌ Learn the Computer through Unfish
A defining mission of Unfish is to teach computer systems by making every layer of language execution visible and tangible. Type any code below and click through the tabs to observe how human-readable text transforms into tokens, syntax trees, bytecode instructions, and runtime memory.
Interactive Architectural Pipeline Explorer
01. Hello World & Text Output
In Unfish, printing to standard output is done with say (appends a newline) or the print(value) function (no trailing newline). Unfish also features Python-style string interpolation via f"...".
tut01_output.uf
02. Values & Primitive Types
Unfish runtime values are represented internally by a 16-byte tagged union (UfValue). The primitive types are:
Functions are declared with function or fn and can include default parameter values:
tut07_functions.uf
08. Higher-Order Functions
Functions can be passed as arguments or returned from other functions. Unfish provides map, filter, reduce, sort, and zip out of the box:
tut08_higher_order.uf
09. Closures & Functional Pipelines
Functions in Unfish capture variables from their enclosing lexical environments. The pipe operator (|>) allows chaining function calls cleanly without nested parentheses.
tut09_closures.uf
10. Arrays, Maps & Byte Buffers
Unfish features dynamic lists, hash maps with string keys, and contiguous binary byte buffers for low-level systems programming:
tut10_collections.uf
11. Destructuring & Rest Patterns
Unpack values from arrays or maps into named variables using let [a, b, ...rest] = ...:
tut11_destructuring.uf
12. Structs & Object Methods
Unfish provides lightweight structs with instance fields and methods bound to self:
tut12_structs.uf
13. Enums & Pattern Matching
Enums in Unfish are tagged sum types whose variants can carry data, matched with match ... when ...:
tut13_enums.uf
14. Error Recovery (try / catch / finally)
Robust exception handling allows recovering from runtime errors or user-thrown exceptions with structured try ... catch err: ... finally: ...:
tut14_errors.uf
15. Memory Model & Garbage Collector
The Unfish runtime is designed for predictable low latency and tiny memory footprints:
Tagged Unions (16 bytes): All stack values fit inside UfValue containing a 64-bit payload and an 8-bit type tag.
Arena Allocators: Parsing AST nodes and compilation bytecode chunks use fast linear bump arenas freed in O(1) time.
Mark-Sweep GC: Objects on the heap (strings, arrays, maps, instances, fibers) are managed by an exact tri-color mark-sweep garbage collector.
16. VM & Native Compilation
Unfish lets you choose your execution strategy depending on performance requirements:
Concurrency in Unfish is based on lightweight green threads (fibers) and Communicating Sequential Processes (CSP) channels, operating with deterministic scheduling and zero race conditions.
tut17_concurrency.uf
18. Traits & Polymorphic Interfaces
Traits define abstract behavioral contracts that structs implement to enable polymorphic dispatch. Traits allow you to write generic algorithms that operate on any type providing required methods.
tut18_traits.uf
19. Pipelines & List Comprehensions
Unfish features point-free pipeline chaining via the |> operator and concise declarative transformations using list and map comprehensions.
tut19_pipelines.uf
20. Systems Programming: Byte Buffers & Bitwise Math
Unfish combines high-level syntax with direct low-level memory buffers (buffer) and 64-bit bitwise operators for wire-level networking and binary protocol framing.
tut20_systems_buffer.uf
21. Asynchronous Coroutines (Async / Await)
Asynchronous programming in Unfish is powered by async fn and await, desugaring to cooperative coroutine fibers and channel-based rendezvous under the centralized runtime scheduler.
tut21_async_await.uf
22. Developer Tooling, Debugger & Language Server
Unfish is equipped with a rich, self-contained developer toolchain built directly into the unified unfish executable:
๐ ๏ธ Language Server (LSP)
Implements LSP 3.17 over standard I/O for VS Code, Neovim, and Sublime with diagnostics, hover, completions, and formatting.
unfish lsp
๐ Interactive Debugger
Step-by-step interactive CLI debugger supporting breakpoints, frame inspection, local variable inspection, and bytecode stepping.
unfish debug app.uf
โจ Code Formatter
Canonical, deterministic indentation and syntax formatter with in-place rewriting.
unfish format -i app.uf
๐งช Conformance Test Runner
High-speed test runner executing test suites with ANSI progress and microsecond execution metrics.
unfish test tests/
๐ฆ Package Manager
Dependency management, project scaffolding, and semantic version validation.
unfish pkg init my_app
โก Native AOT Transpiler
Transpiles Unfish source into standalone, dependency-free ANSI C99 or bare-metal ARM firmware.
unfish emit-c -o app.c app.uf
Core Built-in Functions
All core functions are available globally in every Unfish program without needing an import statement.
Function
Signature
Description
say(val)
(Any) -> null
Prints value followed by a newline to standard output
print(val)
(Any) -> null
Prints value without a trailing newline
len(coll)
(Any) -> Number
Returns length of string, array, or byte buffer
type_of(val)
(Any) -> String
Returns type name: "number", "string", "channel", etc.
push(arr, val)
(Array, Any) -> null
Appends value to dynamic array
pop(arr)
(Array) -> Any
Removes and returns last element
range(a, [b, step])
(Number, ...) -> Array
Generates range of numbers
concat(a, b)
(Array, Array) -> Array
Non-destructively merges two arrays
flatten(arr)
(Array) -> Array
Flattens one level of nested arrays
zip(a, b)
(Array, Array) -> Array
Combines two arrays element-wise into pairs
pad_start(s, len, [p])
(String, Number, [String]) -> String
Pads string on the left to target length
pad_end(s, len, [p])
(String, Number, [String]) -> String
Pads string on the right to target length
trim(s)
(String) -> String
Removes leading and trailing whitespace
trim_start(s)
(String) -> String
Removes leading whitespace
trim_end(s)
(String) -> String
Removes trailing whitespace
chars(s)
(String) -> Array
Returns array of single-character strings
count(s, sub)
(String, String) -> Number
Count of non-overlapping occurrences
clock()
() -> Number
High-resolution wall-clock timestamp in seconds
sqrt(x)
(Number) -> Number
Square root of non-negative number
pow(base, exp)
(Number, Number) -> Number
Exponentiation calculation
round(x)
(Number) -> Number
Rounds number to nearest integer
assert(cond, [msg])
(Any, [String]) -> null
Raises AssertionError if condition is falsy
Module: sys
Access operating system parameters, environment variables, and process control. Import with import sys or from sys import platform, exit.
Function
Signature
Description
sys.platform()
() -> String
Returns host OS name: "linux", "darwin", "windows"
sys.exit(code)
(Number) -> never
Terminates process with integer exit code
sys.args()
() -> Array
Array of CLI argument strings passed to script
sys.cwd()
() -> String
Current working directory path string
sys.env(key)
(String) -> String|null
Returns environment variable value, or null
sys.set_env(k, v)
(String, String) -> Boolean
Sets host environment variable
sys.exec(cmd)
(String) -> Number
Executes shell command via system()
Module: fs (Sandboxed Filesystem)
File I/O and directory operations. Import with import fs.
Function
Signature
Description
fs.read_text(path)
(String) -> String|null
Reads entire file contents as UTF-8 string
fs.write_text(path, text)
(String, String) -> Boolean
Overwrites file with text
fs.append_text(path, text)
(String, String) -> Boolean
Appends text to end of file
fs.exists(path)
(String) -> Boolean
Returns true if file or directory exists
fs.delete_file(path)
(String) -> Boolean
Deletes file at path
fs.list_dir(path)
(String) -> Array|null
Returns array of entry filenames in directory
fs.mkdir(path)
(String) -> Boolean
Creates new directory
fs.remove_dir(path)
(String) -> Boolean
Removes empty directory
fs.is_file(path)
(String) -> Boolean
Returns true if path is a regular file
fs.is_dir(path)
(String) -> Boolean
Returns true if path is a directory
fs.file_size(path)
(String) -> Number|null
Returns file size in bytes
Module: time
High-resolution wall clocks, timestamps, and delays. Import with import time.
Function
Signature
Description
time.clock()
() -> Number
High-resolution wall clock in fractional seconds
time.sleep(sec)
(Number) -> null
Pauses execution for specified seconds
time.timestamp()
() -> Number
UNIX epoch timestamp in seconds
time.format(ts)
(Number) -> String
Formats timestamp to human-readable string
time.iso(ts)
(Number) -> String
Formats timestamp to ISO 8601 string
Module: random
Pseudo-random number generation, shuffling, and seeding. Import with import random.
Function
Signature
Description
random.random()
() -> Number
Returns float in [0.0, 1.0)
random.random_int(min, max)
(Number, Number) -> Number
Returns integer in [min, max] inclusive
random.seed(val)
(Number) -> null
Seeds the PRNG state
random.shuffle(arr)
(Array) -> Array
Returns randomly shuffled array copy
Module: json
Fast JSON parsing and serialization into Unfish maps and arrays. Import with import json.
Function
Signature
Description
json.parse(str)
(String) -> Any
Parses JSON text into Unfish value
json.stringify(val)
(Any) -> String
Serializes Unfish value into JSON string
Module: testing
Assertions and test suite framework. Run tests via unfish test.
Function
Signature
Description
testing.assert(cond, [msg])
(Any, [String]) -> null
Asserts truthy condition
testing.assert_eq(a, b, [msg])
(Any, Any, [String]) -> null
Asserts value equality
testing.assert_throws(fn, [msg])
(Function, [String]) -> null
Asserts function raises an error
๐ Formal Grammar & Language Specification
Unfish syntax is formally specified in Extended Backus-Naur Form (ISO/IEC 14977). The language utilizes lexical indentation (the off-side rule), structured expression pipelines, algebraic data types, and pattern matching.
Lexical Note: Indentation in Unfish is significant. The lexer tracks leading whitespace on non-blank lines using an internal indentation stack. An increase in indentation emits a virtual TOKEN_INDENT, while a decrease emits one or more TOKEN_DEDENT tokens. Newlines within parentheses (...), brackets [...], and braces {...} are ignored.
The Unfish Virtual Machine is a bytecode-interpreted stack machine implemented in pure ANSI C99. It evaluates 16-byte tagged values (UfValue) on an operand stack with dynamic bounds checking, lexical upvalues for closures, and frame-pointer call unwinding.
Binary Bytecode Container (.ufc Format)
Precompiled Unfish bytecode modules are structured in a binary format with fixed endianness:
Removes top exception handler on try block success
0x37
OP_RETHROW
โ
[err] -> []
Reraises caught exception to outer handler
0x38
OP_AWAIT
โ
[prom] -> [res]
Suspends fiber until asynchronous promise resolves
0x39
OP_MATCH_SHAPE
u8 shape, u16 count
[val] -> [bool]
Tests a match pattern's shape: array length, map-or-instance, or field count
0x3A
OP_MATCH_FIELD
u16 index
[val] -> [field]
Positional field of a struct instance or enum value (null if absent)
๐ 256-Register VM Architecture (RegVM)
While the stack VM provides a compact bytecode footprint, real-world interpretation often incurs high memory traffic due to constant operand pushes and pops. The Unfish RegVM provides a high-throughput alternative based on a 3-address register architecture.
Run RegVM: Execute any Unfish script directly using the Register VM by passing the --regvm flag:
unfish run --regvm script.uf
32-Bit Fixed Instruction Formats
All RegVM instructions are encoded as 32-bit words for optimal cache line alignment and CPU branch prediction:
Format ABC: [ Opcode : 8b ] [ Reg A : 8b ] [ Reg B : 8b ] [ Reg C : 8b ]
Format ABx: [ Opcode : 8b ] [ Reg A : 8b ] [ Constant/Offset Bx : 16b ]
Format AsBx: [ Opcode : 8b ] [ Reg A : 8b ] [ Signed Offset sBx : 16b ]
Stack VM vs. Register VM Trace Comparison
Consider the arithmetic statement: let result = (a * b) + (c - d)
Stack VM (6 Instructions)
OP_LOAD_LOCAL 0 # push a
OP_LOAD_LOCAL 1 # push b
OP_MUL # pop b, pop a, push (a*b)
OP_LOAD_LOCAL 2 # push c
OP_LOAD_LOCAL 3 # push d
OP_SUB # pop d, pop c, push (c-d)
OP_ADD # pop (c-d), pop (a*b), push sum
OP_STORE_LOCAL 4 # store into result
Each execution fiber maintains an activation register file of 256 registers. Upon invoking a function:
The caller places function arguments into consecutive registers starting at r(A+1).
The call instruction ROP_CALL A, B, C shifts the callee's frame base pointer to rA.
Inside the callee, argument values map directly to r0, r1, ... without copying.
Upon return, ROP_RETURN A transfers the return value back to destination register rC.
๐ C99 AOT & Host Embedding Guide
Unfish was engineered from line 1 to be embeddable into host C and C++ applications. The entire runtime is self-contained in unfish_runtime.h with zero external dependencies beyond standard C99.
Compiling to Native Standalone C99
You can transpile any Unfish source code directly into portable ANSI C99 source code:
# Transpile Unfish to C99 source file
unfish emit-c -o game_logic.c game_logic.uf
# Compile directly to a native binary using GCC or Clang
gcc -O3 game_logic.c -lm -o game_logic
Embedding Unfish in a C Host Application
Here is a complete, working host program that embeds Unfish, executes scripts, and registers a custom C function callable from Unfish:
For microcontrollers (ARM Cortex-M, ESP32, RISC-V) without standard POSIX memory allocators, compile with -DUF_EMBEDDED. Unfish replaces dynamic malloc with a fixed static buffer and routes text output to a custom UART transmitter:
/* Direct serial output to UART hardware */
void uart_send_char(char c) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = c;
}
int main(void) {
/* Route Unfish say statements to hardware UART */
uf_set_putchar(uart_send_char);
/* Runs fully within 256 KB static RAM buffer */
uf_run_compiled_embedded();
while (1);
}
โ๏ธ Language Comparison Matrix
Unfish combines the syntax elegance of Python, the lightweight embeddability of Lua, and the concurrency ergonomics of Go into a unified, zero-dependency C99 engine.