A lightweight scripting language and interpreter implemented in C# (.NET 8), designed for embedding, scripting, and experimenting with language design.
- Async functions with
await— coroutine-style async execution - Promise chaining via
->then()and->error()on any async call - First-class functions — anonymous functions (
fn(...)) as values and arguments - Destructuring — array and object unpacking into local bindings
- Switch expressions — pattern-matching expressions that yield a value
- Error handling —
try/catchblocks, including acrossawaitboundaries - Data types — arrays
[...], objects{...}, numbers, strings, booleans - Loops —
whileloops with full expression support - Closures — functions capture their enclosing scope
- Embeddable — clean C# core designed to be hosted in other applications
fn add(a, b) async {
return a + b;
}
// Promise chaining with ->then() and ->error()
add(0, 1)
->then(fn(data) {
return data + 1;
})
->then(fn(data) {
return data * 2;
})
->then(cb);
add(0, "foccer")
->error(fn(data) {
print "THEN";
})
->error(cbError);
fn willThrow() async {
2 + "focc!";
}
fn caller() async {
try {
await willThrow();
} catch (err) {
print "caught:", err;
}
}
caller();
println(switch (val) {
0 => "zero",
1 => "one",
_ => "other"
});
const returnArray = fn() {
return [1, 2];
};
var [x, y] = returnArray();
local { a: b, c: d } = { a: "Dog", c: "Cat" };
print b, d;
Build and run with .NET 8:
dotnet build
dotnet run -- -r path/to/script.obi| Flag | Description |
|---|---|
-r, --run <path> |
Run an obiwan source file |
-t, --test |
Run the internal test suite |
-h, --help |
Show help |
To publish a self-contained binary (AOT-compiled):
dotnet publish -c ReleaseAOT publishing is enabled via
<PublishAot>true</PublishAot>in the project file.
The interpreter is implemented as a classic pipeline:
Source (.obi)
│
▼
Lexer.cs — tokenizes source into a stream of Token/TokenType
│
▼
Parser.cs — builds an AST (Ast.cs / AstType.cs)
│
▼
Compiler.cs — emits bytecode (OpCode.cs / Code.cs)
│
▼
Vm.cs — executes bytecode via stack frames (Frame.cs)
| File | Role |
|---|---|
Program.cs |
CLI entry point |
Lexer.cs |
Tokenizer |
Token.cs, TokenType.cs |
Token model |
Parser.cs |
AST construction |
Ast.cs, AstType.cs |
AST node types |
Compiler.cs |
Bytecode emitter |
OpCode.cs, OpCodeDebug.cs |
Instruction set |
Code.cs |
Compiled code object |
Vm.cs |
Virtual machine / interpreter |
Frame.cs |
Call-stack frame |
Future.cs, FutureState.cs |
Async promise model |
Cell.cs |
Closure cell (captured variable) |
ObiwanValue.cs, ValueType.cs |
Runtime value types |
Symbol.cs, SymbolTable.cs |
Name resolution |
ScopeType.cs, LookupDetail.cs |
Scope metadata |
State.cs |
VM state |
TryBlock.cs |
Exception-handling frame |
ErrorHandler.cs |
Error dispatch |
InvalidSwitchValueException.cs, ObiwanCompileError.cs |
Error types |
IBuiltin.cs |
Builtin function interface |
Position.cs |
Source position tracking |
tests/lang.obi, tests/import.obi |
Example obiwan programs |
test.js |
Test harness (JavaScript) |
obiwan.csproj |
Project file (net8.0, AOT) |
// line comment
/* block
comment */
var x = 10;
const y = 20;
local z = 30; // block-scoped
fn add(a, b) {
return a + b;
}
// anonymous
var double = fn(x) { return x * 2; };
// async
fn fetch(url) async {
return await request(url);
}
if (cond) { ... } else { ... }
while (g < 10) { g = g + 1; }
switch (val) {
case 1:
case 2: { println("1 or 2"); }
default: println("other");
}
var label = switch (n) {
0 => "zero",
1 => "one",
_ => "other"
};
var arr = [1, 2, 3, 4, 5];
var obj = {
Hello: "World",
add: fn(a, b) { return a + b; }
};
var [a, b] = someArray();
local { key: alias } = someObject();
fn doWork() async {
var result = await someAsyncFn();
return result;
}
asyncFn()
->then(fn(data) { return data + 1; })
->then(callback)
->error(errorHandler);
try {
await riskyFn();
} catch (err) {
print "Error:", err;
}
MIT License — Copyright © 2026 Philipp Andrew Redondo
See LICENSE for full text.