UNPKG

fez-lisp

Version:

Lisp interpreted & compiled to JavaScript

1,445 lines (1,430 loc) 91.4 kB
import { APPLY, ATOM, FALSE, KEYWORDS, MUTATION_SUFFIX, MUTATORS_SET, PLACEHOLDER, PREDICATE_SUFFIX, PREDICATES_OUTPUT_SET, SPECIAL_FORMS_SET, STATIC_TYPES, STATIC_TYPES_SET, TRUE, TYPE, VALUE, WORD } from './keywords.js' import { isLeaf } from './parser.js' import { SPECIAL_FORM_TYPES, toTypeNames, ARG_COUNT, VARIADIC, STATS, ARGUMENTS, UNKNOWN, RETURNS, SCOPE_NAME, TYPE_PROP, SIGNATURE, MAX_RETRY_DEFINITION, MAX_ARGUMENT_RETRY, COLLECTION, ANY, formatType, ANONYMOUS_FUNCTION_TYPE_PREFIX, validateLambda, NIL, TRUE_WORD, FALSE_WORD, BOOLEAN_SUBTYPE, formatSubType, BOOLEAN, IS_ARGUMENT, NUMBER, NUMBER_SUBTYPE, SubType, GET_ARRAY_INFERENCE_SET, GENERIC, TYPE_NAME, RETURN_NAME } from './types.js' import { Brr, getSuffix, hasApplyLambdaBlock, hasBlock, log, logExp, stringifyArgs, wrapInApplyLambda, wrapInArray, wrapInBlock } from './utils.js' export const identity = (name) => [ [0, 'let'], [1, name], [ [0, 'lambda'], [1, 'x'], [1, 'x'] ] ] export const typeSetDefaultFunction = (Types, name, env, exp) => Types.set(withScope(name, env), () => formatType(name, env)) const returnType = (rest) => { const body = rest.at(-1) const rem = hasBlock(body) ? body.at(-1) : body return isLeaf(rem) ? rem : rem[0] } const drillReturnType = (rest, condition) => { const body = rest.at(-1) const rem = hasBlock(body) ? body.at(-1) : body const returns = isLeaf(rem) ? rem : rem[0] return condition(returns) ? drillReturnType(rem.at(-1), condition) : [returns, rem] } const deepLambdaReturn = (rest, condition) => { const body = rest.at(-1) const rem = hasBlock(body) ? body.at(-1) : body return condition(rem) ? rem : deepLambdaReturn(rem, condition) } export const isUnknownNotAnyType = (stats) => stats && !isAnyType(stats) && isUnknownType(stats) export const isUnknownNotAnyReturn = (stats) => stats && !isAnyReturn(stats) && isUnknownReturn(stats) export const castType = (stats, type) => { return ( (stats[TYPE_PROP][0] = type[RETURNS][0]), type[RETURNS][1] && (stats[TYPE_PROP][1] = type[RETURNS][1]) ) } export const castReturn = (stats, type) => { return ( (stats[RETURNS][0] = type[RETURNS][0]), type[RETURNS][1] && (stats[RETURNS][1] = type[RETURNS][1]) ) } export const isGenericReturn = (stats) => stats[RETURNS].length === 3 && stats[RETURNS][2][0] !== -1 export const isGenericType = (stats) => stats[TYPE_PROP].length === 3 && stats[TYPE_PROP][2][0] !== -1 export const isGenericProp = (stats, prop) => stats[prop].length === 3 && stats[prop][2][0] !== -1 export const isTypeAbstraction = (stats) => stats[TYPE_PROP] === APPLY export const setPropToAtom = (stats, prop) => { return ( (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && ((stats[prop][0] = ATOM), (stats[prop][1] = NUMBER_SUBTYPE())) ) } const setReturnToGeneric = (stats, index) => { stats[RETURNS] = [UNKNOWN, undefined, [index, 1]] } export const setPropToPredicate = (stats, prop) => { return (stats[prop][1] = BOOLEAN_SUBTYPE()) } export const setReturnToPredicate = (stats) => { return (stats[RETURNS][1] = BOOLEAN_SUBTYPE()) } export const setTypeToPredicate = (stats) => { return (stats[RETURNS][1] = BOOLEAN_SUBTYPE()) } export const setPropToAbstraction = (stats, prop) => { if (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) { stats[prop][0] = APPLY // since we don't know how many they are we just set the args to variadic if (!stats[ARG_COUNT]) stats[ARG_COUNT] = VARIADIC } return APPLY } export const setPropToCollection = (stats, prop) => { return ( (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && (stats[prop][0] = COLLECTION) ) } export const setProp = (stats, prop, value) => { if ( (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && value[prop][0] !== UNKNOWN && value[prop][0] !== ANY ) { stats[prop][0] = value[prop][0] if (value[prop][1]) stats[prop][1] = value[prop][1] } } export const setPropToReturn = (stats, prop, value) => { if ( (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && value[RETURNS][0] !== UNKNOWN && value[RETURNS][0] !== ANY ) { stats[prop][0] = value[RETURNS][0] if (value[RETURNS][1]) stats[prop][1] = value[RETURNS][1] } } export const setPropToReturnRef = (stats, prop, value) => { if ( stats[prop] && (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && value[RETURNS][0] !== UNKNOWN && value[RETURNS][0] !== ANY ) stats[prop] = value[RETURNS] } export const setPropToType = (stats, prop, value) => { if ( stats[prop] && (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && value[TYPE_PROP][0] !== ANY ) { stats[prop][0] = value[TYPE_PROP][0] if (value[TYPE_PROP][1]) stats[prop][1] = value[TYPE_PROP][1] } } export const setPropToTypeRef = (stats, prop, value) => { if (stats[prop] && (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY)) stats[prop] = value[TYPE_PROP] } export const setReturnToAtom = (stats) => { if (isUnknownReturn(stats) || isAnyReturn(stats)) { stats[RETURNS][0] = ATOM stats[RETURNS][1] = NUMBER_SUBTYPE() } } export const setTypeToAtom = (stats) => (isUnknownType(stats) || isAnyType(stats)) && ((stats[TYPE_PROP][0] = ATOM), (stats[TYPE_PROP][1] = NUMBER_SUBTYPE())) export const setTypeToCollection = (stats) => (isUnknownType(stats) || isAnyType(stats)) && (stats[TYPE_PROP][0] = COLLECTION) export const setTypeToAbstraction = (stats) => (isUnknownType(stats) || isAnyType(stats)) && (stats[TYPE_PROP][0] = APPLY) export const setReturnToAbstraction = (stats) => isUnknownReturn(stats) && (stats[RETURNS][0] = APPLY) export const setTypeRef = (stats, value) => (isUnknownType(stats) || isAnyType(stats)) && (stats[TYPE_PROP] = value[TYPE_PROP]) export const setReturnRef = (stats, value) => { // to prevent overiding the type of sepcial form or if the type of the function is any // any could also be deliberate as some functions like array:get and array;first // are sort of special forms too // In general anything that has type of any should not be infered if (SPECIAL_FORMS_SET.has(value[SIGNATURE]) || isAnyReturn(value)) { return setReturn(stats, value) } return isUnknownReturn(stats) && (stats[RETURNS] = value[RETURNS]) } export const setReturnToTypeRef = (stats, value) => { return ( (isUnknownReturn(stats) || isAnyReturn(stats)) && (stats[RETURNS] = value[TYPE_PROP]) ) } export const setStats = (a, b) => (a[STATS] = b[STATS]) export const cloneStats = (name, x) => ({ [STATS]: { ...x[STATS], [SIGNATURE]: name } }) export const setStatsRef = (a, b) => (a[STATS] = b[STATS]) export const setTypeToReturnRef = (stats, value) => { // To prevent getters overwritting the array subtype // Change main type if unknown if (isUnknownType(stats) || isAnyType(stats)) if (!isUnknownReturn(value)) stats[TYPE_PROP][0] = value[RETURNS][0] // TODO: Figure out what we do if generic things get inferred // change sub type if it doesn't have if (!hasSubType(stats) || getSubType(stats).has(UNKNOWN)) if (hasSubReturn(value)) stats[TYPE_PROP][1] = value[RETURNS][1] } export const setPropRef = (stats, prop, value) => { return ( (stats[prop][0] === UNKNOWN || stats[prop][0] === ANY) && (stats[prop] = value[prop]) ) } export const setReturn = (stats, value) => { return ( isUnknownReturn(stats) && !isUnknownReturn(value) && ((stats[RETURNS][0] = value[RETURNS][0]), value[RETURNS][1] && (stats[RETURNS][1] = value[RETURNS][1])) ) } export const setType = (stats, value) => (isUnknownType(stats) || isAnyType(stats)) && !isUnknownType(value) && ((stats[TYPE_PROP][0] = value[TYPE_PROP][0]), value[TYPE_PROP][1] && (stats[TYPE_PROP][1] = value[TYPE_PROP][1])) export const setSubType = (stats, value) => // makes no sense to protect this for now hasSubType(value) && (stats[TYPE_PROP][1] = value[TYPE_PROP][1]) export const setPropToSubType = (stats, prop, value) => // makes no sense to protect this for now hasSubType(value) && (stats[prop][1] = value[TYPE_PROP][1]) export const setPropToSubReturn = (stats, prop, value) => // makes no sense to protect this for now hasSubReturn(value) && (stats[prop][1] = value[RETURNS][1]) export const setTypeToReturn = (stats, value) => (isUnknownType(stats) || isAnyType(stats)) && !isUnknownReturn(value) && ((stats[TYPE_PROP][0] = value[RETURNS][0]), value[RETURNS][1] && (stats[TYPE_PROP][1] = value[RETURNS][1])) export const setReturnToType = (stats, value) => (isUnknownReturn(stats) || isAnyReturn(stats)) && !isUnknownType(value) && ((stats[RETURNS][0] = value[TYPE_PROP][0]), value[TYPE_PROP][1] && (stats[RETURNS][1] = value[TYPE_PROP][1])) export const isAnyReturn = (stats) => stats && stats[RETURNS][0] === ANY export const isAnyType = (stats) => stats && stats[TYPE_PROP][0] === ANY export const isUnknownType = (stats) => stats && stats[TYPE_PROP][0] === UNKNOWN export const isUnknownProp = (stats, prop) => stats && stats[prop][0] === UNKNOWN export const isSubType = (subtype) => subtype instanceof SubType export const isSubTypeUknown = (subtype) => subtype.size === 1 && subtype.has(UNKNOWN) export const matchSub = (a, b) => a.has(UNKNOWN) || b.has(UNKNOWN) || a.has(ANY) || b.has(ANY) || a.isMatching(b) export const isUnknownReturn = (stats) => stats[RETURNS][0] === UNKNOWN export const getType = (stats) => stats && stats[TYPE_PROP][0] export const getTypes = (stats) => stats && stats[TYPE_PROP] export const getReturn = (stats) => stats && stats[RETURNS][0] export const getReturns = (stats) => stats && stats[RETURNS] export const getSubType = (stats) => stats && stats[TYPE_PROP][1] export const hasSubType = (stats) => stats && isSubType(stats[TYPE_PROP][1]) export const getSubReturn = (stats) => stats && stats[RETURNS][1] export const hasSubReturn = (stats) => stats && isSubType(stats[RETURNS][1]) export const isUnknownSubReturn = (stats) => !hasSubReturn(stats) || (stats && stats[RETURNS] && stats[RETURNS][1] && isSubTypeUknown(stats[RETURNS][1])) export const isUnknownSubType = (stats) => hasSubType(stats) && stats && stats[TYPE_PROP] && stats[TYPE_PROP][1] && isSubTypeUknown(stats[TYPE_PROP][1]) export const isAtomType = (stats) => isAnyType(stats) || stats[TYPE_PROP][0] === ATOM export const isAtomReturn = (stats) => isAnyType(stats) || stats[RETURNS][0] === ATOM export const equalTypes = (a, b) => { const isAnyAny = isAnyType(a) || isAnyType(b) if (isAnyAny) return true const isSameType = a[TYPE_PROP][0] === b[TYPE_PROP][0] if (!isSameType) return false return true } const isRedefinedInLambda = (env, name, exp) => { if (exp.slice(1, -1).some((x) => x[VALUE] === name)) return true else if ( exp .at(-1) .some( (x) => !isLeaf(x) && x[0][TYPE] === APPLY && x[0][VALUE] === KEYWORDS.DEFINE_VARIABLE && x[1][VALUE] === name ) ) return true else return false } const getGenericPropNest = (ref, prop) => ref[STATS][prop][0] === COLLECTION ? isSubType(ref[STATS][prop][1]) ? ref[STATS][prop][1].nestedLevels() : 0 : 0 const getGenericProp = (ref, rem, prop) => { const [index, multiplier] = ref[STATS][prop][2] const generic = rem.slice(1)[index] const nestGeneric = getGenericPropNest(ref, prop) return [ generic, isLeaf(generic) ? generic : generic[0], nestGeneric, multiplier ] } const getGenericReturn = (ref, rem) => getGenericProp(ref, rem, RETURNS) const getGenericType = (ref, rem) => getGenericProp(ref, rem, TYPE_PROP) const resolveGenericNest = (ref, prop, nestGeneric, multiplier) => { const hasSubType = isSubType(ref[STATS][prop][1]) const T = hasSubType ? ref[STATS][prop][1].types : [ref[STATS][prop][0]] if (multiplier === -1) { if (nestGeneric === 0) { if (T.at(-1) === NUMBER || T.at(-1) === BOOLEAN) { if (hasSubType) { if (ref[STATS][prop][1].types.length === 1) ref[STATS][prop][0] = ATOM else { ref[STATS][prop][1] = new SubType( ref[STATS][prop][1].types.slice(1) ) ref[STATS][prop][0] = COLLECTION ref[STATS][prop].length = 2 } } else ref[STATS][prop][0] = ATOM } } else { if (T.length - nestGeneric - 1) { if (hasSubType) for (let i = 0; i < nestGeneric + 1; ++i) ref[STATS][prop][1].types.shift() ref[STATS][prop].length = 2 } else { if (T.at(-1) === NUMBER || T.at(-1) === BOOLEAN) { ref[STATS][prop][0] = ATOM ref[STATS][prop][1] = new SubType([T.at(-1)]) ref[STATS][prop].length = 2 } else { ref[STATS][prop][0] = APPLY ref[STATS][prop].length = 1 } } } } else if (multiplier === 1) { const st = new SubType([]) for (let i = 0; i < nestGeneric; ++i) st.add(COLLECTION) if (ref[STATS][prop][0] === COLLECTION) st.add(COLLECTION) st.add(...T) ref[STATS][prop][0] = COLLECTION ref[STATS][prop][1] = st } } export const equalReturns = (a, b) => isAnyReturn(a) || isAnyReturn(b) || a[RETURNS][0] === b[RETURNS][0] export const equalsTypeWithReturn = (a, b) => isAnyType(a) || isAnyReturn(b) || a[TYPE_PROP][0] === b[RETURNS][0] const IsPredicate = (leaf) => (leaf[TYPE] === ATOM && (leaf[VALUE] === TRUE || leaf[VALUE] === FALSE)) || (leaf[TYPE] === WORD && (leaf[VALUE] === TRUE_WORD || leaf[VALUE] === FALSE_WORD || leaf[VALUE] === NIL || getSuffix(leaf[VALUE]) === PREDICATE_SUFFIX)) || (leaf[TYPE] === APPLY && (PREDICATES_OUTPUT_SET.has(leaf[VALUE]) || getSuffix(leaf[VALUE]) === PREDICATE_SUFFIX)) const equalSubTypes = (a, b) => { return ( !hasSubType(a) || !hasSubType(b) || matchSub(getSubType(a), getSubType(b)) ) } const equalSubReturns = (a, b) => { return ( !hasSubReturn(a) || !hasSubReturn(b) || matchSub(getSubReturn(a), getSubReturn(b)) ) } const equalSubTypesWithSubReturn = (a, b) => { return ( !hasSubType(a) || !hasSubReturn(b) || matchSub(getSubType(a), getSubReturn(b)) ) } const isAtomABoolean = (atom) => atom === TRUE || atom === FALSE const checkPredicateName = (exp, rest) => { if (getSuffix(rest[0][VALUE]) === PREDICATE_SUFFIX) { const last = rest.at(-1) if (last[TYPE] !== APPLY && isLeaf(last) && !IsPredicate(last)) { if (!IsPredicate(last)) throw new TypeError( `Assigning predicate (ending in ?) variable (${ rest[0][VALUE] }) to an (${ STATIC_TYPES.ATOM }) that is not (or ${TRUE} ${FALSE}) or to another variable which is not a predicate (also ending in ?) or to a variable that is not (or true false nil) (${stringifyArgs( exp )}) (check #100)` ) } else if (last[0][0] === APPLY) { const application = last[0] if (application[VALUE] === KEYWORDS.CALL_FUNCTION) return checkPredicateName(exp, [rest[0], last.at(-1)]) else if (application[VALUE] !== KEYWORDS.IF && !IsPredicate(application)) throw new TypeError( `Assigning predicate (ending in ?) variable (${ application[VALUE] }) to another variable which is not a predicate (also ending in ?) or to a variable that is not (or true false nil) (${stringifyArgs( exp )}) (check #101)` ) else if (application[VALUE] === KEYWORDS.IF) { return ( checkPredicateName(exp, [rest[0], last[2]]) && checkPredicateName(exp, [rest[0], last[3]]) ) } } return true } return false } const checkPredicateNameDeep = (name, exp, rest, returns) => { if (returns[VALUE] === KEYWORDS.CALL_FUNCTION) { const fn = rest.at(-1).at(-1).at(-1) return checkPredicateName(exp, [ [WORD, name], isLeaf(fn) ? fn // when apply is a word (let x? (lambda (apply [] array:empty!))) : drillReturnType(fn, (r) => r[VALUE] === KEYWORDS.CALL_FUNCTION) // when apply is an anonymous lambda // (let fn? (lambda x (apply x (lambda x (array:empty! []))))) ]) } return checkPredicateName(exp, [[WORD, name], returns]) } const fillUnknownArgs = (n) => Array.from({ length: n }) .fill(null) .map(() => ({ [STATS]: { retried: 0, [SIGNATURE]: PLACEHOLDER, [TYPE_PROP]: [UNKNOWN], [RETURNS]: [UNKNOWN], [ARGUMENTS]: [], [ARG_COUNT]: 0 } })) const getScopeNames = (scope) => { const scopeNames = [] let current = scope while (current) { if (current[SCOPE_NAME]) { scopeNames.push(current[SCOPE_NAME]) } current = Object.getPrototypeOf(current) } return scopeNames.reverse() } export const withScope = (name, scope) => { const chain = getScopeNames(scope) return `${chain.join(' ')} ${name}` } const retry = (stats, ctx, stack, cb, method = 'prepend') => { if ( (isUnknownNotAnyType(stats) || isUnknownNotAnyReturn(stats)) && stats.retried < MAX_RETRY_DEFINITION ) { stats.retried += 1 stagger(stack, method, ctx, cb) } } const once = (stats, ctx, stack, cb, method = 'prepend') => { if ( (isUnknownNotAnyType(stats) || isUnknownNotAnyReturn(stats)) && !stats.tried ) { stats.tried = true stagger(stack, method, ctx, cb) } } const doOnce = (stats, ctx, stack, cb, method = 'prepend') => { if (!stats.tried) { stats.tried = true stagger(stack, method, ctx, cb) } } const retryArgs = (stats, ctx, stack, cb) => { if (stats.counter < MAX_ARGUMENT_RETRY) { stats.counter++ stagger(stack, 'prepend', ctx, cb) } } const IfApplyBranch = ({ leaf, branch, re, prop, ref, env, stack, exp, check }) => { switch (leaf[VALUE]) { case KEYWORDS.IF: return ifExpression({ re: re.slice(2), env, ref, prop, stack, exp, check }) case KEYWORDS.CREATE_ARRAY: setPropToReturnRef(ref[STATS], prop, initArrayType({ rem: re, env })) break case KEYWORDS.ANONYMOUS_FUNCTION: setPropToAbstraction(ref[STATS], prop) ref[STATS][RETURNS] = [UNKNOWN] ref[STATS][ARG_COUNT] = re.length - 2 ref[STATS][ARGUMENTS] = fillUnknownArgs(re.length - 2) break case KEYWORDS.CALL_FUNCTION: if (re.at(-1)[TYPE] === WORD) { const name = re.at(-1)[VALUE] if (env[name] && re.at(-1)[VALUE] !== ref[STATS][SIGNATURE]) setPropToReturnRef(ref[STATS], prop, env[name][STATS]) } else { const returns = returnType(re.at(-1)) if (env[returns[VALUE]] && returns[VALUE] !== ref[STATS][SIGNATURE]) IfApplyBranch({ branch: env[returns[VALUE]], ref, env, prop, leaf: re.at(-1), re: re.at(-1).slice(2), stack, exp }) } break default: if ( getType(ref[STATS]) === UNKNOWN && getReturn(branch[STATS]) === UNKNOWN ) { retry(ref[STATS], exp, stack, () => setPropToReturn(ref[STATS], prop, branch[STATS]) ) return true } return setPropToReturn(ref[STATS], prop, branch[STATS]) } } const inferIf = (branch, re, env, name) => { switch (branch[TYPE]) { case ATOM: return [[ATOM, NUMBER_SUBTYPE()]] case WORD: if (branch[VALUE] === NIL) return [[UNKNOWN]] if (env[branch[VALUE]]) return [env[branch[VALUE]][STATS][TYPE_PROP]] break case APPLY: if (branch[VALUE] !== name && env[branch[VALUE]]) if (branch[VALUE] === KEYWORDS.CREATE_ARRAY) { return [initArrayType({ rem: re, env })[RETURNS]] } else if (branch[VALUE] === KEYWORDS.IF) { const conc = isLeaf(re[2]) ? re[2] : re[2][0] const alt = isLeaf(re[3]) ? re[3] : re[3][0] return inferIf(conc, re[2], env, name).concat( inferIf(alt, re[3], env, name) ) } else return [env[branch[VALUE]][STATS][RETURNS]] break } return [] } const validateIfMatchingBranches = ( concequent, alternative, env, exp, re, name ) => { const A = inferIf(concequent, re[0], env, name).filter( (x) => x[0] !== UNKNOWN && x[0] !== ANY ) const B = inferIf(alternative, re[1], env, name).filter( (x) => x[0] !== UNKNOWN && x[0] !== ANY ) if (!A.length || !B.length) return const isSame = (A, B) => { if ( A[0] !== B[0] || (isSubType(A[1]) && isSubType(B[1]) && !A[1].isMatching(B[1])) ) { throw new TypeError( `(if) needs to have matching concequent and alternative branches but got (${formatSubType( A )}) and (${formatSubType(B)}) (${stringifyArgs(exp)}) (check #1005)` ) } } A.forEach((x) => isSame(A[0], x)) B.forEach((x) => isSame(B[0], x)) const FA = A[0] const FB = B[0] if ( FA[0] !== FB[0] || (isSubType(FA[1]) && isSubType(FB[1]) && !FA[1].isMatching(FB[1])) ) { throw new TypeError( `(if) needs to have matching concequent and alternative branches but got (${formatSubType( FA )}) and (${formatSubType(FB)}) (${stringifyArgs(exp)}) (check #1005)` ) } } const ifExpression = ({ re, env, ref, prop, stack, exp, check }) => { const conc = isLeaf(re[0]) ? re[0] : re[0][0] const alt = isLeaf(re[1]) ? re[1] : re[1][0] const concequent = env[conc[VALUE]] const alternative = env[alt[VALUE]] if (re[0][TYPE] === ATOM || re[1][TYPE] === ATOM) { setPropToAtom(ref[STATS], prop) } // TODO check that both branches are predicates if one is else { // TODO make this more simple - it's so many different things just because types are functions or not // WHY not consider making return types for everything if (concequent) if (conc[TYPE] === WORD) { setPropToTypeRef(ref[STATS], prop, concequent[STATS]) } else if ( conc[TYPE] === APPLY && getType(concequent[STATS]) === APPLY && // Making sure the recursive function don't look for their own return type concequent[STATS][SIGNATURE] !== ref[STATS][SIGNATURE] ) IfApplyBranch({ leaf: conc, branch: concequent, re: re[0], prop, env, ref, stack, exp, check }) if (alternative) if (alt[TYPE] === WORD) { setPropToTypeRef(ref[STATS], prop, alternative[STATS]) } else if ( alt[TYPE] === APPLY && getType(alternative[STATS]) === APPLY && // Making sure the recursive function don't look for their own return type alternative[STATS][SIGNATURE] !== ref[STATS][SIGNATURE] ) IfApplyBranch({ leaf: alt, branch: alternative, re: re[1], prop, env, ref, stack, exp, check }) } validateIfMatchingBranches(conc, alt, env, exp, re, ref[STATS][SIGNATURE]) } const resolveCondition = ({ rem, name, env, exp, prop, stack, check }) => { const ret = rem[0] const re = rem.slice(2) const ref = env[name] checkPredicateName(exp, [[WORD, name], isLeaf(re[0]) ? re[0] : re[0][0]]) checkPredicateName(exp, [[WORD, name], isLeaf(re[1]) ? re[1] : re[1][0]]) switch (ret[VALUE]) { case KEYWORDS.IF: ifExpression({ re, env, ref, prop, exp, stack, check }) break default: if (env[ret[VALUE]]) setPropRef(ref[STATS], prop, env[ret[VALUE]][STATS]) else stagger(stack, 'append', exp, () => { if (env[ret[VALUE]]) setPropRef(ref[STATS], prop, env[ret[VALUE]][STATS]) }) break } } const resolveGetterRec = ([head, tail], env, times = 0) => { if (GET_ARRAY_INFERENCE_SET.has(head[VALUE])) { return resolveGetterRec(tail, env, ++times) } else { if (Array.isArray(head)) { tail = head[VALUE] head = head[TYPE] } if (head !== WORD && head !== APPLY) return const prop = head === WORD ? TYPE_PROP : RETURNS if (!env[tail] || env[tail][STATS][prop][0] === UNKNOWN) return const types = isSubType(env[tail][STATS][prop][1]) ? env[tail][STATS][prop][1].types : [] const sub = types.at(-1) const type = sub === ATOM || sub === NUMBER || sub === BOOLEAN ? ATOM : sub === APPLY ? APPLY : COLLECTION const len = types.length ? types.length + 1 : times + 1 return [times, len, type, types, tail] } } const resolveGetter = ({ rem, prop, name, env, caller, exp }) => { const array = isLeaf(rem[1]) ? rem[1] : rem[1][0] if (!env[array[VALUE]] || !env[name]) return true switch (array[TYPE]) { case APPLY: // TODO: figure out recursively what is the inner type of all nested getters if (GET_ARRAY_INFERENCE_SET.has(array[VALUE])) { const rec = resolveGetterRec(rem, env) if (!rec) return true const [times, level, type, types] = resolveGetterRec(rem, env) const isUnknown = types.at(-1) === UNKNOWN || types.at(-1) === COLLECTION if (!isUnknown && times >= level) throw new RangeError( `(${caller}) is trying to access nested structure at level (${level}) which is deeper than it's (${ times - 1 }) levels at (${stringifyArgs(exp)}) (check #1003)` ) if (times === level - 1) setPropToType(env[name][STATS], prop, { [TYPE_PROP]: types.length && !isUnknown ? [type, new SubType([types.at(-1)])] : types.at(-1) === COLLECTION ? [COLLECTION] : [UNKNOWN] }) else setPropToType(env[name][STATS], prop, { [TYPE_PROP]: types.length ? [COLLECTION, new SubType(types.slice(times))] : [UNKNOWN] }) return true } if ( getReturn(env[array[VALUE]][STATS]) === UNKNOWN || getReturn(env[array[VALUE]][STATS]) === ANY ) return true if (getReturn(env[array[VALUE]][STATS]) !== COLLECTION) throw new TypeError( `Incorrect type of argument (${0}) for (${caller}). Expected (${formatSubType( getTypes(env[caller][STATS][ARGUMENTS][0][STATS]) )}) but got (${formatSubType( getReturns(env[array[VALUE]][STATS]) )}) (${stringifyArgs(exp)}) (check #1001)` ) if (hasSubReturn(env[array[VALUE]][STATS])) { const rightSub = getSubReturn(env[array[VALUE]][STATS]) const isAtom = rightSub.has(NUMBER) || rightSub.has(BOOLEAN) const isCollection = rightSub.has(COLLECTION) if (isAtom && !isCollection) { setPropToAtom(env[name][STATS], prop) setPropToSubReturn(env[name][STATS], prop, env[array[VALUE]][STATS]) } else if (!isAtom && isCollection) { const [f, ...r] = env[array[VALUE]][STATS][RETURNS][1].types setPropToReturn(env[name][STATS], prop, { [RETURNS]: [f, new SubType(r)] }) } else if (rightSub.has(APPLY)) // TODOD: abstractions go here but what can we do with them // perhaps show the signature? setPropToAbstraction(env[name][STATS], prop) } break case WORD: { if ( getType(env[array[VALUE]][STATS]) === UNKNOWN || getType(env[array[VALUE]][STATS]) === ANY ) return true if (getType(env[array[VALUE]][STATS]) !== COLLECTION) throw new TypeError( `Incorrect type of argument (${0}) for (${caller}). Expected (${formatSubType( getTypes(env[caller][STATS][ARGUMENTS][0][STATS]) )}) but got (${formatSubType( getType(env[array[VALUE]][STATS]) )}) (${stringifyArgs(exp)}) (check #1002)` ) if (hasSubType(env[array[VALUE]][STATS])) { const rightSub = getSubType(env[array[VALUE]][STATS]) const isAtom = rightSub.has(ATOM) || rightSub.has(NUMBER) || rightSub.has(BOOLEAN) const isCollection = rightSub.has(COLLECTION) if (isAtom && !isCollection) { setPropToAtom(env[name][STATS], prop) setPropToSubType(env[name][STATS], prop, env[array[VALUE]][STATS]) } else if (!isAtom && isCollection) { const [f, ...r] = env[array[VALUE]][STATS][TYPE_PROP][1].types setPropToType(env[name][STATS], prop, { [TYPE_PROP]: [f, new SubType(r)] }) } else if (rightSub.has(APPLY)) // TODOD: abstractions go here but what can we do with them // perhaps show the signature? setPropToAbstraction(env[name][STATS], prop) } } break } return true } const resolveSetter = (first, rest, env, stack) => { if ( getSuffix(first[VALUE]) === MUTATION_SUFFIX && MUTATORS_SET.has(first[VALUE]) && rest[0] && isLeaf(rest[0]) && rest[0][TYPE] !== ATOM && env[rest[0][VALUE]] ) { const name = rest[0][VALUE] const current = env[name] const right = isLeaf(rest.at(-1)) ? rest.at(-1) : rest.at(-1)[0] const currentSubType = hasSubType(current[STATS]) ? getSubType(current[STATS]) : new SubType([UNKNOWN]) switch (right[TYPE]) { case ATOM: if ( !currentSubType.has(ANY) && !currentSubType.has(UNKNOWN) && !currentSubType.has(NUMBER) ) throw new TypeError( `Incorrect array type at (${ first[VALUE] }). ${name} is (${formatSubType( getTypes(current[STATS]) )}) but insertion is (${formatSubType([ ATOM, NUMBER_SUBTYPE() ])}) (${stringifyArgs([first, rest])}) (check #199)` ) current[STATS][TYPE_PROP][1] = NUMBER_SUBTYPE() break case WORD: if (env[right[VALUE]]) { if (hasSubType(env[right[VALUE]][STATS])) { if (currentSubType.has(UNKNOWN)) { if (env[right[VALUE]][STATS][TYPE_PROP][0] === COLLECTION) { current[STATS][TYPE_PROP][1] = new SubType([ COLLECTION, ...getSubType(env[right[VALUE]][STATS]).types ]) } else current[STATS][TYPE_PROP][1] = new SubType( getSubType(env[right[VALUE]][STATS]).types ) } else if (!equalSubTypes(current[STATS], env[right[VALUE]][STATS])) throw new TypeError( `Incorrect array type at (${ first[VALUE] }). ${name} is (${formatSubType( getTypes(current[STATS]) )}) but insertion is (${formatSubType( getTypes(env[right[VALUE]][STATS]) )}) (${stringifyArgs([first, rest])}) (check #198)` ) // current[STATS][TYPE_PROP][1] = new SubType( // getSubType(env[right[VALUE]][STATS]) // ) } else retry(env[right[VALUE]][STATS], [first[VALUE], rest], stack, () => resolveSetter(first, rest, env, stack) ) } break case APPLY: if (env[right[VALUE]]) { if (right[VALUE] === KEYWORDS.CREATE_ARRAY) { const inner = initArrayType({ rem: rest.at(-1), env })[RETURNS][1] current[STATS][TYPE_PROP][0] = COLLECTION if (!inner.has(UNKNOWN)) current[STATS][TYPE_PROP][1] = inner break } if (hasSubReturn(env[right[VALUE]][STATS])) { if (currentSubType.has(UNKNOWN)) { if (env[right[VALUE]][STATS][RETURNS][0] === COLLECTION) { current[STATS][TYPE_PROP][1] = new SubType([ COLLECTION, ...getSubReturn(env[right[VALUE]][STATS]).types ]) } else current[STATS][TYPE_PROP][1] = new SubType( getSubReturn(env[right[VALUE]][STATS]).types ) } else if ( !equalSubTypesWithSubReturn( current[STATS], env[right[VALUE]][STATS] ) ) throw new TypeError( `Incorrect array type at (${ first[VALUE] }). ${name} is (${formatSubType( getTypes(current[STATS]) )}) but insertion is (${formatSubType( getReturns(env[right[VALUE]][STATS]) )}) (${stringifyArgs([first, rest])}) (check #198)` ) // current[STATS][TYPE_PROP][1] = new SubType([ // ...getSubReturn(env[right[VALUE]][STATS]) // ]) } // else // retry(env[right[VALUE]][STATS], [first[VALUE], rest], stack, () => // resolveSetter(first, rest, env, stack) // ) } break } setTypeToCollection(current[STATS]) // // Retry setting the sub-type if infered it out later // if (!hasSubType(current[STATS]) || getSubType(current[STATS]).has(UNKNOWN)) // retry(current[STATS], [first[VALUE], rest], stack, () => // resolveSetter(first, rest, env, stack) // ) } } const countLevels = (arr, lvl = -1) => Array.isArray(arr[0]) ? countLevels(arr[0], ++lvl) : lvl const initArrayTypeRec = ({ rem, env }) => rem.slice(1).map((x) => { if (isLeaf(x)) if (x[TYPE] === WORD) if (env[x[VALUE]]) return getTypes(env[x[VALUE]][STATS]) else return [UNKNOWN] else return [ x[TYPE], x[TYPE] === ATOM ? NUMBER_SUBTYPE() : new SubType([UNKNOWN]) ] else if (env[x[0][VALUE]]) if (x.length > 1 && x[0][VALUE] === KEYWORDS.CREATE_ARRAY) return initArrayTypeRec({ rem: x, env }) else if ( x.length > 1 && env[x[0][VALUE]][STATS][RETURNS][0] === COLLECTION ) return [getReturns(env[x[0][VALUE]][STATS])] else if (GET_ARRAY_INFERENCE_SET.has(x[0][VALUE])) { const res = resolveGetterRec(x, env) if (!res) return [UNKNOWN] const name = resolveGetterRec(x, env).at(-1) resolveGetter({ rem: x, prop: RETURNS, name, env, caller: x[0][VALUE], exp: rem }) return getReturns(env[name][STATS]) } else return getReturns(env[x[0][VALUE]][STATS]) else return [UNKNOWN] }) const initArrayType = ({ rem, env }) => { const ret = initArrayTypeRec({ rem, env }) const flat = ret.flat(Infinity).filter((x) => !isSubType(x)) const subTypes = ret.flat(Infinity).filter((x) => isSubType(x)) const known = flat.find((x) => x !== ANY && x !== UNKNOWN) const isCollection = ret.length && Array.isArray(ret[0]) if ( known && ret.length && !flat.some((x) => known !== x) && !subTypes.some((x) => subTypes[0].types[0] !== x.types[0]) ) { if (Array.isArray(ret[0][0])) { let head = ret[0][0] ret[0].length = 0 const subT = new SubType([COLLECTION]) ret[0].push(COLLECTION, subT) while (Array.isArray(head) && !isSubType(head[1])) { if (head[0] === APPLY) subT.types.push(APPLY) else subT.add(COLLECTION) head = head[0] } if (Array.isArray(head) && head[1].types.length) subT.add(head[1].types[0]) } const [main, sub] = ret[0] return { [TYPE_PROP]: [APPLY], [RETURNS]: [ COLLECTION, isCollection ? new SubType(isSubType(sub) ? [...sub] : [COLLECTION]) : new SubType(isSubType(sub) ? [...sub] : [main]) ] } } else return { [TYPE_PROP]: [APPLY], [RETURNS]: [ COLLECTION, isCollection ? new SubType([ ...Array.from({ length: countLevels(ret) }).fill(COLLECTION), UNKNOWN ]) : new SubType([UNKNOWN]) ] } } const resolveReturnType = ({ returns, rem, stack, prop, exp, name, env, check }) => { if (returns[TYPE] === ATOM) setPropToAtom(env[name][STATS], prop) else if (returns[TYPE] === WORD) { if (env[returns[VALUE]]) { if (!isUnknownType(env[returns[VALUE]][STATS])) setPropToType(env[name][STATS], prop, env[returns[VALUE]][STATS]) else once(env[name][STATS], exp, stack, () => { setPropToTypeRef(env[name][STATS], prop, env[returns[VALUE]][STATS]) // if (isUnknownProp(env[name][STATS], prop)) { // // TODO: DRY // const index = env[name][STATS][ARGUMENTS] // ? env[name][STATS][ARGUMENTS].findIndex( // (x) => x[STATS][SIGNATURE] === returns[VALUE] // ) // : -1 // if (index >= 0) { // setReturnToGeneric(env[name][STATS], index) // return true // } else if (!env[returns[VALUE]]) return false // } }) } } else { switch (returns[VALUE]) { case KEYWORDS.CREATE_ARRAY: setPropToCollection(env[name][STATS], prop) setPropToSubReturn(env[name][STATS], prop, initArrayType({ rem, env })) break case KEYWORDS.IF: resolveCondition({ rem, name, env, exp, prop, stack, check }) break default: { if (GET_ARRAY_INFERENCE_SET.has(returns[VALUE])) { resolveGetter({ rem, prop, name, env, caller: returns[VALUE], exp }) retry(env[name][STATS], exp, stack, () => { resolveGetter({ rem, prop, name, env, caller: returns[VALUE], exp }) }) } checkPredicateNameDeep(name, exp, exp.slice(1), returns) if (!env[returns[VALUE]]) return false else if (getType(env[returns[VALUE]][STATS]) === APPLY) { if (returns[TYPE] === WORD) setReturnToAbstraction(env[name][STATS]) // ALWAYS APPLY // rest.at(-1)[0][TYPE] === APPLY // Here is upon application to store the result in the variable if (isGenericReturn(env[returns[VALUE]][STATS])) { // env[name][STATS][TYPE_PROP] = const [genericReturn, head, nestGeneric, multiplier] = getGenericReturn(env[returns[VALUE]], rem) switch (head[TYPE]) { case ATOM: setTypeToAtom(env[name][STATS]) break case WORD: if (env[head[VALUE]]) setPropToType( env[name][STATS], prop, env[head[VALUE]][STATS] ) break case APPLY: switch (head[VALUE]) { case KEYWORDS.ANONYMOUS_FUNCTION: { // TODO figure out a better way to do this // This is initialization of identity or any other // function that returns it's argument // Redefine the variable but since it's an error doing that // Delete it first delete env[name] check( [ [APPLY, KEYWORDS.DEFINE_VARIABLE], [WORD, name], genericReturn ], env, exp ) // const n = genericReturn.length // setTypeToAbstraction(env[name][STATS]) // env[name][STATS][ARG_COUNT] = n - 2 // env[name][STATS][ARGUMENTS] = fillUnknownArgs( // n - 2 // ) // checkReturnType({ // exp: [genericReturn], // stack, // name, // env, // check // }) } break case KEYWORDS.CREATE_ARRAY: { setTypeToCollection(env[name][STATS]) setPropToSubReturn( env[name][STATS], TYPE_PROP, initArrayType({ rem: genericReturn, env }) ) } break default: break } break default: if (env[head[VALUE]]) setTypeToReturn(env[name][STATS], env[head[VALUE]][STATS]) break } if (env[returns[VALUE]][STATS][RETURNS][0] === COLLECTION) { resolveGenericNest(env[name], prop, nestGeneric, multiplier) } } if (isUnknownType(env[name][STATS])) stagger(stack, 'prepend', exp, () => { setTypeToReturnRef(env[name][STATS], env[returns[VALUE]][STATS]) }) else { // if (SPECIAL_FORMS_SET.has(returns[VALUE])) // setReturn(env[name][STATS], env[returns[VALUE]][STATS]) // else setReturnRef(env[name][STATS], env[returns[VALUE]][STATS]) } } } break } } return true } const checkReturnType = ({ exp, stack, name, env, check }) => { const last = exp.at(-1).at(-1) const body = hasApplyLambdaBlock(last) ? last.at(-1).at(-1) : last const rem = hasBlock(body) ? body.at(-1) : body const returns = isLeaf(rem) ? rem : rem[0] return resolveReturnType({ returns, rem, prop: RETURNS, exp, name, env, stack, check }) } const stagger = (stack, method, data, fn) => { stack[method]({ data, fn }) } export const typeCheck = ( ast, ctx = SPECIAL_FORM_TYPES, typeSet = typeSetDefaultFunction ) => { const Types = new Map() const stack = new Brr() const rootScopeIndex = 1 let scopeIndex = 0 // TODO also handle casting const match = ({ rest, args, i, env, scope, exp }) => { const first = exp[0] let actual = rest[i][0][VALUE] === KEYWORDS.CREATE_ARRAY ? initArrayType({ rem: rest[i], env }) : env[rest[i][0][VALUE]][STATS] let expected = args[i][STATS] retryArgs(args[i][STATS], exp, stack, () => match({ rest, args, i, env, scope, exp }) ) if (!isUnknownType(expected) && !isUnknownReturn(actual)) if (!equalsTypeWithReturn(expected, actual)) throw new TypeError( `Incorrect type of argument (${i}) for (${ first[VALUE] }). Expected (${formatSubType( getTypes(expected) )}) but got (${formatSubType(getReturns(actual))}) (${stringifyArgs( exp )}) (check #16)` ) else if (!equalSubTypesWithSubReturn(expected, actual)) { throw new TypeError( `Incorrect type of argument (${i}) for (${ first[VALUE] }). Expected (${formatSubType( getTypes(expected) )}) but got (${formatSubType(getReturns(actual))}) (${stringifyArgs( exp )}) (check #206)` ) } else { switch (getType(expected)) { // almost exclusively for anonymous lambdas case APPLY: { const argsN = rest[i].length - 2 if ( env[rest[i][0][VALUE]][STATS][SIGNATURE] === KEYWORDS.ANONYMOUS_FUNCTION ) { if ( args[i][STATS][ARG_COUNT] !== VARIADIC && argsN !== args[i][STATS][ARG_COUNT] ) throw new TypeError( `Incorrect number of arguments for (${ args[i][STATS][SIGNATURE] }) the (${KEYWORDS.ANONYMOUS_FUNCTION}) argument of (${ first[VALUE] }) at position (${i}). Expected (= ${ args[i][STATS][ARG_COUNT] }) but got ${argsN} (${stringifyArgs(exp)}) (check #777)` ) else { // ANONYMOUS LAMBDAS TYPE CHECKING const local = Object.create(env) const lambdaName = `${ANONYMOUS_FUNCTION_TYPE_PREFIX}${i}::${++scopeIndex}` check( [ [APPLY, KEYWORDS.DEFINE_VARIABLE], [WORD, lambdaName], rest[i] ], local, scope ) // TODO delete this maybe // #C2 // It will not be possible to know return type const match1 = () => { const actual = local[lambdaName] const expected = args[i] if (isGenericReturn(expected[STATS])) return // if ( // isGenericReturn(args[i][STATS]) && // !isUnknownReturn(actual[STATS]) && // !isAnyReturn(actual[STATS]) // ) { // args[i][STATS][RETURNS] = actual[STATS][RETURNS] // return // } if ( !isUnknownReturn(expected[STATS]) && !isUnknownReturn(actual[STATS]) && !equalReturns(expected[STATS], actual[STATS]) ) throw new TypeError( `Incorrect return type for (${ expected[STATS][SIGNATURE] ?? ANONYMOUS_FUNCTION_TYPE_PREFIX }) the (${KEYWORDS.ANONYMOUS_FUNCTION}) argument of (${ first[VALUE] }) at position (${i}). Expected (${formatSubType( getReturns(expected[STATS]) )}) but got (${formatSubType( getReturns(actual[STATS]) )}) (${stringifyArgs(exp)}) (check #779)` ) else if (!equalSubReturns(expected[STATS], actual[STATS])) throw new TypeError( `Incorrect return type for (${ expected[STATS][SIGNATURE] }) the (${KEYWORDS.ANONYMOUS_FUNCTION}) argument of (${ first[VALUE] }) at position (${i}). Expected (${formatSubType( getReturns(expected[STATS]) )}) but got (${formatSubType( getReturns(actual[STATS]) )}) (${stringifyArgs(exp)}) (check #783)` ) else retry( actual[STATS], [[WORD, lambdaName], local], stack, match1 ) } match1() const match2 = () => { for (let j = 0; j < args[i][STATS][ARGUMENTS].length; ++j) { const actual = local[lambdaName][STATS][ARGUMENTS][j] const expected = args[i][STATS][ARGUMENTS][j] if (isGenericType(expected[STATS])) continue // TODO: after refactoring the generic nesting and unnesting logic // apply it here to judge lambda arguments based on the signature // if ( // isGenericType(expected[STATS]) && // !isUnknownType(actual[STATS]) && // !isAnyType(actual[STATS]) // ) { // expected[STATS][TYPE_PROP] = actual[STATS][TYPE_PROP] // return // } if ( !isUnknownType(actual[STATS]) && !isUnknownType(expected[STATS]) && (!equalTypes(actual[STATS], expected[STATS]) || !equalSubTypes(actual[STATS], expected[STATS])) ) throw new TypeError( `Incorrect type for (${ KEYWORDS.ANONYMOUS_FUNCTION }) (${ args[i][STATS][SIGNATURE] }) argument at position (${j}) named as (${ local[lambdaName][STATS][ARGUMENTS][j][STATS][ SIGNATURE ] }). Expected (${formatSubType( getTypes(expected[STATS])