Skip to content

TS-DEV-DEBUG-V2/holyjs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HolyJS Logo

HolyJS

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.


Version License C++17 Target JS Status Single Pass Parser Pratt Parsing GC WeakRef ES Modules CommonJS Linux macOS Windows No Deps Single File Build Time Binary Size .hj Structs Match Pipe Range Arrows Spread Rest Templates Ternary Shorthand Nil Exports Imports While Node.js Bun Deno Browser g++ clang++ MSVC CMake ES2020+ npm compatible Zero Runtime Deps Transpiler No Semicolons Strict Equality Immutable Default


Table of Contents


What is HolyJS?

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?

  • fn instead of function (6 chars saved every time)
  • ret instead of return (3 chars saved every time)
  • let is const by default (immutable first)
  • mut when you actually need mutation
  • log() instead of console.log() (8 chars saved)
  • == compiles to === (no more loose equality bugs)
  • No parentheses required on if, for, while conditions
  • Pattern matching with match expressions
  • 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.


Quick Start

# 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.hj

Output:

Hello from HolyJS

Building from Source

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.cpp

With clang++:

clang++ -std=c++17 -O2 -o holyjs src/main.cpp

With 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.cpp

The 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/

CLI Reference

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

Flags can appear in any order. The input file is detected as the first argument that is not a flag or flag value.


Language Reference

Variables

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

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

Control Flow

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

Loops

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 Expressions

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

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 and Lambdas

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)

Pipe Operator

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

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()}`)

Objects

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

Arrays

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 and Rest

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

Ternary Expressions

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 Literal

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

Boolean Literals

true and false work exactly as expected.

let alive = true
let dead = false

if alive {
  log("still kicking")
}

Comparison Operators

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.


Compound Assignment

mut x = 10
x += 5       // x = x + 5
x -= 3       // x = x - 3
x *= 2       // x = x * 2
x /= 4       // x = x / 4

Comments

// single line comment

/* multi-line
   comment */

Module System

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.

ES Module Imports

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

CommonJS Imports

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

Exports

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

Importing Other HolyJS Files

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.


Garbage Collector

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.

How It Works

The GC uses two mechanisms:

  1. FinalizationRegistry -- Receives a callback when tracked objects are garbage collected by the JS engine. This cleans up the internal tracking set automatically.

  2. 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.

What Gets Tracked

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 new expression: 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.

GC Runtime API

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())

Disabling the GC

For production builds or when you do not want the overhead, pass --no-gc:

./holyjs app.hj -o app.js --no-gc

This 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.


JS Library Interop

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.


Compiler Architecture

Pipeline Overview

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.

Lexer

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 0x prefix)
  • 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.

Parser

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.

AST Nodes

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

Code Generator

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 LetDecl level, avoiding double-wraps)
  • Operator translation (== to ===, != to !==)
  • log() to console.log() rewriting
  • .hj to .js import path rewriting
  • Match expression compilation to IIFE + if/else chain
  • Struct compilation to ES6 class + auto-constructor
  • Pipe operator inversion (a |> f to f(a))

Full Syntax Reference Table

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)

Complete Example Program

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}`)

Error Messages

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

This prints the full AST tree, showing how the parser interpreted your code.


Project Structure

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.


FAQ

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.


License

MIT License. Use it however you want.

About

Holy JS is a programming language that aims to perfect the goofy syntax of JavaScript, this compiler compiles HolyJS (.hj) files into JS files that can run in the browser or any javascript runtime like bun deno nodejs etc

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors