hindimejs
Version:
A programming language with a desi twist. It uses commonly used hindi words for commands.🫡
487 lines (464 loc) • 17.1 kB
JavaScript
#!/usr/bin/env node
/* HindimeJS — a tiny meme language in Hindi slang
*
* Syntax (whitespace-separated, one command per line):
* YAAR <name> <number> # declare variable with yaar energy
* ADD <name> <number> # add to variable (add kar de)
* MINUS <name> <number> # subtract from variable (minus kar de)
* MULTIPLY <name> <number> # multiply variable (multiply kar de)
* DIVIDE <name> <number> # divide variable (divide kar de)
* SUM <vals...> # sum args and print
* PRODUCT <vals...> # multiply args and print
* MAXMIN <high/low> <vals...> # find max/min of args (sabse bada/chota)
* RANDOM <name> # random value (kuch bhi ho sakta hai)
* POWER <name> <number> # exponentiate value (power de)
* ZERO <name> # set to 0 (zero kar de)
* ROUND <up/down> <name> # round up/down (round kar de)
* PRINT <value|name|"str"> # print value (print kar de)
* FUNCTION <name> <params...> { body } # define function
* kaam_karo <name> <args...> # call function
* <name>(<args...>) # call function with parentheses
*
* Extras:
* - Comments start with '#'
* - Strings use double quotes, e.g., "Masti hai!"
* - Numbers can be integers or decimals
* - Built-in desi wisdom and motivation
*/
"use strict";
const { lineTokeniser } = require("./src/lineTokeniser.js");
const { lineInterpreter } = require("./src/lineInterpreter.js");
const { pathHandler } = require("./lib/pathHandler.js");
const { FunctionParser } = require("./src/functionParser.js");
const { ControlParser } = require("./src/controlParser.js");
// Module cache and loading guards for lao imports
const moduleCache = new Map();
const loadingModules = new Set();
/* =========================
* Runner (multi-line program)
* ========================= */
function runProgram(text, filePath) {
const env = { vars: Object.create(null), __filePath: filePath || null };
const functionParser = new FunctionParser();
const functionManager = functionParser.getFunctionManager();
const controlParser = new ControlParser();
const lines = text.trim().split(/\r?\n/);
for (let ln = 0; ln < lines.length; ln++) {
const line = lines[ln];
try {
const tokenisedLine = lineTokeniser(line);
// If currently parsing a function definition, capture lines until closing brace
if (functionParser.isParsingFunction()) {
if (functionParser.handleBrace(line)) {
const func = functionParser.finishFunction(env);
console.log(`🫡 Function "${func.name}" complete ho gayi!`);
} else {
functionParser.addToFunctionBody(tokenisedLine);
}
continue;
}
// helper to capture a control block at a given starting line index for nahi_to/warna
const captureControlAt = (startIndex) => {
let headerLine = lines[startIndex];
if (headerLine === undefined) return null;
const headerTokens = lineTokeniser(headerLine);
if (headerTokens.length === 0 || headerTokens[0].type !== "WORD")
return null;
const kind = headerTokens[0].value.toLowerCase();
if (kind !== "nahi_to" && kind !== "warna") return null;
// ensure there's an opening brace either on this line or next
let body = [];
let idx = startIndex;
let braceCount = 0;
while (idx < lines.length) {
const l = lines[idx];
const toks = lineTokeniser(l);
const opens = (l.match(/\{/g) || []).length;
const closes = (l.match(/\}/g) || []).length;
braceCount += opens - closes;
if (idx > startIndex) {
body.push(toks);
} else {
const bracePos = toks.findIndex(
(t) => t.type === "BRACE" && t.value === "{"
);
if (bracePos !== -1) {
const trailing = toks.slice(bracePos + 1);
if (trailing.length > 0) body.push(trailing);
}
}
if (braceCount <= 0 && idx >= startIndex && idx !== startIndex) break;
idx++;
}
return {
kind,
headerTokens,
body,
endLn: Math.min(idx, lines.length - 1),
};
};
// If capturing a control block
if (controlParser.isCapturing()) {
if (controlParser.handleBrace(line)) {
const blk = controlParser.finish();
const kind = blk.kind;
if (kind === "agar") {
const condTokens = blk.headerTokens
.slice(1)
.filter((t) => t.type !== "BRACE");
const tmpName = "__if_cond";
lineInterpreter(
[
{ type: "WORD", value: tmpName },
{ type: "WORD", value: "hai" },
...condTokens,
],
env,
functionManager
);
const condVal = env.vars[tmpName];
delete env.vars[tmpName];
if (condVal) {
for (const bodyLine of blk.body) {
lineInterpreter(bodyLine, env, functionManager);
if (env.breakSignal || env.returnSignal !== undefined) break;
}
let look = captureControlAt(ln + 1);
while (look) {
ln = look.endLn;
look = captureControlAt(ln + 1);
}
} else {
let consumed = false;
let cursor = ln + 1;
while (true) {
const nextBlk = captureControlAt(cursor);
if (!nextBlk) break;
const k = nextBlk.kind;
if (k === "nahi_to") {
const condTokens2 = nextBlk.headerTokens
.slice(1)
.filter((t) => t.type !== "BRACE");
const tmp2 = "__elif_cond";
lineInterpreter(
[
{ type: "WORD", value: tmp2 },
{ type: "WORD", value: "hai" },
...condTokens2,
],
env,
functionManager
);
const cond2 = env.vars[tmp2];
delete env.vars[tmp2];
if (cond2 && !consumed) {
for (const bodyLine of nextBlk.body) {
lineInterpreter(bodyLine, env, functionManager);
if (env.breakSignal || env.returnSignal !== undefined)
break;
}
consumed = true;
}
cursor = nextBlk.endLn + 1;
} else if (k === "warna") {
if (!consumed) {
for (const bodyLine of nextBlk.body) {
lineInterpreter(bodyLine, env, functionManager);
if (env.breakSignal || env.returnSignal !== undefined)
break;
}
consumed = true;
}
cursor = nextBlk.endLn + 1;
break;
}
}
ln = cursor - 1;
}
} else if (kind === "jab_tak") {
const condTokens = blk.headerTokens
.slice(1)
.filter((t) => t.type !== "BRACE");
const tmpName = "__while_cond";
let safety = 100000;
while (safety-- > 0) {
lineInterpreter(
[
{ type: "WORD", value: tmpName },
{ type: "WORD", value: "hai" },
...condTokens,
],
env,
functionManager
);
const condVal = env.vars[tmpName];
delete env.vars[tmpName];
if (!condVal) break;
for (const bodyLine of blk.body) {
lineInterpreter(bodyLine, env, functionManager);
if (env.returnSignal !== undefined) break;
if (env.breakSignal) {
env.breakSignal = false;
safety = 0;
break;
}
if (env.continueSignal) {
env.continueSignal = false;
break;
}
}
if (env.returnSignal !== undefined) break;
}
} else if (kind === "har_ek") {
const hdr = blk.headerTokens.slice(1);
const nameTok = hdr[0];
const inTok = hdr[1];
const exprTokens = hdr.slice(2).filter((t) => t.type !== "BRACE");
if (
!nameTok ||
nameTok.type !== "WORD" ||
!inTok ||
inTok.type !== "WORD" ||
inTok.value.toLowerCase() !== "in"
) {
throw new SyntaxError(
"har_ek syntax: har_ek <name> in <expr> { ... }"
);
}
const iterTmp = "__for_iter";
lineInterpreter(
[
{ type: "WORD", value: iterTmp },
{ type: "WORD", value: "hai" },
...exprTokens,
],
env,
functionManager
);
const iterable = env.vars[iterTmp];
delete env.vars[iterTmp];
if (!iterable || !iterable[Symbol.iterator])
throw new TypeError("har_ek expects an iterable");
for (const value of iterable) {
env.vars[nameTok.value] = value;
for (const bodyLine of blk.body) {
lineInterpreter(bodyLine, env, functionManager);
if (env.returnSignal !== undefined) break;
if (env.breakSignal) {
env.breakSignal = false;
break;
}
if (env.continueSignal) {
env.continueSignal = false;
break;
}
}
if (env.returnSignal !== undefined || env.breakSignal) {
env.breakSignal = false;
break;
}
}
}
continue;
} else {
controlParser.addLine(tokenisedLine);
continue;
}
}
// Function parsing remains
if (
tokenisedLine.length > 0 &&
tokenisedLine[0].type === "WORD" &&
["function", "kaam"].includes(tokenisedLine[0].value.toLowerCase())
) {
const funcName = tokenisedLine[1]?.value;
if (!funcName) {
throw new SyntaxError("FUNCTION needs a name");
}
const parameters = [];
let i = 2;
while (i < tokenisedLine.length && tokenisedLine[i].type === "WORD") {
parameters.push(tokenisedLine[i].value);
i++;
}
if (i < tokenisedLine.length && tokenisedLine[i].value === "{") {
const closingIndex = tokenisedLine.findIndex(
(t, idx) => idx > i && t.type === "BRACE" && t.value === "}"
);
if (closingIndex !== -1) {
const bodyTokens = tokenisedLine.slice(i + 1, closingIndex);
const bodyLines = [];
let currentLine = [];
const isCommandWord = (tok) =>
tok.type === "WORD" &&
[
"YAAR",
"ADD",
"MINUS",
"MULTIPLY",
"DIVIDE",
"PRINT",
"SUM",
"PRODUCT",
"MAXMIN",
"RANDOM",
"POWER",
"ZERO",
"ROUND",
"kaam_karo",
"BOLO",
].includes(tok.value.toUpperCase());
for (const token of bodyTokens) {
if (isCommandWord(token) && currentLine.length > 0) {
bodyLines.push(currentLine);
currentLine = [token];
} else {
currentLine.push(token);
}
}
if (currentLine.length > 0) {
bodyLines.push(currentLine);
}
functionManager.defineFunction(
funcName,
parameters,
bodyLines,
env
);
} else {
functionParser.startFunction(funcName, parameters);
const trailingTokens = tokenisedLine.slice(i + 1);
if (trailingTokens.length > 0) {
functionParser.addToFunctionBody(trailingTokens);
}
functionParser.handleBrace(line);
}
} else {
functionParser.startFunction(funcName, parameters);
if (line.includes("{")) {
functionParser.handleBrace(line);
}
}
continue;
}
// Control flow starters: agar, nahi_to, warna, jab_tak, har_ek
if (tokenisedLine.length > 0 && tokenisedLine[0].type === "WORD") {
const kw = tokenisedLine[0].value.toLowerCase();
if (["agar", "nahi_to", "warna", "jab_tak", "har_ek"].includes(kw)) {
controlParser.start(kw, tokenisedLine);
if (line.includes("{")) controlParser.handleBrace(line);
continue;
}
if (kw === "lao") {
// Syntax: lao "path.hindi"
const fs = require("fs");
const path = require("path");
const argTok = tokenisedLine[1];
if (!argTok || argTok.type !== "STRING")
throw new SyntaxError("lao needs a string path");
const modPath = path.resolve(
filePath ? path.dirname(filePath) : process.cwd(),
argTok.value
);
if (filePath && modPath === filePath) {
continue; // skip self-import
}
if (loadingModules.has(modPath)) {
continue; // prevent cycles
}
if (moduleCache.has(modPath)) {
const childEnv = moduleCache.get(modPath);
if (childEnv.exports) {
for (const [k, v] of Object.entries(childEnv.exports)) {
if (k === "__fns") continue;
env.vars[k] = v;
}
}
if (
childEnv.exports &&
Array.isArray(childEnv.exports.__fns) &&
childEnv.__functionManager
) {
for (const fnName of childEnv.exports.__fns) {
const fnMap = childEnv.__functionManager.functions;
if (fnMap && fnMap.has(fnName)) {
const def = fnMap.get(fnName);
functionManager.defineFunction(
fnName,
def.parameters,
def.body,
env
);
}
}
}
continue;
}
loadingModules.add(modPath);
const src = fs.readFileSync(modPath, "utf8");
const childEnv = runProgram(src, modPath);
moduleCache.set(modPath, childEnv);
loadingModules.delete(modPath);
if (childEnv.exports) {
for (const [k, v] of Object.entries(childEnv.exports)) {
if (k === "__fns") continue;
env.vars[k] = v;
}
}
if (
childEnv.exports &&
Array.isArray(childEnv.exports.__fns) &&
childEnv.__functionManager
) {
for (const fnName of childEnv.exports.__fns) {
const fnMap = childEnv.__functionManager.functions;
if (fnMap && fnMap.has(fnName)) {
const def = fnMap.get(fnName);
functionManager.defineFunction(
fnName,
def.parameters,
def.body,
env
);
}
}
}
continue;
}
}
lineInterpreter(tokenisedLine, env, functionManager);
} catch (e) {
const where = `Line ${ln + 1}: ${line.trim()}`;
const msg = e && e.message ? e.message : String(e);
throw new Error(`${msg}\n -> ${where}`);
}
}
env.__functionManager = functionManager;
return env; // expose env for tests/embedding
}
/* =========================
* CLI
* =========================
* Usage:
* hindijs sample.hindi
* hindijs --help
* hindijs --version
*/
if (require.main === module) {
const fs = require("fs");
const path = pathHandler(process.argv[2]);
try {
const src = fs.readFileSync(path, "utf8");
runProgram(src, path);
} catch (err) {
if (err.code === "ENOENT") {
console.error(`🫡 Error: File '${path}' not found`);
console.error("🫡 Make sure the file exists and try again");
} else {
console.error(err.message || err);
}
process.exit(1);
}
}
/* Exports for embedding/tests */
module.exports = { runProgram };