Releases: rubiojr/rugo
v0.28.0
New Features
File embedding (embed)
Bake file contents into your compiled binary at build time — no external files needed at runtime:
embed "config.yaml" as config
embed "assets/template.html" as template
puts config
puts len(template)Paths are relative to the source file and restricted to the same directory or subdirectories, matching Go's embed rules. This applies equally to main scripts and required libraries, so a module can bundle its own data files.
Embedded variables are regular strings — use them in expressions, interpolation, function arguments, or across function boundaries. Clear compile-time errors for invalid paths, missing files, duplicates, and misplaced embed statements.
Binary build cache
Compiled Rugo programs are now cached. On repeat runs of the same source, the cached binary is reused instead of invoking go build (~300ms → ~5ms per cache hit). Cached binaries are gzip-compressed and evicted LRU-style with a 10GB cap.
This benefits any workflow that repeatedly compiles the same code: RATS tests with shared fixtures, linter tools, eval.file() calls, etc.
RATS suite timing with warm cache: 52s → 28s (46% faster).
Improvements
Go bridge reliability
- Reader/Writer interop — Go bridge now handles
io.Reader/io.Writerinterface parameters, enabling APIs likexml.new_decoderandxml.escape_text. - Struct pointer-slice fields — bridged Go structs now expose pointer-slice fields as wrapped values.
- WalkDir-style callbacks — named interface types inside callback signatures are now supported, making APIs like
filepath.walk_diravailable. - Variadic function-option params — remote module docs now include functions with variadic named option params (e.g.,
go-gpx.read(...ReadOption)). - Plain text
rugo doc— doc output is now plain text automatically when stdout is not a TTY, so grep/pipe workflows work out of the box.
Bug Fixes
next/breakin try handlers — usingnextorbreakinside atry...or err...endblock within a loop now works correctly. Previously it failed with"next is not in a loop".- Shell interpolation with shadowed params — string interpolation in shell commands (e.g.,
echo "#{dest}") no longer breaks when a function elsewhere declares a parameter with the same name. - Bad for-loop errors — malformed
forloops now produce clearer error messages. - Go module import validation — invalid imports from Go modules are now caught at compile time.
v0.27.0
New Features
Case/of multi-branch matching
Rugo now has a case/of/elsif/else/end construct for matching a value against multiple branches — the idiomatic alternative to long if/elsif chains:
status = "ok"
case status
of "ok"
puts "all good"
of "error", "fail"
puts "something went wrong"
else
puts "unknown"
endEach of branch can match multiple comma-separated values. For concise one-liners, the arrow form (->) puts the branch body on the same line:
code = 404
case code
of 200, 201 -> puts("success")
of 301, 302 -> puts("redirect")
of 400, 404 -> puts("client error")
of 500, 502 -> puts("server error")
else -> puts("other")
endelsif branches add boolean conditions that don't compare against the subject — of branches are checked first by equality, then elsif conditions, then else:
score = 75
case score
of 100 -> puts("perfect")
of 0 -> puts("zero")
elsif score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
endcase also works as an expression — assign the result directly to a variable, pass it to a function, or use it as a return value. Variables inside expression branches are scoped and don't leak into the surrounding context:
label = case status
of "ok" -> "success"
of "error" -> "failure"
else -> "unknown"
end
puts label # "success"Postfix if modifier
Simple conditional assignments can now be written on a single line using a trailing if:
x = 10 if true
puts x # 10This follows the Ruby convention of placing the condition after the expression for concise one-liners.
@latest for remote imports
Remote require statements now accept @latest to always fetch the newest version of a dependency:
require "github.com/user/repo@latest"Sandbox $VAR expansion
Sandbox path configurations now expand $VAR references at runtime, allowing environment-variable-driven sandbox policies.
Improvements
Ruby-compatible || and && semantics
The || and && operators now return the actual value that determined the result instead of a boolean. This matches Ruby semantics and enables idiomatic patterns like default values:
name = nil
greeting = name || "stranger"
puts greeting # "stranger"
x = "hello"
y = x && x.upcase
puts y # "HELLO"Character-based len() and string slicing
len() now returns the character count instead of the byte count for strings. String slicing also operates on characters instead of bytes. This means Unicode strings behave correctly:
s = "héllo"
puts len(s) # 5 (was 6 with byte counting)
puts s[1..3] # "éll" (correct character slice)Improved value display
nilis now displayed as"nil"instead of Go's"<nil>"- Float values always show a decimal point (e.g.,
1.0instead of1) - Arrays and hashes print in Rugo format instead of Go format
- Runtime error messages use Rugo type names instead of Go type names
Paren-free namespace calls
Paren-free function calls like str.trim value now work consistently across all namespaces, not just built-in modules.
Better error messages
- Namespace conflicts — importing a Go package that collides with an already-loaded
usemodule now produces a clear error at compile time instead of silent shadowing. - Builtin redefinition — defining a function with the same name as a builtin (
puts,len, etc.) now raises an error instead of silently breaking. - Reserved keywords — using a reserved keyword as an identifier produces a specific error message instead of a generic parse failure.
- Float as array index — using a float to index an array now raises a clear error instead of silently truncating to an integer.
- Non-iterable iteration — iterating over a non-iterable type (e.g.,
for x in 42) now produces a user-friendly error message. - Destructuring underflow — destructuring an array with more variables than elements now raises a clear error instead of a cryptic Go panic.
Bug Fixes
- Postfix-if nil —
x = 10 if falsenow correctly leavesxasnilinstead of assigning a zero value. - Negative literal parsing — a negative number on its own line is no longer misinterpreted as a subtraction from the previous expression.
- Float literal folding — arithmetic on float literals (e.g.,
1.5 + 2.5) now produces the same result as the equivalent computation using variables. .eachcallback mutation — modifying outer variables inside an.eachcallback no longer fails to compile.try/orvariable fallback —try/ornow accepts a variable as the inline fallback value, not just literals.- Mixed return types — functions that return different types from different branches (e.g., a string in one path and an integer in another) no longer fail to compile.
--separator stripping — the--separator is now correctly stripped when passing arguments to scripts viarugo run.- Shell fallback for assignments — assignments like
bar = ls -laare no longer incorrectly handled when the right-hand side looks like a shell command. - Path-based commands — commands specified with an explicit path (e.g.,
./myscript) are now correctly recognized as shell commands. eval.runwith imports —eval.run()no longer fails when the evaluated code usesimport "os"or other Go bridge imports.
v0.26.0
New Features
Go bridge callbacks for widget events
The Go bridge now handles struct-method callbacks with super patterns — function parameters whose first argument is itself a function pointer (e.g., OnPaintEvent(func(super func(*QPaintEvent), event *QPaintEvent))). This unlocks event overrides on external types like Qt widgets without writing any Go wrapper code:
require "github.com/mappu/miqt/qt6@v0.13.0"
win = qt6.new_qwidget2()
win.on_paint_event(fn(super_paint, event)
super_paint(event) # call the default handler
puts "paint event!"
end)
win.on_mouse_press_event(fn(super_mouse, event)
super_mouse(event)
puts "mouse click!"
end)The bridge auto-discovers pointer-to-function parameters, named function type casts, and nested callback signatures. Previously, these patterns were silently skipped as unbridgeable.
eval module now supports use and import
Code executed via eval.run() and eval.file() can now use stdlib modules (use "str", use "json", etc.) and Go bridge imports (import "strings"). Previously, eval'd code compiled into a standalone binary with an empty module registry, so any use statement failed at runtime.
All 20 stdlib modules (excluding ast, eval, and sqlite) are now registered in the eval binary via blank imports.
eval error handling improvements
eval.run() and eval.file() no longer panic on compile errors. They return the standard result hash with "status" => 1 and the error message in "output", matching what rugo run produces on failure. This makes eval safe to use with try/or:
use "eval"
result = eval.run("invalid code here ===")
puts result["status"] # 1
puts result["output"] # error: ...rugo eval error path scrubbing
The rugo eval CLI command now scrubs temporary file paths from error messages in all cases. Previously, some error paths leaked the internal temp directory (e.g., /tmp/rugo-eval-abc123/eval.rugo:1) instead of showing the clean eval:1 prefix.
Bug Fixes
-
Parallel remote dependency pulling — when multiple
requirestatements fetched the same remote module concurrently, the cache directory could be corrupted by overlapping writes. Git clones now use atomic installation: the repo is cloned into a unique temp directory and renamed to the final cache path. Lock file writes are also atomic (write to temp, then rename). If another process wins the race, the duplicate clone is silently cleaned up. -
Undefined variable errors in required files — the
check_identspass now tracks source file changes acrossrequireboundaries. Previously, an undefined variable in a required file reported the wrong file name in the error message. The walker now saves and restoressourceFilewhen entering functions, test/bench blocks, and individual statements from required files. -
Go bridge: functions returning structs — Go bridge functions that return struct values (not pointers) from imported packages are now classified and bridged correctly. The classifier now handles additional patterns including interface-typed callback parameters with assertion casts, named function types, and nested function signatures in callback parameters.
-
Go imports reliability — improved consistency of
import-ed Go packages, particularly for time-related calls and packages with complex type hierarchies.
Refactoring
RATS tests migrated from fixture shellouts to eval
186 fixture files deleted. ~30 RATS test files rewritten to use eval.run() inline instead of shelling out to rugo run on separate fixture scripts. Tests now compile and run snippets in-process, which is faster and eliminates the need to maintain separate .rugo fixture files for each test case. The test assertions remain identical — only the execution mechanism changed.
Go bridge test coverage
Expanded test coverage across the bridge subsystem:
- New classifier tests for pointer-to-function params, named function type casts, and nested callback signatures.
- New struct bridge tests for functions returning struct values from external packages.
- 530+ new lines of Go bridge time package tests covering duration parsing, formatting, timestamps, and sleep.
v0.25.0
New Features
Undefined variable and function detection
Rugo now catches undefined variables and functions before compilation, reporting them instantly with file and line info instead of waiting for the Go compiler to fail with cryptic errors:
script.rugo:3: undefined variable 'xyz'
script.rugo:5: undefined function helper.nonexistent
This also validates namespaced calls (ns.func()) against the actual functions exported by required modules, stdlib modules, and Go bridge packages. Typos in module function names are caught at compile time:
use "str"
str.uper("hello") # error: unknown function str.uper — did you mean str.upper?
173 new RATS tests cover top-level variables, function bodies, forward references, if/for/try scoping, lambda parameters, required module namespaces, and stdlib module validation.
rugo bench --count flag
The bench command now accepts a --count (-n) flag to run each benchmark file multiple times:
rugo bench --count 5 benchmarks/
Each file runs N times with the best result kept, useful for reducing noise in benchmark comparisons.
Bug Fixes
-
Implicit return from if/else —
if/elseblocks as the last expression in a function or lambda now correctly return the branch value instead ofnil:def classify(x) if x > 10 "big" else "small" end end classify(20) # => "big" (was nil)
-
returninsidetry/orhandlers —returnstatements insidetry/orerror handlers no longer cause a compile error. They now correctly set the handler result value:matches = try find_stuff(input) or err return nil # sets matches = nil (previously failed to compile) end
-
CLI dispatch with required modules — using
requirewith a module that defines a function matching a CLI command name caused either a duplicate key compile error or silent wrong dispatch. The CLI dispatch map now only includes functions defined in the main file.
Refactoring
Structured Go output AST
The entire code generation pipeline has been rewritten to produce Go source through a structured AST instead of raw string assembly. This was a multi-step migration across 20+ commits:
-
Go output AST types and printer — a complete set of Go AST node types (
GoFile,GoFuncDecl,GoIfStmt,GoForStmt, etc.) and a printer that serializes them to formatted Go source with correct indentation and braces. 17 printer tests cover all node types. -
Statement codegen — all AST statement types (assignments, returns, breaks,
if/else,while,for, range optimization) now buildGoStmtnodes instead of writing strings directly. -
Function codegen —
buildFuncreturnsGoFuncDeclnodes handling param unpacking, arity checks, recursion guards, and default return values. -
Expression codegen — the entire expression pipeline flows through
buildExpr→GoExprnodes → printer. Leaf expressions, operators, collections, string interpolation, and complex expressions (try,spawn,parallel, lambdas) all produce structured nodes. -
Old infrastructure deleted —
goWriter,goir.go,captureOutput, and 20+ deadwrite*methods removed. All Go source output flows throughPrintGoFile.
AST transform pipeline
A composable Transform interface and Chain compositor replace inline AST rewriting in codegen:
ConcurrencyLowering— rewritesspawn/parallel/tryconstructs and their nested return statements into lowered node types before codegen.ImplicitReturnLowering— converts last-expression-as-return-value into explicit AST nodes for functions, lambdas, and try handlers.
Codegen no longer inspects AST structure to decide how to emit returns — it switches on node type and emits mechanical Go code.
Package reorganization
preprocess/— preprocessing pipeline extracted fromast/into its own package, since preprocessing runs before AST construction.preprocess/string_tracker.go— reusableCodeScannertype extracted from 17 repeated string/escape/backtick tracking implementations.util/— shared utilities (levenshtein.go) extracted into a dedicated package.compiler/codegen.gosplit — the monolithic codegen file split into six focused files:codegen.go(core),codegen_expr.go,codegen_stmt.go,codegen_func.go,codegen_runtime.go,codegen_scope.go.
Fuzzer improvements
- 22 of 23 stdlib modules now registered in the fuzzer (all except
evalwhich causes an import cycle). - 55+ new seed corpus entries for recent language features.
- New
checkGoLeaks()helper detects Go internals leaking into error messages (interface{}, goroutine traces,panic:,*main.).
v0.24.0
New Features
Default parameter values for functions and lambdas
Functions and lambdas now support default parameter values using = expr syntax. Parameters with defaults can be omitted by callers, and the default expression is evaluated at call time:
def greet(name, greeting = "Hello")
puts "#{greeting}, #{name}!"
end
greet("Alice") # Hello, Alice!
greet("Alice", "Hey") # Hey, Alice!Multiple defaults and any expression (including nil, booleans, arithmetic) work as defaults:
def connect(host, port = 8080, tls = true)
puts "#{host}:#{port} tls=#{tls}"
end
connect("example.com") # port=8080, tls=true
connect("example.com", 443) # port=443, tls=true
connect("example.com", 443, false) # port=443, tls=falseA function with all-optional parameters can be called with zero arguments:
def label(text = "default", color = nil)
puts text
end
label() # both default
label("hello") # color defaults to nilLambdas use the same syntax:
transform = fn(x, factor = 2) x * factor end
puts transform(5) # 10
puts transform(5, 3) # 15Arity is now a range (min required .. max total). Required parameters must come before parameters with defaults — mixing them the other way is a compile error. 25 new RATS tests cover def defaults, lambda defaults, all-optional params, arity errors, and validation errors.
Bytes type
Go bridge functions returning []byte (like sha256.sum256, json.marshal, os.read_file) now return a proper Bytes type instead of silently converting to String. This eliminates unnecessary copies when []byte values flow between bridge functions.
import "crypto/sha256"
import "encoding/hex"
hash = sha256.sum256("hello")
type_of(hash) # "Bytes"
len(hash) # 32
# Zero-copy pipeline — bytes flow directly between bridge calls
encoded = hex.encode_to_string(hash)
puts encoded # 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824type_of()returns"Bytes"for[]bytevaluesputsand string interpolation work naturallylen()returns byte count; slicing and indexing work- Indexing returns an integer; slicing returns
Bytes - Explicit
conv.to_s()needed to compare with strings - String concatenation (
+) withBytesis a deliberate error — use interpolation orconv.to_s()
Bug Fixes
- Heredocs in return statements — heredocs used with
return(e.g.,return <<~'CSS' ... CSS) were rejected with a spurious "semicolons are not supported" error. Only heredocs in assignment context worked. Now heredocs work in any expression position including return. - Variadic bridge functions with mixed params — Go bridge functions with a fixed parameter followed by variadic parameters (e.g.,
url.join_path(base, elem...)) failed when called with 2+ arguments. Arguments are now correctly converted using the element type instead of the slice type. --recapshows all failures at the end — previously,rugo rats --recaponly showed failure details inline with each file's output. When running many test files, failures got buried in hundreds of lines. Now,--recapprints a consolidated "Failed tests" section at the very end, grouped by file.- macOS RATS test compatibility — several tests relied on Linux-specific behavior (non-portable shell commands, hardcoded binary paths, Linux-only files). Updated to work on both Linux and macOS.
v0.23.0
New Features
Auto-introspected Go stdlib bridge
Go stdlib packages are now lazily introspected at compile time instead of relying on hand-written bridge files. When a script does import "strings" or import "math", the compiler inspects the compiled Go package directly, classifies its exported functions, and makes them available automatically — no generated code required.
This replaces ~4000 lines of generated and custom bridge code with a single introspection pass. The 17 previously hand-maintained stdlib packages (strings, math, os, strconv, filepath, base64, hex, time, rand, unicode, path, html, crypto/md5, crypto/sha256, sort, encoding/json, net/url) now bridge through the same mechanism as require'd Go modules.
The rugo dev bridgegen command has been removed — it is no longer needed.
New array dot methods
Arrays gain four new built-in methods:
arr = [1, 2, 3, 2, 1]
arr.contains(3) # true
arr.index(2) # 1
arr.reverse() # [1, 2, 3, 2, 1] (returns new array)
arr.compact() # [1, 2, 3, 2, 1] (removes consecutive duplicates).contains(value)— checks if the array contains a value (deep equality).index(value)— returns the index of the first occurrence, or -1.reverse()— returns a new reversed array.compact()— removes consecutive duplicate elements
New hash dot methods
.clone()— returns a shallow copy of the hash
Sorted hash keys
.keys() now returns keys in sorted order instead of Go's random map iteration order:
h = {"c" => 3, "a" => 1, "b" => 2}
puts h.keys() # ["a", "b", "c"]Byte slice handling improvements
Functions that accept []byte parameters now accept both strings and arrays of integers. Functions like hex.encode and hex.decode that use output-buffer patterns (func(dst, src []byte)) are auto-wrapped into simple single-argument calls:
import "encoding/hex"
import "crypto/sha256"
puts hex.encode_to_string("hello") # 68656c6c6f
puts hex.encode_to_string(sha256.sum256("hello")) # 2cf24dba...Breaking Changes
mapsandslicesbridge packages removed — these were custom pseudo-packages wrapping Rugo arrays/hashes. Use native dot methods instead:.contains(),.index(),.reverse(),.compact(),.keys(),.values(),.clone().rugo dev bridgegenremoved — the bridge generation tool is no longer needed with auto-introspection.- Some curated bridge aliases removed — auto-introspection uses 1:1 Go function names. For example,
base64.encode/base64.decodebecomebase64.std_encoding_encode_to_string/base64.std_encoding_decode_string. For friendlier names, prefer theuse "base64"stdlib module. json.unmarshalviaimportremoved — useuse "json"withjson.parse()instead.url.parsestruct decomposition removed —url.parseno longer returns a hash with scheme/host/path fields.sort.strings/sort.intsremoved — custom in-place sorting helpers are gone. Usesort.strings_are_sorted,sort.search_strings, or the native.sort_by()dot method.time.now_unix/time.sleep_msremoved — useuse "time"stdlib module for these functions.
Other
- Removed ~4000 lines of generated and custom bridge code (net -3200 lines).
- Updated
docs/modules.mdto remove stalemaps/slicesbridge references. - New RATS tests for
.contains(),.index(),.reverse(),.compact(),.clone(), and sorted.keys().
v0.22.0
New Features
Comprehensive standard library
Rugo's use modules are now the recommended, idiomatic way to work with strings, files, math, time, encoding, and more. Seven new modules and major expansions to four existing ones bring the stdlib to 125+ curated functions with Ruby-inspired naming.
The Go bridge (import) remains available as a power-user escape hatch for the full Go stdlib, but use modules now cover the common cases — one obvious way to do things.
New modules
math— 23 functions for common math operationsfilepath— 11 functions for path manipulationtime— 6 functions for timestamps, sleeping, and formattingbase64— standard and URL-safe encodinghex— hexadecimal encodingcrypto— hash digestsrand— random number and string generation
Expanded modules
strosjsonconv
do...end trailing block syntax
Pass a block to any function using do...end instead of fn()...end:
vbox do
label("Hello")
button("Quit") do
quit()
end
end
This is equivalent to vbox(fn() ... end). When the call already has arguments, the block is appended: button("Quit") do...end becomes button("Quit", fn()...end). Nesting, assignment, and paren-free forms are all supported.
GoOpaque: struct-typed callback parameters
Callbacks with struct-typed parameters like OnItemDoubleClicked(func(item *QListWidgetItem)) are now bridged automatically. Previously, signal connectors for many Qt widgets were silently missing from the generated bridge.
Bug Fixes
- GoOpaque bridge fixes — various fixes to the auto-bridge for external Go module types.
- Cross-platform RATS tests — fixed test portability issues across platforms.
v0.21.0
New Features
GoOpaque bridge — use any Go library from Rugo
Go modules loaded via require can now use types from external dependencies. The bridge auto-discovers types, methods, signal callbacks, and class hierarchies — no Go wrapper code needed.
require "github.com/mappu/miqt/qt6@v0.13.0"
import "os"
qt = {
app: fn() qt6.new_qapplication(os.args()) end,
exec: fn() qt6.qapplication_exec() end,
quit: fn() qt6.qcore_application_quit() end,
window: fn() qt6.new_qwidget2() end,
button: fn(text) qt6.new_qpush_button3(text) end,
label: fn(text) qt6.new_qlabel3(text) end,
vbox: fn(parent) qt6.new_qvbox_layout(parent) end
}
app = qt.app()
win = qt.window()
win.set_window_title("Hello from Rugo!")
win.resize(320, 200)
layout = qt.vbox(win)
lbl = qt.label("Click count: 0")
layout.add_widget(lbl)
count = 0
btn = qt.button("Click Me")
btn.on_clicked(fn()
count = count + 1
lbl.set_text("Click count: #{count}")
end)
layout.add_widget(btn)
win.show()
qt.exec()This example bridges 3600+ functions, 600+ struct types, and hundreds of methods per type from the miqt Qt6 bindings — all auto-discovered.
What the bridge handles automatically
- Methods on external types —
win.set_window_title("Hello"),btn.on_clicked(fn() ... end) - Rugo lambdas as Go callbacks — signal connections, iterators, any
func()parameter - Embedded struct navigation —
lbl.qframe.qwidgetwalks Qt's class hierarchy - Auto-upcasting — pass a
QPushButtonwhere aQWidgetis expected, the bridge walks the embedded field chain automatically - Enum and named type casts —
qt6.GestureType,int8,int16etc. handled transparently - Module-aware type resolution — external dependencies resolved via
go list -export
os.args()
New function returning command-line arguments as a string array:
import "os"
puts(os.args())--show-warnings flag
Bridge warnings about unbridgeable Go functions (e.g., unsafe.Pointer params) are now hidden by default. Use --show-warnings to see them:
rugo build --show-warnings main.rugo -o app
rugo run --show-warnings main.rugo
Bug Fixes
- Remote Go sub-package require —
require "github.com/user/repo/subpkg@v1.0.0"now correctly resolves Go sub-packages from remote repositories. withclause version path —require "github.com/user/repo@v1.0.0" with subpkgno longer puts the version in the wrong position.- Stale module cache — empty cache directories from failed fetches are now re-fetched instead of treated as cached.
- Void GoFunc adapter — Go callbacks with no return value (
func()) no longer generate invalidreturnstatements. - Named return types in callbacks — signal callbacks with named return types (e.g.,
func() qt6.Orientation) now compile correctly. - Improved interpolation error messages — clearer errors for malformed string interpolation.
- Handler variable types — handler vars are now correctly typed as
interface{}at the Go level. - Variable shadowing in def — reading top-level vars before shadowing them inside a
defnow works correctly.
Other
- New
var-shadowlinter rule. - Implicit return documentation.
- Updated
docs/gobridge.mdwith full GoOpaque reference. - New
examples/require_go_qt/— Qt6 GUI in pure Rugo.
v0.20.0
New Features
require Go modules directly
Rugo scripts can now require Go packages — the compiler introspects the source, discovers bridgeable functions, and makes them available automatically:
require "mymod"
puts mymod.greet("world")
require "github.com/user/rugo-slug@v1.0.0" as slug
slug.make("Hello World!")Works with local directories, remote git repos, sub-packages, with, as aliases, rugo build, and rugo doc.
Go structs in require'd modules
Go modules can now expose structs. Create instances, read and write fields with dot syntax, call methods, and pass structs back to Go functions:
require "mymod"
c = mymod.new_config("app", 8080)
c.port = 9090
puts c.summary()Methods are discovered at compile time and dispatched through generated switch statements — no reflection. type_of() and rugo doc work with structs too.
Integer range iteration and range() builtin
For-loops now support iterating over integers:
for i in 100
puts i
end
# prints 0, 1, ..., 99
for i in range(5, 20)
puts i
end
# prints 5, 6, ..., 19range() also works as a standalone function to create arrays:
arr = range(10)
# [0, 1, 2, ..., 9]Module visibility with underscore prefix
Functions prefixed with _ are now private to their module:
# In mymod.rugo
def _helper() # private — not callable from outside
return "internal"
endCalling a private function from outside produces a compile error. Private functions are also hidden from rugo doc output.
Performance
Range for-loops now emit pure native Go arithmetic — same performance as while loops and equivalent Go code:
| Loop style | v0.19.0 | v0.20.0 |
|---|---|---|
for i in 10000 |
N/A | 2.0 µs/op |
for i in range(0, 10000) |
N/A | 2.0 µs/op |
while loop (10k iterations) |
2.0 µs/op | 2.0 µs/op |
The type inference engine now recognizes integer ranges, so loop variables and accumulators stay as native int instead of boxing through interface{}.
Bug Fixes
- Struct error messages — Error messages no longer leak internal wrapper type names. Shows
undefined method .Add() on Counterinstead of*main.rugo_struct_sm_Counter. /vNmodule paths —go.modplaceholder versions now match major version suffixes correctly (e.g.v3.0.0-...for/v3modules).- Value-type struct returns — Methods returning struct values (not pointers) now work correctly.
rugo evalerror paths — Showseval:1instead of/tmp/rugo-eval-XXXX/eval.rugo:1in error messages.rugo docindex — Module index now shows only the summary line instead of the full multi-line description.
Documentation
- New progressbar example using require'd Go modules.
- Added
astmodule documentation.
v0.19.0
Rugo v0.19.0
New Features
Multi-return values and array destructuring
Go bridge functions that return multiple values now return arrays, and a new destructuring syntax unpacks them into variables:
before, after, found = strings.cut("key=value", "=")
dir, file = filepath.split("/home/user/test.txt")
a, b, c = [10, 20, 30]Works with any expression returning an array — function calls, bridge multi-return functions, or plain array literals.
Automated bridge generation (rugo dev bridgegen)
New developer tool that scaffolds Go bridge packages automatically from Go's standard library:
rugo dev bridgegen math # generate gobridge/math_gen.go
rugo dev bridgegen --dry-run os # preview without writing
make bridgegen # regenerate everything
Uses go/types to enumerate exported functions, classify them by bridgeability, and generate complete bridge files with snake_case names and smoke tests. Bridge coverage is now 19 packages with 400 functions.
Expanded math bridge (67 functions)
The math bridge now covers 67 functions including hypot, trunc, round_to_even, copysign, fma, sinh, cosh, tanh, sincos, ldexp, and more.
New bridge packages: path and html
- path —
base,clean,dir,ext,is_abs,join,match,split - html —
escape_string,unescape_string
Sandbox env: permission
Sandboxed scripts can now restrict which environment variables are visible:
sandbox env: ["PATH", "HOME"]
sandbox ro: ["/etc"], env: ["PATH", "HOME"], connect: [443]When env: is specified, all env vars are cleared and only the listed ones are preserved. Also available via CLI: rugo run --sandbox --env PATH.
rugo doc disambiguation
Names like os and json exist as both a stdlib module and a bridge package. rugo doc now detects the conflict and asks you to be explicit:
rugo doc use:os # stdlib module
rugo doc import:os # bridge package
Unambiguous names continue to work without a prefix.
Documentation
- New Rugo by Example guide with practical examples and verified outputs.
- Rewritten gobridge.md reflecting the current auto-generation architecture.