fez-lisp
Version:
Lisp interpreted & compiled to JavaScript
514 lines (508 loc) • 14.7 kB
JavaScript
import std from '../lib/baked/std.js'
import {
APPLY,
ATOM,
KEYWORDS,
STATIC_TYPES,
TYPE,
VALUE,
WORD
} from './keywords.js'
import { evaluate } from './evaluator.js'
import { isLeaf, LISP } from './parser.js'
import {
deSuggarAst,
deSuggarSource,
handleUnbalancedQuotes
} from './macros.js'
import { enhance, OPTIMIZATIONS } from './enhance.js'
import { type } from './check.js'
import stdT from '../lib/baked/std-T.js'
import { definedTypes, withCtxTypes } from './types.js'
import { compile } from './compiler.js'
export const logError = (error) =>
console.log('\x1b[31m', `\n${error}\n`, '\x1b[0m')
export const logSuccess = (output) => console.log('\x1b[32m', output, '\x1b[0m')
export const wrapInBracesString = (exp) => `(${stringifyArgs(exp)})`
export const logExp = function (exp, ...args) {
console.log(wrapInBracesString(exp), ...args)
return exp
}
export const log = (x) => {
console.log(x)
return x
}
export const formatCallstack = (callstack) =>
callstack
.reverse()
.map((x, i) => `${Array(i + 2).join(' ')}(${x} ...)`)
.join('\n')
export const formatErrorWithCallstack = (error, callstack) =>
`${error.message}\n${formatCallstack(callstack)}`
// TODO figure out why name is sometimes undefined
export const getSuffix = (str) => (str ? str[str.length - 1] : '')
export const getPrefix = (str) => str?.split(':')[0]
export const removeNoCode = (source) =>
source
.replace(/(;.+|;)/g, '')
.replace(/[\s\s]/g, ' ')
.trim()
export const isBalancedParenthesis = (sourceCode) => {
let count = 0
const stack = []
const str = sourceCode.match(/[/\(|\)]/g) ?? []
for (let i = 0; i < str.length; ++i) {
const current = str[i]
if (current === '(') stack.push(current)
else if (current === ')') if (stack.pop() !== '(') ++count
}
return count - stack.length
}
export const escape = (Char) => {
switch (Char) {
case '\\':
return '\\'
case 'n':
return '\n'
case 'r':
return '\r'
case 't':
return '\t'
case 's':
return ' '
case '"':
return '"'
default:
return ''
}
}
export const stringifyArrayTypes = (type) =>
Array.isArray(type)
? `(array${type.length ? ' ' : ''}${type
.map((x) => stringifyArrayTypes(x))
.join(' ')
.trim()})`
: 'number'
export const stringifyType = (type) => {
if (!isLeaf(type)) {
const [car] = type
if (car == undefined) return '(array)'
else if (car[TYPE] === APPLY && car[VALUE] === KEYWORDS.CREATE_ARRAY)
return `(array ${type
.map((t) => stringifyType(t))
.join(' ')
.trim()})`
return type
.map((t) => stringifyType(t))
.join(' ')
.trim()
} else if (type[TYPE] === ATOM) return 'number'
}
export const stringifyArgs = (args) =>
args
.map((x) =>
!isLeaf(x)
? `(${stringifyArgs(x)})`
: x[TYPE] === APPLY || x[TYPE] === WORD
? x[VALUE]
: JSON.stringify(x[VALUE])
.replace(new RegExp(/\[/g), '(')
.replace(new RegExp(/\]/g), ')')
.replace(new RegExp(/\,/g), ' ')
.replace(new RegExp(/"/g), '')
)
.join(' ')
const KEYWORDS_SET = Object.values(KEYWORDS).reduce((a, b) => {
a.add(b)
return a
}, new Set())
export const isForbiddenVariableName = (name) => {
switch (name) {
case '_':
case OPTIMIZATIONS.RECURSION:
case OPTIMIZATIONS.CACHE:
return true
default:
return KEYWORDS_SET.has(name) || !isNaN(name[0])
}
}
export const isEqual = (a, b) =>
+(
(Array.isArray(a) &&
a.length === b.length &&
!a.some((_, i) => !isEqual(a.at(i), b.at(i)))) ||
a === b ||
0
)
export const isEqualTypes = (a, b) =>
(typeof a !== 'object' && typeof b !== 'object' && typeof a === typeof b) ||
(Array.isArray(a) &&
Array.isArray(b) &&
(!a.length ||
!b.length ||
!(a.length > b.length ? a : b).some(
(_, i, bigger) =>
!isEqualTypes(
bigger.at(i),
(a.length > b.length ? b : a).at(
i % (a.length > b.length ? b : a).length
)
)
))) ||
false
export const isPartialTypes = (a, b) =>
(typeof a !== 'object' && typeof b !== 'object' && typeof a === typeof b) ||
(Array.isArray(a) &&
Array.isArray(b) &&
(!a.length ||
!b.length ||
!(a.length < b.length ? a : b).some(
(_, i, smaller) =>
!isEqualTypes(
smaller.at(i),
(a.length < b.length ? b : a).at(
i % (a.length < b.length ? b : a).length
)
)
))) ||
false
export const handleUnbalancedParens = (source) => {
const diff = isBalancedParenthesis(removeNoCode(source))
if (diff !== 0)
throw new SyntaxError(
`Parenthesis are unbalanced by ${diff > 0 ? '+' : ''}${diff}`
)
return source
}
export const removeMutation = (source) => source.replace(new RegExp(/!/g), 'ǃ')
const isDefinition = (x) =>
x[TYPE] === APPLY && x[VALUE] === KEYWORDS.DEFINE_VARIABLE
// [[, [, libs]]] is because std is wrapped in (apply (lambda (do ...)))
const toDeps = ([[, [, libs]]]) =>
// slice 1 so we get rid of do
libs
.slice(1)
.reduce(
(a, x, i) => a.set(x.at(1)[VALUE], { value: x, index: i }),
new Map()
)
const deepShake = (tree, deps, visited = new Set(), ignored = new Set()) => {
const type = tree[TYPE]
const value = tree[VALUE]
if (!isLeaf(tree)) {
const [car, ...rest] = tree
if (car == undefined) return
if (isDefinition(car)) {
if (
!isLeaf(rest.at(-1)) &&
rest
.at(-1)
.some(
(x) => x[TYPE] === APPLY && x[VALUE] === KEYWORDS.ANONYMOUS_FUNCTION
)
) {
const args = rest.at(-1).filter((x) => !isDefinition(x))
const body = args.pop()
// const params = new Set(args.map((x) => x[VALUE])
for (const arg of args) ignored.add(arg[VALUE])
deepShake(body, deps, visited, ignored)
} else rest.forEach((x) => deepShake(x, deps, visited, ignored))
} else tree.forEach((x) => deepShake(x, deps, visited, ignored))
} else if (
(type === APPLY || type === WORD) &&
deps.has(value) &&
!visited.has(value) &&
!ignored.has(value)
) {
visited.add(value)
deepShake(deps.get(value).value, deps, visited, ignored)
}
}
export const hasBlock = (body) =>
body[0] && body[0][TYPE] === APPLY && body[0][VALUE] === KEYWORDS.BLOCK
export const hasApplyLambdaBlock = (body) =>
body[0] &&
body[0][TYPE] === APPLY &&
body[0][VALUE] === KEYWORDS.CALL_FUNCTION &&
body[1][0][VALUE] === KEYWORDS.ANONYMOUS_FUNCTION &&
body[1][1][0][VALUE] === KEYWORDS.BLOCK
const extractDeps = (visited, deps) =>
[...visited]
.map((x) => deps.get(x))
.sort((a, b) => a.index - b.index)
.map((x) => x.value)
const toIgnore = (ast) => {
const out = []
const dfs = (exp) => {
const [head, ...tail] = isLeaf(exp) ? [exp] : exp
if (head == undefined) return []
switch (head[TYPE]) {
case WORD:
break
case ATOM:
break
case APPLY:
{
switch (head[VALUE]) {
case KEYWORDS.DEFINE_VARIABLE:
out.push(tail[0][VALUE])
break
default:
for (const r of tail) dfs(r)
break
}
}
break
}
}
dfs(ast[0])
return out
// ast.filter(([x]) => isDefinition(x)).map(([_, x]) => x[VALUE])
}
export const treeShake = (ast, libs) => {
const deps = toDeps(libs)
const visited = new Set()
const ignored = new Set(toIgnore(ast))
deepShake(ast, deps, visited, ignored)
return extractDeps(visited, deps)
}
export const shakedList = (ast, libs) => {
const deps = toDeps(libs)
const visited = new Set()
const ignored = new Set(toIgnore(ast))
deepShake(ast, deps, visited, ignored)
const out = []
for (const [key] of deps) if (visited.has(key)) out.push(key)
return out
}
export const dfs = (tree, callback) => {
if (!isLeaf(tree)) for (const leaf of tree) dfs(leaf)
else callback(tree)
}
export const wrapInBlock = (ast) => [
[APPLY, KEYWORDS.CALL_FUNCTION],
[
[APPLY, KEYWORDS.ANONYMOUS_FUNCTION],
[[APPLY, KEYWORDS.BLOCK], ...ast]
]
]
export const interpret = (ast, keywords) =>
ast.reduce((_, x) => evaluate(x, keywords), 0)
export const shake = (parsed, std) => treeShake(parsed, std).concat(parsed)
export const tree = (source, std) =>
std
? shake(LISP.parse(deSuggarSource(removeNoCode(source))), std)
: LISP.parse(deSuggarSource(removeNoCode(source)))
export const minify = (source) =>
LISP.source(deSuggarAst(LISP.parse(deSuggarSource(removeNoCode(source)))))
export const prep = (source) =>
deSuggarAst(LISP.parse(removeNoCode(deSuggarSource(source))))
export const src = (source, deps) =>
LISP.source(
wrapInBlock(
shake(
prep(source),
deps.reduce((a, b) => a.concat(b), [])
)
)
)
export const ast = (source, deps) =>
wrapInBlock(
shake(
prep(source),
deps.reduce((a, b) => a.concat(b), [])
)
)
export const astWithStd = (source) => wrapInBlock(shake(prep(source), std))
export const unwrapped = (source) => shake(prep(source), std)
export const parse = (source) =>
wrapInBlock(
shake(
deSuggarAst(
LISP.parse(
removeNoCode(
handleUnbalancedQuotes(
handleUnbalancedParens(deSuggarSource(source))
)
)
)
),
std
)
)
export const addTypeIdentities = (ast) => {
const block = ast[1][1]
const temp = block.shift()
block.unshift(
temp,
identity(STATIC_TYPES.ABSTRACTION),
identity(STATIC_TYPES.ATOM),
identity(STATIC_TYPES.COLLECTION),
identity(STATIC_TYPES.BOOLEAN),
identity(STATIC_TYPES.NUMBER),
identity(STATIC_TYPES.ANY),
identity(STATIC_TYPES.UNKNOWN)
)
}
export const UTILS = {
handleUnbalancedQuotes,
handleUnbalancedParens,
logError,
logSuccess,
formatErrorWithCallstack,
wrapInBlock,
isEqual,
stringifyArgs,
shake
}
export class Brr {
constructor(...items) {
this._left = [Brr._negativeZeroSymbol]
this._right = []
if (items.length === 0) return this
const half = (items.length / 2) | 0.5
for (let i = half - 1; i >= 0; --i) this._left.push(items[i])
for (let i = half; i < items.length; ++i) this._right.push(items[i])
return this
}
_addToLeft(item) {
this._left.push(item)
}
_addToRight(item) {
this._right.push(item)
}
_removeFromLeft() {
const len = this.length
if (len) {
if (len === 1) this.clear()
else if (this._left.length > 0) this._left.pop()
}
}
_removeFromRight() {
const len = this.length
if (len) {
if (len === 1) this.clear()
else if (this._right.length > 0) this._right.pop()
}
}
static _negativeZeroSymbol = Symbol('-0')
static isBrr(entity) {
return entity instanceof Brr
}
_offsetLeft() {
return (this._left.length - 1) * -1
}
_offsetRight() {
return this._right.length
}
get length() {
return this._left.length + this._right.length - 1
}
get first() {
return this.get(0)
}
get last() {
return this.get(-1)
}
get(offset) {
if (offset < 0) offset = this.length + offset
const offsetIndex = offset + this._offsetLeft()
const index = offsetIndex < 0 ? offsetIndex * -1 : offsetIndex
return offsetIndex >= 0 ? this._right[index] : this._left[index]
}
set(index, value) {
index = index < 0 ? this.length + index : index
const offset = index + this._offsetLeft()
if (offset >= 0) this._right[offset] = value
else this._left[offset * -1] = value
return this
}
append(item) {
this._addToRight(item)
return this
}
prepend(item) {
this._addToLeft(item)
return this
}
cut() {
if (this._offsetRight() === 0) this.balance()
const last = this.last
this._removeFromRight()
return last
}
chop() {
if (this._offsetLeft() === 0) this.balance()
const first = this.first
this._removeFromLeft()
return first
}
head() {
if (this._offsetRight() === 0) this.balance()
this._removeFromRight()
return this
}
tail() {
if (this._offsetLeft() === 0) this.balance()
this._removeFromLeft()
return this
}
clear() {
this._left.length = 1
this._right.length = 0
return this
}
isBalanced() {
return this._offsetRight() + this._offsetLeft() === 0
}
balance() {
if (this.isBalanced()) return this
const initial = [...this]
this.clear()
const half = (initial.length / 2) | 0.5
for (let i = half - 1; i >= 0; --i) this._addToLeft(initial[i])
for (let i = half; i < initial.length; ++i) this._addToRight(initial[i])
return this
}
static from(iterable) {
const out = new Brr()
const half = (iterable.length / 2) | 0.5
for (let i = half - 1; i >= 0; --i) out._addToLeft(iterable[i])
for (let i = half; i < iterable.length; ++i) out._addToRight(iterable[i])
return out
}
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate — A function that accepts up to three arguments.
* The filter method calls the predicate function one time for each element in the array.
*/
filter(callback = _Identity) {
const out = []
for (let i = 0, len = this.length; i < len; ++i) {
const current = this.get(i)
const predicat = callback(current, i, this)
if (predicat) out.push(current)
}
return Brr.from(out)
}
// reverse() {
// const left = this._left
// const right = this._right
// right.unshift(left.shift())
// this._left = right
// this._right = left
// return this
// }
*[Symbol.iterator]() {
for (let i = 0, len = this.length; i < len; ++i) yield this.get(i)
}
}
export const fez = (ast, c = false) => {
try {
if (!c) type(ast, withCtxTypes(definedTypes(stdT)))
const opt = enhance(ast)
return [c ? compile(ast) : evaluate(opt), null]
} catch (err) {
return [null, err]
}
}