MinJ, short for minimalistic Java, is a lightweight interpreter designed to bring a concise, statically typed scripting language to the JVM. Built from the ground up in Java with ANTLRv4 for lexical and syntactic analysis, MinJ provides an accessible platform for experimenting with language design and interpreter implementation. Its clean grammar and modular visitor‑based execution model ensure fast parsing and straightforward extensibility.
MinJ currently supports variable declarations (var and val), arithmetic and comparison operations, conditional statements (if/elseif/else), loops (while, for, foreach), list manipulation, and printing literals or variables, enabling you to write programs like FizzBuzz, iterate over collections, and perform calculations with ease.
MinJ is built using Gradle with the ANTLR plugin and ShadowJar for packaging a standalone fat‑JAR. To use the language yourself: simply download the release and execute your scripts via java -jar minjc-<VERSION>.jar <yourfile>.mj. A comprehensive CI/CD pipeline on GitHub Actions ensures that every change is validated, tested, and packaged automatically for reliable releases.
Key Features
- Statically‑typed declarations with
int,String,boolean, etc., plus dynamic variables viavarand immutable constants viaval. - Rich expression syntax: arithmetic, comparisons, boolean operators (
&&/and,||/or,^/xor,!/not). - Control flow:
if/elseif/else,while,for … to … [step …],foreach … in …. - First‑class lists: literal syntax
[1,2,3]and iteration withforeach. - Functions & methods: global
func/method, instance methods,new‑based object creation,return. - Built‑in I/O:
print(...)andinput(...)for interactive prompts. - Error checking: static vs. dynamic type enforcement, immutability (
val) violations, undefined‑name errors. - Extensible grammar: modular ANTLR4
.g4grammar with clearly layered rules (declarations, statements, expressions, primary).
MinJ ships as a standalone “fat‑JAR” built with Gradle and the ANTLR plugin. Every commit runs through a CI/CD pipeline (GitHub Actions) that regenerates the parser, runs the full test suite (unit tests, self‑test scripts), and publishes versioned releases.
-
Download the latest release: https://github.com/Conava/MinJ/releases
-
Unzip the archive.
-
Run the interpreter:
java -jar minjc-<VERSION>.jar <yourfile>.mj
Replace
<VERSION>with the actual version number and<yourfile>with your MinJ script. -
Explore the examples in the
examplesdirectory to see MinJ in action. -
Write your own MinJ scripts using the provided grammar as a reference.
- JDK 21 (or adjust Gradle toolchain to your installed JDK)
- Gradle 8.5+
- ANTLR 4.13.0 (included via Gradle plugin)
For grammar development and live parse tree visualization, it's recommended to use IntelliJ IDEA Ultimate with the ANTLR v4 plugin. This setup provides:
- Real-time grammar validation and error highlighting
- Automatic generation of lexer/parser code
- Live parse tree preview for rapid debugging
- Syntax-aware editing and refactoring
-
Build fat-JAR (includes ANTLR runtime):
./gradlew clean shadowJar
-
Run interpreter:
java -jar build/libs/minjc-<VERSION>.jar <PROGRAM_NAME>.mj
Below is an in-depth look at the MinJ grammar defined in src/main/antlr/MinJ.g4, annotated with extensive explanations. Wherever a code‑block would normally begin or end with triple backticks (), you'll see the marker **** instead.
Grammar snippet
program
: (topLevelDecl? NEWLINE)*
topLevelDecl?
EOF
;
- program: a sequence of zero or more lines, each optionally containing a top‑level declaration or statement, followed by EOF.
- Blank lines are allowed because
topLevelDecl?can be empty. - Every physical line break is matched by NEWLINE, so line orientation is significant.
Grammar snippet
topLevelDecl
: classDecl
| methodDecl
| statement
;
- classDecl: defines a class, its fields and methods.
- methodDecl: defines a standalone (global) function or a method inside a class.
- statement: any executable line (variable declaration, assignment, control flow, etc.).
Grammar snippet
varDecl
: (type | VAR | VAL) idList (ASSIGN expr)?
;
idList
: ID (COMMA ID)*
;
type
: INT_TYPE | FLOAT_TYPE | DOUBLE_TYPE | BOOLEAN_TYPE | CHAR_TYPE | STRING_TYPE
;
- type may be any built‑in type keyword (e.g.
int,String,bool). - VAR introduces a dynamically‑typed mutable variable.
- VAL introduces an immutable constant (single assignment).
- You may declare multiple names at once:
var x, y = 3.
Grammar snippet
assign
: idList ASSIGN expr
;
printStmt
: PRINT expr
;
- assign covers both re‑assignment and initial assignment of existing names.
- printStmt is a built‑in for console output.
Grammar snippet
ifStmt
: IF expr THEN COLON block
(ELSEIF expr THEN COLON block)*
(ELSE COLON block)?
END
;
block
: (statement? NEWLINE)*
;
- IF … THEN opens a branch; ELSEIF and ELSE are optional.
- Terminated by END.
- block represents an indented group of statements (you may leave lines blank).
While
whileStmt
: WHILE expr DO COLON block END
;
For
forStmt
: FOR (varDecl | assign) TO expr (STEP assign)? DO COLON block END
;
Foreach
foreachStmt
: FOREACH ID IN expr DO COLON block END
;
- while loops test before each iteration.
- for loops support an initial declaration or assignment, an upper bound, an optional
step, then a body. - foreach iterates over lists.
Operator precedence (highest → lowest):
- Unary:
!(NOT),-(negation) - Multiplicative:
*/% - Additive:
+- - Relational:
<><=>===!= - Logical:
&&/and,||/or,^/xor - Primary values
Grammar snippet
expr
: NOT expr // unary NOT
| SUB expr // unary minus
| expr op=(MUL|DIV|MOD) expr // *,/, %
| expr op=(ADD|SUB) expr // +, -
| expr op=(LT|GT|LE|GE|EQ|NE) expr // comparisons
| expr op=(AND|OR|XOR) expr // boolean ops
| primary // literals, names, calls
;
Grammar snippet
primary
: NEW ID LPAREN RPAREN # NewExpr
| ID LPAREN argList? RPAREN # CallExprPrimary
| primary DOT ID LPAREN argList? RPAREN # DotCallExpr
| INT # IntLiteral
| FLOAT_LIT # FloatLiteral
| DOUBLE_LIT # DoubleLiteral
| STRING # StringLiteral
| CHAR # CharLiteral
| BOOL_LIT # BoolLiteral
| ID # VarReference
| LPAREN expr RPAREN # ParenExpr
| listLiteral # ListExpr
;
listLiteral
: '[' (expr (COMMA expr)*)? ']'
;
argList
: expr (COMMA expr)*
;
- NEW ID() constructs a new object.
- CallExprPrimary handles global function calls.
- DotCallExpr handles method calls on instances.
- listLiteral builds a
List<Object>.
Grammar snippet
NEWLINE : '\r'? '\n' ;
WS : [ \t]+ -> skip ;
LINE_COMMENT : '//' ~[\r\n]* -> skip ;
BLOCK_COMMENT: '/*' .*? '*/' -> skip ;
// Keywords
IF : 'if' ; FOR : 'for' ; VAR : 'var' ; VAL : 'val' ; …
// Operators/punctuation
ASSIGN : '=' ; LT : '<' ; EQ : '==' ; AND : '&&' | 'and' ; OR : '||' | 'or' ; …
// Literals
INT : [0-9]+ ;
FLOAT_LIT : [0-9]+ '.' [0-9]+ [fF] ;
STRING : '"' (~["\\\r\n] | '\\' .)* '"' ;
BOOL_LIT : 'true' | 'false' ;
ID : [a-zA-Z_] [a-zA-Z_0-9]* ;
var any = 10
any = "now a String!" // OK: var is dynamic
int n = 5
// n = "oops" // ERROR: type mismatch
val PI = 3.14
// PI = 3.0 // ERROR: reassign valCopy
Edit
var a = true and false
var b = true xor true
var c = a or b
var d = !a && !b
print a // false
print b // false
print c // false
print d // trueclass Counter:
var count = 0
method inc():
count = count + 1
end
method get():
return count
end
end
var c = new Counter()
c.inc()
print c.get() // 1func factorial(n):
if n <= 1 then:
return 1
end
return n * factorial(n - 1)
end
func factLoop(n):
var r = 1
for i = 1 to n do:
r = r * i
end
return r
end
print factorial(5) // 120
print factLoop(5) // 120var nums = [1, 2, 3, 4, 5]
print "Numbers:"
foreach n in nums do:
print n
end
var names = ["Alice", "Bob", "Charlie"]
print "Names:"
foreach name in names do:
print name
endvar sum = 0
while sum < 10 do:
if sum % 2 == 0 then:
print sum
end
sum = sum + 1
endThe interpreter is packaged as a fat-JAR in the release section, including all dependencies. You can run it directly from the command line:
cd Downloads/MinJ-<VERSION>
java -jar minjc-<VERSION>.jar <PROGRAM_NAME>.mj-
ANTLR Generation
- Gradle’s
antlrplugin readsMinJ.g4and generatesMinJLexer.java,MinJParser.java,MinJBaseVisitor.java, etc.
- Gradle’s
-
Parsing (
Main.java)- Reads source file via
CharStreams.fromPath() - Feeds into
MinJLexer→CommonTokenStream→MinJParser.program()→ParseTree
- Reads source file via
-
Evaluation (
EvalVisitor.java)-
Extends
MinJBaseVisitor<Object> -
Overrides:
visitVarDecl→ allocate inenv(markvalimmutable)visitAssign→ updateenv(error if immutable)visitPrintStmt→ print values, explicitly showing'\0'and""for empty char/stringvisitIfStmt→ evaluate conditions in order, execute matchingblockvisitExpr→ perform arithmetic & comparisons viactx.op.getText()visitPrimary→ parse literals, look up variables, handle parentheses
-
Calling
visitor.visit(tree)walks the AST and executes statements
-
-
Add Grammar Rule Example: how the while loop has been added to the language:
in MinJ.g4:
- Add the
whileStmtrule:
whileStmt: 'while' expr 'do' ':' block ;
-
expris the condition,blockis the body. -
Add
whileStmtto thestatementrule:
statement: varDecl | assign | printStmt | ifStmt | whileStmt ;- Add
WHILE,DO, andENDkeywords to the lexer rules:
WHILE: 'while' ; DO: 'do' ; END: 'end' ;
- Add the
-
Implement Visitor Logic
- In
EvalVisitor.java, implement thevisitWhileStmtmethod:
@Override public Object visitWhileStmt(MinJParser.WhileStmtContext ctx) { while ((Boolean) visit(ctx.expr())) { visitBlock(ctx.block()); } return null; }
- This method evaluates the condition and executes the block repeatedly until the condition is false.
- In
3Rebuild
./gradlew clean generateGrammarSource compileJava shadowJar 4Implement Visitor java @Override public Object visitWhileStmt(MinJParser.WhileStmtContext ctx) { while ((Boolean) visit(ctx.expr())) { visitBlock(ctx.block()); } return null; }
5Test
- Create a
.mjfile usingwhile, runjava -jar build/libs/minjc-0.1.0.jar yourLoop.mj, and verify the loop executes as expected.
GitHub Pages automatically serves the project documentation from the master branch (configured in repository settings). A GitHub Actions workflow triggers on pushes to main, builds the site by copying README.md into a static site, and deploys to gh-pages. The site is available at https://Conava.github.io/MinJ.
The CI/CD pipeline is defined in .github/workflows/ci.yml. On each push or pull request to release:
- Checkout repository
- Set up JDK 21 via
actions/setup-java - Run
./gradlew clean generateGrammerSource shadowJarto build the fat‑JAR including all dependencies - Execute
./gradlew testfor unit tests - Archive the
build/libs/minjc-*.jarartifact - Package the minjc jar with the examples and this README to a zip file
- Upload the zip file as a release asset
- Finish and Tag the Release on GitHub
MinJ leverages several tools to automate and streamline language development, parsing, and distribution:
- What it is: A powerful parser generator that reads a grammar file and produces a lexer and parser in Java.
- How it works: ANTLR builds a parse tree based on your grammar. We extend the generated
MinJBaseVisitorto walk this tree, interpreting each node. - Usage: Configured via Gradle’s ANTLR plugin; source generation runs in the
generateGrammarSourcephase. The generated Java files reside inbuild/generated-src/antlr/main. - Why use it: ANTLR supports complex grammars (left recursion, precedence), robust error recovery, and a visitor/listener API for easy AST traversal and custom logic.
- Gradle: Provides dependency management, task orchestration, and a plugin ecosystem. We declare the ANTLR and ShadowJar plugins in
build.gradle, automating grammar generation, compilation, testing, and packaging. - ShadowJar: Bundles all runtime dependencies (ANTLR runtime, etc.) into a single “fat” JAR, simplifying distribution and execution.
- Java: The host platform for MinJ; we target Java 21 to leverage modern language features.
- Toolchain: Gradle’s toolchain settings ensure consistent builds across environments.
- Automates builds, tests, and releases on every commit.
- Publishes artifacts and deploys documentation to GitHub Pages.
These tools together create a robust, reproducible development workflow, from grammar changes to production-ready interpreters.