CaspianJ GitHub issue
- Overview
- Core Principle
- Comments
- Expressions
- Statements
- Control Flow
- Blocks
- Function and Closure Definitions
- Return
- Exception Handling
- System Methods
- Documentation Statements
- Source Position Annotations
- Known Gaps
- Open Questions
Overview GitHub issue
vibecode
{"vibecode": { "section": "overview", "format": "CaspianJ", "alias": "caspj", "purpose": "canonical_runtime_format_for_caspian_programs", "not": "bytecode", "convention": "share_as_caspian_source_cjs_is_runtime_artifact", "bootstrap_note": "parser_must_be_written_directly_in_cjs" }}
CaspianJ (informally: caspj) is the canonical runtime format for Caspian programs. It is not bytecode — it is a full representation of the program as a JSON data structure. Caspian transpiles to CaspianJ for execution.
CaspianJ is a runtime artifact. By convention, code is shared as Caspian source, not as CaspianJ.
CaspianJ also serves as a canonical semantic intermediate between source forms. The current pipeline is Caspian source → CaspianJ → execution, but the architecture permits multiple source syntaxes (different parsers fanning in to CaspianJ) and multiple pretty-printers (different surface forms fanning out from CaspianJ). The project shipped with a single canonical surface (Caspian), but the multi-syntax option remains open — no spec change is required for an alternate-syntax parser to be added later. No such alternates are planned for v1.
The bootstrap parser must be written directly in CaspianJ, since Caspian cannot parse itself before the parser exists.
Core Principle GitHub issue
vibecode
{"vibecode": { "section": "core_principle", "value_receiver_form": "[receiver, method, arg1?, arg2?, ...]", "bwc_receiver_form": "[{bwc: name}, arg1?, arg2?, ...]", "receiver": "any_expression_variable_literal_sys_bwc_or_nested_call", "method": "string_naming_method_or_operator_required_for_value_receivers_omitted_for_bwc_receivers", "args": "zero_or_more_positional_expressions_spread_at_indices_3_plus; or_a_single_kwargs_hash_at_index_3_distinguishable_by_absence_of_expression_marker_key", "two_shapes": "value_receiver_shape_for_method_calls_operators_assignment; bwc_shape_for_built_in_commands_where_the_bwc_name_is_the_call" }}
Every statement begins with a receiver. The receiver determines the shape:
- Value receivers (variables, literals, system methods, nested calls) take a method name next, then zero or more positional args spread directly across the array:
[receiver, method, arg1?, arg2?, ...]. This covers method calls, operators, assignment. - Bwc receivers (
{"bwc": "name"}) don't take a method — the bwc name IS the call. Args spread the same way:[{"bwc": name}, arg1?, arg2?, ...]. This covers built-in commands likeputs,if,while,return,raise.
Positional arguments are each their own slot in the array. Each comma-separated expression in Caspian source becomes one slot in CaspianJ:
| Caspian | CaspianJ |
|---|---|
$foo.bar |
[{"var": "foo"}, "bar"] |
$foo.bar(a) |
[{"var": "foo"}, "bar", a] |
$foo.bar(a, b, c) |
[{"var": "foo"}, "bar", a, b, c] |
puts 'hi' |
[{"bwc": "puts"}, {"value": "hi"}] |
Keyword arguments are a single hash at index 3:
| Caspian | CaspianJ |
|---|---|
$foo.bar(name: 'X') |
[{"var": "foo"}, "bar", {"name": {"value": "X"}}] |
$foo.bar(name: 'X', age: 30) |
[{"var": "foo"}, "bar", {"name": {"value": "X"}, "age": {"value": 30}}] |
Distinguishing single positional from kwargs. When index 3 is a hash, the dispatcher tells which kind it is by checking for an expression-marker key: value, var, bwc, array, hash, call, closure, sys, etc. A hash that carries one of those keys is a single positional expression (e.g. a hash literal {"hash": [...]}); a hash without any expression-marker key is kwargs.
| Caspian | CaspianJ | Interpretation |
|---|---|---|
$foo.bar({key: 'X'}) |
[{"var": "foo"}, "bar", {"hash": [...]}] |
Single positional, hash literal |
$foo.bar(key: 'X') |
[{"var": "foo"}, "bar", {"key": {"value": "X"}}] |
Kwargs |
Positional args and kwargs don't mix in the same call (per Caspian's source-side rule — kwargs are all-or-nothing in a single call site).
Comments GitHub issue
vibecode
{"vibecode": { "section": "comments", "form": "{\"comment\": \"...\"}", "behavior": "no_op_ignored_by_interpreter", "placement": "anywhere_in_statement_array" }}
A {"comment": "..."} object anywhere in a statement array is a human-readable no-op. It is ignored by the interpreter.
[
{"comment": "greet the user"},
[{"var": "name"}, "=", {"value": "Jean-Luc"}]
]
Expressions GitHub issue
vibecode
{"vibecode": { "section": "expressions", "forms": { "literal": "{\"value\": ..., \"class\"?: \"uns/name\"}", "variable": "{\"var\": \"foo\"}", "ivar": "{\"ivar\": \"foo\"}", "varobj": "{\"varobj\": \"foo\"}", "sys": "{\"sys\": \"name\"}", "bwc": "{\"bwc\": \"name\"}", "array": "{\"array\": [...]}", "hash": "{\"hash\": [[key, expr], ...]}", "function": "{\"function\": {\"params\": [...], \"body\": [...]}}", "closure": "{\"closure\": {\"params\": [...], \"body\": [...]}}" }, "hash_note": "pairs_preserve_insertion_order" }}
Expressions are JSON objects that produce a value.
Literals GitHub issue
vibecode
{"vibecode": { "section": "literals", "form": "{\"value\": <json>, \"class\"?: <uns>}", "inferred_classes": {"json_string": "puck.uno/string", "json_integer": "puck.uno/integer", "json_decimal": "puck.uno/decimal", "json_true": "puck.uno/true", "json_false": "puck.uno/false", "json_null": "puck.uno/null"}, "explicit_class_field": "optional; names the puck UNS the value should be an instance of; engine calls that class's materializer to parse the JSON value", "primitives_omit_class": "the class field is omitted for primitives because the JSON type already determines it; including it is allowed but redundant", "non_primitives_require_class": "any value whose target class cannot be inferred from JSON type must name it explicitly via class field" }}
A literal is {"value": <json>} with an optional class field naming the UNS of the class the value should be an instance of.
For primitives, omit the class field. The engine infers the target class from the JSON type:
{"value": "hello"} // puck.uno/string
{"value": 42} // puck.uno/integer
{"value": 3.14} // puck.uno/decimal
{"value": true} // puck.uno/true
{"value": false} // puck.uno/false
{"value": null} // puck.uno/null
For non-primitives, include the class field. The named class's materializer is responsible for parsing the JSON value into an instance:
{"value": "2026-05-27", "class": "puck.uno/date"}
{"value": "550e8400-e29b-41d4-a716-...", "class": "puck.uno/uuid"}
{"value": {"lat": 47.6, "lon": -122.3}, "class": "geo.uno/point"}
The materializer is a class-level method (declared on the class that the literal targets) that accepts the raw JSON value and returns a constructed instance. Most user-defined classes won't declare one — values of those classes typically come from .new calls in source, not from CaspianJ literal expressions. Classes that do declare a materializer can appear as literals; those that don't, can't.
Mismatch handling. If class is specified and the JSON type doesn't match what the class's materializer accepts, the materializer raises. The engine doesn't second-guess the materializer; whatever it accepts is the contract.
Stack at instantiation. A literal produces an instance with its initial platter stack — the shadow class plus the named class plus any platters the class declares (truthiness, marker classes, etc.). The class field expresses the instantiation class, not a full platter stack. Programs that want more platters add them via .classes.add after materialization. Full-stack round-tripping of serialized state uses a different mechanism (see nulls.md § Serialization for the id-marker-in-classes-hash pattern).
Variables GitHub issue
{"var": "foo"} // $foo
{"ivar": "foo"} // @foo (%bucket['foo'])
{"varobj": "foo"} // $$foo
{"sys": "chain"} // %chain
Bare Word Commands GitHub issue
A {"bwc": "name"} defers lookup to the runtime. The interpreter resolves the name through the scope dispatcher to find the associated object and method. It is syntactic sugar — it does not expand in CaspianJ.
{"bwc": "puts"} // puts
{"bwc": "exit"} // exit
Array Literals GitHub issue
{"array": [{"value": 1}, {"value": 2}, {"value": 3}]}
Caspian equivalent: [1, 2, 3]
Hash Literals GitHub issue
Hashes are represented as an array of [key, expr] pairs to preserve insertion order.
{"hash": [["name", {"value": "Picard"}], ["rank", {"value": "Captain"}]]}
Caspian equivalent: {name: 'Picard', rank: 'Captain'}
Functions and Closures GitHub issue
{"function": {"params": ["a", "b"], "body": [stmt, ...]}}
{"closure": {"params": ["a", "b"], "body": [stmt, ...]}}
A function does not capture the outer scope. A closure does.
Statements GitHub issue
vibecode
{"vibecode": { "section": "statements", "forms": ["assignment", "method_calls", "function_calls", "bwc_calls", "operators"], "assignment_form": "[{\"var\": \"foo\"}, \"=\", expr]", "method_call_form": "[receiver, \"method\", {kw_args}]", "function_call_form": "[{\"var\": \"foo\"}, \"call\", {kw_args}]", "operator_form": "[receiver, \"op\", operand]" }}
Assignment GitHub issue
Assignment is the = operator — same [receiver, method, args] shape as every other method call. The dispatcher does not special-case =.
[{"var": "foo"}, "=", {"value": "hello"}]
Caspian equivalent: $foo = 'hello'
[{"var": "greeting"}, "=", [{"var": "foo"}, "+", {"value": " world"}]]
Caspian equivalent: $greeting = $foo + ' world'
How the dispatcher handles it. When {"var": "foo"} appears in receiver position, it materializes to an lvalue handle — a runtime value whose class has =, +=, -=, etc. as methods that write back to scope. In expression position ({"var": "foo"} as an argument or sub-expression), the same form materializes to the variable's current value.
Same JSON shape, two contexts: assignment target vs read. The dispatcher already knows which from where the expression sits in the parent statement (receiver at index 0 → handle; anywhere else → value). No special form, no setvar keyword, no four-element statement shape. Pure [receiver, method, args?] everywhere.
Method Calls GitHub issue
[{"var": "foo"}, "save"]
Caspian equivalent: $foo.save
[{"var": "foo"}, "greet", {"name": {"value": "Jean-Luc"}}]
Caspian equivalent: $foo.greet(name: 'Jean-Luc')
Chained calls — the receiver of the outer call is the result of the inner:
[[{"var": "foo"}, "bar"], "gup"]
Caspian equivalent: $foo.bar.gup
Function Calls GitHub issue
&foo calls the function object in $foo. This is a call method on the variable:
[{"var": "foo"}, "call"]
Caspian equivalent: &foo
[{"var": "foo"}, "call", {"name": {"value": "Picard"}}]
Caspian equivalent: &foo(name: 'Picard')
Bare Word Command Calls GitHub issue
[{"bwc": "puts"}, {"value": "hello world"}]
Caspian equivalent: puts 'hello world'
[{"bwc": "puts"}]
Caspian equivalent: puts
Operators GitHub issue
Operators are method calls. The left operand is the receiver, the operator is the method, the right operand is the argument:
[{"var": "foo"}, "==", {"value": "bar"}]
[{"var": "x"}, "+", {"value": 1}]
[{"var": "a"}, "&&", {"var": "b"}]
Caspian equivalents: $foo == 'bar', $x + 1, $a && $b
Control Flow GitHub issue
vibecode
{"vibecode": { "section": "control_flow", "constructs": ["if_elsif_else", "while"], "if_form": "[{\"bwc\": \"if\"}, {\"branches\": [...], \"else\": [...]}]", "while_form": "[{\"bwc\": \"while\"}, {\"cond\": expr, \"body\": [...]}]", "notes": ["branches_and_else_are_optional"] }}
If / elsif / else GitHub issue
[{"bwc": "if"}, {
"comment": "branches evaluated top to bottom; first matching 'when' wins",
"branches": [
{"when": [{"var": "rank"}, "==", {"value": "Captain"}],
"then": [[{"bwc": "puts"}, {"value": "Aye, captain"}]]},
{"when": [{"var": "rank"}, "==", {"value": "Commander"}],
"then": [[{"bwc": "puts"}, {"value": "Aye, commander"}]]}
],
"else": [[{"bwc": "puts"}, {"value": "Aye"}]]
}]
if ($rank == 'Captain')
puts 'Aye, captain'
elsif ($rank == 'Commander')
puts 'Aye, commander'
else
puts 'Aye'
end
branches and else are both optional. If both are absent or empty ([{"bwc": "if"}, {}]), the if is a no-op — nothing runs and the statement returns null. Not an error.
While GitHub issue
[{"bwc": "while"}, {
"cond": [{"var": "i"}, "<", {"value": 10}],
"body": [
[{"var": "i"}, "=", [{"var": "i"}, "+", {"value": 1}]]
]
}]
while ($i < 10)
$i = $i + 1
end
Break GitHub issue
vibecode
{"vibecode": { "section": "break_bwc", "form": "[{\"bwc\": \"break\"}, level_expr?]", "level_expr": "optional_integer_expression_default_1", "function_boundary": "does_not_escape_user_defined_functions_or_closures", "block_boundary": "DOES_escape_through_do_end_blocks_passed_to_method_calls", "history": "added_post_soft_lock_2026-05-17_as_deliberate_v1_addition", "see": "documentation/caspian/loops.md#break" }}
break exits the innermost enclosing loop. With an integer argument, exits N enclosing loops. Does not escape user-defined function or closure boundaries; does flow through do ... end blocks passed to methods like .each.
[{"bwc": "break"}]
Caspian equivalent: break
[{"bwc": "break"}, {"value": 2}]
Caspian equivalent: break 2
The level argument is any expression that evaluates to a positive integer — a literal, a variable, or a computed value:
[{"bwc": "break"}, {"var": "depth"}]
Caspian equivalent: break $depth
See loops.md § break for full semantics, including interaction with structural blocks and the open question about break $named_loop as a targeting alternative.
Blocks GitHub issue
vibecode
{"vibecode": { "section": "blocks", "form": "block key in args object", "structure": "{\"block\": {\"params\": [...], \"body\": [...]}}", "caspian_equivalent": "$items.each($item) do...end" }}
A block is a closure passed to a method call. It is attached to the call via a block key in the args object:
[{"var": "items"}, "each", {
"block": {
"params": ["item"],
"body": [[{"bwc": "puts"}, {"var": "item"}]]
}
}]
$items.each($item) do
puts $item
end
Function and Closure Definitions GitHub issue
vibecode
{"vibecode": { "section": "function_and_closure_definitions", "function_form": "{\"function\": {\"params\": [...], \"body\": [...]}}", "closure_form": "{\"closure\": {\"params\": [...], \"body\": [...]}}", "named_function_is": "assignment_of_function_to_var", "difference": "closure_captures_scope_function_does_not" }}
Since function &foo is sugar for $foo = function(...), a named function definition is just an assignment:
[{"var": "greet"}, "=", {
"function": {
"params": ["name", "rank"],
"body": [
[{"bwc": "return"}, [
[{"var": "rank"}, "+", {"value": " "}], "+", {"var": "name"}
]]
]
}
}]
function &greet($name, $rank) do
return $rank + ' ' + $name
end
A closure is identical but uses "closure" instead of "function":
[{"var": "greeter"}, "=", {
"closure": {
"params": ["name"],
"body": [[{"bwc": "puts"}, [{"var": "prefix"}, "+", {"var": "name"}]]]
}
}]
$greeter = closure($name) do
puts $prefix + $name
end
Return GitHub issue
vibecode
{"vibecode": { "section": "return", "form": "[{\"bwc\": \"return\"}, expr]", "no_value_form": "[{\"bwc\": \"return\"}]" }}
[{"bwc": "return"}, {"var": "result"}]
Caspian equivalent: return $result
Return with no value:
[{"bwc": "return"}]
Exception Handling GitHub issue
vibecode
{"vibecode": { "section": "exception_handling", "constructs": ["catch", "raise"], "catch_form": "[{\"var\": \"e\"}, \"=\", [{\"bwc\": \"catch\"}, {\"class\": ..., \"body\": [...]}]]", "raise_form": "[{\"bwc\": \"raise\"}, class_string_expr]" }}
catch GitHub issue
[{"var": "exception"}, "=", [{"bwc": "catch"}, {
"class": {"value": "borg.com/exception/assimilation"},
"body": [[{"var": "foo"}, "call"]]
}]]
$exception = catch('borg.com/exception/assimilation')
&foo
end
raise GitHub issue
[{"bwc": "raise"}, {"value": "borg.com/exception/assimilation"}]
Caspian equivalent: raise 'borg.com/exception/assimilation'
System Methods GitHub issue
vibecode
{"vibecode": { "section": "system_methods", "expression_form": "{\"sys\": \"name\"}", "call_pattern": "[receiver, method, args?]", "example_chain_set": "[{\"sys\": \"chain\"}, \"set\", {\"key\": ..., \"value\": ...}]" }}
System methods appear as expressions using {"sys": "name"} and follow the same [receiver, method, args?] call pattern:
[{"sys": "chain"}, "set", {"key": {"value": "user"}, "value": {"value": "picard"}}]
Caspian equivalent: %chain['user'] = 'picard'
[{"sys": "chain"}, "get", {"key": {"value": "user"}}]
Caspian equivalent: %chain['user']
Documentation Statements GitHub issue
vibecode
{"vibecode": { "section": "documentation_statements", "types": ["%vibecode", "%documentation"], "forms": { "vibecode": "{\"vibecode\": {...}}", "documentation": "{\"documentation\": {\"type\": \"text/markdown\", \"content\": \"...\"}}" }, "runtime_behavior": "no_op" }}
%vibecode, %comment, and other %documentation statements are saved as statement objects in the program array. They are no-ops at runtime.
vibecode
{"vibecode": {"purpose": "assign the active officer collection"}}
{"documentation": {"type": "text/markdown", "content": "## Notes\nSee the design doc."}}
Source Position Annotations GitHub issue
vibecode
{"vibecode": { "section": "source_position_annotations", "purpose": "preserve_caspian_source_line_numbers_through_transpilation_to_caspianj", "use_case": "include_line_numbers_in_jasmine_log_entries_and_error_messages", "shape": "optional_line_field_on_caspianj_nodes" }}
When Caspian source is transpiled to CaspianJ, line-number information from the original source is preserved so that downstream consumers (Jasmine logging, error messages, debuggers) can refer back to the source position of any executing code.
The mechanism: each CaspianJ node optionally carries a line annotation indicating the source line it came from.
{"line": 42, "var": "foo"}
{"line": 42, "value": 1}
[{"line": 42, "var": "greet"}, "=", {"line": 42, "function": {"params": ["name"], "body": [...]}}]
The transpiler populates line on every emitted node. The runtime preserves the annotation as it dispatches and can expose the current executing position via runtime introspection — used by Jasmine for log frame location fields (see jasmine.md), by error messages for "this error happened at line N," etc.
What gets annotated GitHub issue
Every node emitted from a Caspian-source transpile carries a line field. Granularity is per-statement at minimum and per-expression where reasonable — enough that any runtime position can resolve back to a source line.
CaspianJ-only origins GitHub issue
Code that originated as CaspianJ directly (no Caspian source) has no line field — there's nothing to annotate. Tools that inspect positions check whether line is present; if it isn't, the source position is genuinely unknown.
Open questions GitHub issue
- File identifier alongside line. Line numbers alone aren't enough to locate code; you also need to know which file. Probably a
filefield at the top of the CaspianJ program, or inherited from the runtime invocation context. - Column numbers. Probably nice-to-have; verbose. Could be a
colfield alongsideline. - Range annotations (start line + end line) for multi-line expressions. Probably more than needed for v1; single line is sufficient for most purposes.
- Generated code — code emitted by macros or DSLs may want to carry both an original-source position AND a generator-source position. Out of scope for v1.
Known Gaps GitHub issue
vibecode
{"vibecode": { "section": "known_gaps", "gaps": ["hash_key_order", "class_definitions_not_yet_designed_in_caspj"], "hash_key_order": "significant_two_hashes_equal_only_if_same_keys_same_values_same_order" }}
Hash key order GitHub issue
Caspian hashes have significant key order — {foo: true, bar: true} and {bar: true, foo: true} are distinct values. A compliant engine must preserve key insertion order through serialization and deserialization.
Open Questions GitHub issue
Class definitions GitHub issue
Class definitions in CaspianJ follow the same[receiver, method, args] pattern as everything else. The class body statements (field, inherits, function, etc.) are regular CaspianJ statements. The structure mirrors both Caspian source syntax and the Mikobase JSON class definition — no special format is needed.