From e6faa2450ae0e2efbfdfc2b44dbc25a0d34154d7 Mon Sep 17 00:00:00 2001 From: Joe Whitfield-Seed Date: Sun, 12 Mar 2017 21:30:09 +0000 Subject: [PATCH 1/3] TypeScript: turn on noImplicitAny option Much stronger static type guarantees. The very last set of changes showed up a number of incorrectly passing tests, which are now fixed! --- .../__snapshots__/type-checker-tests.ts.snap | 14 ++-- src/__tests__/helpers.ts | 12 ++-- src/__tests__/parser-tests.ts | 8 +-- src/__tests__/type-checker-tests.ts | 35 ++++----- src/__tests__/unify-tests.ts | 16 +++-- src/array.ts | 4 +- src/bin/peach.ts | 20 +++--- src/env.ts | 4 +- src/errors.ts | 4 +- src/function.ts | 36 ++++++---- src/interpreter.ts | 29 ++++---- src/node-types.ts | 13 ++++ src/parser.ts | 2 +- src/repl.ts | 13 ++-- src/stdlib.ts | 71 +++++++++++-------- src/type-checker.ts | 49 ++++++------- src/types.ts | 16 ++--- src/unify.ts | 52 +++++--------- src/util.ts | 11 ++- tsconfig.json | 3 +- 20 files changed, 226 insertions(+), 186 deletions(-) diff --git a/src/__tests__/__snapshots__/type-checker-tests.ts.snap b/src/__tests__/__snapshots__/type-checker-tests.ts.snap index 72bfd8a..75ef705 100644 --- a/src/__tests__/__snapshots__/type-checker-tests.ts.snap +++ b/src/__tests__/__snapshots__/type-checker-tests.ts.snap @@ -28,7 +28,7 @@ exports[` exports[` id = x => x ((pair (id 3)) (id true)) - 1`] = `"G"`; + 1`] = `"[Number*Boolean]"`; exports[` id = x => x @@ -85,9 +85,9 @@ x => { exports[`((() => \`look ma no args\`)) 1`] = `"String"`; -exports[`((addZero 1) 2) 1`] = `"D"`; +exports[`((addZero 1) 2) 1`] = `"Boolean"`; -exports[`((pair 1) 2) 1`] = `"F"`; +exports[`((pair 1) 2) 1`] = `"[Number*Number]"`; exports[`([1|[2|t]] => t) 1`] = `"Array -> Array"`; @@ -95,17 +95,17 @@ exports[`([1|[true|t]] => t) 1`] = `"Type error: type mismatch"`; exports[`([h|[true|t]] => [[h 1] t]) 1`] = `"Type error: type mismatch"`; -exports[`([x|[y|t]] => [[x y] t]) 1`] = `"Array -> Array>"`; +exports[`([x|[y|t]] => [[x y] t]) 1`] = `"Array -> Array>"`; exports[`(a => 1) 1`] = `"B -> Number"`; -exports[`(addZero 1) 1`] = `"C"`; +exports[`(addZero 1) 1`] = `"Number -> Boolean"`; exports[`(if (true) 1 else \`two\`) 1`] = `"Type error: type mismatch"`; exports[`(inc 1) 1`] = `"Number"`; -exports[`(pair 1) 1`] = `"E"`; +exports[`(pair 1) 1`] = `"C -> [Number*C]"`; exports[`(zero 0) 1`] = `"Boolean"`; @@ -127,7 +127,7 @@ exports[`array-destructure.peach 1`] = `"Array"`; exports[`f => (f f) 1`] = `"Type error: recursive unification"`; -exports[`f => g => arg => (g (f arg)) 1`] = `"(H -> I) -> (I -> J) -> H -> J"`; +exports[`f => g => arg => (g (f arg)) 1`] = `"(D -> E) -> (E -> F) -> D -> F"`; exports[`fibonacci.peach 1`] = `"Array"`; diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts index 47b5ae3..6d8681f 100644 --- a/src/__tests__/helpers.ts +++ b/src/__tests__/helpers.ts @@ -7,22 +7,22 @@ import parse from '../parser' import typeCheck from '../type-checker' import interpret from '../interpreter' -export function fixture (fileName) { +export function fixture (fileName: string) { const filePath = join(__dirname, 'fixtures', fileName) return readFileSync(filePath, 'utf8') } -export function run (program) { +export function run (source: string) { const env = getRootEnv() const typeEnv = getTypeEnv(env) - const ast = parse(program) + const ast = parse(source) const [typed] = typeCheck(ast, typeEnv) return interpret(typed, getRootEnv()) } -export function testResult (program, expectedOutput) { - test(program, () => { - expect(run(program)[0]).toEqual(expectedOutput) +export function testResult (source: string, expectedOutput: any) { + test(source, () => { + expect(run(source)[0]).toEqual(expectedOutput) }) } diff --git a/src/__tests__/parser-tests.ts b/src/__tests__/parser-tests.ts index fc985bb..7a265bc 100644 --- a/src/__tests__/parser-tests.ts +++ b/src/__tests__/parser-tests.ts @@ -6,15 +6,15 @@ import { fixture } from './helpers' // snapshot tests for the parser // -function testFixture (fixtureName) { +function testFixture (fixtureName: string) { test(fixtureName, () => { expect(parse(fixture(fixtureName))).toMatchSnapshot() }) } -function testParse (peachCode) { - test(peachCode, () => { - expect(parse(peachCode)).toMatchSnapshot() +function testParse (source: string) { + test(source, () => { + expect(parse(source)).toMatchSnapshot() }) } diff --git a/src/__tests__/type-checker-tests.ts b/src/__tests__/type-checker-tests.ts index a7479c0..d752961 100644 --- a/src/__tests__/type-checker-tests.ts +++ b/src/__tests__/type-checker-tests.ts @@ -4,6 +4,7 @@ import typeCheck from '../type-checker' import { getRootEnv, getTypeEnv } from '../env' import { fixture } from './helpers' import { clone } from '../util' +import { Type } from '../types' import { TypedNode, AstNode } from '../node-types' import { @@ -14,23 +15,23 @@ import { BooleanType } from '../types' -function testTypeCheck (code, env = getTypeEnv(getRootEnv())) { - test(code, () => { - const parsed = parse(code) +function testTypeCheck (source: string, env = getTypeEnv(getRootEnv())) { + test(source, () => { + const parsed = parse(source) const [lastNode] = typeCheck(parsed, env) const type = lastNode.exprType.toString() expect(type).toMatchSnapshot() }) }; -function testFails (code, env = getTypeEnv(getRootEnv())) { - test(code, () => { - const parsed = parse(code) +function testFails (source: string, env = getTypeEnv(getRootEnv())) { + test(source, () => { + const parsed = parse(source) expect(() => typeCheck(parsed, env)).toThrowErrorMatchingSnapshot() }) } -function testFixture (fixtureName, env = getTypeEnv(getRootEnv())) { +function testFixture (fixtureName: string, env = getTypeEnv(getRootEnv())) { test(fixtureName, () => { const parsed = parse(fixture(fixtureName)) const [lastNode] = typeCheck(parsed, env) @@ -86,7 +87,7 @@ testFails(`if (1) 1 else 2`) // the peach type checker works over nodes with an `exprType` property. // helper for creating nodes for synthetic type tests -function typed (type): TypedNode { +function typed (type: Type): TypedNode { return { exprType: type, type: 'Str', @@ -96,11 +97,11 @@ function typed (type): TypedNode { // simulate a user-defined type class PairType extends TypeOperator { - constructor (a, b) { + constructor (a: Type, b: Type) { super('*', [a, b]) } - static of (name, [a, b]) { + static of (name: string, [a, b]: Type[]) { return new PairType(a, b) } @@ -109,9 +110,9 @@ class PairType extends TypeOperator { } } -const A = typed(new TypeVariable()) -const B = typed(new TypeVariable()) -const AB = typed(new PairType(A, B)) +const A = new TypeVariable() +const B = new TypeVariable() +const AB = new PairType(A, B) const testEnv = () => ({ // No unit type yet, so all functions take exactly one argument @@ -121,14 +122,14 @@ const testEnv = () => ({ // // Number -> Boolean zero: typed(new FunctionType(NumberType, BooleanType)), - add: typed(new FunctionType(typed(NumberType), typed(new FunctionType(typed(NumberType), typed(NumberType))))), - sub: typed(new FunctionType(typed(NumberType), typed(new FunctionType(typed(NumberType), typed(NumberType))))), + add: typed(new FunctionType(NumberType, new FunctionType(NumberType, NumberType))), + sub: typed(new FunctionType(NumberType, new FunctionType(NumberType, NumberType))), // Number -> Number -> Boolean - addZero: typed(new FunctionType(typed(NumberType), typed(new FunctionType(typed(NumberType), typed(BooleanType))))), + addZero: typed(new FunctionType(NumberType, new FunctionType(NumberType, BooleanType))), // A -> B -> A*B - pair: typed(new FunctionType(A, typed(new FunctionType(B, AB)))) + pair: typed(new FunctionType(A, new FunctionType(B, AB))) }) // testTypeCheck(`inc`, testEnv()) diff --git a/src/__tests__/unify-tests.ts b/src/__tests__/unify-tests.ts index 852e72d..2081655 100644 --- a/src/__tests__/unify-tests.ts +++ b/src/__tests__/unify-tests.ts @@ -1,9 +1,11 @@ /* eslint-env jest */ import unify from '../unify' +import { StringType } from '../types' +import { AstNode, AstStringNode, AstNumeralNode, AstNameNode } from '../node-types' describe('unify', () => { it('can unify one matching value', () => { - const patterns = [{ type: 'Str', value: 'hai' }] + const patterns: AstStringNode[] = [{ type: 'Str', value: 'hai' }] const values = ['hai'] expect(unify(patterns, values)).toEqual({ didMatch: true, @@ -12,7 +14,7 @@ describe('unify', () => { }) it('can unify several matching values', () => { - const patterns = [{ type: 'Numeral', value: 1 }, { type: 'Numeral', value: 2 }] + const patterns: AstNumeralNode[] = [{ type: 'Numeral', value: 1 }, { type: 'Numeral', value: 2 }] const values = [1, 2] expect(unify(patterns, values)).toEqual({ didMatch: true, @@ -22,25 +24,25 @@ describe('unify', () => { // TODO this test is better handled by a type system it('cannot unify a loosely equal value', () => { - const patterns = [{ type: 'Numeral', value: 0 }] + const patterns: AstNumeralNode[] = [{ type: 'Numeral', value: 0 }] const values = [false] expect(unify(patterns, values)).toEqual({ didMatch: false, bindings: {} }) }) it('cannot unify when the number of patterns and values differs', () => { - const patterns = [{ type: 'Numeral', value: 1 }, { type: 'Numeral', value: 2 }] + const patterns: AstNumeralNode[] = [{ type: 'Numeral', value: 1 }, { type: 'Numeral', value: 2 }] const values = [1] expect(unify(patterns, values)).toEqual({ didMatch: false, bindings: {} }) }) it('cannot unify when some but not all patterns match', () => { - const patterns = [{ type: 'Numeral', value: 1 }, { type: 'Numeral', value: 2 }] + const patterns: AstNumeralNode[] = [{ type: 'Numeral', value: 1 }, { type: 'Numeral', value: 2 }] const values = [1, 3] expect(unify(patterns, values)).toEqual({ didMatch: false, bindings: {} }) }) it('can unify a variable', () => { - const patterns = [{ type: 'Name', name: 'x' }] + const patterns: AstNameNode[] = [{ type: 'Name', name: 'x' }] const values = [1] expect(unify(patterns, values)).toEqual({ didMatch: true, @@ -49,7 +51,7 @@ describe('unify', () => { }) it('can unify mixed variables and values', () => { - const patterns = [{ type: 'Name', name: 'x' }, { type: 'Numeral', value: 2 }] + const patterns: AstNode[] = [{ type: 'Name', name: 'x' }, { type: 'Numeral', value: 2 }] const values = [1, 2] expect(unify(patterns, values)).toEqual({ didMatch: true, diff --git a/src/array.ts b/src/array.ts index 04c180c..99ff724 100644 --- a/src/array.ts +++ b/src/array.ts @@ -1,12 +1,12 @@ // TODO Array wrapper type -export function isArray ({ type }) { +export function isArray ({ type }: { type: string }) { return type === 'Array' } // TODO generic value-based equality. This fails for nested arrays // because the `every` check uses ===. It needs to find the value equality // function for the operand types. -export function isEqual (arrayA, arrayB) { +export function isEqual (arrayA: any[], arrayB: any[]) { return arrayA.length === arrayB.length && arrayA.every((v, i) => v === arrayB[i]) } diff --git a/src/bin/peach.ts b/src/bin/peach.ts index 758722c..f8888a9 100644 --- a/src/bin/peach.ts +++ b/src/bin/peach.ts @@ -11,19 +11,19 @@ import { getRootEnv, getTypeEnv } from '../env' import interpret from '../interpreter' import typeCheck from '../type-checker' -function readArgs (inputArgs) { +function readArgs (inputArgs: string[]) { const argv = parseArgs(inputArgs) const inputPath = argv._[0] || null return Object.assign({ inputPath }, argv) } -function read (filePath) { +function read (filePath: string) { return readFileSync(filePath, 'utf8') } -function runScript (path) { +function runScript (filePath: string) { try { - const ast = parse(read(path)) + const ast = parse(read(filePath)) const env = getRootEnv() const typeEnv = getTypeEnv(env) @@ -32,7 +32,7 @@ function runScript (path) { return 0 } catch (e) { if (/ENOENT/.test(e.message)) { - console.error(`Could not open the file ${path}`) + console.error(`Could not open the file ${filePath}`) return 1 } else { console.error(e.message) @@ -41,18 +41,18 @@ function runScript (path) { } } -function runPath (args, done) { +function runPath (args: any, done: OnComplete) { const scriptPath = resolve(args.inputPath) const status = runScript(scriptPath) return done(status) } -function repl (args, done) { +function repl (args: any, done: OnComplete) { startRepl(args, () => done(0)) } -function run (args, onComplete) { +function run (args: any, onComplete: OnComplete) { if (args.inputPath == null) { repl(args, onComplete) } else { @@ -60,5 +60,7 @@ function run (args, onComplete) { } } +type OnComplete = (status: number) => void + const args = readArgs(process.argv.slice(2)) -run(args, (status) => process.exit(status)) +run(args, status => process.exit(status)) diff --git a/src/env.ts b/src/env.ts index ac869cb..2fe1bee 100644 --- a/src/env.ts +++ b/src/env.ts @@ -8,12 +8,14 @@ export function getRootEnv (): RuntimeEnv { } export function getTypeEnv (valueEnv: RuntimeEnv): TypeEnv { + const initialState: TypeEnv = {} + return Object.keys(valueEnv).reduce((env, name) => { env[name] = extend(valueEnv[name], { exprType: valueEnv[name].exprType }) return env - }, {}) + }, initialState) } export type TypeEnv = { [name: string]: TypedNode } diff --git a/src/errors.ts b/src/errors.ts index 84e387e..261038f 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,6 +1,6 @@ // with help from http://stackoverflow.com/a/32749533/2806996 export default class PeachError extends Error { - constructor (message) { + constructor (message: string) { super(message) this.name = this.constructor.name this.message = message @@ -12,4 +12,4 @@ export default class PeachError extends Error { this.stack = (new Error(message).stack) } } -} \ No newline at end of file +} diff --git a/src/function.ts b/src/function.ts index 1df2eb9..7a156ec 100644 --- a/src/function.ts +++ b/src/function.ts @@ -1,22 +1,31 @@ import unify from './unify' import { create, restAndLast } from './util' import PeachError from './errors' -import { makeFunctionType } from './types' -import { TypedFunctionNode } from './node-types' +import { Type, makeFunctionType } from './types' +import { TypedFunctionNode, Value } from './node-types' +import { RuntimeEnv } from './env' +import { Visitor } from './interpreter' const ANONYMOUS = 'anonymous' +export interface PeachFunction { + name: string, + arity: number, // TODO deprecate + call: (args: Value[]) => Value, + toString: () => string +} + // Trampoline notes // https://jsfiddle.net/v6j4a9qh/6/ // Make a user-defined function from an AST node -export function makeFunction (functionNode: TypedFunctionNode, parentEnv, visit) { +export function makeFunction (functionNode: TypedFunctionNode, parentEnv: RuntimeEnv, visit: Visitor) { const { clauses } = functionNode const name = getName(functionNode) const arity = getArity(functionNode) - const call = (...args) => { + const call = (...args: Value[]) => { for (const { pattern, body } of clauses) { const { didMatch, bindings } = unify(pattern, args) if (didMatch !== false) { @@ -58,7 +67,7 @@ export function makeFunction (functionNode: TypedFunctionNode, parentEnv, visit) const toString = () => `λ: ${name}` - const pFunction = { + const pFunction: PeachFunction = { name, arity, call, @@ -68,7 +77,7 @@ export function makeFunction (functionNode: TypedFunctionNode, parentEnv, visit) return pFunction } -export function makeNativeFunction (name, jsFunction, argTypes, returnType) { +export function makeNativeFunction (name: string, jsFunction: Function, argTypes: Type[], returnType: Type) { const exprType = makeFunctionType(argTypes, returnType) return { @@ -80,10 +89,10 @@ export function makeNativeFunction (name, jsFunction, argTypes, returnType) { } } -export function applyFunction (pFunction, args) { +export function applyFunction (pFunction: PeachFunction, args: Value[]) { // the type checker catches this; sanity check only if (args.length > pFunction.arity) { - throw new PeachError(`Function ${pFunction.name} was called with too many arguments. It expected ${pFunction.maxArity} arguments, but it was called with ${args.length}: ${JSON.stringify(args)}`) + throw new PeachError(`Function ${pFunction.name} was called with too many arguments. It expected ${pFunction.arity} arguments, but it was called with ${args.length}: ${JSON.stringify(args)}`) } return (args.length >= pFunction.arity) @@ -91,21 +100,22 @@ export function applyFunction (pFunction, args) { : curry(pFunction, args) } -function getName (node) { +// FIXME include boundName in BaseAstNode? +function getName (node: any) { return node.boundName || ANONYMOUS } // FIXME the type checker should annotate the function node with types and/or arity -function getArity (node) { +function getArity (node: TypedFunctionNode) { return node.clauses[0].pattern.length } // (slow) tail call optimisation with a trampoline function -function call (pFunction, args) { +function call (pFunction: PeachFunction, args: Value[]) { return trampoline(pFunction.call, args) } -function trampoline (fn, args) { +function trampoline (fn: Function, args: any[]) { let continuation = fn.apply(null, args) while (typeof continuation === 'function') { continuation = continuation() @@ -114,7 +124,7 @@ function trampoline (fn, args) { return continuation } -function curry (pFunction, appliedArgs) { +function curry (pFunction: PeachFunction, appliedArgs: Value[]) { return Object.assign({}, pFunction, { arity: pFunction.arity - appliedArgs.length, call: pFunction.call.bind(null, ...appliedArgs) diff --git a/src/interpreter.ts b/src/interpreter.ts index 928d95d..b1e8c3a 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -16,17 +16,18 @@ export default function interpret (ast: TypedAst, rootEnv: RuntimeEnv = getRootE return [result, env] } -type Visitor = (node: TypedNode, env: RuntimeEnv) => InterpreterResult +export type Visitor = (node: TypedNode, env: RuntimeEnv) => InterpreterResult // Visit each of `nodes` in order, returning the result // and environment of the last node. -function visitSerial (nodes, rootEnv) { +function visitSerial (nodes: TypedNode[], rootEnv: RuntimeEnv): InterpreterResult { + const initialState: [TypedNode, RuntimeEnv] = [null, rootEnv] return nodes.reduce(([, env], node) => ( visit(node, env) - ), [null, rootEnv]) + ), initialState) } -function visitUnknown (node, env): InterpreterResult { +function visitUnknown (node: TypedNode, env: RuntimeEnv): InterpreterResult { console.log(JSON.stringify(node, null, 2)) throw new PeachError(`unknown node type: ${node.type}`) } @@ -60,11 +61,11 @@ function visit (node: TypedNode, env: RuntimeEnv): InterpreterResult { } } -function visitProgram (node: TypedProgramNode, env): InterpreterResult { +function visitProgram (node: TypedProgramNode, env: RuntimeEnv): InterpreterResult { return visitSerial(node.expressions, env) } -function visitDef ({ name, value }: TypedDefNode, env): InterpreterResult { +function visitDef ({ name, value }: TypedDefNode, env: RuntimeEnv): InterpreterResult { if (env.hasOwnProperty(name)) { throw new PeachError(`${name} has already been defined`) } @@ -81,7 +82,7 @@ function visitDef ({ name, value }: TypedDefNode, env): InterpreterResult { return [result, env] } -function visitName ({ name }: TypedNameNode, env): InterpreterResult { +function visitName ({ name }: TypedNameNode, env: RuntimeEnv): InterpreterResult { if (!(name in env)) { throw new PeachError(`${name} is not defined`) } @@ -89,35 +90,35 @@ function visitName ({ name }: TypedNameNode, env): InterpreterResult { return [env[name], env] } -function visitNumeral ({ value }: TypedNumeralNode, env): InterpreterResult { +function visitNumeral ({ value }: TypedNumeralNode, env: RuntimeEnv): InterpreterResult { return [value, env] } -function visitBool ({ value }: TypedBooleanNode, env): InterpreterResult { +function visitBool ({ value }: TypedBooleanNode, env: RuntimeEnv): InterpreterResult { return [value, env] } -function visitStr ({ value }: TypedStringNode, env): InterpreterResult { +function visitStr ({ value }: TypedStringNode, env: RuntimeEnv): InterpreterResult { return [value, env] } -function visitCall ({ fn, args }: TypedCallNode, env): InterpreterResult { +function visitCall ({ fn, args }: TypedCallNode, env: RuntimeEnv): InterpreterResult { const [fnResult] = visit(fn, env) const argResults = args.map((arg) => visit(arg, env)[0]) return [applyFunction(fnResult, argResults), env] } -function visitArray ({ values }: TypedArrayNode, env): InterpreterResult { +function visitArray ({ values }: TypedArrayNode, env: RuntimeEnv): InterpreterResult { const results = values.map((value) => visit(value, env)[0]) return [results, env] } -function visitFn (node: TypedFunctionNode, env): InterpreterResult { +function visitFn (node: TypedFunctionNode, env: RuntimeEnv): InterpreterResult { const fn = makeFunction(node, env, visit) return [fn, env] } -function visitIf ({ condition, ifBranch, elseBranch }: TypedIfNode, env): InterpreterResult { +function visitIf ({ condition, ifBranch, elseBranch }: TypedIfNode, env: RuntimeEnv): InterpreterResult { const [testResult] = visit(condition, env) const branch = (testResult) ? ifBranch : elseBranch diff --git a/src/node-types.ts b/src/node-types.ts index 7a0929a..f0ab5a8 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -156,6 +156,19 @@ export interface TypedIfNode extends AstIfNode, Typed { elseBranch: TypedNode } +// +// Nodes with shared traits +// +export type AstLiteralNode = AstStringNode | AstBooleanNode | AstNumeralNode +export type TypedLiteralNode = TypedStringNode | TypedBooleanNode | TypedNumeralNode + +// +// Guard functions +// +export function isAstLiteralNode (node: AstNode): node is AstLiteralNode { + return ['Bool', 'Str', 'Numeral'].includes(node.type) +} + // // Runtime values // TODO diff --git a/src/parser.ts b/src/parser.ts index ad8e049..b0c1295 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -8,6 +8,6 @@ const parser = generate(parserSource) export default function parse (source: string): Ast { const ast = parser.parse(source) - // console.log(JSON.stringify(ast)) + // console.log(`TRACE parse: `, JSON.stringify(ast)) return ast } diff --git a/src/repl.ts b/src/repl.ts index 6a00773..8f2c588 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -7,11 +7,12 @@ const { Recoverable } = require('repl') import { getRootEnv, getTypeEnv, RuntimeEnv, TypeEnv } from './env' import interpret from './interpreter' import typeCheck from './type-checker' +import { Value } from './node-types' import { parse, PeachError } from '.' const { version } = require('../package.json') -export default function startRepl (options, onExit) { +export default function startRepl (options: any, onExit: (status: Number) => void): void { console.log(`🍑 peach v${version}`) // remember the environment from each command to pass to the next @@ -20,9 +21,9 @@ export default function startRepl (options, onExit) { let lastEnv: RuntimeEnv = getRootEnv() let lastTypeEnv: TypeEnv = getTypeEnv(lastEnv) - function evalPeach (src, context, filename, callback) { + function evalPeach (source: string, context: any, filename: string, callback: any) { try { - const ast = parse(src) + const ast = parse(source) const [typed, nextTypeEnv] = typeCheck(ast, lastTypeEnv) const [result, nextEnv] = interpret(typed, lastEnv) @@ -49,11 +50,11 @@ export default function startRepl (options, onExit) { server.on('exit', onExit) } -function isRecoverableError (e) { +function isRecoverableError (e: Error) { return e.message.endsWith('but end of input found.') } -function getErrorMessage (e) { +function getErrorMessage (e: Error): string | Error { // A runtime error (which will one day be impossible!) in a Peach program if (e instanceof PeachError) { return `❗ ${e.message}` @@ -64,6 +65,6 @@ function getErrorMessage (e) { } } -function getOutput (value) { +function getOutput (value: Value): string { return value ? value.toString() : value } diff --git a/src/stdlib.ts b/src/stdlib.ts index af521b5..a5855c2 100644 --- a/src/stdlib.ts +++ b/src/stdlib.ts @@ -1,36 +1,46 @@ -import { makeNativeFunction, applyFunction } from './function' +import { makeNativeFunction, applyFunction, PeachFunction } from './function' import { TypeVariable, ArrayType, NumberType, StringType, BooleanType, - makeFunctionType + makeFunctionType, + Type } from './types' +import { Value } from './node-types' + +// TODO +// Temporary type aliasas until I implement proper runtime value types +type PNumber = number +type PBoolean = boolean +type PComparable = number | string | boolean + // TODO idiomatic export export default { // operators - '+': binaryOp('+', (a, b) => a + b, NumberType, NumberType), - '-': binaryOp('-', (a, b) => a - b, NumberType, NumberType), - '*': binaryOp('*', (a, b) => a * b, NumberType, NumberType), - '/': binaryOp('/', (a, b) => a / b, NumberType, NumberType), - '%': binaryOp('%', (a, b) => a % b, NumberType, NumberType), + '+': binaryOp('+', (a: PNumber, b: PNumber) => a + b, NumberType, NumberType), + '-': binaryOp('-', (a: PNumber, b: PNumber) => a - b, NumberType, NumberType), + '*': binaryOp('*', (a: PNumber, b: PNumber) => a * b, NumberType, NumberType), + '/': binaryOp('/', (a: PNumber, b: PNumber) => a / b, NumberType, NumberType), + '%': binaryOp('%', (a: PNumber, b: PNumber) => a % b, NumberType, NumberType), // TODO "comparable" type - '>': binaryOp('>', (a, b) => a > b, NumberType, BooleanType), - '>=': binaryOp('>=', (a, b) => a >= b, NumberType, BooleanType), - '<': binaryOp('<', (a, b) => a < b, NumberType, BooleanType), - '<=': binaryOp('<=', (a, b) => a <= b, NumberType, BooleanType), - '==': binaryOp('==', (a, b) => a === b, anyType(), BooleanType), - - '!': makeNativeFunction('!', a => !a, [BooleanType], BooleanType), - '&&': binaryOp('&&', (a, b) => a && b, BooleanType, BooleanType), - '||': binaryOp('||', (a, b) => a || b, BooleanType, BooleanType), - - '<=>': binaryOp('<=>', (a, b) => { - if (a > b) return 1 - if (a < b) return -1 + '>': binaryOp('>', (a: PNumber, b: PNumber) => a > b, NumberType, BooleanType), + '>=': binaryOp('>=', (a: PNumber, b: PNumber) => a >= b, NumberType, BooleanType), + '<': binaryOp('<', (a: PNumber, b: PNumber) => a < b, NumberType, BooleanType), + '<=': binaryOp('<=', (a: PNumber, b: PNumber) => a <= b, NumberType, BooleanType), + '==': binaryOp('==', (a: PNumber, b: PNumber) => a === b, anyType(), BooleanType), + + '!': makeNativeFunction('!', (a: PBoolean) => !a, [BooleanType], BooleanType), + '&&': binaryOp('&&', (a: PBoolean, b: PBoolean) => a && b, BooleanType, BooleanType), + '||': binaryOp('||', (a: PBoolean, b: PBoolean) => a || b, BooleanType, BooleanType), + + // TODO comparable tpye + '<=>': binaryOp('<=>', (a: PComparable, b: PComparable) => { + if (a > b) { return 1 } + if (a < b) { return -1 } return 0 }, NumberType, BooleanType), @@ -43,13 +53,13 @@ export default { fold: fold(), // type conversion - str: makeNativeFunction('str', (x) => '' + x, [anyType], StringType), + str: makeNativeFunction('str', (x: any) => '' + x, [anyType], StringType), // utils - print: makeNativeFunction('print', (x) => '' + x, [anyType], StringType) + print: makeNativeFunction('print', (x: any) => '' + x, [anyType], StringType) } -function binaryOp (name, fn, argType, returnType) { +function binaryOp (name: string, fn: (a: any, b: any) => any, argType: Type, returnType: Type) { return makeNativeFunction(name, fn, [argType, argType], returnType) } @@ -64,8 +74,13 @@ function arrayType (itemType = anyType()) { } // Utility -function proxyArrayMethod (name) { - return (pFunction, list) => list[name](e => applyFunction(pFunction, [e])) + +// Because Peach Arrays are just JavaScript arrays for now, we can proxy certain +// method directly to the runtime value. +function proxyArrayMethod (name: string) { + const method: any = Array.prototype[name as any] + return (pFunction: PeachFunction, list: any[]) => + method.call(list, (e: any) => applyFunction(pFunction, [e])) } // @@ -110,7 +125,7 @@ function find () { function reverse () { const inputArrayType = arrayType() const returnType = inputArrayType - const fn = (list) => list.reverse() + const fn = (list: any[]) => list.reverse() return makeNativeFunction('reverse', fn, [inputArrayType], returnType) } @@ -121,7 +136,7 @@ function fold () { const returnType = anyType() const iterateeType = makeFunctionType([itemType, returnType], returnType) - const fn = (pFunction, init, list) => + const fn = (pFunction: PeachFunction, init: Value, list: Value[]) => list.reduce((e, a) => applyFunction(pFunction, [e, a]), init) @@ -132,7 +147,7 @@ function fold () { function cons () { const headType = anyType() const tailType = arrayType(headType) - const fn = (item, list) => [item, ...list] + const fn = (item: Value, list: Value[]) => [item, ...list] return makeNativeFunction('cons', fn, [headType, ArrayType], tailType) } diff --git a/src/type-checker.ts b/src/type-checker.ts index 0b5776c..104d127 100644 --- a/src/type-checker.ts +++ b/src/type-checker.ts @@ -76,7 +76,7 @@ function visit (node: AstNode, env: TypeEnv, nonGeneric: Set): TypeCheckRe } } -function visitProgram (node: AstProgramNode, env, nonGeneric): TypeCheckResult { +function visitProgram (node: AstProgramNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { const [expressions, finalEnv] = visitAll(node.expressions, env, nonGeneric) const programType = typeOf(last(expressions)) @@ -84,16 +84,18 @@ function visitProgram (node: AstProgramNode, env, nonGeneric): TypeCheckResult { +function visitDef (node: AstDefNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { if (env.hasOwnProperty(node.name)) { throw new PeachError(`${node.name} has already been defined`) } // allow for recursive binding by binding ahead of evaluating the child - // analogous to Lisp's letrec, but in the enclosing scope. + // analogous to Lisp's letrec, but in the enclosing scope. We don't know + // the value or concrete type yet. // TODO immutable env const t = new TypeVariable() - env[node.name] = { ...node, exprType: t } + const typedStubNode: TypedDefNode = { ...node, value: null, exprType: t } + env[node.name] = typedStubNode // if we are defining a function, mark the new identifier as // non-generic inside the evaluation of the body. @@ -108,7 +110,7 @@ function visitDef (node: AstDefNode, env, nonGeneric): TypeCheckResult { +function visitName (node: AstNameNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { if (!(node.name in env)) { throw new PeachError(`${node.name} is not defined`) } @@ -120,19 +122,19 @@ function visitName (node: AstNameNode, env, nonGeneric): TypeCheckResult { +function visitNumeral (node: AstNumeralNode, env: TypeEnv): TypeCheckResult { return [{...node, exprType: NumberType }, env] } -function visitBool (node: AstBooleanNode, env): TypeCheckResult { +function visitBool (node: AstBooleanNode, env: TypeEnv): TypeCheckResult { return [{ ...node, exprType: BooleanType }, env] } -function visitStr (node: AstStringNode, env): TypeCheckResult { +function visitStr (node: AstStringNode, env: TypeEnv): TypeCheckResult { return [{ ...node, exprType: StringType }, env] } -function visitCall (node: AstCallNode, env, nonGeneric): TypeCheckResult { +function visitCall (node: AstCallNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { const [fn] = visit(node.fn, env, nonGeneric) const args: TypedNode[] = node.args.map((arg) => visit(arg, env, nonGeneric)[0]) @@ -145,13 +147,13 @@ function visitCall (node: AstCallNode, env, nonGeneric): TypeCheckResult { - let itemType - let typedValues +function visitArray (node: AstArrayNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { + let itemType: Type + let typedValues: TypedNode[] if (node.values.length > 0) { typedValues = node.values.map((value) => visit(value, env, nonGeneric)[0]) - const types = typedValues.map(value => value.exprType) + const types = typesOf(typedValues) // arrays are homogenous: all items must have the same type unifyAll(types) @@ -167,7 +169,7 @@ function visitArray (node: AstArrayNode, env, nonGeneric): TypeCheckResult { +function visitDestructuredArray (node: AstDestructuredArrayNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { const { head, tail } = node const boundHeadType = new TypeVariable() @@ -202,7 +204,7 @@ function visitDestructuredArray (node: AstDestructuredArrayNode, env, nonGeneric return [typedNode, env] } -function visitFn (node: AstFunctionNode, parentEnv, outerNonGeneric): TypeCheckResult { +function visitFn (node: AstFunctionNode, parentEnv: TypeEnv, outerNonGeneric: Set): TypeCheckResult { // TODO clauses must be exhaustive - functions must accept any input of the right types const clauses: TypedFunctionClauseNode[] = node.clauses.map((clauseNode) => { const nonGeneric = new Set([...outerNonGeneric]) @@ -242,7 +244,7 @@ function visitFn (node: AstFunctionNode, parentEnv, outerNonGeneric): TypeCheckR return [typedNode, parentEnv] } -function visitIf (node: AstIfNode, env, nonGeneric): TypeCheckResult { +function visitIf (node: AstIfNode, env: TypeEnv, nonGeneric: Set): TypeCheckResult { const [condition] = visit(node.condition, env, nonGeneric) const [ifBranch] = visit(node.ifBranch, env, nonGeneric) const [elseBranch] = visit(node.elseBranch, env, nonGeneric) @@ -259,7 +261,7 @@ function typeOf (node: TypedNode): Type { } // return an array of types for the given array of typed nodes -function typesOf (typedNodes) { +function typesOf (typedNodes: TypedNode[]) { return typedNodes.map(node => node.exprType) } @@ -270,10 +272,10 @@ function typesOf (typedNodes) { // Return a new copy of a type expression // * generic variables duplicated // * non-generic variables shared -function fresh (type, nonGeneric) { +function fresh (type: Type, nonGeneric: Set) { const mappings = new Map() - const f = (t) => { + const f = (t: Type): Type => { const pruned = prune(t) if (pruned instanceof TypeVariable) { @@ -299,7 +301,7 @@ function fresh (type, nonGeneric) { } // attempt to unify all elements of `list` to the same type -function unifyAll (typeList) { +function unifyAll (typeList: Type[]) { prune(typeList[0]) // ??? typeList.forEach((type, i) => { if (i > 0) { @@ -351,18 +353,17 @@ function prune (t: Type): Type { } // Returns true if `type` does not occur in `nonGeneric` -function isGeneric (typeVar, nonGeneric) { +function isGeneric (typeVar: Type, nonGeneric: Set) { return !occursIn(typeVar, nonGeneric) } // Return true if typeVar appears in any of types -// types: Set -function occursIn (typeVar, types) { +function occursIn (typeVar: Type, types: Iterable): boolean { return [...types].some(type => occursInType(typeVar, type)) } // Returns true if the pruned `typeVar` occurs in the type expression `type`. -function occursInType (typeVar, type) { +function occursInType (typeVar: Type, type: Type) { const prunedType = prune(type) if (typeVar === prunedType) { return true diff --git a/src/types.ts b/src/types.ts index 50c6783..d9eba86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ export class Type { name: string - constructor(name) { + constructor (name: string) { this.name = name } } @@ -10,13 +10,13 @@ export class Type { export class TypeOperator extends Type { typeArgs: Array - constructor (name, typeArgs = []) { + constructor (name: string, typeArgs: Type[] = []) { super(name) this.typeArgs = typeArgs } // polymorphic factory - static of (name, typeArgs) { + static of (name: string, typeArgs: Type[]) { return new TypeOperator(name, typeArgs) } @@ -30,11 +30,11 @@ export const StringType = new TypeOperator('String') export const BooleanType = new TypeOperator('Boolean') export class FunctionType extends TypeOperator { - constructor (argType, returnType) { + constructor (argType: Type, returnType: Type) { super('->', [argType, returnType]) } - static of (name, types) { + static of (name: string, types: Type[]) { const [argType, returnType] = types return new FunctionType(argType, returnType) } @@ -69,11 +69,11 @@ export class FunctionType extends TypeOperator { } export class ArrayType extends TypeOperator { - constructor (argType) { + constructor (argType: Type) { super('Array', [argType]) } - static of (name, types) { + static of (name: string, types: Type[]) { return new ArrayType(types[0]) } @@ -112,7 +112,7 @@ export class TypeVariable extends Type { } // Factory for curried function types that take any number of arguments -export function makeFunctionType (argTypes, returnType) { +export function makeFunctionType (argTypes: Type[], returnType: Type): Type { const [firstArgType, ...tailArgTypes] = argTypes if (tailArgTypes.length === 0) { diff --git a/src/unify.ts b/src/unify.ts index 97ea0d6..52bb20e 100644 --- a/src/unify.ts +++ b/src/unify.ts @@ -1,13 +1,17 @@ import PeachError from './errors' import { isArray, isEqual } from './array' +import { zip } from './util' +import { AstNode, AstLiteralNode, AstDestructuredArrayNode, Value, isAstLiteralNode } from './node-types' + +// FIXME tagged union - with didMatch: false, there are never bindings. +type binding = { [name: string]: Value } -// FIXME union type - with didMatch: false, there are never bindings. interface unification { didMatch: boolean, - bindings: object + bindings: binding } -export default function unify (patterns, values) : unification { +export default function unify (patterns: AstNode[], values: Value[]): unification { if (patterns.length !== values.length) { return didNotMatch } @@ -25,24 +29,23 @@ export default function unify (patterns, values) : unification { return didMatch(bindings) } -function unifyOne (pattern, value) { - // TODO value equality operator - if (isValue(pattern) && pattern.value === value) { +function unifyOne (pattern: AstNode, value: Value): binding { + if (isAstLiteralNode(pattern) && pattern.value === value) { // the pattern matched, but there is nothing to bind return {} } - if (isName(pattern)) { + if (pattern.type === 'Name') { // the pattern matched; return a new binding return { [pattern.name]: value } } // TODO generic value equality - if (isArray(pattern) && isEqual(pattern.values, value)) { + if (pattern.type === 'Array' && isEqual(pattern.values, value)) { return {} } - if (isDestructuredArray(pattern)) { + if (pattern.type === 'DestructuredArray') { return destructure(pattern, value) } @@ -51,14 +54,14 @@ function unifyOne (pattern, value) { } // TODO this will need to change when Array is a wrapped type -function destructure ({ head, tail }, array) { - if (array.length === 0) { +function destructure ({ head, tail }: AstDestructuredArrayNode, values: Value[]): binding { + if (values.length === 0) { throw new PeachError(`Empty arrays cannot be destructured because they don't have a head`) } - const boundHead = unifyOne(head, array[0]) + const boundHead = unifyOne(head, values[0]) if (boundHead !== null) { - const boundTail = unifyOne(tail, array.slice(1)) + const boundTail = unifyOne(tail, values.slice(1)) if (boundTail) { return Object.assign(boundHead, boundTail) } @@ -67,33 +70,14 @@ function destructure ({ head, tail }, array) { return null } -const didNotMatch : unification = { +const didNotMatch: unification = { didMatch: false, bindings: {} } -function didMatch (bindings) : unification { +function didMatch (bindings: binding): unification { return { didMatch: true, bindings } } - -// TODO these belong with type definitions -function isName ({ type }) { - return type === 'Name' -} - -function isValue ({ type }) { - return ['Bool', 'Str', 'Numeral'].includes(type) -} - -function isDestructuredArray ({ type }) { - return type === 'DestructuredArray' -} - -// TODO stdlib -function zip (arrayA, arrayB) { - return arrayA.map((e, i) => [e, arrayB[i]]) -} - diff --git a/src/util.ts b/src/util.ts index 3443ea2..bbf2b62 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,5 @@ // _.extend, but immutable by default -export function extend (source, ...extensions) { +export function extend (source: any, ...extensions: any[]) { return Object.assign({}, source, ...extensions) } @@ -7,7 +7,7 @@ export const clone = extend // shortcut for creating an object with the given prototype and // properties with default behaviour -export function create (proto, properties = null) { +export function create (proto: any, properties: any = null) { return Object.assign(Object.create(proto), properties) } @@ -20,3 +20,10 @@ export function restAndLast (arr: T[]): [T[], T] { export function last (arr: T[]): T { return arr[arr.length - 1] } + +export function zip (a: A[], b: B[]): [A, B][] { + return a.map((aValue, i): [A, B] => { + const bValue = b[i] + return [aValue, bValue] + }) +} diff --git a/tsconfig.json b/tsconfig.json index b4e7a29..20a68e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "moduleResolution": "node", "outDir": "./dist", "target": "es2017", - "sourceMap": true + "sourceMap": true, + "noImplicitAny": true }, "include": [ "./src/**/*" From 0f6d66a1d5f76d9f8225eace1d5eb75eda86d159 Mon Sep 17 00:00:00 2001 From: Joe Whitfield-Seed Date: Sun, 12 Mar 2017 23:53:46 +0000 Subject: [PATCH 2/3] TypeScript: turn on noInplicitThis and alwaysStrict Part of enabling string TypeScript: #37 --- tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 20a68e0..ddea758 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,9 @@ "outDir": "./dist", "target": "es2017", "sourceMap": true, - "noImplicitAny": true + "noImplicitAny": true, + "noImplicitThis": true, + "alwaysStrict": true }, "include": [ "./src/**/*" From d3b8dc4c5126c798f3689febbb237f36290ade93 Mon Sep 17 00:00:00 2001 From: Joe Whitfield-Seed Date: Mon, 13 Mar 2017 00:06:05 +0000 Subject: [PATCH 3/3] TypeScript: turn on strictNullChecks --- src/interpreter.ts | 2 +- src/node-types.ts | 16 ++++++++++++++++ src/type-checker.ts | 20 ++++++++++++++------ src/types.ts | 2 +- src/unify.ts | 6 +++--- tsconfig.json | 1 + 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index b1e8c3a..3bef955 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -21,7 +21,7 @@ export type Visitor = (node: TypedNode, env: RuntimeEnv) => InterpreterResult // Visit each of `nodes` in order, returning the result // and environment of the last node. function visitSerial (nodes: TypedNode[], rootEnv: RuntimeEnv): InterpreterResult { - const initialState: [TypedNode, RuntimeEnv] = [null, rootEnv] + const initialState: [TypedNode, RuntimeEnv] = [nodes[0], rootEnv] return nodes.reduce(([, env], node) => ( visit(node, env) ), initialState) diff --git a/src/node-types.ts b/src/node-types.ts index f0ab5a8..cba2307 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -25,6 +25,7 @@ export type AstNode | AstFunctionNode | AstFunctionClauseNode | AstIfNode + | AstDefPreValueNode // Typed*: type checker output. AST nodes augmented with Peach static types. export type TypedNode @@ -40,6 +41,7 @@ export type TypedNode | TypedFunctionNode | TypedFunctionClauseNode | TypedIfNode + | TypedDefPreValueNode export interface Typed { exprType: Type @@ -156,6 +158,20 @@ export interface TypedIfNode extends AstIfNode, Typed { elseBranch: TypedNode } +// +// Pseudo-node types +// + +// A special marker pseudo-node used to represent the value of a +// `Def` node during type checking, before descending into the value itself. +// We only need the Typed variant, but the Ast*Node is required to maintain +// the AstNode -> TypedNode mapping. +export interface AstDefPreValueNode { + type: 'DefPreValue' +} + +export interface TypedDefPreValueNode extends Typed, AstDefPreValueNode { } + // // Nodes with shared traits // diff --git a/src/type-checker.ts b/src/type-checker.ts index 104d127..798e6a2 100644 --- a/src/type-checker.ts +++ b/src/type-checker.ts @@ -8,7 +8,8 @@ import { AstDestructuredArrayNode, AstFunctionNode, AstIfNode, TypedAst, TypedNode, TypedProgramNode, TypedDefNode, TypedNameNode, TypedNumeralNode, TypedBooleanNode, TypedStringNode, TypedCallNode, TypedArrayNode, - TypedDestructuredArrayNode, TypedFunctionNode, TypedFunctionClauseNode, TypedIfNode + TypedDestructuredArrayNode, TypedFunctionNode, TypedFunctionClauseNode, TypedIfNode, + TypedDefPreValueNode } from './node-types' import { @@ -89,12 +90,17 @@ function visitDef (node: AstDefNode, env: TypeEnv, nonGeneric: Set): TypeC throw new PeachError(`${node.name} has already been defined`) } - // allow for recursive binding by binding ahead of evaluating the child - // analogous to Lisp's letrec, but in the enclosing scope. We don't know - // the value or concrete type yet. - // TODO immutable env + // Allow for recursive binding by binding ahead of evaluating the child + // analogous to Lisp's letrec, but in the enclosing scope. We don't know + // the value or concrete type yet. + // The binding must be created before visiting the value (in case the value + // is a recursive function). We can't create a TypedEnv value for the def + // value without visiting it, so bind a temporary value with a new TypeVariable. + // Once the value is visited, unify the placeholder type and the concrete value + // type. The TypeEnv only really cares about the type of its values so we can + // continue to use the typed stub. const t = new TypeVariable() - const typedStubNode: TypedDefNode = { ...node, value: null, exprType: t } + const typedStubNode: TypedDefPreValueNode = { type: 'DefPreValue', exprType: t } env[node.name] = typedStubNode // if we are defining a function, mark the new identifier as @@ -294,6 +300,8 @@ function fresh (type: Type, nonGeneric: Set) { // FIXME find out how to do this with type safety, given type erasure. return (pruned.constructor as any).of(pruned.name, freshTypeArgs) + } else { + return t } } diff --git a/src/types.ts b/src/types.ts index d9eba86..f16e059 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,7 +94,7 @@ export class TypeVariable extends Type { instance: Type constructor () { - super(null) + super('') this.id = TypeVariable.nextId ++ } diff --git a/src/unify.ts b/src/unify.ts index 52bb20e..3660bd4 100644 --- a/src/unify.ts +++ b/src/unify.ts @@ -3,7 +3,7 @@ import { isArray, isEqual } from './array' import { zip } from './util' import { AstNode, AstLiteralNode, AstDestructuredArrayNode, Value, isAstLiteralNode } from './node-types' -// FIXME tagged union - with didMatch: false, there are never bindings. +// FIXME tagged union - with didMatch: false, there are never bindings (hence binding | null return types) type binding = { [name: string]: Value } interface unification { @@ -29,7 +29,7 @@ export default function unify (patterns: AstNode[], values: Value[]): unificatio return didMatch(bindings) } -function unifyOne (pattern: AstNode, value: Value): binding { +function unifyOne (pattern: AstNode, value: Value): binding | null { if (isAstLiteralNode(pattern) && pattern.value === value) { // the pattern matched, but there is nothing to bind return {} @@ -54,7 +54,7 @@ function unifyOne (pattern: AstNode, value: Value): binding { } // TODO this will need to change when Array is a wrapped type -function destructure ({ head, tail }: AstDestructuredArrayNode, values: Value[]): binding { +function destructure ({ head, tail }: AstDestructuredArrayNode, values: Value[]): binding | null { if (values.length === 0) { throw new PeachError(`Empty arrays cannot be destructured because they don't have a head`) } diff --git a/tsconfig.json b/tsconfig.json index ddea758..d28a080 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "target": "es2017", "sourceMap": true, "noImplicitAny": true, + "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true },