C+- is a compact C-like language compiled into bytecode for its own stack-based virtual machine. Programs always run correctly in the interpreter, while hot functions may be JIT-compiled with a guaranteed interpreter fallback.
Source
→ Lexer
→ Parser (AST)
→ Bytecode Generator
→ Virtual Machine
-
Lexer Produces a token stream with source positions
-
Parser Builds an abstract syntax tree for functions, statements, and expressions
-
Bytecode Generator Emits one bytecode chunk per function Inserts
ENTERinstructions for locals Patches jump targets -
Virtual Machine Stack-based execution model with
- value stack
- call-frame stack
- reference-counted heap for arrays
- optional JIT for hot functions
There is no separate semantic analysis pass Errors surface during code generation or at runtime
void int float true false
if else while for break continue return
malloc copy println scan
{ } ( ) [ ] , .
+ - * / %
= += -= *= /= %=
== != < > <= >=
&& || !
-
Identifiers follow
[A-Za-z_][A-Za-z0-9_]* -
Literals include decimal
int32and decimalfloat -
Comments
//line comments/* ... */block comments
-
Whitespace is ignored except as token separation
-
int
- 32-bit signed integer
/and%are integer operations- divide or modulo by zero is a runtime error
-
float
- IEEE 754 single precision
- divide by zero is a runtime error
int[],float[]- Heap-allocated and zero-initialized
- Reference counted
- Assignments update reference counts
- Represented as
int 0is false- non-zero is true
program ::= function*
function ::= type IDENT "(" params? ")" block
params ::= param ("," param)*
param ::= type IDENT
block ::= "{" statement* "}"
int
int[]
float
float[]
void
int main()
float main()
The return value of main is printed after execution
From lowest to highest precedence
-
Assignment
= += -= *= /= %= -
Logical OR
||without short-circuit -
Logical AND
&&without short-circuit -
Equality
== != -
Comparison
< <= > >= -
Additive
+ - -
Multiplicative
* / % -
Unary
! - -
Postfix function call array indexing
.copy()parentheses
- literals
- identifiers
true,false- parenthesized expressions
- unary
-x,!x - arithmetic and comparisons with matching types
- logical
&&,||forint - function calls
- array indexing
malloc<T>(n)ref.copy()println(expr)scan(ident)
Evaluation order is left to right
decl ::= type IDENT "=" expr ";"
assign ::= target assign-op expr ";"
target ::= IDENT | IDENT "[" expr "]"
assign-op ::= = += -= *= /= %=
if ::= "if" "(" expr ")" statement ("else" statement)?
while ::= "while" "(" expr ")" statement
for ::= "for" "(" init cond ";" step ")" statement
return ::= "return" expr? ";"
break ::= "break" ";"
continue ::= "continue" ";"
Rules
-
Conditions use integer truthiness
-
returnwithout an expression yields0forint0.0forfloat
-
Missing
forcondition means an infinite loop -
breakandcontinueare only valid inside loops
-
Lexical scoping via
{ } -
All variables are local
-
Variables must be initialized
-
On scope or frame exit
- reference values are decremented
malloc<int>(n)
malloc<float>(n)
Errors occur if
n < 0- out of memory
Arrays are zero-initialized
-
Assigning a reference
- decrements the old reference
- increments the new reference
-
copy()- increments the reference count
- returns the same reference
- error on non-reference
-
Array indexing
- zero-based
- bounds checked
-
int arithmetic
int32_t- divide or modulo by zero is an error
-
float arithmetic
- IEEE 754
- divide by zero is an error
-
logical not
!xreturns1ifx == 0- otherwise
0
-
Comparisons and logical operators return
int -
&&and||evaluate both operands
value_t = int | float | ref
PUSH_I32
PUSH_F32
POP
DUP
SWAP
ENTER n
LEAVE
LOAD_LOCAL
STORE_LOCAL
LOAD_LOCAL_F32
STORE_LOCAL_F32
LOAD_GLOBAL
STORE_GLOBAL
LOAD_GLOBAL_F32
STORE_GLOBAL_F32
ADD SUB MUL DIV MOD NEG
ADD_F32 SUB_F32 MUL_F32 DIV_F32 NEG_F32
CMP_*
CMP_*_F32
AND OR NOT
JMP
JMP_IF_TRUE
JMP_IF_FALSE
CALL
RET
ALLOC_I32
ALLOC_F32
LOAD_ARR_I32
STORE_ARR_I32
LOAD_ARR_F32
STORE_ARR_F32
COPY_REF
INC_REF
DEC_REF
PRINTLN
PRINTLN_F32
SCAN_I32
SCAN_F32
- Functions become hot after approximately 1000 calls
- Hot functions may be JIT-compiled
- The interpreter remains authoritative
- JIT supports the full float opcode set
- Any JIT failure falls back to the interpreter
Examples include
- stack underflow or overflow
- instruction pointer out of bounds
- invalid locals or globals
- divide or modulo by zero
- negative allocation size
- invalid reference
- array bounds violation
- using a reference where a scalar is required
- calling
copy()on a non-reference - invalid input for
scan
Execution stops immediately on runtime error
float mix(float a, float b) {
if (a < b) return a * 2.0;
return b / 2.0;
}
float main() {
float x = 1.5;
float y = 2.5;
return mix(x, y);
}float main() {
float[] arr = malloc<float>(4);
arr[0] = 1.0;
arr[1] = 2.0;
arr[2] = 3.0;
arr[3] = 4.0;
float s = 0.0;
for (int i = 0; i < 4; i += 1) {
s += arr[i];
}
println(s);
return s;
}int main() {
float v = 3.5;
if (v > 2.0) {
println(v);
}
return 0;
}compiler/ lexer, parser, AST, bytecode generator
VM/ runtime state, interpreter, JIT
cli/ cpm CLI
benchmarks/
compiler/tests/
VM/tests/
docs/ language specification
- C++20 compiler
- Meson 1.1 or newer
- Ninja
- Threads
- spdlog
- GTest
- Bundled asmjit subproject with AArch64 enabled
meson setup build --buildtype=debug
meson compile -C build
meson test -C build./build/cpm build program.cpm -o program.bc
./build/cpm run program.cpm
./build/cpm run program.cpm --only-interp
./build/cpm run program.cpm --logsmainmust exist- The program return value is printed after execution
- Also JIT can be disabled at build time using
LANG_VM_DISABLE_JIT
- Stack-based virtual machine
- Value stack and call stack
- Reference-counted heap for arrays
- Interpreter is the source of truth
- JIT is optional and transparent
- Runtime errors terminate execution immediately
Project version: 1.0.0