CURRENTLY WIP
DOES NOT CATCH ERRORS ON COMPILE !! (sometimes)
ima be for real this isnt going to be a top-level language but it will probably reach that tag in like 20 years or somthing.
This language was inspired by Terry A. Davis's HolyC language.
A compiled language with cleaner syntax that transpiles to standard JavaScript.
- What is HolyJS?
- Quick Start
- Building from Source
- CLI Reference
- Language Reference
- Module System
- Garbage Collector
- JS Library Interop
- Compiler Architecture
- Full Syntax Reference Table
- Complete Example Program
- Error Messages
- Project Structure
- FAQ
- License
HolyJS is a programming language that compiles to JavaScript. It strips away the cruft and gives you a cleaner syntax while keeping full compatibility with the entire JavaScript ecosystem. Every npm package, every browser API, every Node.js module works out of the box.
The compiler is written in C++17 as a single file with zero dependencies. It produces clean, readable JavaScript that runs anywhere JS runs.
Why HolyJS?
fninstead offunction(6 chars saved every time)retinstead ofreturn(3 chars saved every time)letisconstby default (immutable first)mutwhen you actually need mutationlog()instead ofconsole.log()(8 chars saved)==compiles to===(no more loose equality bugs)- No parentheses required on
if,for,whileconditions - Pattern matching with
matchexpressions - Pipe operator
|>for function composition - Structs with auto-generated constructors
- Built-in garbage collection tracking
The output is standard ES2020+ JavaScript. No runtime library needed (GC runtime is inlined in the output). No source maps to debug. The generated code is readable and maps 1:1 to your source.
# Build the compiler
g++ -std=c++17 -O2 -o holyjs src/main.cpp
# Write your first program
echo 'log("Hello from HolyJS")' > hello.hj
# Compile and run
./holyjs --run hello.hjOutput:
Hello from HolyJS
HolyJS has zero dependencies. You need a C++17 compiler and nothing else.
With g++ (recommended):
g++ -std=c++17 -O2 -o holyjs src/main.cppWith clang++:
clang++ -std=c++17 -O2 -o holyjs src/main.cppWith CMake:
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)With MSVC on Windows:
cl /std:c++17 /O2 /Fe:holyjs.exe src\mai.cppThe resulting binary is fully self-contained. Copy it anywhere on your system or add it to your PATH.
# Optional: install system-wide
sudo cp holyjs /usr/local/bin/holyjs <input.hj> Compile to stdout
holyjs <input.hj> -o <output.js> Compile to file
holyjs <input.hj> --no-gc Disable GC runtime emission
holyjs <input.hj> --ast Print the AST (debug mode)
holyjs --run <input.hj> Compile to temp file and run with Node.js
holyjs --help Show help
holyjs -h Show help
Examples:
# See the generated JS without saving
./holyjs app.hj
# Compile to a file
./holyjs app.hj -o dist/app.js
# Compile without GC overhead for production
./holyjs app.hj -o dist/app.js --no-gc
# Quick test during development
./holyjs --run app.hj
# Debug the parser output
./holyjs app.hj --astFlags can appear in any order. The input file is detected as the first argument that is not a flag or flag value.
HolyJS is immutable-first. let creates constants, mut creates mutable variables.
let name = "HolyJS" // compiles to: const name = "HolyJS";
mut counter = 0 // compiles to: let counter = 0;
let pi = 3.14159 // compiles to: const pi = 3.14159;
let hex = 0xFF // compiles to: const hex = 0xFF;
let compiles to const. The value cannot be reassigned. Use it by default.
mut compiles to let. Use it only when you need to reassign the variable later.
There is no var. It does not exist in HolyJS.
Functions are declared with fn and return with ret.
fn add(a, b) {
ret a + b
}
fn greet(name) {
ret "Hello, " + name
}
fn doSomething() {
log("no return value")
}
Compiles to:
function add(a, b) {
return (a + b);
}
function greet(name) {
return ("Hello, " + name);
}
function doSomething() {
console.log("no return value");
}log() is a built-in that compiles to console.log(). It accepts any number of arguments.
log("hello") // console.log("hello");
log("x =", x, "y =", y) // console.log("x =", x, "y =", y);
log(1, 2, 3) // console.log(1, 2, 3);
If / Else:
No parentheses required around the condition. Braces are required.
if x == 5 {
log("five")
}
if score > 90 {
log("A")
} else {
log("not A")
}
if x > 100 {
log("big")
} else if x > 50 {
log("medium")
} else {
log("small")
}
Compiles to:
if (x === 5) {
console.log("five");
}
if (score > 90) {
console.log("A");
} else {
console.log("not A");
}
if (x > 100) {
console.log("big");
} else if (x > 50) {
console.log("medium");
} else {
console.log("small");
}For-in with range():
range(n) generates a C-style for loop from 0 to n-1.
for i in range(10) {
log(i)
}
Compiles to:
for (let i = 0; i < 10; i++) {
console.log(i);
}range(start, end) generates a loop from start to end-1.
for i in range(5, 10) {
log(i)
}
Compiles to:
for (let i = 5; i < 10; i++) {
console.log(i);
}For-in with range operator (..):
for i in 0..10 {
log(i)
}
Compiles to:
for (let i = 0; i < 10; i++) {
console.log(i);
}For-in with iterables:
Iterating over arrays, sets, maps, or any iterable compiles to for...of.
let fruits = ["apple", "banana", "cherry"]
for fruit in fruits {
log(fruit)
}
Compiles to:
for (const fruit of fruits) {
console.log(fruit);
}While loops:
mut x = 10
while x > 0 {
log(x)
x -= 1
}
Compiles to:
let x = 10;
while (x > 0) {
console.log(x);
x -= 1;
}match is an expression that returns a value. It compiles to an IIFE with if/else chains using strict equality.
let status = "active"
let message = match status {
"active" => "User is online",
"idle" => "User is away",
"banned" => "User is banned",
_ => "Unknown status"
}
Compiles to:
const message = (() => { const __m = status;
if (__m === "active") return "User is online";
else if (__m === "idle") return "User is away";
else if (__m === "banned") return "User is banned";
return "Unknown status";
})();_ is the wildcard/default case. It matches anything.
Match arms can also contain blocks:
let result = match code {
200 => "OK",
404 => {
log("not found")
ret "Missing"
},
_ => "Error"
}
Match can also be used as a standalone statement:
match direction {
"north" => log("going up"),
"south" => log("going down"),
_ => log("unknown")
}
Structs compile to ES6 classes with auto-generated constructors. Fields are declared as bare identifiers in the struct body. Methods are declared with fn.
struct Player {
name,
hp,
score,
fn takeDamage(amount) {
this.hp = this.hp - amount
if this.hp < 0 {
this.hp = 0
}
}
fn isAlive() {
ret this.hp > 0
}
fn toString() {
ret `${this.name} (HP: ${this.hp})`
}
}
Compiles to:
class Player {
constructor(name, hp, score) {
this.name = name;
this.hp = hp;
this.score = score;
__hj_gc.track(this);
}
takeDamage(amount) {
this.hp = (this.hp - amount);
if ((this.hp < 0)) {
this.hp = 0;
}
}
isAlive() {
return (this.hp > 0);
}
toString() {
return `${this.name} (HP: ${this.hp})`;
}
}The constructor parameters are generated automatically from the field declarations. Fields are assigned to this in declaration order. When GC is enabled, __hj_gc.track(this) is called in the constructor to register the object for tracking.
Creating instances:
let p = new Player("Steve", 100, 0)
p.takeDamage(30)
log(p.hp)
log(p.isAlive())
log(p.toString())
Arrow functions work the same as JavaScript, with full support for expression bodies and block bodies.
Single parameter (no parens needed):
let double = x => x * 2
Multiple parameters:
let add = (a, b) => a + b
Block body:
let process = (data) => {
let result = data.trim()
ret result.toUpperCase()
}
No parameters:
let greet = () => "hello"
Used inline with higher-order functions:
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map((n) => n * 2)
let evens = nums.filter((n) => n % 2 == 0)
let sum = nums.reduce((a, b) => a + b, 0)
The pipe operator |> passes the left-hand value as the first argument to the right-hand function.
fn double(x) {
ret x * 2
}
fn addOne(x) {
ret x + 1
}
let result = 5 |> double |> addOne
log(result)
Compiles to:
const result = addOne(double(5));
console.log(result);This enables a left-to-right reading order for function composition chains, instead of deeply nested calls.
Template strings use backticks and ${expression} interpolation, identical to JavaScript template literals.
let name = "world"
let count = 42
let msg = `hello ${name}, you have ${count} items`
log(msg)
Compiles to:
const msg = `hello ${name}, you have ${count} items`;
console.log(msg);Any expression works inside ${}:
log(`result: ${2 + 2}`)
log(`upper: ${name.toUpperCase()}`)
Object literals use the same { key: value } syntax as JavaScript.
let user = { name: "Alice", age: 30, role: "admin" }
Property shorthand:
When the key and variable name match, you can use shorthand.
let name = "Alice"
let age = 30
let user = { name, age, role: "admin" }
Compiles to:
const user = { name, age, role: "admin" };Spread in objects:
let defaults = { theme: "dark", lang: "en" }
let config = { ...defaults, lang: "fr" }
Array literals use standard bracket syntax.
let nums = [1, 2, 3, 4, 5]
let empty = []
let mixed = ["hello", 42, true, nil]
All standard JavaScript array methods work:
let doubled = nums.map((n) => n * 2)
let total = nums.reduce((a, b) => a + b, 0)
let first = nums[0]
let len = nums.length
Spread operator in arrays and objects:
let a = [1, 2, 3]
let b = [...a, 4, 5, 6]
let base = { x: 1, y: 2 }
let extended = { ...base, z: 3 }
Rest parameters in functions:
fn sum(...nums) {
ret nums.reduce((a, b) => a + b, 0)
}
log(sum(1, 2, 3, 4, 5))
Compiles to:
function sum(...nums) {
return nums.reduce((a, b) => (a + b), 0);
}
console.log(sum(1, 2, 3, 4, 5));Standard conditional expressions:
let status = isOnline ? "online" : "offline"
let max = a > b ? a : b
let label = count == 1 ? "item" : "items"
Compiles to:
const status = (isOnline ? "online" : "offline");
const max = ((a > b) ? a : b);
const label = ((count === 1) ? "item" : "items");nil compiles to null. Use it instead of null.
let nothing = nil
if nothing == nil {
log("it's nil")
}
Compiles to:
const nothing = null;
if (nothing === null) {
console.log("it's nil");
}true and false work exactly as expected.
let alive = true
let dead = false
if alive {
log("still kicking")
}
HolyJS eliminates loose equality bugs by design.
| HolyJS | JavaScript | Notes |
|---|---|---|
== |
=== |
Always strict equality |
!= |
!== |
Always strict inequality |
< |
< |
Less than |
> |
> |
Greater than |
<= |
<= |
Less than or equal |
>= |
>= |
Greater than or equal |
&& |
&& |
Logical AND |
|| |
|| |
Logical OR |
! |
! |
Logical NOT |
There is no loose == or != in HolyJS. You cannot accidentally use them.
mut x = 10
x += 5 // x = x + 5
x -= 3 // x = x - 3
x *= 2 // x = x * 2
x /= 4 // x = x / 4
// single line comment
/* multi-line
comment */
HolyJS supports both ES Modules and CommonJS require. Imports and exports compile directly to their JavaScript equivalents, so every npm package works with zero configuration.
Named imports:
import { readFileSync, writeFileSync } from "fs"
import { useState, useEffect } from "react"
Compiles to:
import { readFileSync, writeFileSync } from "fs";
import { useState, useEffect } from "react";Default imports:
import express from "express"
import React from "react"
Compiles to:
import express from "express";
import React from "react";Namespace imports:
import * as path from "path"
import * as fs from "fs"
Compiles to:
import * as path from "path";
import * as fs from "fs";Aliased imports:
import { readFileSync as read } from "fs"
Compiles to:
import { readFileSync as read } from "fs";Use the use keyword for CommonJS require-style imports.
use "express" as express
use "lodash" as _
use "fs" as fs
Compiles to:
const express = require("express");
const _ = require("lodash");
const fs = require("fs");Without an alias, it compiles to a bare require:
use "dotenv/config"
Compiles to:
require("dotenv/config");Prefix any declaration with export to export it.
export fn add(a, b) {
ret a + b
}
export let PI = 3.14159
export struct Vector {
x,
y,
}
Compiles to:
export function add(a, b) {
return (a + b);
}
export const PI = 3.14159;
export class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
}When you import from a .hj file, the compiler automatically rewrites the extension to .js in the output.
import { helpers } from "./utils.hj"
import config from "./config.hj"
Compiles to:
import { helpers } from "./utils.js";
import config from "./config.js";This means you can build multi-file HolyJS projects. Compile each .hj file to .js, and the imports resolve correctly.
HolyJS includes a built-in garbage collection runtime that tracks object allocations using modern JavaScript APIs. It is emitted at the top of every compiled file by default.
The GC uses two mechanisms:
-
FinalizationRegistry-- Receives a callback when tracked objects are garbage collected by the JS engine. This cleans up the internal tracking set automatically. -
WeakRef-- Stores weak references to tracked objects. The GC can check if objects are still alive without preventing their collection.
The runtime maintains a Set of WeakRef entries. When the allocation count hits a threshold (1000 by default), a sweep runs to prune dead references from the set.
At the end of the program, __hj_gc.collectAll() runs a final sweep.
When GC is enabled, the following allocations are wrapped in __hj_alloc():
- Variables assigned an array literal:
let nums = [1, 2, 3] - Variables assigned an object literal:
let cfg = { a: 1 } - Variables assigned a
newexpression:let p = new Point(1, 2) - Variables assigned a function call result:
let data = fetch(url)
Struct constructors automatically call __hj_gc.track(this) to register every instance.
Primitive assignments (numbers, strings, booleans, nil) are not tracked.
The emitted runtime exposes these functions (available in the compiled JS output):
__hj_gc.track(obj) // Register an object for tracking
__hj_gc.sweep() // Prune dead WeakRefs from the tracking set
__hj_gc.stats() // Returns { tracked: number, alive: number }
__hj_gc.collectAll() // Final sweep (called at program end)
__hj_alloc(obj) // Track and return the object (used by compiler)
__hj_log(...args) // console.log wrapper (used by compiler)You can call __hj_gc.stats() from your HolyJS code for debugging:
let a = [1, 2, 3]
let b = { x: 1 }
let p = new Point(0, 0)
log(__hj_gc.stats())
For production builds or when you do not want the overhead, pass --no-gc:
./holyjs app.hj -o app.js --no-gcThis skips emitting the GC runtime entirely. No __hj_gc, no __hj_alloc wrappers, no __hj_log helper. All allocations emit raw JavaScript. log() still compiles to console.log() directly.
HolyJS has full interop with the JavaScript ecosystem. Every npm package, every browser API, every Node.js built-in works.
Using npm packages:
import express from "express"
import { PrismaClient } from "@prisma/client"
use "lodash" as _
let app = express()
let prisma = new PrismaClient()
app.get("/", (req, res) => {
res.send("Hello from HolyJS")
})
app.listen(3000)
Using browser APIs:
let canvas = document.getElementById("game")
let ctx = canvas.getContext("2d")
ctx.fillStyle = "red"
ctx.fillRect(0, 0, 100, 100)
Using Node.js built-ins:
import { readFileSync, writeFileSync } from "fs"
import { join } from "path"
let data = readFileSync("input.txt", "utf-8")
let output = data.toUpperCase()
writeFileSync(join("dist", "output.txt"), output)
There are no wrappers, no FFI, no bindings. The compiled output is standard JavaScript, so anything that works in JS works in HolyJS.
Source (.hj)
|
v
Lexer ----------> Token stream
|
v
Parser ---------> Abstract Syntax Tree (AST)
|
v
Code Generator -> JavaScript source (.js)
The compiler is a single-pass source-to-source transpiler. There are no intermediate representations, no optimization passes, and no type checking. The output maps directly to the input.
The lexer (class Lexer) converts raw source text into a flat vector of tokens. It handles:
- Single and multi-character operators (
==,!=,<=,>=,=>,->,&&,||,|>,..,...,+=,-=,*=,/=) - Keywords (
fn,let,mut,ret,if,else,for,in,while,match,import,from,as,export,struct,new,nil,use,log,true,false) - String literals (single and double quoted, with escape sequences)
- Template string literals (backtick-delimited)
- Numeric literals (decimal and hex
0xprefix) - Identifiers (alphanumeric, underscore, dollar sign)
- Single-line comments (
//) and multi-line comments (/* */) - Newline tokens (significant for statement termination)
Each token carries its line and column number for error reporting.
The parser (class Parser) is a recursive descent parser with Pratt parsing for expressions. It builds an AST from the token stream.
Statement parsing dispatches on the current token:
| Token | Parse method | AST node |
|---|---|---|
fn |
parseFn() |
FnDecl |
let |
parseLet() |
LetDecl |
mut |
parseLet() |
LetDecl |
ret |
parseReturn() |
Return |
if |
parseIf() |
If |
for |
parseFor() |
ForRange / ForIn |
while |
parseWhile() |
While |
match |
parseMatch() |
Match |
struct |
parseStruct() |
StructDecl |
import |
parseImport() |
Import |
use |
parseUse() |
UseImport |
export |
parseExport() |
(wraps next) |
| (other) | parseExprStatement() |
ExprStatement / Assign / CompoundAssign |
Expression parsing uses Pratt precedence:
| Precedence | Operators |
|---|---|
| 0 | ? (ternary) |
| 1 | || |
| 2 | && |
| 3 | |> (pipe) |
| 4 | == != |
| 5 | < > <= >= |
| 6 | .. (range) |
| 7 | + - |
| 8 | * / % |
Unary operators (!, -, ...) are parsed before primary expressions. Postfix operations (function calls, member access, index access) are parsed after.
Every node is a shared_ptr<ASTNode> with these fields:
NodeType type // discriminant
string strVal // name, operator, source path
string strVal2 // secondary string (import kind, alias)
double numVal // numeric value
bool boolVal // boolean value
vector<Node> children // child nodes
vector<string> params // parameter names, field names
vector<string> paramDefaults // default values
bool isMutable // let vs mut
bool isExported // export prefix
bool isRest // rest parameter
int line // source line number
Node types:
Program, FnDecl, LambdaExpr, LetDecl, Return, If,
ForIn, ForRange, While, Match, MatchArm, Import,
UseImport, Export, StructDecl, NewExpr, ExprStatement,
BinOp, UnaryOp, Call, Member, Index, Assign,
CompoundAssign, NumLit, StrLit, TemplateLit, BoolLit,
NilLit, Ident, ArrayLit, ObjectLit, ObjectProp,
SpreadExpr, Block, TernaryExpr, PipeExpr
The code generator (class CodeGenerator) walks the AST recursively and emits JavaScript strings. It handles:
- Indentation tracking for readable output
- GC wrapping logic (only at
LetDecllevel, avoiding double-wraps) - Operator translation (
==to===,!=to!==) log()toconsole.log()rewriting.hjto.jsimport path rewriting- Match expression compilation to IIFE + if/else chain
- Struct compilation to ES6 class + auto-constructor
- Pipe operator inversion (
a |> ftof(a))
| HolyJS | JavaScript Output |
|---|---|
fn name(a, b) { ... } |
function name(a, b) { ... } |
ret value |
return value; |
let x = 5 |
const x = 5; |
mut x = 5 |
let x = 5; |
log("hi") |
console.log("hi"); |
if x == 5 { } |
if (x === 5) { } |
if x != 5 { } |
if (x !== 5) { } |
for i in range(10) { } |
for (let i = 0; i < 10; i++) { } |
for i in range(2, 8) { } |
for (let i = 2; i < 8; i++) { } |
for i in 0..10 { } |
for (let i = 0; i < 10; i++) { } |
for x in arr { } |
for (const x of arr) { } |
while cond { } |
while (cond) { } |
match val { a => b, _ => c } |
(() => { ... if/else chain ... })() |
struct Name { x, y, fn m() {} } |
class Name { constructor(x,y){...} m(){} } |
let p = new Thing(1, 2) |
const p = __hj_alloc(new Thing(1, 2)); |
x |> f |
f(x) |
(a, b) => a + b |
(a, b) => (a + b) |
x => x * 2 |
(x) => (x * 2) |
`hello ${name}` |
`hello ${name}` |
nil |
null |
true / false |
true / false |
...arr |
...arr |
fn f(...args) { } |
function f(...args) { } |
import { x } from "y" |
import { x } from "y"; |
import x from "y" |
import x from "y"; |
import * as x from "y" |
import * as x from "y"; |
use "x" as y |
const y = require("x"); |
export fn f() { } |
export function f() { } |
x += 1 |
x += 1; |
cond ? a : b |
(cond ? a : b) |
Here is a full HolyJS program that demonstrates the major features together:
import { readFileSync } from "fs"
struct Task {
id,
title,
done,
fn toggle() {
this.done = !this.done
}
fn toString() {
let mark = this.done ? "[x]" : "[ ]"
ret `${mark} ${this.id}: ${this.title}`
}
}
fn createTask(id, title) {
ret new Task(id, title, false)
}
fn printAll(tasks) {
for task in tasks {
log(task.toString())
}
}
fn countDone(tasks) {
ret tasks.filter((t) => t.done).length
}
let tasks = [
createTask(1, "Build compiler"),
createTask(2, "Write tests"),
createTask(3, "Ship it")
]
tasks[0].toggle()
printAll(tasks)
let done = countDone(tasks)
let total = tasks.length
log(`Progress: ${done}/${total}`)
let status = match done {
0 => "not started",
total => "all done",
_ => "in progress"
}
log(`Status: ${status}`)
The compiler reports errors with line and column numbers:
Error: Line 15:8 - Expected RPAREN, got 'NEWLINE'
Error: Line 23 - Unexpected token: '>'
Error: Line 1 - Invalid match pattern
Error: Cannot open file: missing.hj
Error: Cannot write file: /readonly/output.js
Common causes:
- Missing closing brace or paren -- Check that every
{has a}and every(has a) - Unexpected token -- Usually a syntax error on that line; check for typos
- Invalid match pattern -- Match arms only accept literals, identifiers, and
_
Use --ast to debug parser output:
./holyjs problem.hj --astThis prints the full AST tree, showing how the parser interpreted your code.
holyjs/
src/
main.cpp # Complete compiler (lexer + parser + codegen)
examples/
demo.hj # Full feature demonstration
test.hj # Minimal runnable test
CMakeLists.txt # CMake build configuration
README.md # This file
The entire compiler is a single C++ file. No headers, no libraries, no build system required beyond a C++17 compiler.
Q: Does HolyJS support TypeScript-style types? A: No. HolyJS is about cleaner syntax, not a type system. The output is untyped JavaScript. Use TypeScript if you want types.
Q: Can I use HolyJS with React/Vue/Svelte?
A: Yes. The output is standard ES modules. Import React, use JSX-like patterns with template strings, or compile your .hj files and import them from your framework code.
Q: Does the GC actually improve performance?
A: The GC is primarily a tracking and monitoring tool. JavaScript engines already have excellent garbage collectors. The HolyJS GC tracks allocations so you can monitor object lifetimes and detect leaks. For production, use --no-gc to eliminate the overhead.
Q: Can I use HolyJS in the browser?
A: Yes. Compile your .hj files to .js, then include them with <script type="module"> or bundle them with any standard bundler (Vite, Webpack, Rollup, esbuild).
Q: Why C++ and not JavaScript for the compiler? A: Speed and portability. The C++ compiler compiles instantly, produces a tiny static binary, and has zero runtime dependencies. No Node.js installation required.
Q: How do I debug compiled code? A: The output is clean, readable JavaScript that maps closely to the source. Variable names, function names, and structure are preserved. Source maps are not currently supported.
Q: Can I contribute new language features? A: The compiler is a straightforward recursive descent parser. Adding a new feature means adding a token type (if needed), a parse function, an AST node type, and a code generation case. The single-file architecture makes it easy to understand the full pipeline.
Q: What JavaScript version does HolyJS target?
A: ES2020+. The output uses const, let, class, template literals, for...of, arrow functions, spread/rest, WeakRef, and FinalizationRegistry. All modern browsers and Node.js 14+ support these.
Q: Is there a VS Code extension?
A: Not yet. You can use JavaScript syntax highlighting for .hj files as a starting point -- most of the syntax is close enough to highlight correctly.
MIT License. Use it however you want.
