Rugo is a Ruby-inspired language that compiles to native binaries via Go.
Pipeline: .rugo source -> preprocess -> parse (LL(1) grammar) -> walk (typed AST) -> codegen (Go source) -> go build -> native binary.
Module path: github.com/rubiojr/rugo. Go version: 1.25.6.
make build # Build bin/rugo
make test # Go unit tests: go test ./... -count=1
make rats # Build + full RATS end-to-end suite
go run . emit script.rugo # Inspect generated Go code (debugging)# Go unit tests (all)
go test ./... -count=1
# Single Go test
go test ./compiler/ -run TestStripComments
# RATS end-to-end tests (all) - ALWAYS run before calling any task done
make rats
# or equivalently:
bin/rugo rats --recap --timing rats/
# Single RATS test file
bin/rugo rats rats/core/02_variables_test.rugo
# Parallel RATS (faster)
bin/rugo rats --jobs 4 --recap --timing rats/
# Full suite (Go tests + all example scripts)
rugo run script/testCRITICAL: Always run the ENTIRE rats/ suite before calling it done.
Run with --timing and --recap to see timing info and errors summarized.
Never hand-edit parser/parser.go. Regenerate from the EBNF grammar:
egg -o parser.go -package parser -start Program -type Parser -constprefix Rugo rugo.ebnf| Directory | Purpose |
|---|---|
ast/ |
AST nodes, walker, preprocessor |
parser/ |
LL(1) parser (generated from rugo.ebnf via egg) |
compiler/ |
Code generation, type inference, build orchestration |
modules/ |
23 Rugo stdlib modules (registration + runtime) |
gobridge/ |
Go stdlib bridge (import keyword) |
cmd/ |
CLI commands |
remote/ |
Remote module fetching and lockfiles |
doc/ |
Documentation generation |
tools/ |
Linter tools |
rats/ |
RATS end-to-end tests (~160 tests, ~410 fixtures) |
examples/ |
65+ example scripts |
docs/ |
Language and module documentation |
| Stage | File(s) | Notes |
|---|---|---|
| Preprocessor | ast/preprocess.go |
New keywords MUST be added here to avoid shell fallback |
| Grammar | parser/rugo.ebnf |
Never hand-edit parser.go; regenerate with egg |
| Walker | ast/walker.go |
Transforms parse tree -> AST nodes (ast/nodes.go) |
| Codegen | compiler/codegen.go |
AST nodes -> Go source |
| Embed | compiler/codegen_embed.go |
File embedding via embed "path" as name |
Stdlib and project-internal imports are intermixed in the first group. Third-party and additional project imports go in a second group separated by a blank line:
import (
"fmt"
"github.com/rubiojr/rugo/ast"
"strings"
"github.com/rubiojr/rugo/gobridge"
"github.com/rubiojr/rugo/modules"
)- Packages: Short, lowercase (
ast,compiler,parser,gobridge,remote) - Module packages: Use
modsuffix when name collides with stdlib (e.g.,package strmod) - Exported types: PascalCase (
Program,Statement,Compiler,SandboxConfig) - Unexported types: camelCase (
codeGen,walker) - Interface markers: Short lowercase methods (
node(),stmt(),expr()) - Generated runtime helpers:
rugo_prefix (e.g.,rugo_to_bool,rugo_add) - Constants: Exported PascalCase (
RugoKeywords), unexported camelCase (rugoBuiltins)
- Standard
if err != nilpattern with contextualfmt.Errorfmessages - Include file:line info in user-facing errors:
fmt.Errorf("%s:%d: ...", g.sourceFile, st.SourceLine, ...) ast.UserErrortype distinguishes user errors from internal compiler errors
- Use
testify(assert/require) for compiler tests; rawt.Fatal/t.Erroris acceptable in parser tests - Prefer table-driven tests with
t.Run:tests := []struct{ name, input, expect string }{...} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) }
- Helper functions must call
t.Helper() - Use
t.Cleanupfor test isolation (e.g., restoring global state)
- Make Go code idiomatic, typesafe, and readable
- Rugo prefers only one way to do things -- avoid adding alternative syntax
- No linter config exists; follow standard Go conventions (
gofmtformatting)
Every stdlib module follows a two-file structure:
modules/X/X.go-- Registration: uses//go:embed runtime.go, callsmodules.Register()ininit()modules/X/runtime.go-- Implementation://go:build ignoretag, methods returninterface{}, pointer receivers
New modules also need a blank import in main.go:
_ "github.com/rubiojr/rugo/modules/mymod"Tests are .rugo files using the test module:
# RATS: Test description
use "test"
rats "test case name"
test.assert_eq(actual, expected)
endAvailable assertions: test.assert_eq, test.assert_neq, test.assert_nil,
test.assert_true, test.assert_contains. Helpers: test.run, test.write_file, test.tmpdir.
Fixture-based tests use eval.run() or test.run() to compile/execute .rugo snippets.
- Add RATS tests in
rats/(happy path + error cases) - Add benchmarks if performance-related
- Add example script in
examples/ - Update
docs/language.mdanddocs/quickstart.mdfor language features - Update
docs/modules.mdfor new/modified modules - When adding doc examples, create and run the snippet to verify it works first
- Run the full RATS suite:
make rats
- Add a regression test in
rats/ - If fixing a git-bug issue, close it with a detailed comment
- Run the full RATS suite:
make rats
Program = { Statement | ';' } .
Statement = UseStmt | ImportStmt | RequireStmt | EmbedStmt | SandboxStmt | FuncDef | TestDef
| BenchDef | IfStmt | WhileStmt | ForStmt | BreakStmt | NextStmt
| ReturnStmt | AssignOrExpr .
UseStmt = "use" str_lit .
ImportStmt = "import" str_lit [ "as" ident ] .
RequireStmt = "require" str_lit [ "as" ( str_lit | ident ) | "with" ident { ',' ident } ] .
EmbedStmt = "embed" str_lit "as" ident .
FuncDef = "def" ident '(' [ ParamList ] ')' Body "end" .
ParamList = Param { ',' Param } .
Param = ident [ '=' Expr ] .
Body = { Statement | ';' } .
IfStmt = "if" Expr Body { "elsif" Expr Body } [ "else" Body ] "end" .
CaseExpr = "case" Expr { "of" ExprList ( "->" Expr | Body ) }
{ "elsif" Expr ( "->" Expr | Body ) } [ "else" ( "->" Expr | Body ) ] "end" .
WhileStmt = "while" Expr Body "end" .
ForStmt = "for" ident [ ',' ident ] "in" Expr Body "end" .
BreakStmt = "break" .
NextStmt = "next" .
ReturnStmt = "return" [ Expr ] .
AssignOrExpr = Expr [ '=' Expr ] .
Expr = OrExpr .
OrExpr = AndExpr { "||" AndExpr } .
AndExpr = CompExpr { "&&" CompExpr } .
CompExpr = AddExpr [ ("==" | "!=" | "<=" | ">=" | '<' | '>') AddExpr ] .
AddExpr = MulExpr { ('+' | '-') MulExpr } .
MulExpr = UnaryExpr { ('*' | '/' | '%') UnaryExpr } .
UnaryExpr = '!' Postfix | '-' Postfix | Postfix .
Postfix = Primary { '(' [ArgList] ')' | '[' Expr [',' Expr] ']' | '.' ident } .
Primary = ident | integer | float_lit | str_lit | raw_str_lit
| "true" | "false" | "nil"
| '[' [Expr {',' Expr}] ']'
| '{' [Expr "=>" Expr {',' Expr "=>" Expr}] '}'
| CaseExpr | TryExpr | SpawnExpr | ParallelExpr | FnExpr | '(' Expr ')' .
TryExpr = "try" Expr "or" ident Body "end" .
SpawnExpr = "spawn" Body "end" .
ParallelExpr = "parallel" Body "end" .
FnExpr = "fn" '(' [ParamList] ')' Body "end" .
x = 42 # integer
pi = 3.14 # float
name = "world" # string (interpolation with #{expr})
raw = 'no #{interp}' # raw string (no interpolation, no escape processing)
flag = true # boolean
nothing = nil # nil
PI = 3.14 # constant (uppercase = immutable, reassignment is compile error)"Hello, #{name}! Result: #{x + 1}"arr = [1, 2, 3]
arr[0] # index (0-based)
arr[-1] # negative index (last element)
arr[1, 2] # slice (start, length)
arr[0] = 99 # index assignment
append arr, 4 # bare append sugar
hash = {name: "Alice", age: 30} # colon syntax (keys become strings)
codes = {404 => "Not Found"} # arrow syntax (any value as key)
hash["name"] # access
hash["email"] = "a@b.com" # assignmenta, b, c = [10, 20, 30]if x > 10
puts("big")
elsif x > 5
puts("medium")
else
puts("small")
end
while i < 10
i += 1
end
for item in arr
puts(item)
end
for key, value in hash
puts("#{key}: #{value}")
end
# case expression
result = case status
of "ok" -> "success"
of "error", "fail" -> "problem"
else -> "unknown"
end
# case with blocks
case code
of 200, 201
puts("success")
of 404
puts("not found")
else
puts("other")
enddef greet(name, greeting="hello")
return "#{greeting}, #{name}!"
end
puts greet("world")double = fn(x) x * 2 end
add = fn(a, b) a + b end
transform = fn(x)
result = x * 2
return "value: #{result}"
endstruct Point
x
y
end
def Point.distance(self, other)
dx = self.x - other.x
dy = self.y - other.y
return math.sqrt(dx * dx + dy * dy)
end
p = Point(10, 20)
puts p.x# Level 1: silent recovery (returns nil on failure)
result = try `might_fail`
# Level 2: default value
name = try `whoami` or "unknown"
# Level 3: handler block
result = try `cat /etc/config` or err
puts("failed: #{err}")
"default_value"
end# spawn β single goroutine with task handle
task = spawn
http.get("https://example.com")
end
result = task.value # block until done
task.done # non-blocking check
task.wait(5) # block with timeout
# spawn one-liner
task = spawn http.get("https://api.com/data")
# parallel β fan-out, wait for all
results = parallel
http.get("https://api1.com")
http.get("https://api2.com")
end
puts results[0]echo "hello" | str.upper | puts # shell β module β builtin
"hello world" | tr a-z A-Z | puts # value β shell β builtin
name = echo "rugo" | str.upper # assign pipe result`hostname` # backtick capture
ls -la # shell fallback (unknown identifiers at top level)
output = `curl -s https://example.com` # capture outputembed "config.yaml" as config # baked into binary, no runtime file needed
embed "assets/logo.txt" as logo # paths relative to source file
# Cannot use ../ to escape source directory (like Go's embed)x += 1 # also -=, *=, /=, %=for item in items
if item == "skip"
next
end
if item == "stop"
break
end
puts(item)
end# Transform
arr.map(fn(x) x * 2 end)
arr.flat_map(fn(x) [x, x] end)
arr.filter(fn(x) x > 0 end)
arr.reject(fn(x) x < 0 end)
arr.reduce(0, fn(acc, x) acc + x end)
# Search
arr.find(fn(x) x > 3 end)
arr.any(fn(x) x > 4 end)
arr.all(fn(x) x > 0 end)
arr.contains(42)
arr.count(fn(x) x > 0 end)
arr.index(42)
# Sort & Order
arr.sort_by(fn(x) x end)
arr.reverse()
# Utilities
arr.first()
arr.last()
arr.min()
arr.max()
arr.sum()
arr.uniq()
arr.flatten()
arr.compact()
arr.clone()
arr.join(", ")
arr.take(3)
arr.drop(2)
arr.chunk(2)
arr.zip(other_arr)
# Hash-specific
hash.keys()
hash.values()
hash.merge(other_hash)
# Chaining
nums.filter(fn(x) x > 2 end).map(fn(x) x * 10 end).join(", ")# use β Rugo stdlib modules
use "str"
use "http"
result = http.get("https://example.com")
# import β Go stdlib bridge
import "strings"
import "path/filepath"
puts strings.to_upper("hello")
# require β user modules
require "helpers" # local file
require "lib/utils" as "u" # with alias
require "mylib" with client, helpers # selective
require "github.com/user/lib@v1.0.0" as "utils" # remote| Module | Functions |
|---|---|
| conv | to_i, to_f, to_s, to_bool, parse_int |
| str | contains, split, trim, starts_with, ends_with, replace, upper, lower, index, join, rune_count, count, repeat, reverse, chars, fields, trim_prefix, trim_suffix, pad_left, pad_right, each_line, center, last_index, slice, empty, byte_size |
| fmt | sprintf, printf |
| json | parse, encode, pretty |
| re | test, find, find_all, replace, replace_all, split, match |
| http | get, post, put, patch, delete |
| os | exec, exit, file_exists, is_dir, read_line, getenv, setenv, cwd, chdir, hostname, read_file, write_file, remove, mkdir, rename, glob, tmp_dir, args, pid, symlink, readlink |
| cli | name, version, about, cmd, flag, bool_flag, run, parse, command, get, args, passthrough, help |
| color | red, green, yellow, blue, magenta, cyan, white, gray, bold, dim, underline, bg_* |
| web | get, post, put, delete, patch, middleware, rate_limit, group, end_group, static, listen, port, free_port, text, html, json, redirect, status |
| sqlite | open, exec, query, query_row, query_val, close |
| queue | new (then .push, .pop, .size, .close, .done) |
| signal | on, wait, reset, ignore (race-free OS signal handling) |
| math | abs, ceil, floor, round, max, min, pow, sqrt, log, log2, log10, sin, cos, tan, pi, e, clamp, random, random_int |
| rand | int, float, string, choice, shuffle, uuid |
| time | now, sleep, format, parse, since, millis |
| base64 | encode, decode, url_encode, url_decode |
| crypto | md5, sha256, sha1 |
| filepath | join, base, dir, ext, abs, rel, glob, clean, is_abs, split, match |
| hex | encode, decode |
| eval | run, file |
| test | run, tmpdir, write_file, assert_eq, assert_neq, assert_true, assert_false, assert_contains, assert_nil, fail, skip |
strings, strconv, math, math/rand/v2, path/filepath, sort, os, time, encoding/json, encoding/base64, encoding/hex, crypto/sha256, crypto/md5, net/url, unicode, slices, maps
| Function | Description |
|---|---|
| puts(val) | Print with newline |
| print(val) | Print without newline |
| len(val) | Length of string, array, or hash |
| append(arr, v) | Append to array (or: append arr, v) |
| raise(msg) | Panic with message |
| type_of(val) | Returns "string", "int", "float", "bool", "nil", "array", "hash", "fn" |
| exit(code) | Exit process |
- No classes, only structs
- No blocks/procs, only fn lambdas
- No symbols, only strings
- Hash colon syntax
{key: val}makes string keys - Arrow syntax
{k => v}for non-string keys - Explicit
endfor all blocks use/import/requireinstead of Ruby's require/include- Shell commands run directly (shell fallback)
- Compiles to native binary, no interpreter
- No gems/bundler; remote modules via
require "github.com/..." - Single
=for assignment (no local/instance var distinction) - Constants are uppercase identifiers (compile-time immutable)
Read JSON file:
use "json"
use "os"
content = os.read_file("config.json")
data = json.parse(content)
for key, value in data
puts "#{key}: #{value}"
endConcurrent URL fetching with spawn:
use "http"
def fetch_all(urls)
tasks = []
for url in urls
task = spawn http.get(url)
append tasks, task
end
results = []
for t in tasks
append results, t.value
end
return results
endCLI tool with flags and color:
use "cli"
use "color"
cli.name("greeter")
cli.version("1.0.0")
cli.about("A colorful greeting tool")
cli.flag("name", "n", "Your name", "World")
cli.bool_flag("loud", "l", "Shout the greeting", false)
cli.run(fn()
name = cli.get("name")
greeting = "Hello, #{name}!"
if cli.get("loud")
greeting = str.upper(greeting)
end
puts color.green(color.bold(greeting))
end)Error handling (three levels of try/or):
result = try `might_fail`
port = try conv.to_i(`cat /etc/port`) or 8080
config = try os.read_file("/etc/app.conf") or err
puts "Warning: #{err}, using defaults"
"{}"
endWeb server with routes:
use "web"
use "json"
web.get("/", fn(req)
web.html("<h1>Welcome to Rugo!</h1>")
end)
web.get("/api/greet/:name", fn(req)
name = req["params"]["name"]
web.json(json.encode({"message" => "Hello, #{name}!"}))
end)
web.listen(3000)Collection method chaining:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = nums
.filter(fn(x) x % 2 == 0 end)
.map(fn(x) x * x end)
.join(", ")
puts "Even squares: #{result}"
total = nums.reduce(0, fn(acc, x) acc + x end)
people = [{"name" => "Charlie", "age" => 30}, {"name" => "Alice", "age" => 25}]
sorted = people.sort_by(fn(p) p["age"] end)File embedding (compile-time):
embed "config.yaml" as config
embed "templates/header.html" as header
puts config + header