Skip to content

joeyzhang-dev/jminus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 

Repository files navigation

jminus - a simple c-based interpreter and virtual machine

build status license: mit language: c tests version


table of contents


about jminus

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

what you'll learn

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

architecture overview

source code → lexer → parser → ast → compiler → bytecode → vm → output
↓
interpreter → output

quickstart

prerequisites

  • gcc or clang compiler
  • make build system
  • bash shell (for running tests)

1. clone and build

git clone https://github.com/joeyzhang-dev/jminus.git
cd jminus/jminus-interpreter
make

2. try the repl

./jminus-repl.exe

example 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

3. run a program file

./jminus.exe start.jminus

4. run tests

make test

expected 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! ===

language features

variables and assignment

// variable declaration
let x = 42;

// variable assignment
x = 99;

// multiple variables
let a = 1;
let b = 2;
let c = a + b;

arithmetic operations

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

comparison operators

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)

output

// print values
yap(42); // prints: 42
yap(x + y); // prints: 13
yap(a == b); // prints: 0 (false)

control flow

if statements

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

while loops

let i = 0;
while (i < 3) {
yap(i);
i = i + 1;
}
// output:
// 0
// 1
// 2

blocks and scope

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)

project structure

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

core components

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

development guide

building from source

# clean build
make clean
make

# rebuild everything
make rebuild

# build with debug symbols
make cflags="-g -o0"

running tests

# run all tests
make test

# run specific test
./build/tests/compiler_tests.exe
./build/tests/vm_tests.exe

debug mode

# run with debug output
./jminus.exe --debug start.jminus

this shows:

  • source code
  • token stream
  • abstract syntax tree
  • execution output

repl commands

command description
:help show available commands
:exit exit the repl
:interp switch to interpreter mode
:vm switch to vm mode (default)

architecture deep dive

lexical analysis

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: (, ), {, }, ;

parsing

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

code generation

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

virtual machine

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

bytecode instructions

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

testing

test philosophy

  • 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

running tests

# 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

adding tests

  1. compiler tests: add to tests/compiler_tests.c
  2. vm tests: add to tests/vm_tests.c
  3. 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");
}

debugging

common issues

  1. memory leaks: use valgrind or address sanitizer
  2. segmentation faults: check array bounds and null pointers
  3. incorrect output: verify bytecode generation and vm execution

debug tools

# build with debug symbols
make cflags="-g -o0"

# run with debug output
./jminus.exe --debug program.jminus

# use gdb for debugging
gdb./jminus.exe

debug output

the --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

contributing

start

  1. fork the repository
  2. clone your fork
  3. create a feature branch
  4. make your changes
  5. test thoroughly
  6. submit a pull request

development workflow

# 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

code style

  • c99 standard compliance
  • consistent indentation (4 spaces)
  • descriptive variable names
  • function documentation for public apis
  • error handling for all operations

areas for contribution

  • 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

license

this project is licensed under the mit license - see the license file for details.


acknowledgments

  • inspired by educational language implementations
  • built for learning compiler construction principles
  • thanks to the c programming community for resources and inspiration

support

  • issues: report bugs and feature requests on github
  • discussions: ask questions and share ideas
  • contributions: pull requests welcome!

*happy coding with jminus! *

About

Small Language with custom lexer, parser, compiler, and VM

Resources

License

Stars

3 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages