1. Scope and Sources
This page is the implementation-facing summary. Canonical normative specifications are in:
SPECS/language.md, SPECS/interpreter.md, SPECS/stream.md, and SPECS/process.md.
language.md: parser grammar and core expression semantics.interpreter.md: runtime behavior and evaluator details.stream.md: stream value kinds, pipeline semantics, and stream members.process.md: process handles, proc/run APIs, and lifecycle guarantees.
2. Grammar and Parsing
Karl is expression-based. Assignments, loops, and recover blocks are expressions that produce values. Parsing is Pratt-style with explicit operator precedence.
2.1 Core Grammar (EBNF)
Canonical source: SPECS/language.md. This EBNF is the practical parser-facing subset used by docs readers.
program = { statement } ;
statement = let_stmt | expr_stmt ;
let_stmt = "let" pattern "=" expr [ ";" ] ;
expr_stmt = expr [ ";" ] ;
expr = if_expr
| match_expr
| for_expr
| lambda_expr
| loop_ctrl
| assign ;
if_expr = "if" expr block [ "else" ( if_expr | block | expr ) ] ;
match_expr = "match" expr "{" { match_arm } "}" ;
match_arm = "case" pattern [ "if" expr ] "->" expr ;
for_expr = "for" expr [ "with" for_bindings ] block [ "then" then_block ] ;
for_bindings = binding { "," binding } ;
binding = pattern "=" expr ;
then_block = block | expr ;
lambda_expr = binder "->" expr ;
binder = pattern | "(" [ pattern { "," pattern } ] ")" ;
loop_ctrl = "break" [ expr ] | "continue" ;
assign = logic_or | lvalue assign_op expr ;
lvalue = IDENT { ( "." IDENT | "[" expr "]" ) } ;
assign_op = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
logic_or = pipe_expr { "||" pipe_expr } ;
pipe_expr = logic_and { "|" logic_and } ;
logic_and = equality { "&&" equality } ;
equality = comparison { ( "==" | "!=" | "eqv" ) comparison } ;
comparison = range { ( "<" | "<=" | ">" | ">=" ) range } ;
range = add [ ".." add [ "step" add ] ] ;
add = mul { ( "+" | "-" ) mul } ;
mul = unary { ( "*" | "/" | "%" ) unary } ;
unary = ( "!" | "-" ) unary
| wait_expr
| import_expr
| spawn_expr
| postfix ;
wait_expr = "wait" unary ;
import_expr = "import" STRING ;
spawn_expr = ( "&" | "spawn" ) spawn_target ;
spawn_target = call_expr
| pipe_expr
| "{" [ call_expr { "," call_expr } ] "}" ;
postfix = call_expr [ "?" expr ] ;
call_expr = primary { call | member | index | inc_dec } ;
call = "(" [ expr { "," expr } [ "," ] ] ")" ;
member = "." ( IDENT | "then" ) ;
index = "[" expr "]" ;
inc_dec = "++" | "--" ;
primary = literal | IDENT | "_" | "(" expr ")" | block | object | array | query_expr | race_expr | struct_init ;
block = "{" { statement } [ expr ] "}" ;
object = "{" [ object_entry { "," object_entry } [ "," ] ] "}" ;
object_entry = IDENT [ ":" expr ] | "..." expr ;
array = "[" [ expr { "," expr } [ "," ] ] "]" ;
race_expr = ( "!&" | "race" ) "{" [ call_expr { "," call_expr } ] "}" ;
query_expr = "from" IDENT "in" expr { "where" expr } [ "orderby" expr ] "select" expr ;
struct_init = IDENT object ;
pattern = "_" | literal | IDENT | range_pattern | object_pattern | array_pattern | tuple_pattern ;
object_pattern = "{" [ pattern_entry { "," pattern_entry } [ "," ] ] "}" ;
pattern_entry = IDENT [ ":" pattern ] ;
array_pattern = "[" [ pattern { "," pattern } ] [ "," "..." pattern ] [ "," ] "]" ;
tuple_pattern = "(" [ pattern { "," pattern } ] [ "," ] ")" ;
range_pattern = literal ".." literal ;
literal = NUMBER | STRING | CHAR | "true" | "false" | "null" | "()" ;
2.2 Notable parser constraints
&/spawntargets must be call expressions or stream pipeline expressions.!&/raceexpects a group of call expressions.|is reserved for stream pipelines.proc(...)starts immediately and& proc(...)is parse-rejected.- Object-vs-block disambiguation follows top-level object markers (colon/spread/trailing comma).
3. Evaluation Semantics
- Left-to-right evaluation for function args and infix operands.
&&and||short-circuit.forreturns expression values viathenor explicitbreak value.==is identity-oriented for composite values;eqvis deep structural equality.- Division by zero is runtime error (recoverable by
? { ... }).
4. Concurrency Model
let t = & (() -> work())()
let u = spawn (() -> work())() // alias
let value = wait t
let first = wait !& {
(() -> slow())(),
(() -> fast())(),
}
- Tasks are first-class values.
waitaccepts<task>and<process>.- Channels are explicit sync points:
send,recv,done. - Cancellation propagates through task/process wait points as recoverable
canceled.
5. Stream Model
Streams are lazy and pull-driven. A pipeline is executed by attaching a sink, or by member reads on stream values.
5.1 Stream value kinds
<stream-source>,<stream-stage>,<stream-sink>,<stream-plan>- Low-level handles:
<stream-reader>,<stream-writer>
5.2 Pipe operator
source | stagereturns<stream-plan>(lazy).source | ... | sinkexecutes immediately in current task.- Runtime rejects non-stream operands for
|.
5.3 Stream member reads
let s = read("events.log", { type: TEXT, }) | lines()
for true with pair = s.read() {
let [line, eof] = pair
if eof { break () }
log(line)
pair = s.read()
} then ()
- First
s.read()lazily opens a cursor for that stream value instance. - After EOF,
s.read()returns[null, true]consistently. s.close()closes early and transitions to EOF state.- Concurrent
s.read()on same stream instance isstream_state.
5.4 Backpressure
Sinks pull from upstream. Slow sinks slow the whole chain, preventing unbounded upstream drift. Runtime must not add implicit buffering that breaks this property.
6. Process Model
Process APIs expose external command execution as first-class handles.
proc is non-blocking handle creation; run is blocking capture convenience.
let p = proc("kubectl", "logs", "-f", "deploy/api", {
stdout: PIPE,
stderr: NULL,
stdoutType: TEXT,
})
let [line, eof] = p.stdout.read()
let st = wait p
6.1 Process forms
proc(spec, opts?) -> <process>proc(command, ...args, opts?) -> <process>run(spec, opts?) -> RunStatusrun(command, ...args, opts?) -> RunStatus
6.2 Process members
p.pid,p.runningp.stdin/p.stdout/p.stderronly when mode isPIPEp.abort(),p.kill(),p.signal(name)wait preturns ProcessStatus (cached after completion)
7. Error Model
Karl uses explicit recovery with postfix ?.
Recover blocks receive implicit error with kind and message.
let cfg = fromJson(raw) ? {
log(error.kind, error.message)
{ mode: "safe", }
}
fail("msg")creates recoverable error kindfail.exit(...)is terminal and not recovered.- Streams and processes define dedicated recoverable kinds (
stream_*,process_*).
8. Runtime Availability
| Area | CLI Native | Bench (WASM) |
|---|---|---|
| Core language / arrays / maps / strings | Yes | Yes |
| Tasks / channels / race | Yes | Yes |
| Streams from channels | Yes | Yes |
Filesystem (readFile, writeFile, read(...)) | Yes | No |
Processes (proc, run, exec sink) | Yes | No |
| SQL built-ins | Yes | No |
signalWatch | Yes | No |
httpServe server | Yes | No |
9. Current Constraints
- Stream plans are linear DAG paths; branch fan-out is modeled via explicit primitives (
tee,partition). partition(...)branch sources are single-consumer.- Unknown fields in process options/spec objects are rejected (strict parsing).
- Process stdio properties throw
process_statewhen mode is notPIPE.