Redblue is a programming language designed to be as readable as plain English. Write code that reads like natural sentences while maintaining the power of modern programming languages.
try
say 1 / 0
catch
say "recovered"
end
test "division by zero is caught, not fatal"
set caught to no
try
say 1 / 0
catch
set caught to yes
end
expect caught to be yes
end
Errors are values you catch. Tests live beside the code and must assert something that can fail. That is the whole philosophy in fifteen lines.
Readable control flow. No braces, no semicolons — blocks close with end:
set total to 0
for each n in [1, 2, 3]
if n is 2 then
skip
end
set total to total + n
end
say total
Errors that don't lie. Every error carries a source span, failures are catchable, and a bare catch runs even with no name binding:
set empty to []
try
set x to empty[0]
catch err
say err
end
Testing built in, not bolted on. Test blocks use expect ... to be ... and every test must be able to fail:
test "empty list index is an error"
set empty to []
try
set x to empty[0]
catch error
set caught to yes
end
expect caught to be yes
end
Values that behave. Numbers are 64-bit floats and always finite — 1 / 0 is a clean runtime error, never infinity. Text is fully Unicode. Records keep insertion order. Objects support inheritance and method dispatch, closures capture lexically.
Two engines that must agree. Programs run on the tree-walking interpreter or compile to versioned bytecode (.rbc). A differential harness runs 370 programs on both and compares outcomes — if the engines ever disagree, it's a bug, and there's a test proving it.
Errors you can read. Every error names its kind and points at the line:
Error: AnalyzerError: Unknown variable 'y'
--> err.rb:2:1
2 | say y
| ^
git clone https://github.com/authorss81/redblue.git
cd redblue
cargo build --release
# the binary is at ./target/release/rbThe binary is not on your PATH after building. cargo build puts it in
target/release/, and rb alone will not be found. Either invoke it by path:
./target/release/rb # Linux / macOS
.\target\release\rb.exe # Windowsor add the directory to your PATH once:
# Linux / macOS — quote the path, it contains spaces in a default checkout
export PATH="$HOME/3D Objects/a/rb/redblue/target/release:$PATH"# Windows PowerShell
$env:PATH += ";$env:USERPROFILE\3D Objects\a\rb\redblue\target\release"Then rb, rb help and so on work from any directory.
# Run a file
rb run hello.rb
# Start REPL
rb
# Format code
rb format hello.rb
# Lint code
rb lint hello.rb
# Run tests
rb test
# Compile to bytecode, inspect it, run it on the VM
rb compile hello.rb
rb dis hello.rbc
rb vm hello.rbc// variables.rb
set name to "Alice"
set age to 30
set fruits to ["apple", "banana", "orange"]
say name
say age
say fruits
// loops.rb
for each number from 1 to 5
say number
end
set items to ["a", "b", "c"]
for each item in items
say item
end
// files.rb
import files
files.write("output.txt", "Hello from Redblue!")
set content to files.read("output.txt")
say content
| Form | Meaning |
|---|---|
set x to <expr> |
assignment |
say <expr> |
|
if … then … else … end |
conditional |
for each x in <list> … end |
iteration |
for each i from A to B … end |
range loop |
while … end, repeat … until … |
loops |
break / skip |
leave / skip one iteration |
unless … end |
negated conditional |
x is 5, x is not 5 |
equality (also ==, !=) |
x is greater than 4 |
comparison (also <, <=, >, >=) |
a and b, a or b, not x |
logic |
list[0] |
indexing (out of bounds is a clean error) |
7 mod 3 |
remainder |
to name(params) … end |
function declaration |
to (x) … end |
function literal |
object Name … has x … to f() … end |
object with fields and methods |
Child extends Parent |
inheritance |
test "name" … end |
test block |
expect a to be b |
assertion |
try … catch [err] … finally … end |
catchable errors |
import Mod, import a, b as c |
modules |
constant PI to 3.14159 |
module-level constant |
Full grammar: docs/GRAMMAR.md. Design rationale: PHILOSOPHY.md. Specification: SPEC.md. Library policy — what is written in Redblue, what is a Rust binding, and what is not built: LIBRARIES.md.
Before adding a module or builtin, read LIBRARIES.md: it is the
standing decision on whether a capability is written in Redblue, bound to a Rust
crate, or not built at all.
files.read(path) // Read file as text
files.write(path, content)
files.append(path, content)
files.exists(path) // Check if file exists
files.lines(path) // Read as list of lines
files.delete(path)
files.copy(from, to) // Copy a file
files.rename(from, to) // Rename it, or move it
path.join("data", "records.rb") // join with the platform's separator
path.parent("data/records.rb") // "data", or nothing for a root
path.filename("data/records.rb") // "records.rb"; a trailing separator is ignored
path.stem("data/records.rb") // "records"
path.extension("data/records.rb") // "rb"
path.is_absolute("data") // no
path.resolve("data") // an absolute path with . and .. removed
path.normalize("data/./records.rb") // clean it, but keep it relative
path.list("data") // sorted entry names; [] for an empty directory
The files functions and bytes.write accept either a text path or a path value, so
files.read(path.join("data", "records.rb")) and
files.read("data/records.rb") are the same call. path.join uses the
platform's separator and lets an absolute part replace what came before it.
path.normalize removes . and .. and repeated separators while keeping a
relative path relative, which is what a program wants when it has built a path
and only needs to clean it. The WebAssembly playground has no working directory,
so path.resolve is refused
there with an IoError naming the platform; run the native rb binary to
resolve a path.
bytes.from_text(text) // Text to a list of byte values (0 to 255)
bytes.text(bytes) // A byte list back to text
bytes.write(path, bytes) // Write a byte list to a file
base64.encode(bytes) // Standard base64 with canonical = padding
base64.decode(text) // A byte list; padded or unpadded input
hex.encode(bytes) // Lowercase hexadecimal text
hex.decode(text) // A byte list
hash.sha256(text or bytes) // Lowercase SHA-256 digest
hash.sha512(text or bytes) // Lowercase SHA-512 digest
uuid.generate() // A new random version-4 UUID, canonical lowercase
uuid.parse(text) // Canonical lowercase; accepts hyphenated, simple, braced and urn:uuid: forms
A decoder never skips bad input: hex.decode and base64.decode raise a
catchable Runtime failure naming the offending position. hex.encode and
base64.encode take a text or a byte list; the decoders answer a byte list.
uuid.parse raises on a text that is not a UUID, and hash.sha256 /
hash.sha512 raise on an input that is not text or a byte list.
collections.counter(items) // {counts: [[key, count], ...], missing: 0}
collections.count(counter, key) // the count, or the counter's missing value
collections.most_common(counter) // [[key, count], ...], most common first
collections.defaultdict(factory) // {entries: [], factory}
collections.get(dict, key) // {value, dict}; a missing key is added
collections.put(dict, key, value) // the dict with the key set
collections.deque() // an empty deque
collections.deque(items) // a deque of the items
collections.push_left(deque, value) // add at the front
collections.push_right(deque, value) // add at the back
collections.pop_left(deque) // {value, deque}, or nothing at the front
collections.pop_right(deque) // {value, deque}, or nothing at the back
length(deque) // the number of items the deque holds
collections.namedtuple(names, values) // a record with those named fields
collections.field(record, name) // the field; a missing field is an error
A counter counts equal values of any kind in one pass and keeps first-appearance
order; collections.count answers zero for a key it has never seen.
A defaultdict's factory is a value copied into each missing key, and get
answers {value, dict} so the updated dictionary is the one to bind. The writer
is put because set is a keyword. A deque pushes and pops at both ends and
answers {value, deque} from a pop; its stack trees keep traversal depth
logarithmic, so a large deque never becomes one nested record per item. A named
tuple is a record, and collections.field refuses a missing name rather than
answering nothing the way a record property does.
time.now() // Get current timestamp
time.sleep(seconds) // Sleep for N seconds
time.format(timestamp_or_date, "format")
time.parse("YYYY-MM-DDTHH:MM:SS+HH:MM") // ISO-8601 text to a date
time.parse("29/02/2024", "%d/%m/%Y") // text in a chosen layout
time.unix("YYYY-MM-DD HH:MM:SS")
time.from_unix(seconds) // Seconds to a date, read as UTC
time.date(year, month, day) // A date at midnight
time.datetime(year, month, day, hour, minute, second)
time.duration(seconds) // A signed span of time
time.add_years(date, years) // Calendar arithmetic
time.add_months(date, months) // Clamps at month end
time.add_days(date, days) // Fixed 24-hour days
time.add_hours(date, hours)
time.add_minutes(date, minutes)
time.add_seconds(date, seconds)
// dates compare with < <= > >=, subtract to a duration, and add a duration
sys.argv() // Arguments after the program path (empty outside `rb run`)
sys.parse(declaration) // Parse those arguments into a record of typed flags
sys.parse(declaration, arguments)
network.get(url) // HTTP GET
network.post(url, body)
json.parse(text) // Parse JSON to record
json.stringify(value)
json.parse_lines(text) // one JSON value per line -> list
json.stringify_lines(list) // a list -> one JSON value per line
csv.parse(text) // Parse CSV to list of lists
csv.stringify(rows) // Write rows, quoting commas, quotes, newlines and space
toml.parse(text) // the documented TOML subset -> record
zip.write(path, files) // files is a record of names to text or byte lists
zip.read(path) // the same record, text when the bytes are UTF-8
tar.write(path, files)
tar.read(path)
PI, E // Constants, not module members: no math.PI
math.abs(x), math.floor(x), math.ceil(x)
math.round(x), math.sqrt(x), math.pow(x, y)
math.sin(x), math.cos(x), math.tan(x)
math.log(x), math.exp(x)
// every one of them is also spelled flat: abs(x), sqrt(x), ...
// math.random is not implemented, and there is no math.random
text.uppercase("text"), text.lowercase("text"), text.trim("text")
text.split("text", by), text.join(list, by)
text.contains("text", sub)
text.starts_with("text", prefix), text.ends_with("text", suffix)
text.replace("text", from, to)
text.length("text") // characters, not bytes: text.length("héllo") is 5
text.wrap("text", width) // a list of lines no longer than width; whitespace runs become one space
text.diff(old, new) // a standard unified diff; empty when the texts are equal
text.dedent("text") // remove the common leading whitespace
text.template("Hi <<name>>!", {name: "Ada"}) // "Hi Ada!"; <<<< writes a literal <<
text.pad("x", 3, "-") // "x--"
text.align("x", 5, "right") // " x"; left, right or center
// uppercase, lowercase, trim, split, join, contains, starts_with, ends_with,
// replace and length are also spelled flat.
say "Hello" // print with a newline
print "Hello" // print without one
console.log("Hello") // `say` under its module spelling
console.error("Could not read") // diagnostics go to stderr
console.clear() // clear the screen
set name to input("Your name? ") // read one line from the terminal
logging.set_level("info") // debug, info, warn, error or off; default off
logging.level() // the current threshold
logging.set_timestamps(no) // deterministic records without a timestamp
logging.set_destination("app.log") // a file path, or "stderr" / "stdout"
logging.debug("looking up a row")
logging.info("read 12 rows")
logging.warn("the cache is cold")
logging.error("the file does not exist")
logging.reset() // threshold off, stderr, timestamps on
The default threshold is off, so a library can call logging.info without
changing its caller's output. Records go to stderr unless a file or stdout is
chosen, carry a level and a message, and carry a UTC timestamp unless
timestamps are turned off for deterministic output.
list.map(xs, to (x) give back x * 2) // every member, through a function
list.length([1, 2, 3]) // 3
length(xs), push(xs, x) // flat list builtins
regex.match(text, pattern) // a match record, or nothing
regex.is_match(text, pattern) // yes/no, searching anywhere
regex.capture(text, pattern, group) // a captured text, or nothing
regex.replace(text, pattern, "user-$1")// every match, with capture references
regex.split(text, pattern) // the text between matches
// text.split keeps its literal separator; regex.split is the pattern form.
matrix.from_lists([[1, 2], [3, 4]]) // a 2x2 matrix; rows must be rectangular
matrix.zeros(2, 3) // every element is 0
matrix.ones(2, 3) // every element is 1
matrix.identity(2) // a 2x2 identity
matrix.shape(a) // [rows, columns]
matrix.to_lists(a) // the rows as nested lists
matrix.add(a, b) // elementwise; equal shapes required
matrix.subtract(a, b) // elementwise; equal shapes required
matrix.multiply(a, b) // elementwise; equal shapes required
matrix.divide(a, b) // elementwise; a zero divisor is refused
matrix.scale(a, 0.5) // every element times 0.5
matrix.matmul(a, b) // matrix product; inner dimensions must match
matrix.transpose(a) // rows and columns exchanged
matrix.sum(a) // the sum; an empty matrix sums to 0
matrix.mean(a) // the mean; an empty matrix is refused
matrix.min(a) // the smallest element
matrix.max(a) // the largest element
matrix.at(a, 1, 0) // one element
matrix.row(a, 0) // one row as a list
matrix.column(a, 0) // one column as a list
matrix is a binding to the ndarray crate, not an implementation in Redblue.
Every element is an f64, so integers are promoted on the way in and never
truncated on the way out; a result that is not finite is refused rather than
stored as NaN or infinity. Elementwise operations require exactly equal shapes
and never broadcast. matrix.at, matrix.row and matrix.column refuse a
fractional, negative or out-of-range index. A 0x0 matrix is defined: its sum is
0, its rows are [], and its mean, minimum and maximum are refused because an
empty matrix has none.
redblue/
├── src/
│ ├── lib.rs # Main library
│ ├── main.rs # CLI entry
│ ├── lexer.rs # Tokenizer
│ ├── parser.rs # AST builder
│ ├── analyzer.rs # Semantic analysis
│ ├── vm.rs # Tree-walking interpreter
│ ├── bytecode/ # Bytecode format, compiler, disassembler, VM
│ ├── stdlib.rs # Standard library
│ ├── formatter.rs # Code formatter
│ ├── linter.rs # Code linter
│ ├── lsp.rs # Language server
│ ├── repl/ # REPL
│ └── testing/ # Test harness
├── tests/ # Rust + Redblue test suites
├── corpus/ # Differential-test programs
├── examples/ # Example programs
├── modules/ # Importable modules
└── tooling/vscode/ # Editor extension (highlighting)
# Build
cargo build
# Run tests
cargo test
# Format code
cargo fmt
# Lint
cargo clippy- Core language (lexer, parser, analyzer, VM)
- Standard library (files, time, network, json, csv, math, text)
- Module system
- Testing framework
- Formatter
- Linter
- Bytecode compiler + VM (
rb compile,rb dis,rb vm) - IDE extensions (in progress)
- Package manager (planned)
- Self-hosting: the compiler rewritten in Redblue, then compiling itself (in progress — see ROADMAP.md)
MIT License - see LICENSE
Code should read like English prose.