Skip to content
authorss81Public

About

Redblue: a programming language as readable as plain English - tree-walking interpreter, bytecode VM, built-in testing

Topics

Resources

Code of conduct

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Latest commit

 

History

212 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Redblue

License: MIT Rust CI

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.

What it can do

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
  | ^

Quick Start

Build

git clone https://github.com/authorss81/redblue.git
cd redblue
cargo build --release
# the binary is at ./target/release/rb

The 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      # Windows

or 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

Examples

// 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

Language at a glance

Form Meaning
set x to <expr> assignment
say <expr> print
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.

Standard Library

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 and Path

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

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, Hex, Hash and UUID

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

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

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

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

network.get(url)     // HTTP GET
network.post(url, body)

JSON and CSV

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 and Tar

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)

Math

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

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.

Console

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

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

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

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

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.

Project Structure

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)

Development

# Build
cargo build

# Run tests
cargo test

# Format code
cargo fmt

# Lint
cargo clippy

Status

  • 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)

License

MIT License - see LICENSE


Code should read like English prose.

About

Redblue: a programming language as readable as plain English - tree-walking interpreter, bytecode VM, built-in testing

Topics

Resources

Code of conduct

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages