- about jminus
- what you'll learn
- architecture overview
- quickstart
- prerequisites
- 1. clone and build
- 2. try the repl
- 3. run a program file
- 4. run tests
- language features
- variables and assignment
- arithmetic operations
- comparison operators
- output
- control flow
- if statements
- while loops
- blocks and scope
- project structure
- core components
- development guide
- building from source
- running tests
- debug mode
- repl commands
- architecture deep dive
- lexical analysis
- parsing
- code generation
- virtual machine
- bytecode instructions
- testing
- test philosophy
- running tests
- adding tests
- debugging
- common issues
- debug tools
- debug output
- contributing
- start
- development workflow
- code style
- areas for contribution
- license
- acknowledgments
- support
jminus is a small, educational programming language designed to demonstrate compiler construction principles. it features a complete toolchain from source code to execution, including:
- lexical analysis: custom tokenizer that converts source code into tokens
- parsing: recursive descent parser that builds an abstract syntax tree (ast)
- code generation: bytecode compiler that translates ast into stack-based instructions
- execution: virtual machine that interprets bytecode, plus a direct interpreter mode
- repl: interactive shell for quick testing and experimentation
this project demonstrates:
- compiler phases (lexing → parsing → code generation → execution)
- stack-based virtual machine architecture
- manual memory management in c (malloc/free)
- variable scope handling with environment chains
- error handling and diagnostics
- testing strategies for language implementations
source code → lexer → parser → ast → compiler → bytecode → vm → output
↓
interpreter → output
- gcc or clang compiler
- make build system
- bash shell (for running tests)
git clone https://github.com/joeyzhang-dev/jminus.git
cd jminus/jminus-interpreter
make./jminus-repl.exeexample session:
welcome to jminus repl
type:help for available commands.
jminus> let x = 5;
jminus> yap(x);
5
jminus>:interp
switched to interpreter mode
jminus> let y = 10;
defined variable y = 10
jminus> yap(y);
yap output: 10
jminus>:exit
goodbye
./jminus.exe start.jminusmake testexpected output:
=== compiling and running all tests ===
[*] building compiler_tests...
[*] running compiler_tests...
let-statement compiles to bc_define_var
assignment compiles to bc_set_var
yap-expression compiles to bc_print
if-statement compiles to jumps
while-statement compiles to jumps
all compiler tests passed!
[*] building lexer_tests...
[*] running lexer_tests...
lexer_tests passed
[*] building parser_tests...
[*] running parser_tests...
parser_tests passed
[*] building vm_tests...
[*] running vm_tests...
arithmetic and print
variable define/assign
if-statement (true branch)
all vm tests passed!
=== all tests passed! ===
// variable declaration
let x = 42;
// variable assignment
x = 99;
// multiple variables
let a = 1;
let b = 2;
let c = a + b;
let x = 10;
let y = 3;
// basic arithmetic
let sum = x + y; // 13
let diff = x - y; // 7
let product = x * y; // 30
let quotient = x / y; // 3
let a = 5;
let b = 10;
// comparisons
let eq = a == b; // false (0)
let ne = a!= b; // true (1)
let lt = a < b; // true (1)
let le = a <= b; // true (1)
let gt = a > b; // false (0)
let ge = a >= b; // false (0)
// print values
yap(42); // prints: 42
yap(x + y); // prints: 13
yap(a == b); // prints: 0 (false)
let x = 10;
if (x > 5) {
yap(1); // prints: 1
} else {
yap(0);
}
// nested if
if (x > 15) {
yap("big");
} else if (x > 5) {
yap("medium"); // prints: medium
} else {
yap("small");
}
let i = 0;
while (i < 3) {
yap(i);
i = i + 1;
}
// output:
// 0
// 1
// 2
let x = 1;
yap(x); // prints: 1
{
let x = 2; // shadows outer x
yap(x); // prints: 2
}
yap(x); // prints: 1 (outer x is back in scope)
jminus-interpreter/
├── main.c # entry point for file execution
├── repl.c # interactive repl shell
├── lexer.c/h # tokenization (source → tokens)
├── parser.c/h # parsing (tokens → ast)
├── compiler.c/h # code generation (ast → bytecode)
├── vm.c/h # virtual machine (bytecode → execution)
├── interpreter.c/h # direct interpretation (ast → execution)
├── environment.c/h # variable scope and environments
├── start.jminus # example program file
├── makefile # build configuration
├── scripts/
│ └── run_tests.sh # test runner
└── tests/
├── compiler_tests.c # compiler unit tests
├── lexer_tests.c # lexer unit tests
├── parser_tests.c # parser unit tests
└── vm_tests.c # vm unit tests
| component | purpose | key files |
|---|---|---|
| lexer | converts source code into tokens | lexer.c/h |
| parser | builds abstract syntax tree | parser.c/h |
| compiler | generates bytecode from ast | compiler.c/h |
| vm | executes bytecode instructions | vm.c/h |
| interpreter | direct ast execution | interpreter.c/h |
| environment | variable scope management | environment.c/h |
# clean build
make clean
make
# rebuild everything
make rebuild
# build with debug symbols
make cflags="-g -o0"# run all tests
make test
# run specific test
./build/tests/compiler_tests.exe
./build/tests/vm_tests.exe# run with debug output
./jminus.exe --debug start.jminusthis shows:
- source code
- token stream
- abstract syntax tree
- execution output
| command | description |
|---|---|
:help |
show available commands |
:exit |
exit the repl |
:interp |
switch to interpreter mode |
:vm |
switch to vm mode (default) |
the lexer (lexer.c) scans source code character by character, recognizing:
- keywords:
let,if,else,while,yap - identifiers: variable names
- literals: numbers (integers only)
- operators:
+,-,*,/,=,==,!=,<,<=,>,>= - delimiters:
(,),{,},;
the parser (parser.c) uses recursive descent parsing to build an ast:
- statements:
let,yap,if,while, blocks, expressions - expressions: literals, variables, binary operations
- error recovery: graceful handling of syntax errors
the compiler (compiler.c) translates ast nodes into bytecode:
- constants: stored in a constants table
- variables: single-character names (ascii codes)
- control flow: jump instructions for if/while
- stack operations: push, pop, arithmetic
the vm (vm.c) executes bytecode using a stack-based architecture:
- stack: operand stack for calculations
- environment: variable storage and lookup
- instruction pointer: current execution position
- constants table: access to literal values
| instruction | description | operand |
|---|---|---|
bc_const |
push constant | index into constants table |
bc_add |
add top two stack values | none |
bc_sub |
subtract top two stack values | none |
bc_mul |
multiply top two stack values | none |
bc_div |
divide top two stack values | none |
bc_print |
print top stack value | none |
bc_load_var |
load variable value | variable name (ascii) |
bc_set_var |
assign to variable | variable name (ascii) |
bc_define_var |
define new variable | variable name (ascii) |
bc_jump |
unconditional jump | target instruction index |
bc_jump_if_false |
conditional jump | target instruction index |
bc_halt |
stop execution | none |
- unit tests for each component (lexer, parser, compiler, vm)
- integration tests for complete programs
- clear feedback with descriptive pass/fail messages
- complete coverage of language features
# all tests
make test
# individual test suites
./build/tests/compiler_tests.exe
./build/tests/lexer_tests.exe
./build/tests/parser_tests.exe
./build/tests/vm_tests.exe- compiler tests: add to
tests/compiler_tests.c - vm tests: add to
tests/vm_tests.c - lexer/parser tests: add to respective test files
test structure:
// test description
{
const char* src = "let x = 42;";
//... test setup...
//... assertions...
print_pass("test description");
}- memory leaks: use
valgrindor address sanitizer - segmentation faults: check array bounds and null pointers
- incorrect output: verify bytecode generation and vm execution
# build with debug symbols
make cflags="-g -o0"
# run with debug output
./jminus.exe --debug program.jminus
# use gdb for debugging
gdb./jminus.exethe --debug flag shows:
---- source start ----
let x = 42;
yap(x);
---- source end ----
--- tokens ---
[line 1] let let
[line 1] identifier x
[line 1] assign =
[line 1] int 42
[line 1] semicolon;
[line 2] yap yap
[line 2] lparen (
[line 2] identifier x
[line 2] rparen )
[line 2] semicolon;
[line 2] eof
--- ast ---
letstmt: x
literal: 42
yapstmt:
variable: x
42
- fork the repository
- clone your fork
- create a feature branch
- make your changes
- test thoroughly
- submit a pull request
# create feature branch
git checkout -b feature/new-feature
# make changes
# ... edit files...
# test changes
make test
# commit changes
git add.
git commit -m "add new feature"
# push to your fork
git push origin feature/new-feature- c99 standard compliance
- consistent indentation (4 spaces)
- descriptive variable names
- function documentation for public apis
- error handling for all operations
- language features: new operators, data types, control structures
- performance: improve vm execution, reduce memory usage
- error handling: better error messages, recovery strategies
- documentation: improve examples, add tutorials
- testing: more test cases, edge case coverage
this project is licensed under the mit license - see the license file for details.
- inspired by educational language implementations
- built for learning compiler construction principles
- thanks to the c programming community for resources and inspiration
- issues: report bugs and feature requests on github
- discussions: ask questions and share ideas
- contributions: pull requests welcome!
*happy coding with jminus! *