UNPKG

fez-lisp

Version:

Lisp interpreted & compiled to JavaScript

742 lines (734 loc) 29.4 kB
import { isLeaf } from './parser.js' import { EXPONENTIATION_RAW, INTEGER_DIVISION, NOT_EQUAL } from '../lib/baked/macros.js' import { APPLY, ATOM, FALSE, KEYWORDS, PLACEHOLDER, STATIC_TYPES, TYPE, VALUE, WORD } from './keywords.js' import { hasBlock, stringifyArgs } from './utils.js' import { NIL } from './types.js' export const SUGGAR = { // Syntactic suggars PIPE: '|>', STR_LIST: '~~', NOT_EQUAL_1: '!=', NOT_EQUAL_2: '<>', REMAINDER_OF_DIVISION_1: '%', UNLESS: 'unless', CREATE_LIST: 'list', CREATE_SET: 'new:set', CREATE_MAP: 'new:map', POWER: '**', INTEGER_DEVISION: '//', CONDITION: 'cond', PROMISES: '*DATA*' } export const deSuggarAst = (ast, scope) => { if (scope === undefined) scope = ast if (ast.length === 0) throw new SyntaxError(`No expressions...`) // for (const node of ast) // if (node[0] && node[0][TYPE] === APPLY && node[0][VALUE] === KEYWORDS.BLOCK) // throw new SyntaxError(`Top level (${KEYWORDS.BLOCK}) is not allowed`) let prev = undefined const evaluate = (exp) => { const [first, ...rest] = isLeaf(exp) ? [exp] : exp if (first != undefined) { switch (first[TYPE]) { case WORD: { switch (first[VALUE]) { case SUGGAR.REMAINDER_OF_DIVISION_1: exp[VALUE] = KEYWORDS.REMAINDER_OF_DIVISION break case SUGGAR.NOT_EQUAL_1: case SUGGAR.NOT_EQUAL_2: exp.length = 0 exp.push(...NOT_EQUAL) break case SUGGAR.POWER: exp.length = 0 exp.push(...EXPONENTIATION_RAW) break case SUGGAR.INTEGER_DEVISION: exp.length = 0 exp.push(...INTEGER_DIVISION) break default: break } } break case ATOM: break case APPLY: { switch (first[VALUE]) { // ; Idea for pattern matching // ; (let f (lambda xs // ; (|= xs // ; [] 10 // ; [1] (array:tail xs) // ; (*) -1))) // (let f (lambda xs // (apply xs (lambda xs (cond // (= (length xs) 0) 10 // (= (get xs 0) 1) (array:tail xs) // (*) -1))))) // case KEYWORDS.CALL_FUNCTION: { // if (prev === undefined && scope[0][VALUE] === KEYWORDS.CALL_FUNCTION) { // exp[0][VALUE] = KEYWORDS.BLOCK // } // } // break case KEYWORDS.BLOCK: { if (rest.length === 0) throw new RangeError( `Invalid number of arguments for (${ KEYWORDS.BLOCK }), expected (>= 1) but got (0) (${ KEYWORDS.BLOCK } ${stringifyArgs(rest)})` ) if ( prev && prev[TYPE] === APPLY && prev[VALUE] !== KEYWORDS.ANONYMOUS_FUNCTION ) { exp[0][VALUE] = KEYWORDS.CALL_FUNCTION exp[0][TYPE] = APPLY exp.length = 1 scope = [[APPLY, KEYWORDS.BLOCK], ...rest] exp[1] = [[APPLY, KEYWORDS.ANONYMOUS_FUNCTION], scope] deSuggarAst(exp, scope) } else { scope = exp } } break case SUGGAR.PIPE: { if (rest.length < 1) throw new RangeError( `Invalid number of arguments to (${ SUGGAR.PIPE }) (>= 1 required). (${SUGGAR.PIPE} ${stringifyArgs( rest )})` ) let inp = rest[0] exp.length = 0 for (let i = 1; i < rest.length; ++i) { if (!rest[i].length || rest[i][0][TYPE] !== APPLY) throw new TypeError( `Argument at position (${i}) of (${ SUGGAR.PIPE }) is not an invoked (${ KEYWORDS.ANONYMOUS_FUNCTION }). (${SUGGAR.PIPE} ${stringifyArgs(rest)})` ) inp = [rest[i].shift(), inp, ...rest[i]] } for (let i = 0; i < inp.length; ++i) exp[i] = inp[i] deSuggarAst(exp, scope) } break case SUGGAR.CONDITION: { if (rest.length < 4) throw new RangeError( `Invalid number of arguments for (${ SUGGAR.CONDITION }), expected (> 3 required) but got ${rest.length} (${ SUGGAR.CONDITION } ${stringifyArgs(rest)})` ) if (rest.length % 2 !== 0) throw new RangeError( `Invalid number of arguments for (${ SUGGAR.CONDITION }), expected even number of arguments but got ${ rest.length } (${SUGGAR.CONDITION} ${stringifyArgs(rest)})` ) if (rest.at(-2)[0][VALUE] !== KEYWORDS.MULTIPLICATION) { throw new ReferenceError( `Last condition of (${ SUGGAR.CONDITION }), has to be a wildcard (${KEYWORDS.MULTIPLICATION}) (${ SUGGAR.CONDITION }) followed by a default result (${stringifyArgs(exp)}))` ) } exp.length = 0 let temp = exp for (let i = 0; i < rest.length; i += 2) { if (i === rest.length - 2) { temp.push([APPLY, KEYWORDS.IF], rest[i], rest.at(-1), [ [APPLY, STATIC_TYPES.ANY], [WORD, NIL] ]) } else { temp.push([APPLY, KEYWORDS.IF], rest[i], rest[i + 1], []) temp = temp.at(-1) } } deSuggarAst(exp, scope) } break case SUGGAR.STR_LIST: { const items = rest[0].slice(1) exp.length = 0 exp.push([APPLY, SUGGAR.CREATE_LIST]) exp.push(...items) deSuggarAst(exp, scope) } break case SUGGAR.CREATE_LIST: { exp.length = 0 let temp = exp for (const item of rest) { temp.push([APPLY, KEYWORDS.CREATE_ARRAY], item, []) temp = temp.at(-1) } temp.push([APPLY, KEYWORDS.CREATE_ARRAY]) } deSuggarAst(exp, scope) break case SUGGAR.INTEGER_DEVISION: { if (rest.length !== 2) throw new RangeError( `Invalid number of arguments for (${ SUGGAR.INTEGER_DEVISION }), expected (= 2) but got ${rest.length} (${ SUGGAR.INTEGER_DEVISION } ${stringifyArgs(rest)})` ) else if (rest.some((x) => x[TYPE] === APPLY)) { exp.length = 0 exp.push( [0, KEYWORDS.CALL_FUNCTION], ...rest, INTEGER_DIVISION ) } else { exp.length = 1 exp[0] = [APPLY, KEYWORDS.BITWISE_OR] exp.push([[APPLY, KEYWORDS.DIVISION], ...rest]) exp.push([ATOM, 0]) } } break case SUGGAR.POWER: { if (rest.length !== 2) throw new RangeError( `Invalid number of arguments for (${ SUGGAR.POWER }), expected (= 2) but got ${rest.length} (${ SUGGAR.POWER } ${stringifyArgs(rest)})` ) const isExponentAtom = exp[1][TYPE] === ATOM const isPowerAtom = exp[2][TYPE] === ATOM const isExponentWord = exp[1][TYPE] === WORD if ((isExponentWord || isExponentAtom) && isPowerAtom) { if (isExponentAtom) { exp[0][VALUE] = KEYWORDS.MULTIPLICATION const exponent = exp[1] const power = exp[2][VALUE] exp.length = 1 exp.push(exponent, [ATOM, exponent[VALUE] ** (power - 1)]) } else if (isExponentWord) { const exponent = exp[1] const power = exp[2] exp.length = 0 exp.push([APPLY, 'math:power'], exponent, power) } } else { const exponent = exp[1] const power = exp[2] exp.length = 0 exp.push([APPLY, 'math:power'], exponent, power) } deSuggarAst(exp, scope) } break case KEYWORDS.MULTIPLICATION: if (!rest.length) { // exp[0][TYPE] = ATOM exp[0][VALUE] = KEYWORDS.NOT exp[1] = [ATOM, FALSE] } else if (rest.length > 2) { exp.length = 0 let temp = exp for (let i = 0; i < rest.length; i += 1) { if (i < rest.length - 1) { temp.push([APPLY, KEYWORDS.MULTIPLICATION], rest[i], []) temp = temp.at(-1) } else { temp.push(...rest[i]) } } deSuggarAst(exp, scope) } break case KEYWORDS.ADDITION: if (!rest.length) { exp[0][TYPE] = ATOM exp[0][VALUE] = FALSE } else if (rest.length > 2) { exp.length = 0 let temp = exp for (let i = 0; i < rest.length; i += 1) { if (i < rest.length - 1) { temp.push([APPLY, KEYWORDS.ADDITION], rest[i], []) temp = temp.at(-1) } else temp.push(...rest[i]) } deSuggarAst(exp, scope) } break case KEYWORDS.SUBTRACTION: if (rest.length > 2) { exp.length = 0 rest.reverse() let temp = exp for (let i = 0; i < rest.length; i += 1) { if (i < rest.length - 1) { temp.push([APPLY, KEYWORDS.SUBTRACTION], [], rest[i]) temp = temp.at(-2) } else temp.push(...rest[i]) } deSuggarAst(exp, scope) } else { if (rest.length !== 1 && rest.length !== 2) throw new RangeError( `Invalid number of arguments for (${ KEYWORDS.SUBTRACTION }), expected (or (= 1) (= 2)) but got ${ rest.length }\n\n(${KEYWORDS.SUBTRACTION} ${stringifyArgs(rest)})` ) if (rest.length === 1) { exp[0][VALUE] = KEYWORDS.MULTIPLICATION exp.push([ATOM, -1]) deSuggarAst(exp, scope) } } break case KEYWORDS.DIVISION: if (!rest.length) { exp[0][VALUE] = KEYWORDS.NOT exp[1] = [ [APPLY, KEYWORDS.NOT], [ATOM, FALSE] ] } else if (rest.length === 1) { exp.length = 1 exp.push([ATOM, 1], rest[0]) } else if (rest.length > 2) { exp.length = 0 rest.reverse() let temp = exp for (let i = 0; i < rest.length; i += 1) { if (i < rest.length - 1) { temp.push([APPLY, KEYWORDS.DIVISION], [], rest[i]) temp = temp.at(-2) } else temp.push(...rest[i]) } deSuggarAst(exp, scope) } break case KEYWORDS.AND: if (!rest.length) { exp[0][TYPE] = ATOM exp[0][VALUE] = FALSE } else if (rest.length > 2) { exp.length = 0 let temp = exp for (let i = 0; i < rest.length; i += 1) { if (i < rest.length - 1) { temp.push([APPLY, KEYWORDS.AND], rest[i], []) temp = temp.at(-1) } else temp.push(...rest[i]) } deSuggarAst(exp, scope) } break case KEYWORDS.OR: if (!rest.length) { exp[0][TYPE] = ATOM exp[0][VALUE] = FALSE } else if (rest.length > 2) { exp.length = 0 let temp = exp for (let i = 0; i < rest.length; i += 1) { if (i < rest.length - 1) { temp.push([APPLY, KEYWORDS.OR], rest[i], []) temp = temp.at(-1) } else temp.push(...rest[i]) } deSuggarAst(exp, scope) } break case SUGGAR.UNLESS: { if (rest.length > 3 || rest.length < 2) throw new RangeError( `Invalid number of arguments for (${ SUGGAR.UNLESS }), expected (or (= 3) (= 2)) but got ${rest.length} (${ SUGGAR.UNLESS } ${stringifyArgs(rest)})` ) exp[0][VALUE] = KEYWORDS.IF const temp = exp[2] exp[2] = exp[3] ?? [WORD, NIL] exp[3] = temp } deSuggarAst(exp, scope) break case KEYWORDS.IF: { if (rest.length > 3 || rest.length < 2) throw new RangeError( `Invalid number of arguments for (${ KEYWORDS.IF }), expected (or (= 3) (= 2)) but got ${rest.length} (${ KEYWORDS.IF } ${stringifyArgs(rest)})` ) if (rest.length === 2) exp.push([WORD, NIL]) } deSuggarAst(exp[1], scope) break case SUGGAR.REMAINDER_OF_DIVISION_1: { if (rest.length !== 2) throw new RangeError( `Invalid number of arguments for (${ exp[0][1] }), expected (= 2) but got ${rest.length} (${ exp[0][1] } ${stringifyArgs(rest)})` ) exp[0][VALUE] = KEYWORDS.REMAINDER_OF_DIVISION deSuggarAst(exp, scope) } break case SUGGAR.NOT_EQUAL_1: case SUGGAR.NOT_EQUAL_2: { if (rest.length !== 2) throw new RangeError( `Invalid number of arguments for (${ exp[0][1] }), expected (= 2) but got ${rest.length} (${ exp[0][1] } ${stringifyArgs(rest)})` ) exp[0][VALUE] = KEYWORDS.NOT exp[1] = [[APPLY, KEYWORDS.EQUAL], exp[1], exp[2]] exp.length = 2 deSuggarAst(exp, scope) } break case KEYWORDS.DEFINE_VARIABLE: { const last = exp.at(-1) if (!isLeaf(exp[1]) && exp[1][0][TYPE] === APPLY) { const left = exp[1].slice(1) const right = last let newScope if (exp[1][0][VALUE] === SUGGAR.CREATE_LIST) { const lastLeft = left.pop() const vars = left if (!isLeaf(right) && right[0][TYPE] !== WORD) { throw new SyntaxError( `Destructuring requires right hand side to be a word but got an apply\n(${stringifyArgs( exp )})` ) } else { newScope = vars .map((x, i) => [x, i]) .filter((x) => x[0][VALUE] !== PLACEHOLDER) .map(([name, n]) => { let wrap = right for (let i = 0; i < n; ++i) { wrap = [ [APPLY, KEYWORDS.GET_ARRAY], wrap, [ATOM, 1] ] } return [ [APPLY, KEYWORDS.DEFINE_VARIABLE], name, [[APPLY, KEYWORDS.GET_ARRAY], wrap, [ATOM, 0]] ] }) if (lastLeft[VALUE] !== PLACEHOLDER) { let wrap = right for (let i = 0; i < vars.length; ++i) { wrap = [ [APPLY, KEYWORDS.GET_ARRAY], wrap, [ATOM, 1] ] } newScope.push([ [APPLY, KEYWORDS.DEFINE_VARIABLE], lastLeft, wrap ]) } } vars[0][TYPE] = WORD exp.length = 0 } else if (exp[1][0][VALUE] === KEYWORDS.CREATE_ARRAY) { if (!isLeaf(right) && right[0][TYPE] !== WORD) { throw new SyntaxError( `Destructuring requires right hand side to be a word but got an apply\n(${stringifyArgs( exp )})` ) } else { const lastLeft = left.pop() // const isList = exp[i][exp[i].length - 2][VALUE] === KEYWORDS.BITWISE_NOT const isSlicing = lastLeft[VALUE] !== PLACEHOLDER const vars = left const indexes = vars.map((x, i) => [i, x]) vars[0][TYPE] = WORD exp.length = 0 newScope = indexes .filter((x) => x[1][VALUE] !== PLACEHOLDER) .map(([i]) => [ [APPLY, KEYWORDS.DEFINE_VARIABLE], vars[i], [[APPLY, KEYWORDS.GET_ARRAY], right, [ATOM, i]] ]) if (isSlicing) newScope.push([ [APPLY, KEYWORDS.DEFINE_VARIABLE], lastLeft, [ [APPLY, 'array:drop'], right, [ATOM, indexes.at(-1)[0] + 1] ] ]) } } exp.iron = true exp.push(newScope) deSuggarAst(scope) } } break case KEYWORDS.ANONYMOUS_FUNCTION: { const block = exp.at(-1) const newBlock = [[APPLY, KEYWORDS.BLOCK]] for (let i = 1; i < exp.length - 1; ++i) { if ( !isLeaf(exp[i]) && exp[i][0][TYPE] === APPLY && (exp[i][0][VALUE] === KEYWORDS.CREATE_ARRAY || exp[i][0][VALUE] === SUGGAR.CREATE_LIST) ) { if (exp[i][0][VALUE] === SUGGAR.CREATE_LIST) { const left = exp[i].slice(1) const right = [WORD, `_arg${i}`] const lastLeft = left.pop() const vars = left const indexes = vars .map((x, i) => [x, i]) .filter((x) => x[0][VALUE] !== PLACEHOLDER) newBlock.push( ...indexes.map(([name, n]) => { let wrap = right for (let i = 0; i < n; ++i) { wrap = [ [APPLY, KEYWORDS.GET_ARRAY], wrap, [ATOM, 1] ] } return [ [APPLY, KEYWORDS.DEFINE_VARIABLE], name, [[APPLY, KEYWORDS.GET_ARRAY], wrap, [ATOM, 0]] ] }) ) if (lastLeft[VALUE] !== PLACEHOLDER) { let wrap = right for (let i = 0; i < vars.length; ++i) { wrap = [ [APPLY, KEYWORDS.GET_ARRAY], wrap, [ATOM, 1] ] } newBlock.push([ [APPLY, KEYWORDS.DEFINE_VARIABLE], lastLeft, wrap ]) } left[0][TYPE] = WORD exp[i] = right exp[i].length = 2 } else if (exp[i][0][VALUE] === KEYWORDS.CREATE_ARRAY) { const left = exp[i].slice(1) const right = [WORD, `_arg${i}`] left[0][TYPE] = WORD const lastLeft = left.pop() const isSlicing = lastLeft[VALUE] !== PLACEHOLDER const vars = left const indexes = vars.map((x, i) => [i, x]) // const tempBlcok = [...block[VALUE]] newBlock.push( ...indexes .filter((x) => x[1][VALUE] !== PLACEHOLDER) .map(([i]) => [ [APPLY, KEYWORDS.DEFINE_VARIABLE], vars[i], [[APPLY, KEYWORDS.GET_ARRAY], right, [ATOM, i]] ]) ) if (isSlicing) newBlock.push([ [APPLY, KEYWORDS.DEFINE_VARIABLE], lastLeft, [ [APPLY, 'array:drop'], right, [ATOM, indexes.at(-1)[0] + 1] ] ]) exp[i] = right exp[i].length = 2 } exp[exp.length - 1] = newBlock.concat( hasBlock(block) ? block.slice(1) : [block] ) deSuggarAst(exp) } } } break } prev = first } break default: { for (const e of exp) evaluate(e) } break } iron(scope) for (const r of rest) evaluate(r) } } evaluate(ast) iron(ast) return ast } export const replaceStrings = (source) => { // const quotes = source.match(/"(.*?)"/g) const quotes = source.match(/"(?:.*?(\n|\r))*?.*?"/g) // TODO: handle escaping if (quotes) for (const q of quotes) source = source.replaceAll( q, `(${KEYWORDS.CREATE_ARRAY} ${[...q.replaceAll('\r', '')] .slice(1, -1) .map((x) => x.charCodeAt(0)) .join(' ')})` ) return source } export const replaceTemplateLiteralStrings = (source) => { // const quotes = source.match(/"(.*?)"/g) const quotes = source.match(/`"(?:.*?(\n|\r))*?.*?"/g) // TODO: handle escaping if (quotes) for (const q of quotes) { const string = [...q.replaceAll('\r', '')].slice(2, -1) let str = `(array ` let isVar = false for (let i = 0; i < string.length; ++i) { if (isVar) { if (string[i] === '}') { isVar = false str += ' (array ' } else str += string[i] } else if (string[i] === '{') { isVar = true str += ') ' } else str += string[i].charCodeAt(0) + ' ' } str += ')' source = source.replaceAll(q, `(array:concat (array ${str}))`) } return source } const iron = (scope) => { const indecies = scope .map((x, i) => { return x.iron ? i : -1 }) .filter((x) => x !== -1) if (indecies.length) { const set = new Set(indecies) const copy = [] for (let i = 0; i < scope.length; ++i) { if (set.has(i)) { delete scope[i].iron copy.push(...scope[i][0]) } else { copy.push(scope[i]) } } for (let i = 0; i < copy.length; ++i) scope[i] = copy[i] } } export const seeIfProgramIsInvalid = (source) => { if (source.includes('()') || source.includes(',') || source.includes(';')) throw new SyntaxError(`Failed to parse due to invalid lisp programm near`) return source } export const replaceQuotes = (source) => source .replaceAll(/\(\(array /g, `(${SUGGAR.STR_LIST} (array `) .replaceAll(/\(\[/g, `(${SUGGAR.CREATE_SET} [`) .replaceAll(/\(\{/g, `(${SUGGAR.CREATE_MAP} [`) .replaceAll(/\[/g, `(${KEYWORDS.CREATE_ARRAY} `) .replaceAll(/\]/g, ')') .replaceAll(/\{/g, `(${SUGGAR.CREATE_LIST} `) .replaceAll(/\}/g, ')') export const deSuggarSource = (source) => replaceQuotes(replaceStrings(replaceTemplateLiteralStrings(source))) export const handleUnbalancedQuotes = (source) => { const diff = (source.match(/\"/g) ?? []).length % 2 if (diff !== 0) throw new SyntaxError(`Quotes are unbalanced "`) return source }