Last active
January 25, 2024 07:07
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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