Skip to content
 
 

Repository files navigation

C+- Language and Virtual Machine

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.


Compilation Pipeline

Source
  → Lexer
  → Parser (AST)
  → Bytecode Generator
  → Virtual Machine

Stages

  • 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 ENTER instructions 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


Lexical Structure

Keywords

void int float true false
if else while for break continue return
malloc copy println scan

Delimiters

{ } ( ) [ ] , .

Operators

+ - * / %
= += -= *= /= %=
== != < > <= >=
&& || !

Other Rules

  • Identifiers follow [A-Za-z_][A-Za-z0-9_]*

  • Literals include decimal int32 and decimal float

  • Comments

    • // line comments
    • /* ... */ block comments
  • Whitespace is ignored except as token separation


Types and Values

Scalar Types

  • 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

Array Types

  • int[], float[]
  • Heap-allocated and zero-initialized
  • Reference counted
  • Assignments update reference counts

Booleans

  • Represented as int
  • 0 is false
  • non-zero is true

Program Structure

program   ::= function*
function  ::= type IDENT "(" params? ")" block
params    ::= param ("," param)*
param     ::= type IDENT
block     ::= "{" statement* "}"

Types

int
int[]
float
float[]
void

Entry Point

int main()
float main()

The return value of main is printed after execution


Expressions and Precedence

From lowest to highest precedence

  1. Assignment = += -= *= /= %=

  2. Logical OR || without short-circuit

  3. Logical AND && without short-circuit

  4. Equality == !=

  5. Comparison < <= > >=

  6. Additive + -

  7. Multiplicative * / %

  8. Unary ! -

  9. Postfix function call array indexing .copy() parentheses

Allowed Expressions

  • literals
  • identifiers
  • true, false
  • parenthesized expressions
  • unary -x, !x
  • arithmetic and comparisons with matching types
  • logical &&, || for int
  • function calls
  • array indexing
  • malloc<T>(n)
  • ref.copy()
  • println(expr)
  • scan(ident)

Evaluation order is left to right


Statements

Declarations and Assignments

decl        ::= type IDENT "=" expr ";"
assign      ::= target assign-op expr ";"
target      ::= IDENT | IDENT "[" expr "]"
assign-op   ::= =  +=  -=  *=  /=  %=

Control Flow

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

  • return without an expression yields

    • 0 for int
    • 0.0 for float
  • Missing for condition means an infinite loop

  • break and continue are only valid inside loops


Scope and Lifetime

  • Lexical scoping via { }

  • All variables are local

  • Variables must be initialized

  • On scope or frame exit

    • reference values are decremented

Memory and Reference Counting

Allocation

malloc<int>(n)
malloc<float>(n)

Errors occur if

  • n < 0
  • out of memory

Arrays are zero-initialized

Reference Semantics

  • 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

Operation Semantics

  • int arithmetic

    • int32_t
    • divide or modulo by zero is an error
  • float arithmetic

    • IEEE 754
    • divide by zero is an error
  • logical not

    • !x returns 1 if x == 0
    • otherwise 0
  • Comparisons and logical operators return int

  • && and || evaluate both operands


VM Bytecode Summary

Stack Values

value_t = int | float | ref

Stack Operations

PUSH_I32
PUSH_F32
POP
DUP
SWAP

Frames and Locals

ENTER n
LEAVE
LOAD_LOCAL
STORE_LOCAL
LOAD_LOCAL_F32
STORE_LOCAL_F32

Globals

LOAD_GLOBAL
STORE_GLOBAL
LOAD_GLOBAL_F32
STORE_GLOBAL_F32

Arithmetic and Logic

ADD SUB MUL DIV MOD NEG
ADD_F32 SUB_F32 MUL_F32 DIV_F32 NEG_F32
CMP_*
CMP_*_F32
AND OR NOT

Control Flow

JMP
JMP_IF_TRUE
JMP_IF_FALSE

Calls

CALL
RET

Heap Operations

ALLOC_I32
ALLOC_F32
LOAD_ARR_I32
STORE_ARR_I32
LOAD_ARR_F32
STORE_ARR_F32
COPY_REF
INC_REF
DEC_REF

I/O

PRINTLN
PRINTLN_F32
SCAN_I32
SCAN_F32

JIT

  • 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

Runtime Errors

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


Examples

Float Arithmetic

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 Arrays

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;
}

Mixed int and float

int main() {
  float v = 3.5;
  if (v > 2.0) {
    println(v);
  }
  return 0;
}

Project Layout

compiler/     lexer, parser, AST, bytecode generator
VM/           runtime state, interpreter, JIT
cli/          cpm CLI
benchmarks/
compiler/tests/
VM/tests/
docs/         language specification

Build Prerequisites

  • C++20 compiler
  • Meson 1.1 or newer
  • Ninja
  • Threads
  • spdlog
  • GTest
  • Bundled asmjit subproject with AArch64 enabled

Build and Test

meson setup build --buildtype=debug
meson compile -C build
meson test -C build

Using the CLI

./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 --logs
  • main must exist
  • The program return value is printed after execution
  • Also JIT can be disabled at build time using
LANG_VM_DISABLE_JIT

Execution Model Recap

  • 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

Version

Project version: 1.0.0

About

Simple programming language based on C

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages