├── .gitignore ├── LICENSE ├── README.md ├── examples ├── if.tls ├── nat.tls ├── tuple.tls └── types.tls ├── index.html ├── interpreter ├── foreign-functions.js ├── helpers.js ├── interpreter.js ├── parser.js ├── pattern-match.js └── types.js ├── prelude.tls ├── termlisp.js ├── tests.js └── typechecker └── typechecker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 <> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Disclaimer: This project is at a very early stage, many things may not work. 2 | 3 | Overview 4 | === 5 | term-lisp is a language for *term* *lis*t *p*rocessing with first-class pattern matching, inspired by Pie, Haskel, Agda et al. 6 | 7 | Term rewriting 8 | --- 9 | Right from when Church and Turing defined it, the concept of computation has been two-fold --- it can be presented either as the process of mutating the values of some state (Turing Machine) or by transforming some terms, using a predefined set of equations (Lambda Calculus). term-lisp leans heavily in the second direction. It does not support variables and its functions, are *rules* that describe how to replace a given term with another one. 10 | 11 | For example, consider how the booleans are defined: 12 | 13 | ``` 14 | (true = Bool True) 15 | (false = Bool False) 16 | ``` 17 | This means "when you see the term "true", replace it with the term "Bool True" 18 | 19 | Also, note that "Bool True" isn't defined anywhere. That is because in term-lisp, unlike in other lisps, an undefined term is not an error, it is just an undefined term. 20 | 21 | First-class pattern matching 22 | --- 23 | term-lisp supports first-class pattern matching. This means that you can have functions that return patterns. 24 | 25 | For example, consider how the if expression is defined: 26 | 27 | ``` 28 | (if (:literal true) a b = a) 29 | (if (:literal false) a b = b) 30 | ``` 31 | Here the terms true and false are automatically expanded, so the expressions above are a shorthand for: 32 | 33 | ``` 34 | (if (:literal (Bool True)) a b = a) 35 | (if (:literal (Bool False)) a b = b) 36 | ``` 37 | Lazy evaluation 38 | --- 39 | Term-rewriting languages sometimes have issues with dealing with functions that perform side-effects, such as `print`, as they don't allow for so fine-grained control over when is the function evaluated. To prevent unwanted execution, expressions in term-lisp are evaluated lazily i.e. only when they are needed. 40 | 41 | For example, consider this function that prints an error when its arguments are not equal: 42 | 43 | ``` 44 | (assertEqual a b = (if (eq a b) () (print (error a is-not-equal-to b)))) 45 | ``` 46 | 47 | If you wish to define `if` as a function in a non-lazy (strict) language, the `print` function will be called no matter if the two expressions are equal, simply because the result would be evaluated before the `if` function is even called. 48 | 49 | Language tutorial 50 | == 51 | Let's start with BNR form: 52 | ``` 53 | ::= | | | 54 | atom ::= | 55 | datatype ::= "(" * ")" 56 | application ::= "(" * ")" 57 | definition ::= "(" "=" ")" 58 | chain ::= "(" * ")" 59 | ``` 60 | Like every other Lisp, term-lisp is based on atoms and lists. Atoms are the primitive values, lists contain them e.g. `foo`, `bar` `3` `+` are atoms, `(foo bar 3)` is a list. 61 | 62 | 63 | Regular Lisps are based on the pair/tuple datatype, the `cons` data constructor, which unites two values in a tuple, and the `car` and `cdr` destructors which retrieve the first and second value of a tuple, respectively. 64 | 65 | ``` 66 | (assertEqual (car (cons foo bar)) foo) 67 | (assertEqual (cdr (cons foo bar)) bar) 68 | ``` 69 | 70 | In term-lisp, the pair is just one of the datatypes that you can define and use. 71 | 72 | ``` 73 | (cons a b = Pair a b) 74 | (car (Pair a b) = a) 75 | (cdr (Pair a b) = b) 76 | ``` 77 | We will review how this is done. 78 | 79 | Functional application 80 | --- 81 | Functional application in term-lisp works as in any other Lisp. A list of the type `(function-name argument1 argument2)` evaluates to the function's return expression e.g. `(car (cons foo bar))` evaluates to `foo`. 82 | 83 | Datatype 84 | --- 85 | What happens if we construct an expression that looks like function application, but the function being applied is not defined? In most languages, this would result in error, but in term-lisp we will create a new datatype/constructor, e.g. the expression `Pair foo bar` would evaluate to... `Pair foo bar` i.e. we will save a new object of type `Pair`, containing the values of `foo` and `bar`. What if the functions `foo` and `bar` aren't defined as well? They would evaluate to themselves too, like constructors without arguments. `True` and `False` are constructors without arguments as well. 86 | 87 | Function definition 88 | --- 89 | A functional definition is a list of arguments and an expression that does something with these arguments, separated by an equals sign. 90 | 91 | For example, here is a function that accepts two arguments and returns a new datatype that unites them into one: 92 | 93 | ``` 94 | (cons a b = Pair a b) 95 | ``` 96 | Functional definitions support a variety of pattern-matching features. 97 | 98 | Functional definitions support *destructuring* arguments. For example, the function 99 | 100 | ``` 101 | (car (Pair a b) = a) 102 | ``` 103 | accepts one argument which has to be a `Pair` datatype and destructures it to its two elements (which can be referred to by the names `a` and `b` in the resulting expression. 104 | 105 | The destructuring can also be used for type-checking. Consider the following function for printing: 106 | 107 | ``` 108 | (print-pair (Pair a b) = print (Pair a b)) 109 | ``` 110 | 111 | The resulting function will behave like `print`, but it will only work with arguments of type `Pair`. 112 | 113 | Functional definitions support *value-matching*, via the `:literal`. For example, consider the implementation of the function `if`: 114 | 115 | ``` 116 | (if (:literal true) a b = a) 117 | ``` 118 | 119 | This means that the function will only work if the first argument is `true` (this is different from `(if true a b = a)` which will assign the symbol `true` to the value of the first parameter). 120 | 121 | Functional definitions also support *multiple implementations* of the same function, like for example the implementation of `if` would be incomplete, as it will fail when the value is `false`. Adding a second implementation makes it total (provided that someone does not define more Bool values). 122 | 123 | ``` 124 | (if (:literal false) a b = b) 125 | ``` 126 | 127 | Functional definitions support *passing functions*, via the `:lambda` keyword: 128 | 129 | ``` 130 | (map a (:lambda fun) = fun a) 131 | ``` 132 | 133 | You can pass an existing function: 134 | 135 | ``` 136 | (this-is a = (this is a)) 137 | (assertEqual (map foo this-is) (this is foo)) 138 | ``` 139 | Or define one inline (be sure to give it a name): 140 | ``` 141 | (assertEqual (map bar (fun a = (this is a))) (this is bar)) 142 | ``` 143 | 144 | Running term-lisp 145 | --- 146 | In the project root, run: 147 | 148 | ``` 149 | node termlisp.js 150 | ``` 151 | It will evaluate the prelude module, plus your file (if you provided one) and go to REPL mode. 152 | 153 | Read [the prelude](/prelude.tls). 154 | -------------------------------------------------------------------------------- /examples/if.tls: -------------------------------------------------------------------------------- 1 | (print (if (eq (a(b)) (a(b))) booleans-work-again booleans-don't-work)) 2 | (print (if (eq (a(c)) ((a(v)))) booleans-don't-work booleans-work)) 3 | -------------------------------------------------------------------------------- /examples/nat.tls: -------------------------------------------------------------------------------- 1 | (0 => nat Zero) 2 | (+1 (nat a) => nat (succ a)) 3 | (-1 (nat (succ a)) => nat a) 4 | 5 | (1 => +1 0) 6 | (2 => +1 1) 7 | (3 => +1 2) 8 | 9 | (print 1) 10 | (print (-1 2) = 1) 11 | 12 | (+ (nat (succ a)) 0 => nat (succ a)) 13 | (+ (nat (succ a)) (nat b) => nat (succ (+ (nat a) (nat b)))) 14 | -------------------------------------------------------------------------------- /examples/tuple.tls: -------------------------------------------------------------------------------- 1 | (cons a b = Pair a b) 2 | (car (Pair a b) = a) 3 | (cdr (Pair a b) = b) 4 | 5 | (a = ((cdr (cons 1 2)))) 6 | (a) 7 | -------------------------------------------------------------------------------- /examples/types.tls: -------------------------------------------------------------------------------- 1 | ;;(print a = _print a) 2 | ;;(printNat (Nat a) = print a) 3 | ;;(a = (printNat foo)) 4 | ;;(:type Bool 5 | ;; (true = Bool True) 6 | ;;(false = Bool False) 7 | ;;) 8 | ;;(if (:literal Bool.true) a b = a) 9 | ;;(if (:literal Bool.false) a b = b) 10 | 11 | (takesFoo (Foo a) = a) 12 | 13 | (takesFooImplicitly b = takesFoo b) 14 | 15 | (wrongCall = takesFooImplicitly (Bar a)) 16 | 17 | (wrongCall) 18 | 19 | ;;(returnBool b = b) 20 | 21 | 22 | ;;(rightCall a = (if (returnBool a) foo bar)) 23 | 24 | ;;this will result in a type error 25 | ;;(wrongCall = (if NotBool foo bar)) 26 | 27 | ;;(callWithBool a = (if a foo bar)) 28 | ;;this will result in a type error 29 | ;;(a = (callWithBool NotBool)) 30 | 31 | ;;this too 32 | ;;(invalid = (Bool Invalid)) 33 | 34 | 35 | ;;(invalid) 36 | 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TermLisp 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /interpreter/foreign-functions.js: -------------------------------------------------------------------------------- 1 | const {formatExpression, equal} = require('./helpers') 2 | debug = false 3 | module.exports = { 4 | _print: (values, exec) => { 5 | console.log(formatExpression((values).map(exec))) 6 | return values 7 | }, 8 | _eq: ([expressionOne, expressionTwo], exec) => { 9 | if (equal(exec(expressionOne), exec(expressionTwo))) { 10 | return ["Bool", "True"] 11 | } else { 12 | return ["Bool", "False"] 13 | } 14 | }, 15 | } 16 | 17 | -------------------------------------------------------------------------------- /interpreter/helpers.js: -------------------------------------------------------------------------------- 1 | const compose = (f, g) => (...args) => f(g(...args)) 2 | 3 | exports.addBrackets = (a) => a.map((arg) => typeof arg === 'string' ? [arg] : arg) 4 | 5 | exports.error = (error, env, object) => { 6 | const trace = env.stack.map((line) => " at "+ formatExpression(line) + "\n") 7 | throw `Error: ${error} \n ${trace}` 8 | return (['error', error, env, object]) 9 | } 10 | 11 | 12 | exports.removeExtraBrackets = (expression) => Array.isArray(expression) && expression.length === 1 ? expression[0] : expression 13 | 14 | const formatExpression = compose((expression) => { 15 | if (Array.isArray(expression)) { 16 | return '(' + expression.map(exports.formatExpression).join(' ') + ')' 17 | } else { 18 | return expression 19 | } 20 | }, exports.removeExtraBrackets) 21 | 22 | exports.equal = (expressionOne, expressionTwo) => 23 | exports.formatExpression(expressionOne) === exports.formatExpression(expressionTwo) 24 | 25 | 26 | exports.print = (env, message, ...args) => console.log((env.stack.map(() => " ").join("")) + message.padEnd(30) , ...args) 27 | 28 | exports.formatExpression = formatExpression 29 | -------------------------------------------------------------------------------- /interpreter/interpreter.js: -------------------------------------------------------------------------------- 1 | const {name, parse } = require('./parser') 2 | const {formatExpression, error} = require('./helpers') 3 | const {parseFunctionDefinition} = require('./types') 4 | const {patternMatchArgumentsByNames} = require('./pattern-match') 5 | const foreignFunctions = require('./foreign-functions') 6 | 7 | const env = () => ({ history: [], stack: [], functions: {}, types: {}}) 8 | 9 | 10 | const print = (env, message, arg) => { 11 | const log = env.stack.map(() => " ").join("") + message.padEnd(30) + arg 12 | //console.log(log) 13 | env.history.push(log) 14 | } 15 | 16 | const addBrackets = (a) => a.map((arg) => typeof arg === 'string' ? [arg] : arg) 17 | 18 | const cloneEnv = (env, argumentFunctions, stack) => ({ 19 | stack, 20 | functions: { ...env.functions, ...argumentFunctions }, 21 | types: env.types, 22 | history: env.history, 23 | }) 24 | 25 | const execDataConstructor = (env, name, args) => { 26 | const newEnv = () => cloneEnv(env, {}, env.stack.slice()) 27 | const expression = args.length > 0 ? [name, ...args.map((arg) => exec(newEnv(), arg))] : [name] 28 | expression.env = env 29 | return expression 30 | } 31 | 32 | const formatEnv = (obj) => Object.keys(obj) 33 | .map((key) => key + " : " + formatExpression(obj[key][0].expression)) 34 | .join(', ') 35 | 36 | const execFunctionApplicationExpression = (env, func, functionArguments) => { 37 | const expression = typeof func.expression === 'string' ? [func.expression] : func.expression 38 | const argumentEnv = cloneEnv(func.env, functionArguments, env.stack.slice()) 39 | // print(env, "Substituting variables :", formatExpression([func.expression])) 40 | print(env, "With local env :", formatEnv(functionArguments)) 41 | //print(env, "With global environment:", func.env.functions) 42 | 43 | const result = exec(argumentEnv, expression) 44 | print(env, "Returning result :", formatExpression(result)) 45 | 46 | // We return the old environment in order not to pollute the global scope 47 | // with the vals that are local to the function 48 | result.env = env 49 | return result 50 | } 51 | 52 | const execFunctionApplication = (env, name, args) => { 53 | const implementations = env.functions[name] 54 | if (implementations !== undefined) { 55 | env.stack.unshift([name, args]) 56 | const patternMatches = implementations.map((func) => 57 | patternMatchArgumentsByNames(addBrackets(func.args), func.env, args, env, exec) 58 | ) 59 | const patternMatchIndex = patternMatches.findIndex(({type}) => type !== 'error') 60 | if (patternMatchIndex !== -1) { 61 | const func = implementations[patternMatchIndex] 62 | print(env, "Executing implementation: ", formatExpression([name, func.args, '=', func.expression])) 63 | const functionArguments = patternMatches[patternMatchIndex] 64 | return execFunctionApplicationExpression(env, func, functionArguments) 65 | } else { 66 | const errors = patternMatches.map(({error, }, i) => 67 | `${formatExpression(implementations[i].args)} - ${error}\n`) 68 | 69 | return error(`Failed to call function '${name}' with arguments ${formatExpression(args)}\n ${errors}`, env) 70 | } 71 | } else { 72 | return error(`Failed to call function '${name}' with arguments ${formatExpression(args)}\n No such function is defined`, env) 73 | } 74 | } 75 | 76 | const execForeignFunctionApplication = (env, name, args) => { 77 | if (foreignFunctions[name] !== undefined) { 78 | //check if val is a foreign function 79 | // if it is a function, execute it. 80 | print(env, "Executing a foreign function ", formatExpression([name, ...args])) 81 | const result = foreignFunctions[name](args, (val) => exec(env, val)) 82 | print(env, "Returning from foreign function: ", formatExpression(result)) 83 | result.env = env 84 | return result 85 | } else { 86 | return error(`Failed to call foreign function '${name}' with arguments ${formatExpression(args.map(exec))}\n No such function is defined`, env) 87 | } 88 | } 89 | 90 | const execTypeDeclaration = (env, args) => { 91 | const [name, ...functions] = args 92 | const moduleEnv = exec(cloneEnv(env, {}, env.stack.slice()) , functions).env 93 | //TODO very sloppy, module environments should have their own scope. 94 | for (fnName of Object.keys(moduleEnv.functions)) { 95 | env.functions[`${name}.${fnName}`] = moduleEnv.functions[fnName] 96 | } 97 | env.functions[name] = [] 98 | result = [] 99 | result.env = env 100 | return result 101 | } 102 | 103 | const execFunctionApplicationOrDataConstructor = (env, name, argsUnexecuted) => { 104 | //print(env, "Encountered a name ", formatExpression([name, ...argsUnexecuted])) 105 | 106 | const makeThunk = (arg) => { 107 | const argument = typeof arg == 'string' ? [arg] : arg 108 | argument.env = env; 109 | return argument 110 | } 111 | if (name === ":type") { 112 | return execTypeDeclaration(env, argsUnexecuted) 113 | } else { 114 | const args = argsUnexecuted.map(makeThunk) 115 | const fun = env.functions[name] 116 | if (fun !== undefined) { 117 | 118 | //if (fun.length === 0 && fun[0].args.length === 0) 119 | // print(env, "Looking up variable: ", formatExpression([name])) 120 | 121 | print(env, "Executing function call: ", formatExpression([name, ...args])) 122 | return execFunctionApplication(env, name, args) 123 | } else if (foreignFunctions[name] !== undefined) { 124 | print(env, "Executing a foreign function call:", formatExpression([name, ...args])) 125 | return execForeignFunctionApplication(env, name, args) 126 | } else { 127 | print(env, "New a data constructor ", formatExpression([name, ...args])) 128 | return execDataConstructor(env, name, args) 129 | } 130 | } 131 | } 132 | 133 | 134 | 135 | const exec = (env, expression) => { 136 | if (!Array.isArray(expression)) { 137 | throw new Error('Invalid expression: ' + typeof expression + " " + formatExpression(expression)) 138 | } else if (typeof env !== 'object') { 139 | console.log('Error at evaluating ', expression, ".") 140 | throw new Error(`${env} is not an object`) 141 | } else if (env.stack.length > 100) { 142 | return error("Stack Overflow", env) 143 | 144 | } else if (env.functions === undefined) { 145 | throw new Error(`Faulty environment object: ${JSON.stringify(env)}`) 146 | } else if (expression.filter((a) => (a === "=")).length > 1) { 147 | throw new Error(`Expression ${formatExpression(expression)} has more than one "=" symbol, did you forget to put it into parenthesis?`) 148 | } else { 149 | print(env, "Executing expression: ", formatExpression(expression)) 150 | const fnIndex = expression.indexOf('=') 151 | 152 | if (fnIndex !== -1 ) { 153 | //Executing function definition 154 | return execFunctionDefinition(env, expression) 155 | 156 | //TODO functions are not always literals, account for that 157 | } else if (typeof expression[0] === 'string') { 158 | //Each expression is a name, and arguments 159 | const [name, ...args] = expression 160 | return execFunctionApplicationOrDataConstructor(env, name, args) 161 | } else { 162 | //print(env, "Executing an expression chain") 163 | //Exec a bunch of statements 164 | let localExpression = [] 165 | localExpression.env = env 166 | for (let anExpression of expression) { 167 | const localEnv = {...localExpression.env, stack: env.stack.slice()} 168 | let newExpression = exec(localEnv, anExpression) 169 | //print(env, "Executing an expression from chain: ") 170 | //if (debug) console.log("Updated environment", Object.keys(newExpression.env.functions).join(', ')) 171 | localExpression = newExpression 172 | } 173 | return localExpression 174 | } 175 | } 176 | } 177 | 178 | const sameSignature = (fun1) => (fun2) => { 179 | if (fun1.length === fun2.length) { 180 | let same = true 181 | for (let i = 0; i 193 | funs 194 | .map((a) => a.args) 195 | .find(sameSignature(fun.args)) 196 | 197 | const execFunctionDefinition = (env, definition) => { 198 | const fun = parseFunctionDefinition(env, definition) 199 | const name = definition[0] 200 | if (env.functions[name] === undefined) { 201 | //debug && console.log(`New function: ${name}`) 202 | env.functions[name] = [fun] 203 | env.functions[name].env = env 204 | return env.functions[name] 205 | } else { 206 | const sig = repeatingSignature(fun, env.functions[name]) 207 | if (sig === undefined ) { 208 | //debug && console.log(`Registering a new implementation of ${name}`) 209 | env.functions[name].push(fun) 210 | env.functions[name].env = env 211 | return env.functions[name] 212 | } else { 213 | return error(`Repeating signature: ${formatExpression(fun.args)} is the same as ${formatExpression(sig)} in function ${name}.` , env) 214 | } 215 | } 216 | } 217 | 218 | const execString = (string, env ) => { 219 | expression = parse(string) 220 | const result = exec(env, expression) 221 | return result 222 | } 223 | 224 | module.exports = { execString, env} 225 | -------------------------------------------------------------------------------- /interpreter/parser.js: -------------------------------------------------------------------------------- 1 | const splitBrackets = (expr) => { 2 | const result = [] 3 | let currentExpression = '' 4 | let expressionStack = [result] 5 | const currentLevel = () => expressionStack[expressionStack.length - 1] 6 | 7 | const flush = () => { 8 | if (currentExpression !== '') { 9 | currentLevel().push(currentExpression) 10 | currentExpression = '' 11 | } 12 | } 13 | for (let char of expr) { 14 | if (char === "(") { 15 | flush() 16 | const newLevel = [] 17 | currentLevel().push(newLevel) 18 | expressionStack.push(newLevel) 19 | } else if (char === ")") { 20 | flush() 21 | expressionStack.pop() 22 | if (currentLevel()=== undefined) { 23 | throw "Unmatched closing bracket" 24 | } 25 | } else if (char === " ") { 26 | flush() 27 | } else { 28 | currentExpression = currentExpression + char 29 | } 30 | } 31 | flush() 32 | if (expressionStack.length > 1) { 33 | throw (`Parsing error: there are ${expressionStack.length - 1 } missing closing brackets`) 34 | } 35 | return result 36 | } 37 | //console.log(splitBrackets('a (a + b) b'), ["a", ["a", "+", "b"], "b"]) 38 | //console.log(splitBrackets('(a (a + b)) b'), [["a",["a", "+", "b"]], "b"]) 39 | //console.log(splitBrackets('aa bb (cc)'), ["aa","bb", ["cc"]]) 40 | const removeComments = (program) => program.split(/\r?\n/) 41 | .map((line) => { 42 | if (line[0] === ';' && line[1] === ';'){ 43 | return '' 44 | } else { 45 | return line 46 | } 47 | }).join('') 48 | 49 | const print = (a) => { 50 | //console.log(a) 51 | return a 52 | } 53 | 54 | exports.parse = (program) => print(splitBrackets(removeComments(program))) 55 | -------------------------------------------------------------------------------- /interpreter/pattern-match.js: -------------------------------------------------------------------------------- 1 | const error = (error, env, object) => ({type: 'error', error, env, object}) 2 | const {formatExpression, equal, print} = require('./helpers') 3 | 4 | const debug = false 5 | 6 | const formatEnvironment = (object) => Object.keys(object) 7 | .map((key) => formatExpression([ 8 | [key], 9 | ])).join('\n') 10 | 11 | const removeExtraBrackets = (expression) => Array.isArray(expression) && expression.length === 1 ? expression[0] : expression 12 | 13 | 14 | const patternMatchArgumentsByNames = (names, namesEnv, args, env, exec) => { 15 | 16 | const execThunk = val => exec(val.env, val) 17 | 18 | if (names.length > 0) { 19 | if (debug) print(env, `Matching expressions ${formatExpression(names)} with values ${formatExpression(args)}`) 20 | //if (debug) console.log(`with environment`, env.functions) 21 | } 22 | 23 | const argumentEnv = {} 24 | for (let i = 0; i < names.length; i++) { 25 | let name = names[i] 26 | let value = args[i] 27 | if (value === undefined) { 28 | return error(`No value supplied for ${formatExpression(name)}`) 29 | } else if (false) { 30 | //TODO check if name === func.name (results in infinite loop) 31 | } else { 32 | if (debug) print(env, `Matching expression ${formatExpression(name)} with value ${formatExpression(value)}`); 33 | if (!Array.isArray(name)) { 34 | //throw Error('System error: Invalid signature') 35 | argumentEnv[name] = [{ args: [], expression: value, env}] 36 | 37 | } else if (name.length === 1) { 38 | argumentEnv[name[0]] = [{ args: [], expression: value, env}] 39 | 40 | // Literal argument 41 | // We check if the value is equal to the argument 42 | 43 | } else if (name[0] === ':literal') { 44 | if (name.length === 2) { 45 | // We have to execute the values in order to compare 46 | nameMatch = [name[1]] 47 | const nameExecuted = exec(namesEnv, nameMatch) 48 | const valueExecuted = execThunk(value) 49 | if (equal(nameExecuted, valueExecuted)) { 50 | // We don't do anything in case of a successfull literal pattern match 51 | } else { 52 | return error(`Unsuccessful pattern match, called with ${formatExpression(valueExecuted)}, expected ${formatExpression(nameExecuted)}`, env, {names, args}) 53 | } 54 | } else { 55 | stack.push(names) 56 | return error(`Wrong use of the :literal keyword, called with ${literal.length} arguments, but expected 1.`) 57 | } 58 | 59 | // Lambda expression 60 | // We add the function to the environment 61 | } else if (name[0] === ':lambda') { 62 | 63 | if (debug) print(env, `Parsing lambda expression ${formatExpression(name)}`); 64 | if (name.length === 2) { 65 | const functionName = name[1] 66 | // If the argument is a function that is already defined, 67 | // search for it's implementation and attach it. 68 | if (value.length === 1) { 69 | 70 | if (debug) print(env, `Lambda expression references to an existing function ${formatExpression(value[0])}`); 71 | const implementation = value.env.functions[value[0]] 72 | if (debug) print(env, `Function has an implementation`, implementation); 73 | argumentEnv[functionName] = implementation 74 | } else { 75 | // If the function is given inline, execute it 76 | if (debug) print(env, `Lambda expression references to a new function ${formatExpression(value)}`); 77 | argumentEnv[functionName] = execThunk(value) 78 | } 79 | } else { 80 | throw Error(`Wrong use of the :lambda keyword, called with ${literal.length} arguments, but expected 1.`) 81 | } 82 | 83 | } else if (name[0] === ':list') { 84 | debug && console.log(`Executing a :list expression at index ${i}`) 85 | if (name.length === 2) { 86 | const argumentName = name[1] 87 | debug && console.log(`Assigning ${argumentName} to ${formatExpression(args.slice(i))}`) 88 | argumentEnv[argumentName] = [{ args: [], expression: ['List'].concat(args.slice(i)), env}] 89 | break 90 | } else { 91 | throw Error(`Wrong use of the :list keyword, called with ${literal.length} arguments, but expected 1.`) 92 | } 93 | 94 | // Data structure 95 | // We destruct it and match it's contents recursively 96 | 97 | } else { 98 | const [nameType, ...nameVals] = name 99 | const [argType, ...argVals] = execThunk(value) 100 | // Typecheck 101 | if (nameType !== argType) { 102 | return error(`Expected a type '${nameType}', but got ${argType}`, env, {name, value}) 103 | } else { 104 | if (debug) console.log(`Assigning values ${formatExpression(nameVals)} with values ${formatExpression(argVals)}`); 105 | //TODO report errors from here 106 | // (actually it seems that it works as is, hm...) 107 | Object.assign(argumentEnv, patternMatchArgumentsByNames(nameVals, namesEnv, argVals, env, exec)) 108 | } 109 | } 110 | } 111 | } 112 | if (debug && Object.keys(argumentEnv).length > 0) console.log("Constructed argument environment", argumentEnv) 113 | 114 | return argumentEnv 115 | } 116 | 117 | module.exports = {patternMatchArgumentsByNames} 118 | -------------------------------------------------------------------------------- /interpreter/types.js: -------------------------------------------------------------------------------- 1 | const {formatExpression, error, print, removeExtraBrackets, equal} = require('./helpers') 2 | 3 | const debug = false 4 | 5 | const extractEnv = (args) => args.reduce((env, arg) => { 6 | if (typeof arg === 'string') { 7 | env[arg] = null 8 | } else { 9 | //TODO make it work for typed args as well 10 | } 11 | return env 12 | }, {}) 13 | 14 | 15 | const matchArgument = (env, arg, value) => { 16 | debug && print(env, "Matching ", formatExpression( arg)) 17 | debug && print(env, "With ", formatExpression(value)) 18 | if (typeof arg === "string") { 19 | debug && print(env, "The argument is untyped, works always", formatExpression( arg)) 20 | return arg 21 | } else { 22 | [argumentType, argumentName] = arg 23 | debug && print(env, "The argument should be of type ", formatExpression( argumentType)) 24 | if (typeof value === "string") { 25 | debug && print(env, "The argument is of unknown type, adding a constraint", formatExpression( argumentType)) 26 | return [argumentType, argumentName] 27 | } else { 28 | [valType, valName] = value 29 | if (valType !== argumentType) { 30 | return {error: `Expected ${argumentName} to be '${argumentType}' but got ${valType}`} 31 | } else { 32 | return matchArguments(env, argumentName, valName) 33 | } 34 | } 35 | } 36 | } 37 | 38 | const getFunctionApplicationType = ( env, [args, expression], argValues) => { 39 | 40 | const argsCorrected = args.map((arg, i) => matchArgument(env, arg, argValues[i])) 41 | 42 | debug && print(env, "Matching ", formatExpression( args)) 43 | debug && print(env, "With ", formatExpression(argValues)) 44 | debug && print(env, "Got ", formatExpression(argsCorrected)) 45 | debug && print(env, "expression", formatExpression(expression)) 46 | return [argsCorrected, expression] 47 | } 48 | 49 | 50 | 51 | const getFunctionTypes = (env, args, expression) => { 52 | const [name, ...fnArgs] = expression 53 | debug && print(env, "Searching for function ", formatExpression(name)) 54 | if (env.functions[name] === undefined) { 55 | return [args, name] 56 | } else { 57 | //results = env.functions[name].map(({types}) => getFunctionApplicationType(env, types, fnArgs)) 58 | return getFunctionApplicationType(env, env.functions[name].types, fnArgs) 59 | } 60 | } 61 | 62 | 63 | exports.parseFunctionDefinition = (env, definition) => { 64 | const fnIndex = definition.indexOf('=') 65 | const [signature, expression] = [definition.slice(0, fnIndex), definition.slice(fnIndex + 1) ] 66 | const [name, ...args] = signature 67 | debug && print(env, "Analysing : ", name) 68 | //const types = getFunctionTypes(env, args, removeExtraBrackets(expression)) 69 | debug && print(env, "Parsing function: ", name) 70 | debug && print(env, "with types : ", types) 71 | return ({args, expression, env, name}) 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /prelude.tls: -------------------------------------------------------------------------------- 1 | ;;Foreign functions 2 | 3 | ;;Print function is reexported, so we can later redefine it 4 | ;;for each different datatype 5 | (print a = _print a) 6 | (eq a b = _eq a b) 7 | 8 | ;;Booleans 9 | 10 | ;;Shorthands for the data constructors of the boolean datatypes 11 | (true = Bool True) 12 | (false = Bool False) 13 | ;;(print (Bool a) = a) 14 | 15 | ;;Define the if clause, using pattern matching 16 | ;;(Only the relevant clause is evaluated, because the language is lazy) 17 | (if (:literal true) a b = a) 18 | (if (:literal false) a b = b) 19 | 20 | ;;We can define a little function we can use to test stuff 21 | (assertEqual a b = (if (eq a b) () (print (Error a Is-not-equal-to b)))) 22 | 23 | ;;Check if the booleans work :) 24 | (assertEqual (if true Foo Bar) Foo) 25 | (assertEqual (if false Foo Bar) Bar) 26 | 27 | ;;Some other boolean functions 28 | (and (:literal true) (:literal true) = true) 29 | (and (Bool a) (Bool a) = false) 30 | 31 | (or (:literal false) (:literal false) = false) 32 | (or (Bool a) (Bool a) = true) 33 | 34 | (not (:literal false) = true) 35 | (not (:literal true) = false) 36 | 37 | ;;Test those, using DeMorgan's laws (or the other way around) 38 | (test-demorgan (Bool a) (Bool b) = ( 39 | (one = not (and (Bool a) (Bool b))) 40 | (two = or (not (Bool a)) (not (Bool b))) 41 | (assertEqual one two) 42 | )) 43 | (test-demorgan true false) 44 | (test-demorgan true true) 45 | (test-demorgan false true) 46 | (test-demorgan false false) 47 | ;; 48 | ;;Pairs 49 | ;; 50 | 51 | ;; Define the pair datatype 52 | (cons a b = Pair a b) 53 | (car (Pair a b) = a) 54 | (cdr (Pair a b) = b) 55 | 56 | ;;(print (Pair a b) = print a . b) 57 | 58 | ;; Check if they work as well 59 | (assertEqual (car (cons Foo Bar)) Foo) 60 | (assertEqual (cdr (cons Foo Bar)) Bar) 61 | 62 | ;; Peano arithmetic 63 | 64 | ;; Define the Peano axioms 65 | (0 = Nat Zero) 66 | (+1 (Nat a) = Nat (Succ a)) 67 | 68 | ;; Shortcuts for some numbers 69 | (1 = +1 0) 70 | (2 = +1 1) 71 | (3 = +1 2) 72 | (4 = +1 3) 73 | 74 | (assertEqual (+1 1) 2) 75 | (assertEqual (+1 2) 3) 76 | 77 | ;; The plus function 78 | (+ (:literal 0) (Nat a) = Nat a) 79 | (+ (Nat (Succ a)) (Nat b) = (+1 (+ (Nat a) (Nat b)))) 80 | 81 | (assertEqual (+ 0 0) 0) 82 | (assertEqual (+ 0 1) 1) 83 | ;; May be occasionally useful 84 | (assertEqual (+ 1 1) 2) 85 | (assertEqual (+ 2 2) 4) 86 | 87 | ;; Lambdas 88 | 89 | (map a (:lambda fun) = fun a) 90 | 91 | ;;Pass an existing function 92 | (this-is a = (This Is a)) 93 | (assertEqual (map Foo this-is) (This Is Foo)) 94 | ;;Pass an inline function (but be sure to give it a name) 95 | (assertEqual (map Bar (fun a = (This Is a))) (This Is Bar)) 96 | 97 | ;;Lists (WIP) 98 | 99 | ;; Define the list datatype 100 | (list (:list elements) = elements) 101 | 102 | (a-list = (list a b c d)) 103 | 104 | ;; Some helper functions 105 | 106 | (head (List list-head (:list list-tail)) = list-head) 107 | (tail (List list-head (:list list-tail)) = list-tail) 108 | 109 | (assertEqual (head (list A B C)) A) 110 | (assertEqual (tail (list A B C)) (list B C)) 111 | 112 | ;;(map (:lambda fun) (List list-head) = (list (fun list-head)) ) 113 | ;;(map (:lambda fun) (List list-head (:rest list-tail)) = (List (fun list-head) (:rest (map fun list-tail)))) 114 | ;;assetEqual (map (fun a = a a) (List a b c)) (List (a a) (b b) (c c)) 115 | (print (All systems go)) 116 | 117 | (foo a = a) 118 | (foo bar) 119 | -------------------------------------------------------------------------------- /termlisp.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const {formatExpression} = require('./interpreter/helpers') 3 | const {execString, env } = require('./interpreter/interpreter') 4 | const {typecheckString} = require('./typechecker/typechecker') 5 | require('./tests') 6 | 7 | const readline = require('readline'); 8 | 9 | const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); 10 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 11 | 12 | const execInteractive = (program) => { 13 | 14 | //let typingErrors = typecheckString(program, env()) 15 | //console.log(typingErrors.env.types) 16 | 17 | let result = execString(program, env()) 18 | const repl = (env) => { 19 | return prompt('> ') 20 | .then((line) => { 21 | env.history = [] 22 | const result = execString(line, env) 23 | 24 | for (log of result.env.history) { 25 | console.log(log) 26 | } 27 | 28 | if (typeof formatExpression(result) === 'string') { 29 | console.log(formatExpression(result)) 30 | } 31 | return result.env 32 | }) 33 | .catch((err) => { console.log(err); return env}) 34 | .then(repl)} 35 | return repl(result.env) 36 | } 37 | 38 | try { 39 | const prelude = fs.readFileSync(__dirname + "/prelude.tls", 'utf8'); 40 | //const prelude = '' 41 | const data = process.argv[2] ? fs.readFileSync( process.argv[2], 'utf8') : '' 42 | execInteractive(prelude+data) 43 | } catch (err) { 44 | console.error(err); 45 | } 46 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('node:assert/strict'); 2 | const { execString, env } = require('./interpreter/interpreter') 3 | 4 | const throws = (code, message) => assert.throws(() => execString(code, env()), message) 5 | 6 | throws('(foo a b = a)(foo x y = y)', /Repeating signature/) 7 | throws('(foo a b = a', /Parsing/) 8 | -------------------------------------------------------------------------------- /typechecker/typechecker.js: -------------------------------------------------------------------------------- 1 | const {name, parse } = require('./../interpreter/parser') 2 | const {formatExpression, print} = require('./../interpreter/helpers') 3 | const {parseFunctionDefinition } = require('./../interpreter/types') 4 | 5 | debug = true 6 | 7 | const typecheckDataConstructor = (env, name, args) => { 8 | console.log({env, name, args}) 9 | const expression = args.length > 0 ? [name, ...args.map(typecheck)] : [name] 10 | if (env.types[name] === undefined) { 11 | env.types[name] = args 12 | } else { 13 | env.types[name] = env.types[name].concat(args) 14 | console.log(`Adding a new term to type ${name} - ${args}`) 15 | } 16 | //console.log("Added a new type to env ", env.types) 17 | expression.env = env 18 | return expression 19 | } 20 | 21 | const typecheck = (env, expression) => { 22 | if (!Array.isArray(expression)) { 23 | throw new Error('Invalid expression: ' + typeof expression + " " + formatExpression(expression)) 24 | } else if (typeof env !== 'object') { 25 | console.log('Error at evaluating ', expression, ".") 26 | throw new Error(`${env} is not an object`) 27 | } else if (env.functions === undefined) { 28 | throw new Error(`Faulty environment object: ${JSON.stringify(env)}`) 29 | } else { 30 | //if (debug) console.log("Typechecking expression", formatExpression(expression)) 31 | const fnIndex = expression.indexOf('=') 32 | 33 | if (fnIndex !== -1 ) { 34 | //Executing function definition 35 | return typecheckFunctionDefinition(env, expression) 36 | 37 | //TODO functions are not always literals, account for that 38 | } else if (typeof expression[0] === 'string') { 39 | 40 | return typecheckFunctionApplicationOrDataConstructor(env, expression) 41 | } else { 42 | //Exec a bunch of statements 43 | //console.log("Typechecking an expression chain ", {expression}) 44 | 45 | let localExpression = [] 46 | localExpression.env = env 47 | for (let anExpression of expression) { 48 | anExpression.env = {...localExpression.env} 49 | let newExpression = typecheck(anExpression.env, anExpression) 50 | debug && print(env, "Typechecking an expression from chain: ", formatExpression(anExpression)) 51 | if (debug) console.log("Updated environment", newExpression.env.functions) 52 | localExpression = newExpression 53 | } 54 | return localExpression 55 | } 56 | } 57 | } 58 | 59 | const typecheckFunctionApplicationOrDataConstructor = (env, expression) => { 60 | debug && print(env, "Typechecking", formatExpression(expression)) 61 | console.log(env) 62 | //Each expression is a name, and arguments 63 | const [name, ...argsUnexecuted] = expression 64 | //First exec the arguments 65 | const args = argsUnexecuted.map((arg) => { 66 | const argument = typeof arg === 'string' ? [arg] : arg 67 | argument.env = env; 68 | return argument 69 | }) 70 | const func = env.functions[name] 71 | if (func !== undefined) { 72 | debug && print(env, "Typechecking function application:", formatExpression([name, argsUnexecuted ])) 73 | return typecheckFunctionApplication(env, name, args) 74 | } else { 75 | // if the name isn't a function, then we assume it's a data constructor i.e. a function without a definition 76 | // so we exec the values and return it as is 77 | debug && print(env, "Returning a new data constructor", formatExpression([name, ...args])) 78 | return typecheckDataConstructor(env, name, args) 79 | } 80 | } 81 | 82 | const typecheckFunctionApplication = (env, name, args) => { 83 | const func = env.functions[name] 84 | console.log(func.returns) 85 | return func.returns 86 | } 87 | 88 | 89 | const newTypeVariable = (argumentName) => argumentName 90 | 91 | const determineArgumentType = (env, expression, argument) => { 92 | console.log("Determining argument type ", argument) 93 | console.log("In expression", func) 94 | if (argument[0] === ':literal') { 95 | return argument.slice(1) 96 | } else if(argument[0] === expression) { 97 | return newTypeVariable(argument[0]) 98 | } else { 99 | 100 | } 101 | } 102 | 103 | const cloneEnv = (env, argumentFunctions) => ({ 104 | functions: { ...env.functions, ...argumentFunctions }, 105 | stack: [], 106 | types: {} 107 | }) 108 | 109 | const argsToEnv = (arguments) => { 110 | const env = {} 111 | for (argument of arguments) { 112 | if (typeof argument === "string") { 113 | env[argument] = [] 114 | } else { 115 | 116 | } 117 | } 118 | } 119 | 120 | const typecheckFunctionDefinition = (env, definition) => { 121 | const fun = parseFunctionDefinition(env, definition) 122 | print(env, `Creating a new function ${fun.name}.`) 123 | 124 | const argumentsEnv = cloneEnv(env, argsToEnv(fun.args)) 125 | const returnType = typecheck(argumentsEnv, fun.expression) 126 | print(env, `With return type ${returnType}`) 127 | fun.returns = returnType 128 | env.functions[fun.name] = fun 129 | fun.env = env 130 | return fun 131 | } 132 | 133 | exports.typecheck = typecheck 134 | exports.typecheckString = (string, env ) => { 135 | expression = parse(string) 136 | expression.env = env 137 | const result = typecheck(env, expression) 138 | return result 139 | } 140 | 141 | --------------------------------------------------------------------------------