Goal
Port KobraScript correctly and minimally to Rust, preserving the existing transpiler behavior (.ks → JavaScript source). The intent is instructional: the author should be able to read the Rust side-by-side with the original JavaScript and learn idiomatic Rust by comparison. All Rust best practices apply — lean on the borrow checker, algebraic data types, Result-based error handling, and zero-cost abstractions.
What KobraScript is today
KobraScript is a source-to-source compiler (transpiler). It does not interpret or execute code — it reads a .ks file and emits JavaScript, which kobra then pipes to node. The pipeline is:
.ks file ──► scanner ──► tokens ──► parser ──► AST ──► analyzer ──► codegen ──► JavaScript
| JS module |
Responsibility |
scanner.js |
Lexical analysis (streaming, line-by-line) → Token[] |
token.js |
Token struct (kind, lexeme, line, col) |
parser.js |
Recursive-descent parser → AST of entities/* nodes |
analyzer.js |
AnalysisContext with parent-chained symbol tables; semantic checks |
entities/*.js |
~30 AST node types, each with toString / analyze / generateJavaScript |
compiler.js |
Orchestrates the pipeline; CLI flags -t -a -o -i |
code-gen/js-beautifier.js |
Post-processes emitted JS with js-beautify |
kobra-cli.js / kobra |
CLI entry points (kobrac = transpile & print, kobra = transpile & run) |
Semantic checks performed today
return / leave only inside a function (isSubroutine context flag)
break / continue only inside a loop (looped context flag)
Notable codegen operator mappings
== → ===, != → !==, ~= → == (coerced), # → || (alt-OR)
** → Math.pow(a,b), -** → (1.0)/Math.pow(a,b)
is → (typeof a === b), ~? → typeof
: =: (swap) → b = [a, a = b][0] trick
say → console.log(...), loge → console.log(...)
close{args} (closure literal) → (function(args){...}(args))
Proposed Rust project structure
kobrascript-rs/
├── Cargo.toml
├── src/
│ ├── main.rs # CLI entry (kobrac equivalent): arg parsing, orchestration
│ ├── token.rs # TokenKind enum + Token struct
│ ├── scanner.rs # scan(source: &str) -> Result<Vec<Token>, CompileError>
│ ├── error.rs # CompileError enum, error reporting, error count
│ ├── ast.rs # Stmt / Expr enums + structs (the AST)
│ ├── parser.rs # Parser struct (recursive descent) -> Result<Program, CompileError>
│ ├── analyzer.rs # AnalysisContext, semantic checks
│ ├── codegen.rs # generate_js(&Program) -> String
│ └── beautify.rs # minimal JS pretty-printer (replaces js-beautify)
└── tests/ # integration tests mirroring test/kobra-code/*
Component-by-component plan
1. token.rs — Tokens as an enum
The JS Token is a plain object with a string kind. In Rust, model token kinds as an enum — this is the first big instructional win: the compiler proves the token set is exhaustive.
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
Dollar, DotDot, ColonColonSwap, // $ .. :=:
Plus, Minus, Star, Slash, Percent,
Pow, NegPow, // ** -**
Assign, PlusAssign, /* ... */
Eq, Neq, ApproxEq, Is, // == != ~= is
AndAnd, OrOr, Hash, // && || #
TildeQ, TildeBang, // ~? ~!
LParen, RParen, LBrace, RBrace, LBracket, RBracket,
Colon, Comma, Dot, Arrow, Semicolon, Bang,
Inc, Dec, New,
// Keywords
Fn, Close, Return, Leave, If, Else, Only, While, For,
Break, Continue, End, Say, Loge, New_,
// Literals (carry their lexeme)
NumLit(String), StrLit(String), BoolLit(bool),
NullLit, UndefinedLit, Id(String),
Eof,
}
#[derive(Debug, Clone)]
pub struct Token {
pub kind: TokenKind,
pub lexeme: String,
pub line: usize,
pub col: usize,
}
Instructional note: contrast this with JS where kind is a free-form string and match() does string compare. The Rust enum makes impossible token states unrepresentable.
2. scanner.rs — Lexer
Port scan() faithfully. Key differences from JS:
- JS streams the file line-by-line with
byline. In Rust, read the whole file into a String (simpler, idiomatic) and iterate lines with .lines().enumerate(). This is a deliberate simplification worth calling out.
- Replace the pile of regexes with
char-pattern matching (char::is_ascii_digit, manual peek/next over a Chars iterator or byte slice). This showcases Rust's explicit character handling vs JS regex.
- String-literal escape handling (
\n, \xNN, \uNNNN, \cX control chars) ports directly.
- Return
Result<Vec<Token>, CompileError> — no global mutable error counter. Each illegal char/number becomes an Err. This is the second major instructional contrast: Rust forces errors to be handled explicitly rather than mutating a module-global error.count.
pub fn scan(source: &str) -> Result<Vec<Token>, CompileError> {
let mut tokens = Vec::new();
for (i, line) in source.lines().enumerate() {
scan_line(line, i + 1, &mut tokens)?; // line is 1-indexed
}
tokens.push(Token::eof());
Ok(tokens)
}
3. ast.rs — The AST as algebraic data types
The JS codebase has ~30 entity classes, each with toString / analyze / generateJavaScript. In idiomatic Rust, model the AST as two enums (Stmt, Expr) with data-carrying variants, then implement behavior with match. This is the centerpiece of the port for instructional value — it shows sum types, Box for recursion, and exhaustive pattern matching replacing dynamic dispatch.
pub enum Stmt {
Declaration { name: String, init: Expr },
FnDecl { name: String, params: Vec<String>, body: Block },
If { conditionals: Vec<Conditional>, else_block: Option<Block> },
OnlyIf { conditional: Conditional, default: Option<Block> },
For { assignments: Vec<Stmt>, condition: Expr, after: Vec<Expr>, body: Box<Block> },
While { condition: Expr, body: Box<Block> },
Say(Expr),
Return(Expr),
Leave,
Break,
Continue,
ExprStmt(Expr),
}
pub enum Expr {
NumLit(String),
StrLit(String),
BoolLit(bool),
NullLit,
UndefinedLit,
Name(String),
Binary { op: TokenKind, left: Box<Expr>, right: Box<Expr>, wrapped: bool },
Unary { op: TokenKind, operand: Box<Expr> },
PostUnary { op: TokenKind, operand: Box<Expr> },
Call { callee: Box<Expr>, args: Vec<Expr> },
Index { target: Box<Expr>, index: Box<Expr> },
Dot { target: Box<Expr>, property: String },
FnLit { name: Option<String>, params: Vec<String>, body: Box<Block> },
ClosureLit { args: Vec<String>, body: Box<Block> },
ArrayLit(Vec<Expr>),
ObjectLit(Vec<Property>),
}
pub struct Block(pub Vec<Stmt>);
pub struct Conditional { pub condition: Expr, pub body: Block }
pub struct Property { pub key: Expr, pub value: Expr }
Why enums over a Node trait with structs? A trait-object approach mirrors the JS OOP design 1:1, but it's less idiomatic Rust and loses exhaustiveness checking. The enum + match approach is what a Rust practitioner would reach for first, and it makes the JS↔Rust contrast sharpest. (A trait-based variant could be shown in an appendix/alternative if desired.)
4. parser.rs — Recursive descent, encapsulated
Port parser.js directly. The JS parser uses module-global tokens and continuing mutable state. In Rust, encapsulate that in a Parser struct — another instructional contrast (no globals; state lives in the struct).
pub struct Parser {
tokens: Vec<Token>,
pos: usize,
continuing: bool,
}
impl Parser {
fn at(&self, kind: TokenKind) -> bool { ... }
fn at_any(&self, kinds: &[TokenKind]) -> bool { ... }
fn next_is(&self, kind: TokenKind) -> bool { ... }
fn match_(&mut self, kind: TokenKind) -> Result<Token, CompileError> { ... }
fn match_any(&mut self, kinds: &[TokenKind]) -> Result<Token, CompileError> { ... }
pub fn parse(tokens: Vec<Token>) -> Result<Program, CompileError> {
let mut p = Parser { tokens, pos: 0, continuing: false };
p.parse_program()
}
}
Each parseExp0..parseExp9 becomes a method. The precedence-climbing grammar maps over verbatim. Result propagates the first parse error (vs JS's "keep going after error").
5. analyzer.rs — Semantic analysis
Port AnalysisContext with an owned parent chain and a HashMap<String, ()> symbol table. The isSubroutine / looped flags become fields on the context. analyze walks the AST via match.
pub struct AnalysisContext<'a> {
parent: Option<&'a AnalysisContext<'a>>,
symbols: HashMap<String, ()>,
is_subroutine: bool,
looped: bool,
}
Checks preserved exactly:
Return / Leave outside a subroutine → CompileError
Break / Continue outside a loop → CompileError
The JS error.count global is replaced by collecting errors into a Vec<CompileError> (or returning on first error). Either is fine; collecting-all is closer to the JS "report everything" behavior.
6. codegen.rs — Emit JavaScript
Port each entity's generateJavaScript(state) as a match arm in fn gen_js(stmt, state) -> String / fn gen_expr(expr, state) -> String. The state struct carries continuing_declaration: bool exactly as in JS.
Operator mappings port verbatim (==→===, **→Math.pow, : =:→swap trick, etc.). Use format! macros where JS used util.format.
7. beautify.rs — Replace js-beautify
js-beautify is a JS library with no Rust equivalent. Two options:
- (Recommended, minimal) Emit already-indented JS during codegen (track depth in
state), so no post-processing beautifier is needed. This is cleaner than the JS approach and a nice instructional improvement.
- (Alternative) Write a tiny indentation-based pretty-printer that re-indents brace-delimited output.
Either keeps the port dependency-free (no JS runtime needed for formatting).
8. main.rs — CLI
Port kobra-cli.js. Use clap (or manual env::args parsing for minimal deps) to mirror the flags:
kobrac <file.ks> — transpile & print JS
-t — scan & print tokens, exit
-a — scan & parse & print AST, exit
-i — up to semantic analysis & print semantic graph, exit
-o — optimize (currently a no-op stub, as in JS)
.ks extension validation
The kobra run-mode (pipe to node) can be a separate binary target or a --run flag that shells out to node on the generated JS.
Testing strategy
Mirror the existing Mocha test suite using Rust's built-in #[test] + tests/ directory:
| JS test file |
Rust equivalent |
test-scanner.js |
Unit tests asserting exact token streams |
test-good-programs.js |
Integration test: every good-programs/*.ks compiles without error |
test-syntax-errors.js |
Every syntax-errors/*.ks produces a parse error |
test-semantic-errors.js |
Every semantic-errors/*.ks produces an analyze error |
Reuse the same .ks fixture files from test/kobra-code/ — copy them into the Rust crate's test fixtures so behavior is directly comparable. This is the validation gate: the Rust port must pass the same corpus.
Scope
In scope (minimal, correct):
- Scanner, parser, analyzer, codegen, CLI — full feature parity with the current JS transpiler.
- All good/syntax/semantic test fixtures passing.
Out of scope (deliberately):
- The
optimize() step (already a no-op stub in JS — keep as a no-op or document as future work).
- A JS runtime / interpreter (KobraScript transpiles; it does not interpret).
- The
hapi-server.ks / promise.ks examples that rely on JS host APIs — these compile (transpile) fine; "running" them is the host's job.
Key Rust idioms this port teaches (the instructional payoff)
- Enums with data replacing ~30 prototype classes — sum types & exhaustive
match.
Result<T, E> replacing the global mutable error.count — errors as values.
Box<T> for recursive AST types — ownership & heap allocation where needed.
- Encapsulated
Parser struct replacing module-global mutable state.
&str / String and char-level scanning replacing regex-heavy JS lexing.
- No
null/undefined — Option<T> for the optional else block, optional fn name, etc.
#[derive(Debug, Clone, PartialEq)] for cheap, correct trait impls.
- Borrow checker enforcing that the AST and context are threaded cleanly (no shared mutable globals).
Suggested implementation order
token.rs + error.rs (foundations)
scanner.rs + scanner tests (validate against test-scanner.js fixtures)
ast.rs (the type definitions)
parser.rs + syntax-error tests
analyzer.rs + semantic-error tests
codegen.rs + good-programs tests (transpile & compare output)
beautify.rs (or inline indentation in codegen)
main.rs CLI wiring
Each step is independently testable against the existing fixture corpus.
Goal
Port KobraScript correctly and minimally to Rust, preserving the existing transpiler behavior (
.ks→ JavaScript source). The intent is instructional: the author should be able to read the Rust side-by-side with the original JavaScript and learn idiomatic Rust by comparison. All Rust best practices apply — lean on the borrow checker, algebraic data types,Result-based error handling, and zero-cost abstractions.What KobraScript is today
KobraScript is a source-to-source compiler (transpiler). It does not interpret or execute code — it reads a
.ksfile and emits JavaScript, whichkobrathen pipes tonode. The pipeline is:scanner.jsToken[]token.jsTokenstruct (kind,lexeme,line,col)parser.jsentities/*nodesanalyzer.jsAnalysisContextwith parent-chained symbol tables; semantic checksentities/*.jstoString/analyze/generateJavaScriptcompiler.js-t -a -o -icode-gen/js-beautifier.jsjs-beautifykobra-cli.js/kobrakobrac= transpile & print,kobra= transpile & run)Semantic checks performed today
return/leaveonly inside a function (isSubroutinecontext flag)break/continueonly inside a loop (loopedcontext flag)Notable codegen operator mappings
==→===,!=→!==,~=→==(coerced),#→||(alt-OR)**→Math.pow(a,b),-**→(1.0)/Math.pow(a,b)is→(typeof a === b),~?→typeof: =:(swap) →b = [a, a = b][0]tricksay→console.log(...),loge→console.log(...)close{args}(closure literal) →(function(args){...}(args))Proposed Rust project structure
Component-by-component plan
1.
token.rs— Tokens as an enumThe JS
Tokenis a plain object with a stringkind. In Rust, model token kinds as an enum — this is the first big instructional win: the compiler proves the token set is exhaustive.2.
scanner.rs— LexerPort
scan()faithfully. Key differences from JS:byline. In Rust, read the whole file into aString(simpler, idiomatic) and iterate lines with.lines().enumerate(). This is a deliberate simplification worth calling out.char-pattern matching (char::is_ascii_digit, manualpeek/nextover aCharsiterator or byte slice). This showcases Rust's explicit character handling vs JS regex.\n,\xNN,\uNNNN,\cXcontrol chars) ports directly.Result<Vec<Token>, CompileError>— no global mutable error counter. Each illegal char/number becomes anErr. This is the second major instructional contrast: Rust forces errors to be handled explicitly rather than mutating a module-globalerror.count.3.
ast.rs— The AST as algebraic data typesThe JS codebase has ~30 entity classes, each with
toString/analyze/generateJavaScript. In idiomatic Rust, model the AST as two enums (Stmt,Expr) with data-carrying variants, then implement behavior withmatch. This is the centerpiece of the port for instructional value — it shows sum types,Boxfor recursion, and exhaustive pattern matching replacing dynamic dispatch.4.
parser.rs— Recursive descent, encapsulatedPort
parser.jsdirectly. The JS parser uses module-globaltokensandcontinuingmutable state. In Rust, encapsulate that in aParserstruct — another instructional contrast (no globals; state lives in the struct).Each
parseExp0..parseExp9becomes a method. The precedence-climbing grammar maps over verbatim.Resultpropagates the first parse error (vs JS's "keep going after error").5.
analyzer.rs— Semantic analysisPort
AnalysisContextwith an owned parent chain and aHashMap<String, ()>symbol table. TheisSubroutine/loopedflags become fields on the context.analyzewalks the AST viamatch.Checks preserved exactly:
Return/Leaveoutside a subroutine →CompileErrorBreak/Continueoutside a loop →CompileError6.
codegen.rs— Emit JavaScriptPort each entity's
generateJavaScript(state)as amatcharm infn gen_js(stmt, state) -> String/fn gen_expr(expr, state) -> String. Thestatestruct carriescontinuing_declaration: boolexactly as in JS.Operator mappings port verbatim (
==→===,**→Math.pow,: =:→swap trick, etc.). Useformat!macros where JS usedutil.format.7.
beautify.rs— Replacejs-beautifyjs-beautifyis a JS library with no Rust equivalent. Two options:state), so no post-processing beautifier is needed. This is cleaner than the JS approach and a nice instructional improvement.Either keeps the port dependency-free (no JS runtime needed for formatting).
8.
main.rs— CLIPort
kobra-cli.js. Useclap(or manualenv::argsparsing for minimal deps) to mirror the flags:kobrac <file.ks>— transpile & print JS-t— scan & print tokens, exit-a— scan & parse & print AST, exit-i— up to semantic analysis & print semantic graph, exit-o— optimize (currently a no-op stub, as in JS).ksextension validationThe
kobrarun-mode (pipe tonode) can be a separate binary target or a--runflag that shells out tonodeon the generated JS.Testing strategy
Mirror the existing Mocha test suite using Rust's built-in
#[test]+tests/directory:test-scanner.jstest-good-programs.jsgood-programs/*.kscompiles without errortest-syntax-errors.jssyntax-errors/*.ksproduces a parse errortest-semantic-errors.jssemantic-errors/*.ksproduces an analyze errorReuse the same
.ksfixture files fromtest/kobra-code/— copy them into the Rust crate's test fixtures so behavior is directly comparable. This is the validation gate: the Rust port must pass the same corpus.Scope
In scope (minimal, correct):
Out of scope (deliberately):
optimize()step (already a no-op stub in JS — keep as a no-op or document as future work).hapi-server.ks/promise.ksexamples that rely on JS host APIs — these compile (transpile) fine; "running" them is the host's job.Key Rust idioms this port teaches (the instructional payoff)
match.Result<T, E>replacing the global mutableerror.count— errors as values.Box<T>for recursive AST types — ownership & heap allocation where needed.Parserstruct replacing module-global mutable state.&str/Stringand char-level scanning replacing regex-heavy JS lexing.null/undefined—Option<T>for the optionalelseblock, optional fn name, etc.#[derive(Debug, Clone, PartialEq)]for cheap, correct trait impls.Suggested implementation order
token.rs+error.rs(foundations)scanner.rs+ scanner tests (validate againsttest-scanner.jsfixtures)ast.rs(the type definitions)parser.rs+ syntax-error testsanalyzer.rs+ semantic-error testscodegen.rs+ good-programs tests (transpile & compare output)beautify.rs(or inline indentation in codegen)main.rsCLI wiringEach step is independently testable against the existing fixture corpus.