commit a8260f2afbeb2dd1bf6e66205db3f1e250358573 Author: Joeri Exelmans Date: Fri Mar 14 16:56:37 2025 +0100 basic functionality, no generics diff --git a/function_registry.js b/function_registry.js new file mode 100644 index 0000000..287eb12 --- /dev/null +++ b/function_registry.js @@ -0,0 +1,15 @@ +import { DefaultMap } from "./util.js"; + +const mapping = new DefaultMap(() => new Map()); + +export const getFnType = (inType, outType) => { + const m2 = mapping.getdefault(inType); + if (m2.has(outType)) { + return m2.get(outType); + } + else { + const fnType = {in: inType, out: outType}; + m2.set(outType, fnType); + return fnType; + } +} \ No newline at end of file diff --git a/lib/id.js b/lib/id.js new file mode 100644 index 0000000..038a100 --- /dev/null +++ b/lib/id.js @@ -0,0 +1,38 @@ +import {Function} from "../metacircular.js"; + +// import {Typed} from "../typed.js"; + +// import {Bool} from "../primitives/symbols.js"; +// import {deepEqual} from "../util.js"; +// import {Conformable, conformanceCheck} from "../typeclasses/conformable.js"; + +// // Identity function +// const idBoundToType = Symbol('idBoundToType'); + +// const id = type => x => x; +// const idFn = {name: "id", inType: Type, outType: {inType: Type, outType: Type}, fn: id}; +// const idIsConform = { +// name: "isConform", +// inType: idBoundToType, +// outType: Bool, +// fn: fn => deepEqual(fnIn(fn), fnOut(fn)) +// } + +// export const ModuleId = [ +// {i: idBoundToType, t: Type}, +// {i: idFn, t: Function}, +// ... makeTypedFnInOut({f: idBoundToType, inType: Type, outType: Type}), + +// {i: idBoundToType, t: Conformable}, +// {i: idIsConform, t: conformanceCheck}, +// ]; + +// generates explicitly typed id-function +export const makeIdFn = typ => { + const Typ_to_Typ = {in: typ, out: typ}; + const id = x => x; + return [ + {i: id , t: Typ_to_Typ}, + {i: Typ_to_Typ, t: Function}, + ]; +}; diff --git a/lib/literals.js b/lib/literals.js new file mode 100644 index 0000000..c972ba9 --- /dev/null +++ b/lib/literals.js @@ -0,0 +1,30 @@ +import {Int, Bool, Double} from "../primitives/symbols.js"; +import { getListType, makeListModule } from "../structures/list_common.js"; + +const ListOfBool = getListType(Bool); +const ListOfBoolModule = makeListModule(Bool); + +const ListOfInt = getListType(Int); +const ListOfIntModule = makeListModule(Int); + +const ListOfListOfInt = getListType(ListOfInt); +const ListOfListOfIntModule = makeListModule(ListOfInt); + +export const ModuleLiterals = [ + {i: 0n, t: Int}, + {i: 42n, t: Int}, + {i: false, t: Bool}, + {i: 3.14159265359, t: Double}, + + {i: {l:[42n, 43n]}, t: ListOfInt}, + + // {i: [[42n, 43n]], t: ListOfListOfInt}, + + // i'm lazy + ...ListOfIntModule, + + + // ...ListOfBoolModule, + + // ...ListOfListOfIntModule, +]; diff --git a/lib/square.js b/lib/square.js new file mode 100644 index 0000000..c4c3907 --- /dev/null +++ b/lib/square.js @@ -0,0 +1,49 @@ +import {Function, getIn, getOut} from "../metacircular.js"; +import {Module} from "../structures/module.js"; +import { deepEqual } from "../util.js"; +import { Typed } from "../typed.js"; + +// import {Num, NumDict, getType, getMul} from "../typeclasses/num.js"; + +// const squareBoundToType = Symbol('squareBoundToType'); // the type of e.g., squareOfInt + +// // returning a function + its signature is the only way to make the outType of square depend on its input +// const square = numDict => { +// const typ = getType(numDict); +// const mul = getMul(numDict); +// return { +// name: "squareOf:" + typ.toString(), +// inType: typ, +// outType: typ, +// fn: x => mul(x)(x), +// }; +// }; + +// export const ModuleSquare = [ +// {i: squareBoundToType, t: Type}, +// {i: {name: "square", inType: NumDict, outType: squareBoundToType, fn: square}, t: Function}, +// ...makeTypedFnInOut({f: squareBoundToType, inType: Num, outType: Num}), +// ]; + + +export const makeSquare = ({i: mul, t: mulFunction}) => { + const numType = getIn(mulFunction); + const boundMulFunction = getOut(mulFunction); + if (!deepEqual(getOut(boundMulFunction), numType) || !deepEqual(getIn(boundMulFunction), numType)) { + console.log(getOut(boundMulFunction), getIn(boundMulFunction), numType); + throw new Error("invalid signature"); + } + const square = x => mul(x)(x); + const squareFunction = {in: numType, out: numType}; + return [ + {i: square , t: squareFunction}, + {i: squareFunction, t: Function}, + ]; +}; + +const makeSquareType = {in: Typed, out: Module}; + +export const ModuleSquare = [ + {i: makeSquare , t: makeSquareType}, + {i: makeSquareType, t: Function}, +]; diff --git a/main.js b/main.js new file mode 100644 index 0000000..9e9c545 --- /dev/null +++ b/main.js @@ -0,0 +1,170 @@ +import {Function, ModuleMetaCircular, getIn, getOut} from "./metacircular.js"; +import {ModuleTyped} from "./typed.js"; +import {ModuleBool} from "./primitives/bool.js"; +import { Double_to_Double_to_Double, ModuleDouble, mulDouble } from "./primitives/double.js"; +import {Int_to_Int_to_Int, ModuleInt, mulInt} from "./primitives/int.js"; +import {Int} from "./primitives/symbols.js"; +import { makeSquare } from "./lib/square.js"; +import {makeIdFn} from "./lib/id.js"; +import {ModuleLiterals} from "./lib/literals.js"; + +import {DefaultMap} from "./util.js"; +import { ModuleModule } from "./structures/module.js"; + +class Context { + constructor(mod) { + // input type -> Function-signature + this.functionsFrom = new DefaultMap(() => []); + this.functionsTo = new DefaultMap(() => []); + this.types = new DefaultMap(() => []); + this.instances = new DefaultMap(() => []); + + + for (const {i, t} of mod) { + this.addInstance({i, t}); + } + + // console.log(this.instances.m); + + + // const callEveryFunction = ({i,t}, indent=0) => { + // for (const fntype of this.functionsFrom.getdefault(t)) { + // for (const fn of this.instances.getdefault(fntype)) { + // console.log(" ", fn, ':', fntype); + // const result = fn(i); + // const resultType = getOut(fntype); + // console.log(" ".repeat(indent), i, '->', result); + // if (this.types.getdefault(resultType).includes(Function)) { + // if (indent < 2) { + // callEveryFunction({i: result, t: resultType}, indent+1) + // } + // } + // } + // } + // }; + + // const callFunction = (typ, fns) => { + // console.log("Functions callable on", typ, ":"); + // for (const fn of fns) { + // console.log(" ", fn); + // for (const FN of this.instances.getdefault(fn)) { + // console.log(" ", FN); + // for (const i of this.instances.getdefault(typ)) { + // const result = FN(i); + // const resultType = getOut(fn); + // console.log(" ", i, '->', result); + // if (this.types.getdefault(resultType).includes(Function)) { + // callFunction(resultType, ) + // } + // } + // } + // } + // }; + + const callFunctionOnEveryValue = (fn, fnType, indent=1) => { + const inType = getIn(fnType); + const outType = getOut(fnType); + console.log("--".repeat(indent), fn, ':', fnType); + for (const i of this.instances.getdefault(inType)) { + const result = fn(i); + console.log("--".repeat(indent+1), i, '->', result); + if (this.types.getdefault(outType).includes(Function)) { + if (indent < 5) { + callFunctionOnEveryValue(result, outType, indent+2); + } + } + } + } + for (const fntypes of this.functionsFrom.m.values()) { + for (const fntype of fntypes) { + for (const fn of this.instances.getdefault(fntype)) { + callFunctionOnEveryValue(fn, fntype); + } + } + } + + // for (const [i, ts] of this.types.m.entries()) { + // for (const t of ts) { + // callEveryFunction({i,t}); + // } + // } + + + // // Conformance check + // for (const fn of this.instances.getdefault(conformanceCheck)) { + // const t = fn.inType; + // for (const i of this.instances.getdefault(t)) { + // if (!fn.fn(i)) { + // console.warn(i, 'is not conform', 'to', t); + // } + // else { + // console.info('👍', i, 'conforms to', t); + // } + // } + // } + + // const MAXDEPTH = 0; + + // let calls = 0; + // const tryEveryFunction = ({i, t}, maxDepth) => { + // calls++; + // for (const fn of this.functionsFrom.getdefault(t)) { + // const outType = getOut(fn); + // // console.log(" ".repeat(MAXDEPTH-maxDepth) + 'calling', fn, 'on', i); + // const result = fn(i); + // console.log(" ".repeat(MAXDEPTH-maxDepth), result, 'should be of type', outType); + // if (outType === TypeLink && result.t === undefined) { + // console.warn(" ".repeat(MAXDEPTH-maxDepth) + ' ^ invalid') + // } + // if (outType === Int && (typeof result) !== "number") { + // console.warn(" ".repeat(MAXDEPTH-maxDepth) + ' ^ invalid') + // } + // this.addInstance({i: result, t: outType}); + // if (maxDepth > 0) { + // tryEveryFunction({i: result, t: outType}, maxDepth-1); + // } + // } + // } + + // for (const {i, t} of mod) { + // tryEveryFunction({i, t}, MAXDEPTH); + // } + + // console.log("calls:", calls); + } + + addInstance({i, t}) { + const arr = this.types.getdefault(i, true); + arr.push(t); + const arrI = this.instances.getdefault(t, true); + arrI.push(i); + if (t === Function) { + const arr = this.functionsFrom.getdefault(getIn(i), true); + arr.push(i); + const arrTo = this.functionsTo.getdefault(getOut(i), true); + arrTo.push(i); + } + else { + } + } +} + + +const ctx = new Context([ + ...ModuleMetaCircular, + ...ModuleTyped, + // ...ModuleConformable, + // ...ModuleConformanceCheckConforms, + // ...ModuleNum, + // ...ModuleEq, + ...ModuleBool, + ...ModuleInt, + ...ModuleDouble, + // ...ModuleSquare, + // ...ModuleList, + ...makeIdFn(Int), + ...makeSquare({i: mulInt, t: Int_to_Int_to_Int}), + ...makeSquare({i: mulDouble, t: Double_to_Double_to_Double}), + ...ModuleLiterals, + ...ModuleModule, +]); diff --git a/metacircular.js b/metacircular.js new file mode 100644 index 0000000..b8a992d --- /dev/null +++ b/metacircular.js @@ -0,0 +1,31 @@ +import { getFnType } from "./function_registry.js"; + +export const Type = Symbol('Type'); +export const Function = Symbol('Function'); + +// Implementation of 'in' and 'out' functions, +// to get input/output type of a function signature: +export const getIn = fn => fn.in; +export const getOut = fn => fn.out; + +// a module is just a set of typed objects +// each 'typed object' is implicitly an instance of TypeLink (defined below) +export const ModuleMetaCircular = [ + // TODO? maybe follow Lean so + // Type.{0} : Type.{1} + // Type.{1} : Type.{2} + // ... + // see: https://lean-lang.org/functional_programming_in_lean/functor-applicative-monad/universes.html + + // Type : Type + {i: Type, t: Type}, + + // Function : Type + {i: Function, t: Type}, + + // (Function -> Type) : Function + {i: getFnType(Function, Type), t: Function}, + + {i: getIn , t: getFnType(Function, Type)}, + {i: getOut, t: getFnType(Function, Type)}, +]; diff --git a/primitives/bool.js b/primitives/bool.js new file mode 100644 index 0000000..98beaf2 --- /dev/null +++ b/primitives/bool.js @@ -0,0 +1,14 @@ +import {Type, Function} from "../metacircular.js"; +import {Bool} from "./symbols.js"; + +const eqBool = x => y => x === y; + +const Bool_to_Bool = {in: Bool, out: Bool}; +const Bool_to_Bool_to_Bool = {in: Bool, out: Bool_to_Bool}; + +export const ModuleBool = [ + {i: Bool , t: Type }, + {i: Bool_to_Bool , t: Function }, + {i: Bool_to_Bool_to_Bool , t: Function }, + {i: eqBool , t: Bool_to_Bool_to_Bool }, +]; diff --git a/primitives/double.js b/primitives/double.js new file mode 100644 index 0000000..3ab3de2 --- /dev/null +++ b/primitives/double.js @@ -0,0 +1,26 @@ +import {Type, Function} from "../metacircular.js"; + +import {Bool, Double} from "./symbols.js"; + +export const addDouble = x => y => x + y; +export const mulDouble = x => y => x * y; +export const eqDouble = x => y => x === y; + +const Double_to_Double = {in: Double, out: Double}; +const Double_to_Bool = {in: Double, out: Bool}; +export const Double_to_Double_to_Double = {in: Double, out: Double_to_Double}; +export const Double_to_Double_to_Bool = {in: Double, out: Double_to_Bool}; + + +export const ModuleDouble = [ + {i: Double , t: Type }, + + {i: Double_to_Double_to_Double, t: Function }, + {i: Double_to_Double_to_Bool , t: Function }, + {i: Double_to_Double , t: Function }, + {i: Double_to_Bool , t: Function }, + + {i: addDouble , t: Double_to_Double_to_Double }, + {i: mulDouble , t: Double_to_Double_to_Double }, + {i: eqDouble , t: Double_to_Double_to_Bool }, +]; diff --git a/primitives/int.js b/primitives/int.js new file mode 100644 index 0000000..cdf1907 --- /dev/null +++ b/primitives/int.js @@ -0,0 +1,26 @@ +import {Type, Function} from "../metacircular.js"; + +import {Bool, Int} from "./symbols.js"; + +export const addInt = x => y => x + y; +export const mulInt = x => y => x * y; +export const eqInt = x => y => x === y; + +const Int_to_Int = {in: Int, out: Int }; +const Int_to_Bool = {in: Int, out: Bool}; +export const Int_to_Int_to_Int = {in: Int, out: Int_to_Int}; +export const Int_to_Int_to_Bool = {in: Int, out: Int_to_Bool}; + + +export const ModuleInt = [ + {i: Int , t: Type }, + + {i: Int_to_Int_to_Int , t: Function }, + {i: Int_to_Int_to_Bool , t: Function }, + {i: Int_to_Int , t: Function }, + {i: Int_to_Bool , t: Function }, + + {i: addInt , t: Int_to_Int_to_Int }, + {i: mulInt , t: Int_to_Int_to_Int }, + {i: eqInt , t: Int_to_Int_to_Bool }, +]; diff --git a/primitives/symbols.js b/primitives/symbols.js new file mode 100644 index 0000000..d4c7591 --- /dev/null +++ b/primitives/symbols.js @@ -0,0 +1,5 @@ +// to break up dependency cycles, symbols of primitive types have their own JS module + +export const Bool = Symbol('Bool'); +export const Int = Symbol('Int'); +export const Double = Symbol('Double'); \ No newline at end of file diff --git a/structures/list.js b/structures/list.js new file mode 100644 index 0000000..0c385bb --- /dev/null +++ b/structures/list.js @@ -0,0 +1,13 @@ +import {Type, Function} from "../metacircular.js"; +import { makeListModule } from "./list_common.js"; + +const Type_to_Type = {in: Type, out: Type}; +const Type_to_Module = {in: Type, out: Module}; + +export const ModuleList = [ + {i: getListType , t: Type_to_Type}, + {i: Type_to_Type , t: Function}, + + {i: makeListModule, t: Type_to_Module}, + {i: Type_to_Module, t: Function}, +]; diff --git a/structures/list_common.js b/structures/list_common.js new file mode 100644 index 0000000..6062a52 --- /dev/null +++ b/structures/list_common.js @@ -0,0 +1,46 @@ +import {Type, Function} from "../metacircular.js"; +import {Int} from "../primitives/symbols.js"; + +// Global store of list types: +const listTypes = new Map(); +export function getListType(elementType) { + if (listTypes.has(elementType)) { + // only generate each list type once + // this would not be necessary if we could define our own equality and hash functions on objects in JavaScript. + return listTypes.get(elementType); + } + else { + const type = Symbol('ListOf:'+elementType.toString()); + listTypes.set(elementType, type); + return type; + } +} + +export const makeListModule = elementType => { + // List type depends on elementType + // generating it another time, will give the same type (structurally equivalent): + const ListOfElement = getListType(elementType); + + const emptyList = {l:[]}; + + const get = ls => i => ls.l[i]; + const getBoundType = {in: Int, out: elementType} + const getType = {in: ListOfElement, out: getBoundType}; + + const push = ls => elem => ({l:ls.l.concat([elem])}); + const pushBoundType = {in: elementType, out: ListOfElement}; + const pushType = {in: ListOfElement, out: pushBoundType}; + + return [ + {i: ListOfElement, t: Type}, + {i: emptyList , t: ListOfElement}, + + {i: get , t: getType}, + {i: getType , t: Function}, + {i: getBoundType , t: Function}, + + {i: push , t: pushType}, + {i: pushType , t: Function}, + {i: pushBoundType, t: Function}, + ]; +}; diff --git a/structures/module.js b/structures/module.js new file mode 100644 index 0000000..a36159f --- /dev/null +++ b/structures/module.js @@ -0,0 +1,6 @@ +import { getListType, makeListModule } from "./list_common.js"; +import { Typed } from "../typed.js"; + +export const Module = getListType(Typed); // a Module is a list of Typeds + +export const ModuleModule = makeListModule(Typed); // the module containing operations on Module diff --git a/typeclasses/conformable.js b/typeclasses/conformable.js new file mode 100644 index 0000000..37b9f29 --- /dev/null +++ b/typeclasses/conformable.js @@ -0,0 +1,33 @@ +// import {Type, Function, makeTypedFnInOut, fnOut} from "../metacircular.js"; +// import {Bool} from "../primitives/symbols.js"; + +// export const Conformable = Symbol('Conformable'); +// export const conformanceCheck = Symbol('conformanceCheck'); + +// export const ModuleConformable = [ +// {i: Conformable , t: Type}, +// {i: conformanceCheck, t: Function}, + +// // Note: outType is just 'Type', we cannot use Bool because a function that has type 'conformanceCheck' will have outType Bool, therefore conformanceCheck must return the type of Bool, which is Type. +// ...makeTypedFnInOut({f: conformanceCheck, inType: Conformable, outType: Type}), +// ]; + + + +// //////////////////////////////////////////////////////// +// // Conformance check functions are themselves conformance checkable +// // This is because we want to restrict their return type to Bool + +// const conformanceCheckIsConform = fn => { +// return fnOut(fn) === Bool; +// }; + +// const conformanceCheckIsConformFn = {name: "isConform", inType: conformanceCheck, outType: Bool, fn: conformanceCheckIsConform}; + + +// export const ModuleConformanceCheckConforms = [ +// // instances of conformanceCheck (= 'isConform'-functions) are themselves conformance checkable +// {i: conformanceCheck, t: Conformable}, +// {i: conformanceCheckIsConformFn, t: conformanceCheck}, +// {i: conformanceCheckIsConformFn, t: Function}, +// ]; diff --git a/typeclasses/num.js b/typeclasses/num.js new file mode 100644 index 0000000..565eb6d --- /dev/null +++ b/typeclasses/num.js @@ -0,0 +1,52 @@ +// import {Type, Function, makeTypedFnInOut, fnIn, fnOut} from "../metacircular.js"; +// import {Conformable, conformanceCheck} from "./conformable.js"; +// import {Bool} from "../primitives/symbols.js"; +// import {deepEqual} from "../util.js"; + +// export const Num = Symbol('Num'); +// export const NumDict = Symbol('NumDict'); +// // export const addType = Symbol('addType'); +// // export const addBoundType = Symbol('addBoundType'); +// // export const mulType = Symbol('mulType'); +// // export const mulBoundType = Symbol('mulBoundType'); + +// // export const getType = numDict => numDict.type; + +// export const getAdd = numDict => numDict.add; +// export const getMul = numDict => numDict.mul; + +// // const addOrMulIsConform = fn => { +// // // function signature must be: a -> a -> a +// // const i0 = fnIn(fn); +// // const o0 = fnOut(fn); +// // const i1 = fnIn(o0); +// // const o1 = fnOut(o0); +// // return deepEqual(i0, i1) && deepEqual(i1, o1); +// // }; + +// // const addIsConform = {name: "isConform", inType: addType, outType: Bool, fn: addOrMulIsConform}; +// // const mulIsConform = {name: "isConform", inType: mulType, outType: Bool, fn: addOrMulIsConform}; + +// export const ModuleNum = [ +// {i: Num, t: Type}, +// {i: NumDict, t: Type}, +// {i: addType, t: Type}, +// {i: addBoundType, t: Type}, +// {i: mulType, t: Type}, +// {i: mulBoundType, t: Type}, +// {i: {name: "getType", inType: NumDict, outType: Num, fn: getType}, t: Function}, +// {i: {name: "getAdd", inType: NumDict, outType: addType, fn: getAdd }, t: Function}, +// {i: {name: "getMul", inType: NumDict, outType: mulType, fn: getMul }, t: Function}, +// ...makeTypedFnInOut({f: addType , inType: Num, outType: addBoundType}), +// ...makeTypedFnInOut({f: addBoundType, inType: Num, outType: Num }), +// ...makeTypedFnInOut({f: mulType , inType: Num, outType: mulBoundType}), +// ...makeTypedFnInOut({f: mulBoundType, inType: Num, outType: Num }), + +// // conformance checking type class +// {i: addType, t: Conformable}, +// {i: mulType, t: Conformable}, +// {i: addIsConform, t: Function}, +// {i: addIsConform, t: conformanceCheck}, +// {i: mulIsConform, t: Function}, +// {i: mulIsConform, t: conformanceCheck}, +// ]; diff --git a/typed.js b/typed.js new file mode 100644 index 0000000..e621e3f --- /dev/null +++ b/typed.js @@ -0,0 +1,19 @@ +import { getFnType } from "./function_registry.js"; +import {Type, Function} from "./metacircular.js"; + +export const Typed = Symbol('Typed'); + +const getInst = lnk => lnk.i; +const getType = lnk => lnk.t; + +// const Typed_to_Type = {in: Typed, out: Type}; +const Typed_to_Type = getFnType(Typed, Type); + +export const ModuleTyped = [ + {i: Typed, t: Type}, + + {i: Typed_to_Type, t: Function}, + + {i: getInst, t: Typed_to_Type}, + {i: getType, t: Typed_to_Type}, +]; diff --git a/util.js b/util.js new file mode 100644 index 0000000..72d912b --- /dev/null +++ b/util.js @@ -0,0 +1,42 @@ +// re-inventing the wheel: +export function deepEqual(a, b) { + if (a === b) return true; // <- shallow equality and primitives + if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { + return false; + } + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) return false; + } + return true; + } + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + for (let key of keysA) { + if (!keysB.includes(key) || !deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + + +export class DefaultMap { + constructor(defaultValue, ...rest) { + this.defaultValue = defaultValue; + this.m = new Map(rest); + } + getdefault(key, addToMapping=false) { + return this.m.get(key) || (() => { + const val = this.defaultValue(); + if (addToMapping) + this.m.set(key, val); + return val; + })(); + } + entries() { + return this.m.entries(); + } +}