Skip to content

Instantly share code, notes, and snippets.

@pythonmcpi
Last active January 25, 2024 07:07
Show Gist options
  • Save pythonmcpi/92cc13342e925ca3afadfc38549cc33c to your computer and use it in GitHub Desktop.
Save pythonmcpi/92cc13342e925ca3afadfc38549cc33c to your computer and use it in GitHub Desktop.
lisp-like syntax for javascript. This should not be allowed within 100 meters of production code. Demo: https://jsfiddle.net/pipythonmc/16Lcozxu/
const lisp = (() => {
const lib = {
version: '2.0.0',
};
lib.ast = {};
const DIGITS = /^[0-9]+$/;
function rawToken (token, line, col) {
return [token, line, col]; // intentionally incompatible with regular types
}
function syntaxError (line, col, reason) {
return new Error(`SyntaxError[${line}:${col}]: ${reason}`);
}
function internalError (reason) {
return new Error(`Internal parser error: ${reason}`);
}
lib.ast.createTree = function (filename) {
if (!filename) filename = "<unknown>";
return {
type: 'tree',
filename: filename,
statements: [],
};
};
lib.ast.pushStatement = function (tree, list) {
tree.statements.push(list);
};
lib.ast.list = function (op, line, col) {
return {
type: 'list',
op: op, // operator used to create the list. used for matching closing tags
line: line,
col: col,
tokens: [],
};
};
lib.ast.pushToken = function (list, token) {
list.tokens.push(token);
};
lib.ast.getTokenAt = function (list, index) {
if (list.type != 'list') throw internalError('Tried to get token from non-list');
if (index >= list.tokens.length) throw internalError('Tried to get token from bad index');
return list.tokens[index];
};
lib.ast.getTokens = function (list) {
if (list.type != 'list') throw internalError('Tried to get tokens from non-list');
return list.tokens;
};
lib.ast.isEmpty = function (list) {
if (list.type != 'list') throw internalError('Tried to check if non-list is empty');
return list.tokens.length == 0;
};
lib.ast.isList = function (node) {
return node.type == 'list';
};
lib.ast.token = function (content, line, col) {
return {
type: 'token',
content: content,
line: line,
col: col,
};
};
lib.ast.getContent = function (token) {
if (token.type != 'token') throw internalError('Tried to get content of non-token');
return token.content;
};
lib.ast.push = function (parent, data) {
// push according to type
if (parent.type == 'tree') lib.ast.pushStatement(parent, data);
else if (parent.type == 'list') lib.ast.pushToken(parent, data);
else throw internalError('Tried to push to unknown parent.');
};
lib.tokenize = function (code) {
const WHITESPACE = [' ', '\t', '\n', '\v', '\f'];
const FILTER = ['\r'];
const QUOTES = ['"', '`', "'"];
const PAREN = ['(', ')']; // remnant from I wanted [...] to autoconvert to ([] ...)
const tokens = [];
let currentToken = "";
let quoteType = null;
let escape = false;
let line = 1;
let base = 0;
code += "\n"; // force trailing newline
function pushToken (i) {
if (currentToken.length > 0)
tokens.push(rawToken(currentToken, line, i - base));
currentToken = "";
}
for (let i = 0; i < code.length; i++) {
const char = code[i];
if (escape) {
escape = false;
currentToken += "\\" + char;
} else if (char == "\\") {
escape = true;
} else if (quoteType != null) {
currentToken += char;
if (char == quoteType) {
quoteType = null;
pushToken(i);
}
} else if (WHITESPACE.includes(char)) {
pushToken(i);
if (char == "\n") {
line++;
base = i;
}
} else if (QUOTES.includes(char)) {
pushToken(i);
currentToken = char;
quoteType = char;
} else if (PAREN.includes(char)) {
pushToken(i);
currentToken += char;
pushToken(i);
} else if (FILTER.includes(char)) {
continue;
} else {
currentToken += char;
}
}
if (quoteType != null) throw syntaxError(line, code.length - 1 - base, 'Unclosed quote');
return tokens;
};
lib.parseAst = function (tokens) {
const OPEN_PAREN = ['(']; // remnant from I wanted [...] to autoconvert to ([] ...)
const CLOSE_PAREN = [')'];
const PAREN_MAPPING = {
'(': ')',
'[': ']',
};
const tree = lib.ast.createTree("<textarea>");
let current = [tree];
let last = null; // most recent token
for (const token of tokens) {
const [op, line, col] = token;
if (OPEN_PAREN.includes(op)) {
const list = lib.ast.list(op, line, col);
current.push(list);
if (op == '[')
lib.ast.pushToken(list, lib.ast.token('__lispjs_internal_array', line, col));
} else if (CLOSE_PAREN.includes(op)) {
if (lib.ast.isEmpty(current[current.length - 1]))
throw syntaxError(line, col, 'Empty list');
if (current.length <= 1) // tree should always remain in current
throw syntaxError(line, col, 'Missing opening parentheses');
if (PAREN_MAPPING[current[current.length - 1].op] != op)
throw syntaxError(line, col, 'List type mismatch');
const toBePushed = current.pop(); // not inlined so that we don't rely on evaluation order
lib.ast.push(current[current.length - 1], toBePushed);
} else {
let escaped = false;
let stripped = "";
for (let c of op) {
if (escaped) stripped += c;
else if (c == "\\") escaped = true;
else stripped += c;
}
const token = lib.ast.token(stripped, line, col)
lib.ast.push(current[current.length - 1], token);
last = token;
}
}
if (current.length != 1) throw syntaxError(current[current.length - 1].line, current[current.length - 1].col, 'Unclosed list');
return tree;
};
lib.ast2js = function (tree, embedLines) {
const macros = new Map();
function defineMacro (subtree) {
const tokens = lib.ast.getTokens(subtree);
if (tokens.length != 4) throw syntaxError(subtree.line, subtree.col, '$macro takes exactly 3 arguments');
if (lib.ast.isList(tokens[1])) throw syntaxError(subtree.line, subtree.col, 'name must be a token, not a list');
const name = lib.ast.getContent(tokens[1]);
const args = tokens[2]; // as a partial ast tree
const code = tokens[3]; // as a partial ast tree
if (!lib.ast.isList(args) && !lib.ast.getContent(args).match(DIGITS)) {
throw syntaxError('argument count must be a number or expression');
}
if (macros.has(name)) throw syntaxError(tokens[1].line, tokens[1].col, `macro name must be unique: ${name}`);
macros.set(name, {
type: 'macro',
args: args,
code: code,
});
return name;
}
function defineJsMacro (subtree) {
const tokens = lib.ast.getTokens(subtree);
if (tokens.length != 4) throw syntaxError(subtree.line, subtree.col, '$jsmacro takes exactly 3 arguments');
if (lib.ast.isList(tokens[1])) throw syntaxError(subtree.line, subtree.col, 'name must be a token, not a list');
if (lib.ast.isList(tokens[3])) throw syntaxError(subtree.line, subtree.col, 'code must be a token, not a list');
const name = lib.ast.getContent(tokens[1]);
const args = tokens[2]; // as a partial ast tree
let code = lib.ast.getContent(tokens[3]);
if (!lib.ast.isList(args) && !lib.ast.getContent(args).match(DIGITS)) {
throw syntaxError(tokens[2].line, tokens[2].col, 'argument count must be a number or expression');
}
if (!code.startsWith('`') || !code.endsWith('`')) throw syntaxError(tokens[3].line, tokens[3].col, 'code must be quoted using backticks');
code = code.substring(1, code.length - 1);
if (macros.has(name)) throw syntaxError(tokens[1].line, tokens[1].col, `macro name must be unique: ${name}`);
macros.set(name, {
type: 'jsmacro',
args: args,
code: code,
});
return name;
}
function callMacro (subtree) {
const tokens = lib.ast.getTokens(subtree);
if (lib.ast.isList(tokens[0])) throw syntaxError(tokens[0].line, tokens[0].col, 'name must be a token, not a list');
const name = lib.ast.getContent(tokens[0]);
const givenArgs = tokens.slice(1);
const {type, args, code} = macros.get(name);
let argCountValid = false;
if (!lib.ast.isList(args)) {
let argCount = Number(lib.ast.getContent(args));
if (Number.isNaN(argCount))
throw syntaxError(tokens[0].line, tokens[0].col, 'argument count in original macro must be a number or expression'); // we should never see this error message
argCountValid = argCount == givenArgs.length;
} else {
let tree = lib.ast.createTree(`<macro condition ${name}>`);
let assignment = lib.ast.list('(', 0, 0);
lib.ast.pushToken(assignment, lib.ast.token('=', 0, 0));
lib.ast.pushToken(assignment, lib.ast.token('n', 0, 0));
lib.ast.pushToken(assignment, lib.ast.token(givenArgs.length.toString(), 0, 0));
lib.ast.pushStatement(tree, assignment);
lib.ast.pushStatement(tree, args);
try {
argCountValid = eval(convert(tree)); // jshint ignore:line
} catch (e) {
throw syntaxError(tokens[0].line, tokens[0].col, `Macro error [${name}]: ${e}`);
}
}
if (!argCountValid) throw syntaxError(tokens[0].line, tokens[0].col, `Invalid argument count for macro ${name}`);
let macroCode;
if (type == "jsmacro") {
macroCode = code;
} else if (type == "macro") {
macroCode = resolveNode(code);
} else {
throw internalError('Invalid macro type');
}
const prepend = `(getContent, resolveNode, isList, getTokens, _convert, embedLines, $$, ${givenArgs.map((_, i) => '$' + i.toString()).join(', ')}) => {`;
const postpend = `}`;
let result;
try {
result = eval(prepend + macroCode + postpend)(lib.ast.getContent, resolveNode, lib.ast.isList, lib.ast.getTokens, _convert, embedLines, givenArgs, ...givenArgs); // jshint ignore:line
} catch (e) {
throw syntaxError(tokens[0].line, tokens[0].col, `Macro error [${name}]: ${e}`);
}
if (result === undefined)
throw syntaxError(tokens[0].line, tokens[0].col, `Macro ${name} returned undefined (hint: you need to use the return keyword)`);
if (!(typeof result == 'string' || result instanceof String)) {
result = resolveNode(result); // If non string returned, assume that it is an ast tree
}
return result;
}
function resolveNode (node, dontAnnotate) {
return (lib.ast.isList(node) ? walk(node, dontAnnotate) : lib.ast.getContent(node)) + ((embedLines && !dontAnnotate) ? ` /* ${node.line}:${node.col} */` : '');
}
function resolveRest (list, after, dontAnnotate) {
after = after != null ? after : 1;
return lib.ast.getTokens(list).slice(after).map(token => resolveNode(token, dontAnnotate));
}
function walk (subtree, dontAnnotate) {
const firstToken = lib.ast.getTokenAt(subtree, 0);
const maybeContent = lib.ast.isList(firstToken) ? null : lib.ast.getContent(firstToken);
const func = resolveNode(firstToken, dontAnnotate);
if (maybeContent == '$macro') {
const name = defineMacro(subtree);
if (!embedLines) return `null`;
else return `null /* macro definition: ${name} */`;
} else if (maybeContent == '$jsmacro') {
const name = defineJsMacro(subtree);
if (!embedLines) return `null`;
else return `null /* js macro definition: ${name} */`;
} else if (macros.has(maybeContent)) {
return callMacro(subtree);
} else {
return `${func}(${resolveRest(subtree, null, dontAnnotate).join(', ')})`;
}
}
function _convert (statements) {
return statements
.map(statement => resolveNode(statement))
.filter(resolved => resolved != "null" && resolved.length > 0) // filter nops
.join(';\n') + ';';
}
function convert (tree) {
return _convert(tree.statements);
}
return convert(tree);
};
lib.simplifyAst = function (tree) {
let newTree = [];
function resolve (node) {
if (lib.ast.isList(node)) return walk(node);
else return lib.ast.getContent(node);
}
function walk (node) {
return lib.ast.getTokens(node).map(token => resolve(token));
}
return tree.statements.map(statement => resolve(statement));
};
lib.code2ast = function (code) {
return lib.parseAst(lib.tokenize(code));
};
lib.code2js = function (code, skipBootstrap, embedLines) {
return lib.ast2js(lib.code2ast((skipBootstrap ? '' : lib.bootstrap) + code), embedLines);
};
lib.eval = function (code, skipBootstrap, embedLines) {
return eval(lib.code2js(code, skipBootstrap, embedLines)); // jshint ignore:line
};
lib.bootstrap = '' + // some of bootstrap.lisp will be autogenerated
['=', '==', '===', '!=', '!==', '>', '<', '>=', '<=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??='] // assignment and comparison
.map(op => "($jsmacro {OP} 2 `return resolveNode($0) + '{OP}' + resolveNode($1)`)".replaceAll("{OP}", op)).join('') +
['+', '-', '*', '/', '%', '**', '&&', '||', '.', '?.', '&', '|', '^', '<<', '>>', '>>>', '??'] // math, boolean logic, and attribute access
.map(op => "($jsmacro {OP} (> n 0) `return $$.map(node => resolveNode(node)).join('{OP}')`)".replaceAll("{OP}", op)).join('') +
['!', '~', '...'].map(op => "($jsmacro {OP} 1 `return '{OP}' + resolveNode($0)`)".replaceAll("{OP}", op)).join('') +
['delete', 'typeof', 'void']
.map(op => "($jsmacro {OP} 1 `return '{OP} ' + resolveNode($0)`)".replaceAll("{OP}", op)).join('') +
['instanceof', 'in']
.map(op => "($jsmacro {OP} 2 `return resolveNode($0) + ' {OP} ' + resolveNode($1)`)".replaceAll('{OP}', op)).join('') +
['var', 'let', 'const']
.map(op => "($jsmacro {OP} 2 `return '{OP} ' + resolveNode($0) + '=' + resolveNode($1)`)".replaceAll('{OP}', op)).join('') +
"($jsmacro , (>= n 2) `return '(' + $$.map(node => resolveNode(node)).join(',') + ')'`)" +
// Misc
"($jsmacro // (>= n 0) `return '/* ' + $$.map(node => resolveNode(node, true)).join(' ') + ' */'`)" +
"($jsmacro debugger 0 `return 'debugger'`)" +
"($jsmacro with (>= n 0) `throw new Error('with is deprecated and should not be used')`)" +
// Array/Object access
"($jsmacro [] (>= n 2) `return resolveNode($0) + '[' + $$.slice(1).map(node => resolveNode(node)).join('][') + ']'`)" +
// Array/Object creation
"($jsmacro _[] (>= n 0) `return '[' + $$.map(node => resolveNode(node)).join(',') + ']'`)" +
"($jsmacro _{} (>= n 0) `return '{' + $$.map(list => isList(list) ? getTokens(list).map(node => resolveNode(node)).join(':') : resolveNode(list)).join(',') + '}'`)" +
"($jsmacro new 1 `return 'new ' + resolveNode($0)`)" +
// Functions
"($jsmacro function 2 `return 'function(' + (isList($0) ? getTokens($0).map(t => resolveNode(t)).join(',') : resolveNode($0)) + '){return ' + resolveNode($1) + '}'`)" +
"($jsmacro => 2 `return '(' + (isList($0) ? getTokens($0).map(t => resolveNode(t)).join(',') : resolveNode($0)) + ')=>' + resolveNode($1)`)" +
"($jsmacro return 1 `return 'return ' + resolveNode($0)`)" +
"($jsmacro class (> n 0) `return 'class ' + resolveNode($0) + '{' + $$.slice(1).map(node => resolveNode(node)).join('') + '}'`)" +
"($jsmacro subclass (> n 1) `return 'class ' + resolveNode($0) + ' extends ' + resolveNode($1) + '{' + $$.slice(2).map(node => resolveNode(node)).join('') + '}'`)" +
['static', 'yield', 'async', 'await']
.map(op => "($jsmacro {OP} 1 `return '{OP} ' + resolveNode($0)`)".replaceAll("{OP}", op)).join('') +
// Control Flow
"($jsmacro ? 3 `return '(' resolveNode($0) + '?' + resolveNode($1) + ':' + resolveNode($2) + ')'`)" +
"($jsmacro if (|| (== n 2) (== n 3)) `return 'if(' + resolveNode($0) + ')' + resolveNode($1) + ($$.length > 2 ? '\\\\n else ' + resolveNode($2) : '')`)" +
"($jsmacro while (> n 0) `return 'while(' + resolveNode($0) + '){' + _convert($$.slice(1)) + '}'`)" +
"($jsmacro do (> n 0) `return 'do{' + _convert($$.slice(1)) + '}while(' + resolveNode($0) + ');'`)" +
"($jsmacro for 4 `return 'for(' + resolveNode($0) + ';' + resolveNode($1) + ';' + resolveNode($2) + ')' + resolveNode($3)`)" +
"($jsmacro for_of 3 `return 'for(' + resolveNode($0) + ' of ' + resolveNode($1) + ')' + resolveNode($2)`)" +
["break", "continue"].map(op => "($jsmacro {OP} 0 `return '{OP}'`)".replaceAll("{OP}", op)).join("") +
'($jsmacro try (|| (== n 2) (== n 3) (== n 4)) `let tryBody,catchBody,catchVar,finallyBody;tryBody=$0;if($$.length==2){catchBody=$1;}else if($$.length==3){catchVar=$1;catchBody=$2;}else if($$.length==4){catchVar=$1;catchBody=$2;finallyBody=$3;}let string ="try{"+resolveNode(tryBody)+"}catch"+(catchVar?"("+resolveNode(catchVar)+")":"")+"{"+resolveNode(catchBody)+"}";if(finallyBody)string +="finally{"+resolveNode(finallyBody)+"}";return string`)' +
"($jsmacro throw 1 `return 'throw ' + resolveNode($0)`)" +
"($jsmacro switch (> n 1) `return 'switch(' + resolveNode($0) + '){' + _convert($$.slice(1)) + '}'`)" +
"($jsmacro case (> n 1) `return 'case ' + resolveNode($0) + ':' + _convert($$.slice(1))`)" +
"($jsmacro default (> n 0) `return 'default:' + _convert($$)`)" +
// Code grouping
"($jsmacro block (>= n 1) `return '{' + _convert($$) + '}'`)" +
"($jsmacro body (>= n 1) `return '(()=>{' + _convert($$) + '})()'`)" +
// A comment
"(// bootstrap.lisp automatically included)";
return lib;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment