UNPKG

fez-lisp

Version:

Lisp interpreted & compiled to JavaScript

333 lines (330 loc) 10.8 kB
import { APPLY, ATOM, PLACEHOLDER, KEYWORDS, TYPE, VALUE, WORD, STATIC_TYPES, DEBUG } from './keywords.js' import { leaf, isLeaf, AST } from './parser.js' import { FALSE_WORD, TRUE_WORD } from './types.js' const deepRename = (name, newName, tree) => { if (!isLeaf(tree)) for (const leaf of tree) { // Figure out a non mutable solution so // I can get rid of deep clone AST.parse(AST.stringify(ast)) if (leaf[VALUE] === name) leaf[VALUE] = newName else deepRename(name, newName, leaf) } } const earMuffsToLodashes = (name) => name.replace(new RegExp(/\*/g), '_') const dotNamesToEmpty = (name) => name.replace(new RegExp(/\./g), '') const arrowFromTo = (name) => name.replace(new RegExp(/->/g), '-to-') const moduleNameToLodashes = (name) => name.replace(new RegExp(/:/g), '_') const questionMarkToPredicate = (name) => name.replace(new RegExp(/\?/g), '_predicate') const exclamationMarkMarkToEffect = (name) => name.replace(new RegExp(/\!/g), '_effect') const toCamelCase = (name) => { let out = name[0] for (let i = 1; i < name.length; ++i) { const current = name[i], prev = name[i - 1] if (current === '-') continue else if (prev === '-') out += current.toUpperCase() else out += current } return out } const dashToLodashes = (name) => name.replace(new RegExp(/-/g), '_') const keywordToHelper = (name) => { switch (name) { case STATIC_TYPES.ABSTRACTION: case STATIC_TYPES.COLLECTION: case STATIC_TYPES.UNKNOWN: case STATIC_TYPES.ATOM: case STATIC_TYPES.BOOLEAN: case STATIC_TYPES.ANY: case STATIC_TYPES.NUMBER: case STATIC_TYPES.NUMBERS: case STATIC_TYPES.ABSTRACTIONS: case STATIC_TYPES.BOOLEANS: case STATIC_TYPES.COLLECTIONS: case DEBUG.LOG: case DEBUG.STRING: return '__identity' case TRUE_WORD: return '__true' case FALSE_WORD: return '__false' case KEYWORDS.ADDITION: return '__add' case KEYWORDS.MULTIPLICATION: return '__mult' case KEYWORDS.DIVISION: return '__div' case KEYWORDS.SUBTRACTION: return '__sub' case KEYWORDS.GREATHER_THAN: return '__gt' case KEYWORDS.EQUAL: return '__eq' case KEYWORDS.GREATHER_THAN_OR_EQUAL: return '__gteq' case KEYWORDS.LESS_THAN: return '__lt' case KEYWORDS.LESS_THAN_OR_EQUAL: return '__lteq' case KEYWORDS.BITWISE_AND: return '__bit_and' case KEYWORDS.BITWISE_OR: return '__bit_or' case KEYWORDS.BITWISE_XOR: return '__bit_xor' case KEYWORDS.BITWISE_NOT: return '__bit_not' case KEYWORDS.BITWISE_LEFT_SHIFT: return '__bit_lshift' case KEYWORDS.BITWISE_RIGHT_SHIFT: return '__bit_rshift' default: return name } } const lispToJavaScriptVariableName = (name) => dashToLodashes( arrowFromTo( dotNamesToEmpty( exclamationMarkMarkToEffect( questionMarkToPredicate( moduleNameToLodashes(earMuffsToLodashes(keywordToHelper(name))) ) ) ) ) ) const Helpers = { __add: `__add=(a,b)=>{return a+b}`, __sub: `__sub=(a,b)=>{return a-b}`, __mult: `__mult=(a,b)=>{return a*b}`, __div: `__div=(a,b)=>{return a/b}`, __gteq: '__gteq=(a,b)=>+(a>=b)', __gt: '__gt=(a,b)=>+(a>b)', __eq: '__eq=(a,b)=>+(a===b)', __lteq: '__lteq=(a,b)=>+(a<=b)', __lt: '__lt=(a,b)=>+(a<b)', __bit_and: '__bit_and=(a,b)=>a&b', __bit_or: '__bit_or=(a,b)=>a|b', __bit_xor: '__bit_xor=(a,b)=>a^b', __bit_not: '__bit_not=(a)=>~a', __bit_lshift: '__bit_lshift=(a,b)=>a<<b', __bit_rshift: '__bit_rshift=(a,b)=>a>>b', array: 'array=(...args)=>args', not: 'not=(a)=>+!a', and: `and=(a, b)=>+(a&&b)`, or: `or=(a, b)=>+(a||b)`, get: 'get=(arr,i)=>arr[i]', length: 'length=(arr)=>arr.length', __identity: `__identity=(x)=>x`, atom_predicate: `atom_predicate=(number)=>+(typeof number==='number')`, lambda_predicate: `lambda_predicate=(fn)=>+(typeof fn==='function')`, set_effect: `set_effect=(array,index,value)=>{array[index] = value;return array}`, pop_effect: `pop_effect=(array)=>{array.pop();return array}` } const semiColumnEdgeCases = new Set([ ';)', ';-', ';+', ';*', ';%', ';&', ';/', ';:', ';.', ';=', ';<', ';>', ';|', ';,', ';?', ',,', ';;', ';]', ';^' ]) const parse = (tail, Drill) => tail.map((x) => comp(x, Drill)) const parseArgs = (tail, Drill, separator = ',') => parse(tail, Drill).join(separator) const comp = (tree, Drill) => { if (!tree) return '' let head, tail if (isLeaf(tree)) head = tree else { head = tree[0] tail = tree.slice(1) } const token = head[VALUE] if (head[TYPE] === APPLY) { switch (token) { case KEYWORDS.BLOCK: { if (tail.length > 1) { return `(${tail .map((x) => (comp(x, Drill) ?? '').toString().trim()) .filter((x) => x !== undefined) .join(',')});` } else { const res = comp(tail[0], Drill) return res !== undefined ? res.toString().trim() : '' } } case KEYWORDS.CALL_FUNCTION: { const head = tail.pop() const rest = tail const apply = comp(head, Drill) return `${ apply[apply.length - 1] === ';' ? apply.substring(0, apply.length - 1) : apply }(${parseArgs(rest, Drill)})` } case KEYWORDS.DEFINE_VARIABLE: { const n = tail[0][VALUE] const name = lispToJavaScriptVariableName(n) Drill.Variables.add(name) return `${name}=${comp(tail[1], Drill)};` } case KEYWORDS.IS_ATOM: Drill.Helpers.add('atom_predicate') return `atom_predicate(${comp(tail[0], Drill)});` case KEYWORDS.IS_LAMBDA: Drill.Helpers.add('lambda_predicate') return `lambda_predicate(${comp(tail[0], Drill)});` case KEYWORDS.CREATE_ARRAY: return `[${parseArgs(tail, Drill)}];` case KEYWORDS.ARRAY_LENGTH: Drill.Helpers.add('length') return `length(${comp(tail[0], Drill)})` case KEYWORDS.GET_ARRAY: Drill.Helpers.add('get') return `get(${comp(tail[0], Drill)}, ${comp(tail[1], Drill)});` case KEYWORDS.ANONYMOUS_FUNCTION: { const functionArgs = tail const body = tail.pop() const InnerDrills = { Variables: new Set(), Helpers: Drill.Helpers } const evaluatedBody = comp(body, InnerDrills) const vars = InnerDrills.Variables.size ? `var ${[...InnerDrills.Variables].join(',')};` : '' const args = parseArgs( functionArgs.map((node, index) => node[VALUE] === PLACEHOLDER ? leaf(node[TYPE], `_${index}`) : leaf(node[TYPE], node[VALUE]) ), InnerDrills ) // const $ = [${args}]; return `((${args})=>{${vars}return ${evaluatedBody .toString() .trim()}});` } case KEYWORDS.AND: return `((${parseArgs(tail, Drill, '&&')}) ? 1 : 0);` case KEYWORDS.OR: return `((${parseArgs(tail, Drill, '||')}) ? 1 : 0);` case KEYWORDS.EQUAL: return `+(${parseArgs(tail, Drill, '===')});` case KEYWORDS.GREATHER_THAN_OR_EQUAL: case KEYWORDS.LESS_THAN_OR_EQUAL: case KEYWORDS.GREATHER_THAN: case KEYWORDS.LESS_THAN: return `+(${parseArgs(tail, Drill, token)});` case KEYWORDS.SUBTRACTION: return `(${parse(tail, Drill) // Add space so it doesn't consider it 2--1 but 2- -1 .map((x) => (typeof x === 'number' && x < 0 ? ` ${x}` : x)) .join(token)});` case KEYWORDS.MULTIPLICATION: return `(${parseArgs(tail, Drill, token)});` case KEYWORDS.DIVISION: return `(${parseArgs(tail, Drill, token)});` case KEYWORDS.ADDITION: return `(${parseArgs(tail, Drill, token)});` case KEYWORDS.BITWISE_AND: case KEYWORDS.BITWISE_OR: case KEYWORDS.BITWISE_XOR: case KEYWORDS.BITWISE_LEFT_SHIFT: case KEYWORDS.BITWISE_RIGHT_SHIFT: return `(${parseArgs(tail, Drill, token)});` case KEYWORDS.REMAINDER_OF_DIVISION: return `(${comp(tail[0], Drill)}%${comp(tail[1], Drill)});` case KEYWORDS.BITWISE_NOT: return `~(${comp(tail[0], Drill)})` case KEYWORDS.NOT: return `(+!${comp(tail[0], Drill)})` case KEYWORDS.IF: { return `(${comp(tail[0], Drill)}?${comp(tail[1], Drill)}:${comp( tail[2], Drill )});` } case KEYWORDS.LOOP: { return `(()=>{while(${comp(tail[0], Drill)}){${comp( tail[1], Drill )}}return -1})();` } case STATIC_TYPES.ABSTRACTION: case STATIC_TYPES.COLLECTION: case STATIC_TYPES.UNKNOWN: case STATIC_TYPES.ATOM: case STATIC_TYPES.BOOLEAN: case STATIC_TYPES.ANY: case STATIC_TYPES.NUMBER: case STATIC_TYPES.NUMBERS: case STATIC_TYPES.ABSTRACTIONS: case STATIC_TYPES.BOOLEANS: case STATIC_TYPES.COLLECTIONS: case DEBUG.LOG: case DEBUG.STRING: return compile(tail[0], Drill) default: { const camelCased = lispToJavaScriptVariableName(token) if (camelCased in Helpers) Drill.Helpers.add(camelCased) return `${camelCased}(${parseArgs(tail, Drill)});` } } } else if (head[TYPE] === ATOM) return head[VALUE] else if (head[TYPE] === WORD) { const camelCased = lispToJavaScriptVariableName(token) if (camelCased in Helpers) Drill.Helpers.add(camelCased) return camelCased } } const HelpersEntries = new Map(Object.entries(Helpers)) export const compile = (ast) => { const Drill = { Variables: new Set(), Helpers: new Set() } const raw = AST.parse(AST.stringify(ast)) // cloning for fn renames mutations .map((tree) => comp(tree, Drill)) .filter((x) => x !== undefined) .join('\n') let program = '' for (let i = 0; i < raw.length; ++i) { const current = raw[i] const next = raw[i + 1] if (!semiColumnEdgeCases.has(current + next)) program += current } const help = Drill.Helpers.size ? `var ${[...Drill.Helpers.keys()] .map((x) => HelpersEntries.get(x)) .join(',')};\n` : '' const vars = Drill.Variables.size ? `var ${[...Drill.Variables].join(',')};` : '' const top = `${help}${vars}` return `(()=>{${top};return ${program}})()` }