go build -o rugo .
go test ./... -count=1
# Full suite (Go tests + all examples)
rugo run script/testUse emit to inspect generated Go code when debugging:
go run . emit script.rgEvery new feature must include:
-
End-to-end RATS tests (
rats/) — this is the most important part. Add test cases to an existing.rtfile or create a new one with fixtures inrats/fixtures/. Cover both happy paths and error cases. -
An example script (
examples/) — a self-contained.rugofile demonstrating the feature. Examples are run byscript/testand serve as living documentation. -
Language docs update (
docs/language.md) — document the syntax, semantics, and any edge cases. -
Module docs (
docs/mods.md) — if the feature involves a new or modified module.
Know which stage you're modifying:
| Stage | File(s) | Notes |
|---|---|---|
| Preprocessor | ast/preprocess.go |
Runs before parsing. 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. |
Regenerate the parser after grammar changes:
egg -o parser.go -package parser -start Program -type Parser -constprefix Rugo rugo.ebnfFollow the existing pattern in docs/mods.md. Each module needs:
modules/mymod/mymod.go(registration)modules/mymod/runtime.go(implementation,//go:build ignore)- Blank import in
main.go
- Forgetting to add new keywords to the preprocessor's known sets → they get treated as shell commands.
- Editing
parser.godirectly instead ofrugo.ebnf. - Skipping RATS tests — if it's not tested end-to-end, it's not done.
The ast and compiler packages expose a public API for tooling (linters, formatters, refactoring tools) that need AST access without full compilation. AST types and parsing live in ast/, code generation and build orchestration live in compiler/.
c := &ast.Compiler{}
// Parse a file into an AST without compiling to Go.
prog, err := c.ParseFile("script.rugo")
// Parse raw source code.
prog, err := c.ParseSource(`puts("hello")`, "script.rugo")Every Statement node has StmtLine() (start line) and StmtEndLine() (end line). Block statements (FuncDef, TestDef, BenchDef, IfStmt, WhileStmt, ForStmt) span multiple lines; non-block statements have EndLine == SourceLine.
for _, s := range prog.Statements {
if fn, ok := s.(*ast.FuncDef); ok {
fmt.Printf("def %s: lines %d-%d\n", fn.Name, fn.StmtLine(), fn.StmtEndLine())
}
}The Program.RawSource field contains the original source before preprocessing (comment stripping, sugar expansion). Use it to correlate comments with AST nodes by line number:
prog, _ := c.ParseFile("lib.rugo")
lines := strings.Split(prog.RawSource, "\n")
for _, s := range prog.Statements {
if fn, ok := s.(*ast.FuncDef); ok {
// Check the line above for a doc comment
if fn.StmtLine() >= 2 && strings.HasPrefix(strings.TrimSpace(lines[fn.StmtLine()-2]), "#") {
fmt.Printf("def %s has a doc comment\n", fn.Name)
}
}
}ti := compiler.Infer(prog)
// ti.FuncTypes["add"].ReturnType == compiler.TypeInt
// ti.ExprType(expr) returns the inferred type of any expression
// ti.VarType("funcName", "varName") returns variable types per scope// Walk all statements (including nested ones inside blocks).
// Return false to skip children of the current statement.
compiler.WalkStmts(prog, func(s ast.Statement) bool {
// called for every statement in the tree
return true // return false to skip children
})
// Walk all expressions. Returns true on first match.
compiler.WalkExprs(prog, func(e ast.Expr) bool {
_, isCall := e.(*ast.CallExpr)
return isCall // stops on first call expression found
})