diff --git a/bin/ski.ts b/bin/ski.ts index b1b6a7d..5b417a5 100644 --- a/bin/ski.ts +++ b/bin/ski.ts @@ -4,24 +4,24 @@ import * as terminalKit from 'terminal-kit' import { hrtime } from 'process' import { create } from 'random-seed' import { Terminal } from 'terminal-kit' -import { Expression, generate } from '../lib/expression' -import { TerminalSymbol } from '../lib/terminal' -import { stepOnce } from '../lib' +import { SKIExpression, generate } from '../lib/ski/expression' +import { SKITerminalSymbol } from '../lib/ski/terminal' +import { stepOnceSKI } from '../lib' -function colorizeSymbol (sym: TerminalSymbol): string { +function colorizeSymbol (sym: SKITerminalSymbol): string { switch (sym) { - case TerminalSymbol.S: + case SKITerminalSymbol.S: return ' ^[red]S^ ' - case TerminalSymbol.K: + case SKITerminalSymbol.K: return ' ^[green]K^ ' - case TerminalSymbol.I: + case SKITerminalSymbol.I: return ' ^[blue]I^ ' default: return '?' } } -function colorizeExpression (expr: Expression): string { +function colorizeExpression (expr: SKIExpression): string { switch (expr.kind) { case 'terminal': return colorizeSymbol(expr.sym) @@ -36,7 +36,7 @@ function colorizeExpression (expr: Expression): string { } } -function formatted (expr: Expression): string { +function formatted (expr: SKIExpression): string { return '> ' + colorizeExpression(expr) + '\n' } @@ -63,7 +63,7 @@ function runTUI (): number { term.grabInput(false) break case 's': { - const stepResult = stepOnce(expression) + const stepResult = stepOnceSKI(expression) expression = stepResult.expr term(formatted(expression)) break @@ -73,7 +73,7 @@ function runTUI (): number { let iterations = 0 while (loop && iterations < MAX_ITER) { - const stepResult = stepOnce(expression) + const stepResult = stepOnceSKI(expression) expression = stepResult.expr if (stepResult.altered) { term(formatted(expression)) diff --git a/lib/combinators.ts b/lib/consts/combinators.ts similarity index 84% rename from lib/combinators.ts rename to lib/consts/combinators.ts index 9c5da21..1ab758f 100644 --- a/lib/combinators.ts +++ b/lib/consts/combinators.ts @@ -1,6 +1,9 @@ -import { apply } from './expression' -import { parse } from './parser' -import { S, K, I } from './terminal' +import { convertLambda } from '../conversion/converter' +import { apply } from '../ski/expression' +import { predLambda } from './lambdas' +import { parseSKI } from '../parser/ski' + +import { S, K, I } from '../ski/terminal' /* * Zero. apply a function to its arguments zero times. @@ -60,7 +63,7 @@ export const One = I * * λnfx.n(fx) ≡ B */ -export const B = parse('S(KS)K') +export const B = parseSKI('S(KS)K') /* * Successor function @@ -166,7 +169,7 @@ export const V = apply(B, C, T) * * λa.aa ≡ M */ -export const M = parse('SII') +export const M = parseSKI('SII') /* * Retrieve the first element in a Cons cell. @@ -209,7 +212,7 @@ export const Cdr = apply(T, Snd) * * λxy.xyy ≡ W */ -export const W = parse('SS(SK)') +export const W = parseSKI('SS(SK)') // λabcd.a(bcd) export const Blk = apply(B, B, B) @@ -219,3 +222,9 @@ export const E = apply(B, apply(B, B, B)) // λabc.cba export const F = apply(E, T, T, E, T) + +// λf.(λx.f(x x))(λx.f(x x)) +export const Y = parseSKI('S(K(SII))(S(S(KS)K)(K(SII)))') + +// note: this is a crossover +export const pred = convertLambda(predLambda) diff --git a/lib/consts/lambdas.ts b/lib/consts/lambdas.ts new file mode 100644 index 0000000..aa5b40b --- /dev/null +++ b/lib/consts/lambdas.ts @@ -0,0 +1,3 @@ +import { parseLambda } from '../parser/untyped' + +export const [, predLambda] = parseLambda('λn.λf.λx.n(λg.λh.h(gf))(λu.x)(λu.u)') diff --git a/lib/conversion/conversionError.ts b/lib/conversion/conversionError.ts new file mode 100644 index 0000000..6d0b8a0 --- /dev/null +++ b/lib/conversion/conversionError.ts @@ -0,0 +1 @@ +export class ConversionError extends Error { } diff --git a/lib/converter.ts b/lib/conversion/converter.ts similarity index 88% rename from lib/converter.ts rename to lib/conversion/converter.ts index 4303834..fa4681d 100644 --- a/lib/converter.ts +++ b/lib/conversion/converter.ts @@ -1,8 +1,9 @@ -import { NonTerminal, nt } from './nonterminal' -import { S, K, I, Terminal } from './terminal' -import { Expression } from './expression' -import { B, C } from './combinators' -import { LambdaVar } from './lambda' +import { NonTerminal, nt } from '../nonterminal' +import { S, K, I, SKITerminal } from '../ski/terminal' +import { SKIExpression } from '../ski/expression' +import { B, C } from '../consts/combinators' +import { LambdaVar } from '../lambda/lambda' +import { ConversionError } from './conversionError' type LambdaAbsMixed = { kind: 'lambda-abs', @@ -12,7 +13,7 @@ type LambdaAbsMixed = { } type LambdaMixed - = Terminal + = SKITerminal | LambdaVar | LambdaAbsMixed | NonTerminal @@ -22,15 +23,13 @@ export type Lambda | LambdaAbsMixed | NonTerminal -export class ConversionError extends Error { } - const mkAbstractMixed = (name: string, body: LambdaMixed): LambdaMixed => ({ kind: 'lambda-abs', name, body }) -export const convertLambda = (lm: Lambda): Expression => { +export const convertLambda = (lm: Lambda): SKIExpression => { const mixed = convert(lm) return assertCombinator(mixed) } @@ -120,7 +119,7 @@ const convert = (lm: LambdaMixed): LambdaMixed => { } } -const assertCombinator = (lm: LambdaMixed): Expression => { +const assertCombinator = (lm: LambdaMixed): SKIExpression => { switch (lm.kind) { case 'terminal': return lm @@ -130,7 +129,9 @@ const assertCombinator = (lm: LambdaMixed): Expression => { throw new ConversionError('lambda abstraction detected in nt') } - return nt(assertCombinator(lm.lft), assertCombinator(lm.rgt)) + return nt( + assertCombinator(lm.lft), assertCombinator(lm.rgt) + ) default: throw new ConversionError('lambda abstraction detected at top') } diff --git a/lib/evaluator.ts b/lib/evaluator/skiEvaluator.ts similarity index 66% rename from lib/evaluator.ts rename to lib/evaluator/skiEvaluator.ts index 71e484f..a88a606 100644 --- a/lib/evaluator.ts +++ b/lib/evaluator/skiEvaluator.ts @@ -1,13 +1,13 @@ -import { Expression, prettyPrint } from './expression' -import { nt } from './nonterminal' -import { TerminalSymbol } from './terminal' +import { SKIExpression, prettyPrint } from '../ski/expression' +import { nt } from '../nonterminal' +import { SKITerminalSymbol } from '../ski/terminal' /** * the shape of an evaluation result. * altered is set if the evaluation step changed the input. * expr is the evaluation output. */ -export interface Result { +export interface SKIResult { altered: boolean; expr: E; } @@ -15,7 +15,7 @@ export interface Result { /** * a computation step; takes an expression and returns a result. */ -export type Step = (expr: E) => Result +export type SKIStep = (expr: E) => SKIResult /** * the SKI combinator reduction function. @@ -24,9 +24,9 @@ export type Step = (expr: E) => Result * * NOTE: this function is not guaranteed to terminate */ -export const stepMany: Step = - (expr: Expression) => { - const result = stepOnce(expr) +export const stepMany: SKIStep = + (expr: SKIExpression) => { + const result = stepOnceSKI(expr) if (result.altered) { return stepMany(result.expr) @@ -35,11 +35,11 @@ export const stepMany: Step = } } -export const loggedStepMany: Step = - (expr: Expression) => { +export const loggedStepMany: SKIStep = + (expr: SKIExpression) => { console.log(prettyPrint(expr)) console.log('->') - const result = stepOnce(expr) + const result = stepOnceSKI(expr) if (result.altered) { return loggedStepMany(result.expr) @@ -53,7 +53,9 @@ export const loggedStepMany: Step = * @param exp the input expression. * @returns the evaluation result. */ -export const reduce = (exp: Expression): Expression => +export const reduceSKI = ( + exp: SKIExpression +): SKIExpression => stepMany(exp).expr /** @@ -61,7 +63,9 @@ export const reduce = (exp: Expression): Expression => * @param exp the input expression. * @returns the evaluation result. */ -export const loggedReduce = (exp: Expression): Expression => +export const loggedReduceSKI = ( + exp: SKIExpression +): SKIExpression => loggedStepMany(exp).expr /** @@ -69,17 +73,18 @@ export const loggedReduce = (exp: Expression): Expression => * @param expr the input expression. * @returns the evaluation result after one step. */ -export const stepOnce: Step = - (expr: Expression) => scanStep(expr, [stepOnceI, stepOnceK, stepOnceS]) +export const stepOnceSKI: SKIStep = + (expr: SKIExpression) => + scanStep(expr, [stepOnceI, stepOnceK, stepOnceS]) -const stepOnceI: Step = - (expr: Expression) => treeStep(expr, stepI) +const stepOnceI: SKIStep = + (expr: SKIExpression) => treeStep(expr, stepI) -const stepOnceK: Step = - (expr: Expression) => treeStep(expr, stepK) +const stepOnceK: SKIStep = + (expr: SKIExpression) => treeStep(expr, stepK) -const stepOnceS: Step = - (expr: Expression) => treeStep(expr, stepS) +const stepOnceS: SKIStep = + (expr: SKIExpression) => treeStep(expr, stepS) /** * @param expr the expression to scan with steppers. @@ -88,8 +93,11 @@ const stepOnceS: Step = * * NOTE: this is an eagerly returning fold */ -const scanStep = (expr: Expression, steppers: Array>): - Result => { +const scanStep = ( + expr: SKIExpression, + steppers: Array> +): + SKIResult => { for (const step of steppers) { const result = step(expr) @@ -114,8 +122,11 @@ const scanStep = (expr: Expression, steppers: Array>): * is the input and a singular function that processes an expression * and returns either nothing or some result, returning eagerly. */ -function treeStep (expr: Expression, step: Step): - Result { +function treeStep ( + expr: SKIExpression, + step: SKIStep +): + SKIResult { switch (expr.kind) { case 'terminal': return ({ @@ -151,8 +162,11 @@ function treeStep (expr: Expression, step: Step): type ExtractStep = (expr: E) => E | false -function extractStep (expr: Expression, extractStep: ExtractStep): - Result { +function extractStep ( + expr: SKIExpression, + extractStep: ExtractStep +): + SKIResult { const extractionResult = extractStep(expr) if (extractionResult) { @@ -166,12 +180,12 @@ function extractStep (expr: Expression, extractStep: ExtractStep): * identity * Ix = x */ -const stepI: Step = (expr: Expression) => +const stepI: SKIStep = (expr: SKIExpression) => extractStep( - expr, (expr: Expression) => + expr, (expr: SKIExpression) => expr.kind === 'non-terminal' && expr.lft.kind === 'terminal' && - expr.lft.sym === TerminalSymbol.I && + expr.lft.sym === SKITerminalSymbol.I && expr.rgt ) @@ -179,13 +193,13 @@ const stepI: Step = (expr: Expression) => * constant * Kxy = x */ -const stepK: Step = (expr: Expression) => +const stepK: SKIStep = (expr: SKIExpression) => extractStep( - expr, (expr: Expression) => + expr, (expr: SKIExpression) => expr.kind === 'non-terminal' && expr.lft.kind === 'non-terminal' && expr.lft.lft.kind === 'terminal' && - expr.lft.lft.sym === TerminalSymbol.K && + expr.lft.lft.sym === SKITerminalSymbol.K && expr.lft.rgt ) @@ -193,15 +207,15 @@ const stepK: Step = (expr: Expression) => * fusion * Sxyz = xz(yz) */ -const stepS: Step = (expr: Expression) => +const stepS: SKIStep = (expr: SKIExpression) => extractStep( - expr, (expr: Expression) => { + expr, (expr: SKIExpression) => { if ( expr.kind === 'non-terminal' && expr.lft.kind === 'non-terminal' && expr.lft.lft.kind === 'non-terminal' && expr.lft.lft.lft.kind === 'terminal' && - expr.lft.lft.lft.sym === TerminalSymbol.S + expr.lft.lft.lft.sym === SKITerminalSymbol.S ) { const x = expr.lft.lft.rgt const y = expr.lft.rgt diff --git a/lib/index.ts b/lib/index.ts index 8c37730..d5771b0 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,9 +1,11 @@ -export * from './church' -export * from './combinators' -export * from './evaluator' -export * from './expression' -export * from './lambda' -export * from './packer' -export * from './parser' -export * from './typedLambda' -export * from './types' +export * from './ski/church' +export * from './consts/combinators' +export * from './consts/lambdas' +export * from './evaluator/skiEvaluator' +export * from './ski/expression' +export * from './lambda/lambda' +export * from './ski/packer' +export * from './parser/ski' +export * from './parser/typed' +export * from './typed/typedLambda' +export * from './typed/types' diff --git a/lib/lambda.ts b/lib/lambda/lambda.ts similarity index 89% rename from lib/lambda.ts rename to lib/lambda/lambda.ts index 2dbadf6..da01741 100644 --- a/lib/lambda.ts +++ b/lib/lambda/lambda.ts @@ -1,4 +1,4 @@ -import { NonTerminal } from './nonterminal' +import { NonTerminal, nt } from '../nonterminal' /** * This is a single term variable with a name. @@ -40,6 +40,9 @@ export type UntypedLambda | UntypedLambdaAbs | NonTerminal +export const typelessApp = (...uts: UntypedLambda[]) => + uts.reduce(nt) + export const prettyPrintUntypedLambda = (ut: UntypedLambda): string => { switch (ut.kind) { case 'lambda-var': diff --git a/lib/appendable.ts b/lib/parser/appendable.ts similarity index 90% rename from lib/appendable.ts rename to lib/parser/appendable.ts index 928a097..b9a29aa 100644 --- a/lib/appendable.ts +++ b/lib/parser/appendable.ts @@ -1,7 +1,7 @@ -import { Expression } from './expression' -import { ParseError } from './parser' -import { NonTerminal, nt } from './nonterminal' -import { Terminal } from './terminal' +import { SKIExpression } from '../ski/expression' +import { NonTerminal, nt } from '../nonterminal' +import { SKITerminal } from '../ski/terminal' +import { ParseError } from './parseError' /** * A variation on the expression type that allows undefined values. @@ -10,7 +10,7 @@ import { Terminal } from './terminal' * while verifying that no undefined values, or 'holes', remain. */ type SyntaxExpression - = Terminal + = SKITerminal | NonTerminal | undefined; @@ -18,7 +18,7 @@ export class Appendable { private syn: SyntaxExpression private insertionSites: NonTerminal[] = [] - public appendSymbol (term: Terminal): void { + public appendSymbol (term: SKITerminal): void { this.appendInternal(term) } @@ -95,11 +95,11 @@ export class Appendable { * @throws {ParseError} if there are any empty internal nodes in the * expression. */ - public flatten (): Expression { + public flatten (): SKIExpression { return this.flattenInternal(this.syn) } - private flattenInternal = (exp: SyntaxExpression): Expression => { + private flattenInternal = (exp: SyntaxExpression): SKIExpression => { if (exp === undefined) { throw new ParseError('expression undefined (empty)') } else if (exp.kind === 'terminal') { diff --git a/lib/parser/parseError.ts b/lib/parser/parseError.ts new file mode 100644 index 0000000..e0ca787 --- /dev/null +++ b/lib/parser/parseError.ts @@ -0,0 +1 @@ +export class ParseError extends Error { } diff --git a/lib/recursiveDescentBuffer.ts b/lib/parser/recursiveDescentBuffer.ts similarity index 85% rename from lib/recursiveDescentBuffer.ts rename to lib/parser/recursiveDescentBuffer.ts index 4237bde..823977b 100644 --- a/lib/recursiveDescentBuffer.ts +++ b/lib/parser/recursiveDescentBuffer.ts @@ -1,4 +1,4 @@ -import { ParseError } from './parser' +import { ParseError } from './parseError' export class RecursiveDescentBuffer { buf!: string @@ -24,6 +24,14 @@ export class RecursiveDescentBuffer { this.idx += n } + matchLP (): void { + this.matchCh('(') + } + + matchRP (): void { + this.matchCh(')') + } + matchCh (ch: string): void { if (this.peek() !== ch) { throw new ParseError(`Expected ${ch} but found ${this.peek() || 'null'}'`) @@ -51,7 +59,7 @@ export class RecursiveDescentBuffer { return next } - peel (): RecursiveDescentBuffer { + peelRemaining (): RecursiveDescentBuffer { return new RecursiveDescentBuffer(this.buf.slice(this.idx)) } } diff --git a/lib/parser/ski.ts b/lib/parser/ski.ts new file mode 100644 index 0000000..d44b87c --- /dev/null +++ b/lib/parser/ski.ts @@ -0,0 +1,39 @@ +import { Appendable } from './appendable' +import { SKIExpression } from '../ski/expression' +import { SKITerminalSymbol, term } from '../ski/terminal' +import { ParseError } from './parseError' + +/** + * @param input a string with an SKI expression to parse. + * @returns an abstract expression corresponding to the parsed input string, + * should one exist. + * @throws {ParseError} if the input string is not a well formed expression. + */ +export function parseSKI (input: string): SKIExpression { + const app = new Appendable() + let parenLevel = 0 + + for (const ch of input) { + if (ch === '(') { + app.appendEmptyBranch() + parenLevel++ + } else if (ch === ')') { + parenLevel-- + + if (parenLevel < 0) { + throw new ParseError('mismatched parens! (early)') + } + } else if (Object.values(SKITerminalSymbol) + .includes(ch as SKITerminalSymbol)) { + app.appendSymbol(term(ch as SKITerminalSymbol)) + } else { + throw new ParseError('unrecognized char: ' + ch) + } + } + + if (parenLevel !== 0) { + throw new ParseError('mismatched parens! (late)') + } + + return app.flatten() +} diff --git a/lib/parser.ts b/lib/parser/typed.ts similarity index 59% rename from lib/parser.ts rename to lib/parser/typed.ts index 37a58ea..6446ef4 100644 --- a/lib/parser.ts +++ b/lib/parser/typed.ts @@ -1,54 +1,16 @@ -import { Appendable } from './appendable' -import { Expression } from './expression' -import { mkVar } from './lambda' -import { nt } from './nonterminal' +import { mkVar } from '../lambda/lambda' +import { nt } from '../nonterminal' +import { TypedLambda, mkTypedAbs } from '../typed/typedLambda' +import { Type, arrow, mkTypeVar } from '../typed/types' +import { ParseError } from './parseError' import { RecursiveDescentBuffer } from './recursiveDescentBuffer' -import { term, TerminalSymbol } from './terminal' -import { mkTypedAbs, TypedLambda } from './typedLambda' -import { Type, arrow, mkTypeVar } from './types' - -export class ParseError extends Error { } - -/** - * @param input a string with an SKI expression to parse. - * @returns an abstract expression corresponding to the parsed input string, - * should one exist. - * @throws {ParseError} if the input string is not a well formed expression. - */ -export function parse (input: string): Expression { - const app = new Appendable() - let parenLevel = 0 - - for (const ch of input) { - if (ch === '(') { - app.appendEmptyBranch() - parenLevel++ - } else if (ch === ')') { - parenLevel-- - - if (parenLevel < 0) { - throw new ParseError('mismatched parens! (early)') - } - } else if (Object.values(TerminalSymbol).includes(ch as TerminalSymbol)) { - app.appendSymbol(term(ch as TerminalSymbol)) - } else { - throw new ParseError('unrecognized char: ' + ch) - } - } - - if (parenLevel !== 0) { - throw new ParseError('mismatched parens! (late)') - } - - return app.flatten() -} function parseApp (rdb: RecursiveDescentBuffer): [string, TypedLambda] { if (rdb.peek() === '(') { - rdb.matchCh('(') + rdb.matchLP() const lft = rdb.parseVariable() const rgt = rdb.parseVariable() - rdb.matchCh(')') + rdb.matchRP() return [`(${lft}${rgt})`, nt(mkVar(lft), mkVar(rgt))] } else { const varStr = rdb.parseVariable() @@ -58,7 +20,7 @@ function parseApp (rdb: RecursiveDescentBuffer): [string, TypedLambda] { function parseTypeInternal (rdb: RecursiveDescentBuffer): [string, Type] { if (rdb.peek() === '(') { - rdb.consume() + rdb.matchLP() const [leftTypeLit, leftTy] = parseTypeInternal(rdb) if ((rdb.peek() === '→')) { @@ -66,7 +28,7 @@ function parseTypeInternal (rdb: RecursiveDescentBuffer): [string, Type] { const [rightTypeLit, rightTy] = parseTypeInternal(rdb) if (rdb.peek() !== ')') throw new ParseError('expected a )') - rdb.consume() + rdb.matchRP() // '(' '→' ')' ) return [`(${leftTypeLit}→${rightTypeLit})`, arrow(leftTy, rightTy)] @@ -101,20 +63,20 @@ function parseTypeInternal (rdb: RecursiveDescentBuffer): [string, Type] { } } } +function parseLambdaInternal (rdb: RecursiveDescentBuffer): +[string, TypedLambda] { + rdb.matchCh('λ') + const varLit = rdb.parseVariable() + rdb.matchCh(':') + const [typeLit, ty] = parseTypeInternal(rdb) + rdb.matchCh('.') + const [bodyLit, term] = parseTypedLambdaInternal(rdb.peelRemaining()) + rdb.consumeN(bodyLit.length) + return [`λ${varLit}:${typeLit}.${bodyLit}`, mkTypedAbs(varLit, ty, term)] +} function parseTypedLambdaInternal (rdb: RecursiveDescentBuffer): [string, TypedLambda] { - function parseLambda (rdb: RecursiveDescentBuffer): [string, TypedLambda] { - rdb.matchCh('λ') - const varLit = rdb.parseVariable() - rdb.matchCh(':') - const [typeLit, ty] = parseTypeInternal(rdb) - rdb.matchCh('.') - const [bodyLit, term] = parseTypedLambdaInternal(rdb.peel()) - rdb.consumeN(bodyLit.length) - return [`λ${varLit}:${typeLit}.${bodyLit}`, mkTypedAbs(varLit, ty, term)] - } - let resultStr = '' let resultExpr: TypedLambda | undefined @@ -122,7 +84,7 @@ function parseTypedLambdaInternal (rdb: RecursiveDescentBuffer): let nextTerm: TypedLambda if (rdb.peek() === 'λ') { - const [lambdaLit, lambdaTerm] = parseLambda(rdb) + const [lambdaLit, lambdaTerm] = parseLambdaInternal(rdb) nextTerm = lambdaTerm resultStr += lambdaLit } else { diff --git a/lib/parser/untyped.ts b/lib/parser/untyped.ts new file mode 100644 index 0000000..8e751b8 --- /dev/null +++ b/lib/parser/untyped.ts @@ -0,0 +1,61 @@ +import { UntypedLambda, mkUntypedAbs, mkVar } from '../lambda/lambda' +import { nt } from '../nonterminal' +import { ParseError } from './parseError' +import { RecursiveDescentBuffer } from './recursiveDescentBuffer' + +export function parseLambda (input: string): [string, UntypedLambda] { + const rdb = new RecursiveDescentBuffer(input) + return parseUntypedLambdaInternal(rdb) +} + +function parseUntypedLambdaInternal (rdb: RecursiveDescentBuffer): +[string, UntypedLambda] { + let resultStr = '' + let resultExpr: UntypedLambda | undefined + + while (rdb.remaining()) { + let nextTerm: UntypedLambda | undefined + + if (rdb.peek() === 'λ') { + const [lambdaLit, lambdaTerm] = parseUntypedLambda(rdb) + resultStr += lambdaLit + nextTerm = lambdaTerm + } else if (rdb.peek() === '(') { + rdb.matchLP() + const [lit1, t1] = parseUntypedLambdaInternal(rdb) + resultStr += '(' + lit1 + rdb.matchRP() + resultStr += ')' + nextTerm = t1 + } else if (rdb.peek() === ')') { + break + } else { + const singleVar = rdb.parseVariable() + resultStr += singleVar + nextTerm = mkVar(singleVar) + } + + if (nextTerm !== undefined) { + if (resultExpr === undefined) { + resultExpr = nextTerm + } else { + resultExpr = nt(resultExpr, nextTerm) + } + } + } + if (resultExpr === undefined) { + throw new ParseError('expected a term') + } + + return [resultStr, resultExpr] +} + +function parseUntypedLambda (rdb: RecursiveDescentBuffer): +[string, UntypedLambda] { + rdb.matchCh('λ') + const varLit = rdb.parseVariable() + rdb.matchCh('.') + const [bodyLit, term] = parseLambda(rdb.peelRemaining().buf) + rdb.consumeN(bodyLit.length) + return [`λ${varLit}.${bodyLit}`, mkUntypedAbs(varLit, term)] +} diff --git a/lib/church.ts b/lib/ski/church.ts similarity index 76% rename from lib/church.ts rename to lib/ski/church.ts index 36b0cc9..ab70b21 100644 --- a/lib/church.ts +++ b/lib/ski/church.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Zero, One, Succ, True, False } from './combinators' -import { Expression, apply } from './expression' -import { TerminalSymbol } from './terminal' +import { Zero, One, Succ, True, False } from '../consts/combinators' +import { SKIExpression, apply } from './expression' +import { SKITerminalSymbol } from './terminal' /** * @see https://en.wikipedia.org/wiki/Church_encoding * @param n a number * @returns an extensionally equivalent Church numeral. */ -export const ChurchN = (n: number): Expression => { +export const ChurchN = (n: number): SKIExpression => { if (n < 0) { throw new Error('only positive integers represented') } else if (n === 0) { @@ -32,12 +32,12 @@ export const ChurchN = (n: number): Expression => { * represents a given Church numeral, regardless of which one it is. This is * the notion of extensional equality. */ -export const UnChurch = (exp: Expression): number => { +export const UnChurch = (exp: SKIExpression): number => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call return toLambda(exp)((x: number) => x + 1)(0) } -export const ChurchB = (b: boolean): Expression => b ? True : False +export const ChurchB = (b: boolean): SKIExpression => b ? True : False /** * This is a somewhat foul construction in TypeScript, which gives insight into @@ -46,21 +46,21 @@ export const ChurchB = (b: boolean): Expression => b ? True : False * @param exp an expression in the SKI combinator language. * @returns a Curried TypeScript lambda which is extensionally equivalent to it */ -const toLambda = (exp: Expression): any => { +const toLambda = (exp: SKIExpression): any => { if (exp.kind === 'non-terminal') { // eslint-disable-next-line @typescript-eslint/no-unsafe-call return toLambda(exp.lft)(toLambda(exp.rgt)) } else { switch (exp.sym) { - case TerminalSymbol.S: + case SKITerminalSymbol.S: return (x: (_: any) => {(_: any): any; _: any }) => (y: (_: any) => any) => (z: any) => // eslint-disable-next-line @typescript-eslint/no-unsafe-return x(z)(y(z)) // eslint-disable-next-line @typescript-eslint/no-unused-vars - case TerminalSymbol.K: return (x: any) => (_y: any) => x - case TerminalSymbol.I: return (x: any) => x + case SKITerminalSymbol.K: return (x: any) => (_y: any) => x + case SKITerminalSymbol.I: return (x: any) => x } } } diff --git a/lib/expression.ts b/lib/ski/expression.ts similarity index 78% rename from lib/expression.ts rename to lib/ski/expression.ts index 4f26f66..5e272d9 100644 --- a/lib/expression.ts +++ b/lib/ski/expression.ts @@ -1,8 +1,8 @@ -import { stepOnce } from './evaluator' -import { NonTerminal, nt } from './nonterminal' +import { stepOnceSKI } from '../evaluator/skiEvaluator' +import { NonTerminal, nt } from '../nonterminal' import { generate as generateTerminal, - Terminal + SKITerminal } from './terminal' import { RandomSeed } from 'random-seed' @@ -21,13 +21,13 @@ import { RandomSeed } from 'random-seed' * * terminal | non-terminal | expression */ -export type Expression = Terminal | NonTerminal +export type SKIExpression = SKITerminal | NonTerminal /** * @param expr an expression to pretty print. * @returns a pretty printed expression. */ -export function prettyPrint (expr: Expression): string { +export function prettyPrint (expr: SKIExpression): string { switch (expr.kind) { case 'terminal': return expr.sym @@ -49,12 +49,12 @@ export function prettyPrint (expr: Expression): string { * @param n the number of symbols to include in the expression. * @returns a randomly generated expression. */ -export function generate (rs: RandomSeed, n: number): Expression { +export function generate (rs: RandomSeed, n: number): SKIExpression { if (n <= 0) { throw new Error('A valid expression must contain at least one symbol.') } - let result: Expression = generateTerminal(rs) + let result: SKIExpression = generateTerminal(rs) for (let i = 0; i < n - 1; i++) { result = splat(rs, result, generateTerminal(rs)) @@ -67,7 +67,7 @@ export function generate (rs: RandomSeed, n: number): Expression { * @param exp an abstract expression. * @returns how many terminals are present in the expression. */ -export function size (exp: Expression): number { +export function size (exp: SKIExpression): number { if (exp.kind === 'terminal') return 1 else return size(exp.lft) + size(exp.rgt) } @@ -77,11 +77,11 @@ export function size (exp: Expression): number { * @param exps an array of expressions. * @returns an unevaluated result. */ -export const apply = (...exps: Expression[]): Expression => { +export const apply = (...exps: SKIExpression[]): SKIExpression => { if (exps.length <= 0) { throw new Error('there must be at least one expression to apply') } else { - return exps.reduce(nt) + return exps.reduce(nt) } } @@ -100,12 +100,12 @@ export function compute ( S: number, N: number, rs: RandomSeed, - onStep: (_: Expression) => void, - onRegenerate: (_: Expression) => void): Expression { + onStep: (_: SKIExpression) => void, + onRegenerate: (_: SKIExpression) => void): SKIExpression { let exp = generate(rs, S) for (let i = 0; i < N; i++) { - const stepResult = stepOnce(exp) + const stepResult = stepOnceSKI(exp) if (stepResult.altered) { exp = stepResult.expr @@ -128,8 +128,8 @@ export function compute ( * @returns an expression with the symbol t added in a "random" but deserving * location. */ -const splat = (randomSeed: RandomSeed, expr: Expression, term: Terminal): - Expression => { +const splat = (randomSeed: RandomSeed, expr: SKIExpression, term: SKITerminal): + SKIExpression => { const direction = randomSeed.intBetween(0, 1) === 1 if (expr.kind === 'terminal') { diff --git a/lib/packer.ts b/lib/ski/packer.ts similarity index 81% rename from lib/packer.ts rename to lib/ski/packer.ts index a6faeca..369521b 100644 --- a/lib/packer.ts +++ b/lib/ski/packer.ts @@ -1,8 +1,8 @@ -import { Expression } from './expression' -import { nt } from './nonterminal' -import { term, TerminalSymbol } from './terminal' +import { SKIExpression } from './expression' +import { nt } from '../nonterminal' +import { term, SKITerminalSymbol } from './terminal' -export type SymbolHeap = Array; +export type SymbolHeap = Array; export type BinaryHeap = Uint8Array @@ -22,9 +22,9 @@ function rgtIndex (heapIdx: number): number { * @param exp an expression. * @returns a symbol heap. */ -export function heapify (exp: Expression): SymbolHeap { +export function heapify (exp: SKIExpression): SymbolHeap { const heapLength = maxHeapIndex(exp) + 1 - const result = new Array(heapLength) + const result = new Array(heapLength) const indexes = [rootIndex] const nodes = [exp] @@ -50,11 +50,11 @@ export function heapify (exp: Expression): SymbolHeap { return result } -export function maxHeapIndex (exp: Expression): number { +export function maxHeapIndex (exp: SKIExpression): number { return maxHeapIndexInternal(exp, 0) } -function maxHeapIndexInternal (exp: Expression, acc: number): number { +function maxHeapIndexInternal (exp: SKIExpression, acc: number): number { if (exp.kind === 'non-terminal') { return Math.max( maxHeapIndexInternal(exp.lft, lftIndex(acc)), @@ -65,7 +65,7 @@ function maxHeapIndexInternal (exp: Expression, acc: number): number { } } -export function unheapify (heapSyms: SymbolHeap): Expression { +export function unheapify (heapSyms: SymbolHeap): SKIExpression { if (heapSyms.length === 0) { throw new Error('expression must be non-empty') } @@ -73,7 +73,7 @@ export function unheapify (heapSyms: SymbolHeap): Expression { return unheapifyFrom(heapSyms, 0) } -function unheapifyFrom (heapSyms: SymbolHeap, heapIdx: number): Expression { +function unheapifyFrom (heapSyms: SymbolHeap, heapIdx: number): SKIExpression { if (heapIdx >= heapSyms.length) { throw new Error(`heap index exceeded: ${heapIdx}. input is corrupt.`) } @@ -101,14 +101,14 @@ function unheapifyFrom (heapSyms: SymbolHeap, heapIdx: number): Expression { * * NOTE: here ∅ represents the empty set, or lack of a value. */ -function packSymbol (sym: TerminalSymbol | undefined): number { +function packSymbol (sym: SKITerminalSymbol | undefined): number { if (sym === undefined) { return 0b00 - } else if (sym === TerminalSymbol.S) { + } else if (sym === SKITerminalSymbol.S) { return 0b01 - } else if (sym === TerminalSymbol.K) { + } else if (sym === SKITerminalSymbol.K) { return 0b10 - } else if (sym === TerminalSymbol.I) { + } else if (sym === SKITerminalSymbol.I) { return 0b11 } else { throw new Error('Impossible.') @@ -123,15 +123,15 @@ function packSymbol (sym: TerminalSymbol | undefined): number { * * @see packSymbol */ -function unpackSymbol (n: number): TerminalSymbol | undefined { +function unpackSymbol (n: number): SKITerminalSymbol | undefined { if (n === 0b00) { return undefined } else if (n === 0b01) { - return TerminalSymbol.S + return SKITerminalSymbol.S } else if (n === 0b10) { - return TerminalSymbol.K + return SKITerminalSymbol.K } else if (n === 0b11) { - return TerminalSymbol.I + return SKITerminalSymbol.I } else { throw new Error(`The number ${n} does not correspond to a symbol in SKI.`) } @@ -211,7 +211,7 @@ export function unpackBinaryHeap (inputBytes: BinaryHeap): SymbolHeap { * @param exp the input expression. * @returns a binary heap packed result. */ -export function packHeap (exp: Expression): BinaryHeap { +export function packHeap (exp: SKIExpression): BinaryHeap { return packSymbolHeap(heapify(exp)) } @@ -220,6 +220,6 @@ export function packHeap (exp: Expression): BinaryHeap { * @param heapBytes the input binary heap. * @returns an expression. */ -export function unpackHeap (heapBytes: BinaryHeap): Expression { +export function unpackHeap (heapBytes: BinaryHeap): SKIExpression { return unheapify(unpackBinaryHeap(heapBytes)) } diff --git a/lib/terminal.ts b/lib/ski/terminal.ts similarity index 56% rename from lib/terminal.ts rename to lib/ski/terminal.ts index b4bb72f..520469f 100644 --- a/lib/terminal.ts +++ b/lib/ski/terminal.ts @@ -1,30 +1,30 @@ import { RandomSeed } from 'random-seed' -export enum TerminalSymbol { +export enum SKITerminalSymbol { S = 'S', K = 'K', I = 'I' } -export interface Terminal { +export interface SKITerminal { kind: 'terminal'; - sym: TerminalSymbol; + sym: SKITerminalSymbol; } -export const term = (sym: TerminalSymbol): Terminal => ({ +export const term = (sym: SKITerminalSymbol): SKITerminal => ({ kind: 'terminal', sym }) -export const S = term(TerminalSymbol.S) -export const K = term(TerminalSymbol.K) -export const I = term(TerminalSymbol.I) +export const S = term(SKITerminalSymbol.S) +export const K = term(SKITerminalSymbol.K) +export const I = term(SKITerminalSymbol.I) /** * @param rs the random seed to use. * @returns a randomly selected terminal symbol. */ -export function generate (rs: RandomSeed): Terminal { +export function generate (rs: RandomSeed): SKITerminal { const die = rs.intBetween(1, 3) if (die === 1) { diff --git a/lib/typed/typeError.ts b/lib/typed/typeError.ts new file mode 100644 index 0000000..5e68c8d --- /dev/null +++ b/lib/typed/typeError.ts @@ -0,0 +1 @@ +export class TypeError extends Error { } diff --git a/lib/typedLambda.ts b/lib/typed/typedLambda.ts similarity index 96% rename from lib/typedLambda.ts rename to lib/typed/typedLambda.ts index 7c5fa3f..5f5ffe1 100644 --- a/lib/typedLambda.ts +++ b/lib/typed/typedLambda.ts @@ -1,5 +1,5 @@ -import { LambdaVar } from './lambda' -import { NonTerminal } from './nonterminal' +import { LambdaVar } from '../lambda/lambda' +import { NonTerminal } from '../nonterminal' import { Type, arrow, @@ -47,8 +47,6 @@ export const mkTypedAbs = ( body }) -export class TypeError extends Error { } - /** * Γ, or capital Gamma, represents the set of mappings from names to types. */ diff --git a/lib/types.ts b/lib/typed/types.ts similarity index 97% rename from lib/types.ts rename to lib/typed/types.ts index 1931d9e..3151c27 100644 --- a/lib/types.ts +++ b/lib/typed/types.ts @@ -1,6 +1,7 @@ -import { UntypedLambda } from './lambda' -import { NonTerminal, nt } from './nonterminal' +import { UntypedLambda } from '../lambda/lambda' +import { NonTerminal, nt } from '../nonterminal' import { Context, TypedLambda, mkTypedAbs, typecheck } from './typedLambda' +import { TypeError } from './typeError' export type TypeVariable = { kind: 'type-var', diff --git a/package.json b/package.json index 6de4160..8cd85a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typed-ski", - "version": "1.0.4", + "version": "1.0.5", "description": "SKI combinators in Typescript", "scripts": { "test": "npx mocha test/**/*.test.ts", diff --git a/test/conversion/converter.test.ts b/test/conversion/converter.test.ts new file mode 100644 index 0000000..5dbe7de --- /dev/null +++ b/test/conversion/converter.test.ts @@ -0,0 +1,49 @@ +import { + mkVar, + reduceSKI, + predLambda, + UnChurch, + ChurchN, + prettyPrintUntypedLambda +} from '../../lib' +import { Lambda, convertLambda } from '../../lib/conversion/converter' +import { nt } from '../../lib/nonterminal' +import { I, S, K } from '../../lib/ski/terminal' +import { UpTo } from '../ski/church.test' +import { apply } from '../../lib/ski/expression' +import { describe, it } from 'mocha' +import { expect } from 'chai' + +describe('Lambda conversion', () => { + const mkAbs = (name: string, body: Lambda): Lambda => ({ + kind: 'lambda-abs', + name, + body + }) + + const id = mkAbs('x', mkVar('x')) + + const konst = mkAbs('x', mkAbs('y', mkVar('x'))) + + const flip = mkAbs('x', mkAbs('y', nt(mkVar('y'), mkVar('x')))) + + it('should convert λx.x to I', () => { + expect(convertLambda(id)).to.deep.equal(I) + }) + + it('should convert λx.λy.x to something that acts like K', () => { + expect(reduceSKI(apply(convertLambda(konst), S, K))).to.deep.equal(S) + }) + + it('should convert λx.λy.y x to something that acts like T', () => { + expect(reduceSKI(apply(convertLambda(flip), S, K))).to.deep.equal(nt(K, S)) + }) + + it(`should convert ${prettyPrintUntypedLambda(predLambda)} to pred`, () => { + UpTo(8).forEach(n => + expect( + UnChurch(reduceSKI(apply(convertLambda(predLambda), ChurchN(n)))) + ).to.deep.equal(Math.max(n - 1, 0)) + ) + }) +}) diff --git a/test/converter.test.ts b/test/converter.test.ts deleted file mode 100644 index 27a45f2..0000000 --- a/test/converter.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Lambda, convertLambda } from '../lib/converter' -import { S, K, I } from '../lib/terminal' -import { reduce } from '../lib/evaluator' -import { apply } from '../lib/expression' -import { nt } from '../lib/nonterminal' -import { mkVar } from '../lib/lambda' - -import { expect } from 'chai' -import { describe, it } from 'mocha' - -describe('Lambda conversion', () => { - const mkAbs = (name: string, body: Lambda): Lambda => ({ - kind: 'lambda-abs', - name, - body - }) - - const id = mkAbs('x', mkVar('x')) - - const konst = mkAbs('x', mkAbs('y', mkVar('x'))) - - const flip = mkAbs('x', mkAbs('y', nt(mkVar('y'), mkVar('x')))) - - it('should convert λx.x to I', () => { - expect(convertLambda(id)).to.deep.equal(I) - }) - - it('should convert λx.λy.x to something that acts like K', () => { - expect(reduce(apply(convertLambda(konst), S, K))).to.deep.equal(S) - }) - - it('should convert λx.λy.y x to something that acts like T', () => { - expect(reduce(apply(convertLambda(flip), S, K))).to.deep.equal(nt(K, S)) - }) -}) diff --git a/test/evaluator.test.ts b/test/evaluator/skiEvaluator.test.ts similarity index 57% rename from test/evaluator.test.ts rename to test/evaluator/skiEvaluator.test.ts index b32f190..38e77fd 100644 --- a/test/evaluator.test.ts +++ b/test/evaluator/skiEvaluator.test.ts @@ -1,20 +1,20 @@ -import { parse } from '../lib/parser' -import { stepOnce } from '../lib/evaluator' -import { Expression, prettyPrint } from '../lib/expression' +import { stepOnceSKI } from '../../lib/evaluator/skiEvaluator' +import { SKIExpression, prettyPrint } from '../../lib/ski/expression' import { describe, it } from 'mocha' import { assert } from 'chai' +import { parseSKI } from '../../lib/parser/ski' -const first = parse('III') -const second = parse('II') -const third = parse('I') -const fourth = parse('KIS') -const fifth = parse('SKKI') -const sixth = parse('SKKII') -const seventh = parse('KI(KI)') +const first = parseSKI('III') +const second = parseSKI('II') +const third = parseSKI('I') +const fourth = parseSKI('KIS') +const fifth = parseSKI('SKKI') +const sixth = parseSKI('SKKII') +const seventh = parseSKI('KI(KI)') describe('stepOnce', () => { - const compareExpressions = (a: Expression, b: Expression): void => { + const compareExpressions = (a: SKIExpression, b: SKIExpression): void => { assert.deepStrictEqual(prettyPrint(a), prettyPrint(b)) assert.deepStrictEqual(a, b) } @@ -22,7 +22,7 @@ describe('stepOnce', () => { it(`evaluates ${prettyPrint(second)} => ${prettyPrint(third)}`, () => { - const result = stepOnce(second) + const result = stepOnceSKI(second) assert(result.altered) compareExpressions(result.expr, third) }) @@ -31,7 +31,7 @@ describe('stepOnce', () => { => ${prettyPrint(third)}`, () => { - const result = stepOnce(stepOnce(first).expr) + const result = stepOnceSKI(stepOnceSKI(first).expr) assert(result.altered) compareExpressions(result.expr, third) }) @@ -39,7 +39,7 @@ describe('stepOnce', () => { it(`evaluates ${prettyPrint(fourth)} => ${prettyPrint(third)}`, () => { - const result = stepOnce(fourth) + const result = stepOnceSKI(fourth) assert(result.altered) compareExpressions(result.expr, third) }) @@ -48,7 +48,7 @@ describe('stepOnce', () => { ${prettyPrint(fifth)} => ${prettyPrint(seventh)}`, () => { - const first = stepOnce(fifth) + const first = stepOnceSKI(fifth) assert(first.altered) compareExpressions(first.expr, seventh) }) @@ -57,11 +57,11 @@ describe('stepOnce', () => { => ${prettyPrint(third)}`, () => { - const firstStep = stepOnce(sixth) + const firstStep = stepOnceSKI(sixth) assert(firstStep.altered) - const secondStep = stepOnce(firstStep.expr) + const secondStep = stepOnceSKI(firstStep.expr) assert(secondStep.altered) - const thirdStep = stepOnce(secondStep.expr) + const thirdStep = stepOnceSKI(secondStep.expr) assert(thirdStep.altered) compareExpressions(thirdStep.expr, third) }) diff --git a/test/appendable.test.ts b/test/parser/appendable.test.ts similarity index 81% rename from test/appendable.test.ts rename to test/parser/appendable.test.ts index a512309..64d9aee 100644 --- a/test/appendable.test.ts +++ b/test/parser/appendable.test.ts @@ -1,7 +1,7 @@ -import { S, K, I } from '../lib/terminal' -import { nt } from '../lib/nonterminal' -import { Appendable } from '../lib/appendable' -import { Expression } from '../lib' +import { S, K, I } from '../../lib/ski/terminal' +import { nt } from '../../lib/nonterminal' +import { Appendable } from '../../lib/parser/appendable' +import { SKIExpression } from '../../lib' import { expect } from 'chai' @@ -48,18 +48,18 @@ describe('appendable expressions', () => { app.appendSymbol(K) app.appendSymbol(I) expect(app.flatten()).to.deep.equal( - nt( - nt( - nt( + nt( + nt( + nt( S, K ), - nt( + nt( I, S ) ), - nt( + nt( K, I ) @@ -79,18 +79,18 @@ describe('appendable expressions', () => { app.appendSymbol(K) app.appendSymbol(I) expect(app.flatten()).to.deep.equal( - nt( - nt( - nt( + nt( + nt( + nt( S, K ), - nt( + nt( I, S ) ), - nt( + nt( K, I ) diff --git a/test/parser/ski.test.ts b/test/parser/ski.test.ts new file mode 100644 index 0000000..b3de7f6 --- /dev/null +++ b/test/parser/ski.test.ts @@ -0,0 +1,79 @@ +import { assert, expect } from 'chai' + +import { SKIExpression, prettyPrint, parseSKI, Y } from '../../lib' +import { nt } from '../../lib/nonterminal' +import { ParseError } from '../../lib/parser/parseError' +import { I, S, K } from '../../lib/ski/terminal' + +describe('parseSKI', () => { + const firstLiteral = '(I(SK))' + const secondLiteral = '(((((SK)I)S)K)I)' + + const assertPrintedParsedPair = ( + a: SKIExpression, + b: SKIExpression + ): void => { + assert.deepStrictEqual(prettyPrint(a), prettyPrint(b)) + assert.deepStrictEqual(a, b) + } + + it(`should parse ${firstLiteral} and variations`, () => { + const expectedISK = nt(I, nt(S, K)) + const parsedISK = parseSKI(firstLiteral) + + assertPrintedParsedPair(parsedISK, expectedISK) + }) + + it('should fail to parse an unrecognized literal', () => { + expect(() => parseSKI('(Q')).to.throw(ParseError, /unrecognized/) + }) + + it(`should parse ${secondLiteral} and variations`, () => { + const expected = + nt( + nt( + nt( + nt( + nt(S, K), I + ), + S), + K), + I) + + assertPrintedParsedPair(parseSKI(secondLiteral), expected) + }) + + it('should parse adjacent chars associating to the left', () => { + assert.deepStrictEqual(parseSKI('SKI'), parseSKI('(SK)I')) + assert.deepStrictEqual(parseSKI('(SK)I'), parseSKI('((SK)I)')) + + assert.notDeepEqual(parseSKI('SKI'), parseSKI('S(KI)')) + }) + + it('should fail to parse mismatched parens', () => { + expect(() => parseSKI('(())(')).to.throw(ParseError, /mismatched/) + expect(() => parseSKI('(')).to.throw(ParseError, /mismatched/) + expect(() => parseSKI('()())')).to.throw(ParseError, /mismatched/) + }) + + it('should parse the Y combinator', () => { + assertReparse(prettyPrint(Y)) + }) + + const assertReparse = (expr: string) => { + const parsed = parseSKI(expr) + const printed = prettyPrint(parsed) + const reparsed = parseSKI(printed) + const reprinted = prettyPrint(reparsed) + + assert.deepStrictEqual(printed, reprinted) + assert.deepStrictEqual(parsed, reparsed) + } + + it('should reparse complicated expressions', () => { + assertReparse('S(K(SKK))SI') + assertReparse('SK(SKK)SI') + assertReparse('SKI') + assertReparse('(IIII)') + }) +}) diff --git a/test/parser.test.ts b/test/parser/typed.test.ts similarity index 61% rename from test/parser.test.ts rename to test/parser/typed.test.ts index c4d7962..26b2b6c 100644 --- a/test/parser.test.ts +++ b/test/parser/typed.test.ts @@ -1,87 +1,20 @@ -import { Expression, prettyPrint } from '../lib/expression' -import { nt } from '../lib/nonterminal' -import { parse, ParseError, parseType, parseTypedLambda } from '../lib/parser' -import { S, K, I } from '../lib/terminal' -import { - mkTypedAbs, - typedTermsLitEq -} from '../lib/typedLambda' -import { mkVar } from '../lib/lambda' +import { expect } from 'chai' + import { + parseTypedLambda, + mkVar, + typedTermsLitEq, + parseType, + typesLitEq, arrow, mkTypeVar, - typesLitEq, - arrows -} from '../lib/types' - -import { assert, expect } from 'chai' -import { describe, it } from 'mocha' - -describe('parse', () => { - const firstLiteral = '(I(SK))' - const secondLiteral = '(((((SK)I)S)K)I)' - - const assertPrintedParsedPair = (a: Expression, b: Expression): void => { - assert.deepStrictEqual(prettyPrint(a), prettyPrint(b)) - assert.deepStrictEqual(a, b) - } - - it(`should parse ${firstLiteral} and variations`, () => { - const expectedISK = nt(I, nt(S, K)) - const parsedISK = parse(firstLiteral) - - assertPrintedParsedPair(parsedISK, expectedISK) - }) - - it('should fail to parse an unrecognized literal', () => { - expect(() => parse('(Q')).to.throw(ParseError, /unrecognized/) - }) - - it(`should parse ${secondLiteral} and variations`, () => { - const expected = - nt( - nt( - nt( - nt( - nt(S, K), I - ), - S), - K), - I) - - assertPrintedParsedPair(parse(secondLiteral), expected) - }) - - it('should parse adjacent chars associating to the left', () => { - assert.deepStrictEqual(parse('SKI'), parse('(SK)I')) - assert.deepStrictEqual(parse('(SK)I'), parse('((SK)I)')) - - assert.notDeepEqual(parse('SKI'), parse('S(KI)')) - }) - - it('should fail to parse mismatched parens', () => { - expect(() => parse('(())(')).to.throw(ParseError, /mismatched/) - expect(() => parse('(')).to.throw(ParseError, /mismatched/) - expect(() => parse('()())')).to.throw(ParseError, /mismatched/) - }) - - const assertReparse = (expr: string) => { - const parsed = parse(expr) - const printed = prettyPrint(parsed) - const reparsed = parse(printed) - const reprinted = prettyPrint(reparsed) - - assert.deepStrictEqual(printed, reprinted) - assert.deepStrictEqual(parsed, reparsed) - } - - it('should reparse complicated expressions', () => { - assertReparse('S(K(SKK))SI') - assertReparse('SK(SKK)SI') - assertReparse('SKI') - assertReparse('(IIII)') - }) + arrows, + mkTypedAbs +} from '../../lib' +import { nt } from '../../lib/nonterminal' +import { ParseError } from '../../lib/parser/parseError' +describe('parseTypedLambda', () => { it('parses a single term application', () => { const parseInput = 'xy' const [parsedLit, term] = parseTypedLambda(parseInput) diff --git a/test/parser/untyped.test.ts b/test/parser/untyped.test.ts new file mode 100644 index 0000000..9647ea6 --- /dev/null +++ b/test/parser/untyped.test.ts @@ -0,0 +1,71 @@ +import { + mkUntypedAbs, + mkVar, + prettyPrintUntypedLambda, + typelessApp +} from '../../lib' +import { parseLambda } from '../../lib/parser/untyped' +import { expect } from 'chai' + +describe('parseUntypedLambda()', () => { + it('parses application', () => { + const input = 'ab' + const [lit, term] = parseLambda(input) + expect(lit).to.equal(input) + expect(term).to.deep.equal(typelessApp(mkVar('a'), mkVar('b'))) + }) + + it('parses application with parens', () => { + const input = '(ab)' + const [lit, term] = parseLambda(input) + expect(lit).to.equal(input) + expect(term).to.deep.equal(typelessApp(mkVar('a'), mkVar('b'))) + }) + + it('parses an unbalanced triplet of vars', () => { + const input = 'a(bc)' + const [lit, term] = parseLambda(input) + expect(lit).to.equal(input) + expect(term).to.deep.equal( + typelessApp(mkVar('a'), typelessApp(mkVar('b'), mkVar('c'))) + ) + }) + + it('parses a var applied to a lambda', () => { + const input = 'a(λb.b(aa))' + const [lit, term] = parseLambda(input) + expect(lit).to.equal(input) + + expect(term).to.deep.equal( + typelessApp(mkVar('a'), + typelessApp(mkUntypedAbs('b', + typelessApp(mkVar('b'), typelessApp(mkVar('a'), mkVar('a')))) + ) + )) + }) + + it('parses pred', () => { + const input = 'λn.λf.λx.n(λg.λh.h(gf))(λu.x)(λu.u)' + + // λn.λf.λx.n(λg.λh.h(gf))(λu.x)(λu.u) + const predLambda = + // λn.λf.λx. + mkUntypedAbs('n', mkUntypedAbs('f', mkUntypedAbs('x', + // n(λg.λh.h(gf))(λu.x)(λu.u) + typelessApp( + mkVar('n'), // n + mkUntypedAbs('g', mkUntypedAbs('h', // λg.λh. + typelessApp( + mkVar('h'), typelessApp(mkVar('g'), mkVar('f')))) + ), // h(gf) + mkUntypedAbs('u', mkVar('x')), // (λu.x) + mkUntypedAbs('u', mkVar('u')) // (λu.u) + ) + ))) + + const [, term] = parseLambda(input) + expect(term).to.deep.equal(predLambda) + const [, reparsed] = parseLambda(prettyPrintUntypedLambda(term)) + expect(reparsed).to.deep.equal(predLambda) + }) +}) diff --git a/test/performance.test.ts b/test/performance.test.ts index f5758bf..2edbef9 100644 --- a/test/performance.test.ts +++ b/test/performance.test.ts @@ -1,4 +1,4 @@ -import { compute } from '../lib/expression' +import { compute } from '../lib/ski/expression' import { hrtime } from 'process' import { Readable } from 'stream' diff --git a/test/church.test.ts b/test/ski/church.test.ts similarity index 70% rename from test/church.test.ts rename to test/ski/church.test.ts index 3afca66..508577f 100644 --- a/test/church.test.ts +++ b/test/ski/church.test.ts @@ -1,21 +1,22 @@ -import { apply } from '../lib/expression' -import { S, K, I } from '../lib/terminal' -import { reduce } from '../lib/evaluator' -import { UnChurch, ChurchN, ChurchB } from '../lib/church' +import { apply } from '../../lib/ski/expression' +import { S, K, I } from '../../lib/ski/terminal' +import { reduceSKI } from '../../lib/evaluator/skiEvaluator' +import { UnChurch, ChurchN, ChurchB } from '../../lib/ski/church' import { Fst, Snd, Car, Cdr, Succ, V, B, False, Zero, True, Plus, - F -} from '../lib/combinators' + F, + pred +} from '../../lib/consts/combinators' import { describe, it } from 'mocha' import { expect } from 'chai' -import { parse } from '../lib/parser' +import { parseSKI } from '../../lib/parser/ski' -const UpTo = (n: number): Array => { +export const UpTo = (n: number): Array => { const result = [] for (let i = 0; i < n; i++) { result.push(i) @@ -23,8 +24,6 @@ const UpTo = (n: number): Array => { return result } -const DupePair = apply(parse('SS(SK)'), V) - /* * This test verifies that numeral systems and boolean logic can be encoded * using only combinators. See https://www.youtube.com/watch?v=6BnVo7EHO_8 this @@ -32,13 +31,15 @@ const DupePair = apply(parse('SS(SK)'), V) */ describe('Church encodings', () => { + const DupePair = apply(parseSKI('SS(SK)'), V) + it('reduces 0 + 1 to 1 ', () => { expect(UnChurch(apply(Succ, ChurchN(0)))) .to.deep.equal(1) }) it('reduces 1 + 1 to 2', () => { - expect(UnChurch(reduce(apply(Succ, ChurchN(1))))) + expect(UnChurch(reduceSKI(apply(Succ, ChurchN(1))))) .to.deep.equal(2) }) @@ -57,7 +58,7 @@ describe('Church encodings', () => { * (AND)FT = F?T:F = F * (AND)FF = F?F:F = F */ - expect(reduce(apply(ChurchB(p), ChurchB(q), ChurchB(p)))) + expect(reduceSKI(apply(ChurchB(p), ChurchB(q), ChurchB(p)))) .to.deep.equal(conj) /* @@ -68,30 +69,30 @@ describe('Church encodings', () => { * (OR)FT = F?F:T = T * (OR)FF = F?F:F = F */ - expect(reduce(apply(ChurchB(p), ChurchB(p), ChurchB(q)))) + expect(reduceSKI(apply(ChurchB(p), ChurchB(p), ChurchB(q)))) .to.deep.equal(dis) }) }) }) it('reduces pairs', () => { - expect(reduce(apply(V, ChurchN(0), ChurchN(1), Fst))) + expect(reduceSKI(apply(V, ChurchN(0), ChurchN(1), Fst))) .to.deep.equal(ChurchN(0)) - expect(reduce(apply(V, ChurchN(0), ChurchN(1), Snd))) + expect(reduceSKI(apply(V, ChurchN(0), ChurchN(1), Snd))) .to.deep.equal(ChurchN(1)) - expect(reduce( + expect(reduceSKI( apply(Car, apply(V, ChurchN(0), ChurchN(1))) )).to.deep.equal(ChurchN(0)) - expect(reduce( + expect(reduceSKI( apply(Cdr, apply(V, ChurchN(0), ChurchN(1))) )).to.deep.equal(ChurchN(1)) expect( - reduce(apply(DupePair, ChurchN(2))) - ).to.deep.equal(reduce(apply(V, ChurchN(2), ChurchN(2)))) + reduceSKI(apply(DupePair, ChurchN(2))) + ).to.deep.equal(reduceSKI(apply(V, ChurchN(2), ChurchN(2)))) }) /* @@ -100,23 +101,23 @@ describe('Church encodings', () => { const IsZero = apply(F, True, apply(K, False)) it('isZero tests for whether a numeral is zero', () => { - expect(reduce( + expect(reduceSKI( apply(ChurchN(0), apply(K, False), True) )).to.deep.equal(ChurchB(true)) - expect(reduce( + expect(reduceSKI( apply(ChurchN(1), apply(K, False), True) )).to.deep.equal(ChurchB(false)) - expect(reduce( + expect(reduceSKI( apply(ChurchN(2), apply(K, False), True) )).to.deep.equal(ChurchB(false)) - expect(reduce( + expect(reduceSKI( apply(IsZero, ChurchN(0)) )).to.deep.equal(ChurchB(true)) - expect(reduce( + expect(reduceSKI( apply(IsZero, ChurchN(1)) )).to.deep.equal(ChurchB(false)) }) @@ -126,17 +127,17 @@ describe('Church encodings', () => { UpTo(8).forEach(n => { // λmn.(m succ)n, or apply m +1s to n expect(UnChurch( - reduce(apply(ChurchN(m), Succ, ChurchN(n))) + reduceSKI(apply(ChurchN(m), Succ, ChurchN(n))) )).to.equal(m + n) // λmnfx.mf((nf)x) ≡ BS(BB) ≡ Plus expect(UnChurch( - reduce(apply(Plus, ChurchN(m), ChurchN(n))) + reduceSKI(apply(Plus, ChurchN(m), ChurchN(n))) )).to.equal(m + n) // λmn.m(n(succ)), or apply m +ns to 0 expect(UnChurch( - reduce(apply(ChurchN(m), apply(ChurchN(n), Succ), Zero)) + reduceSKI(apply(ChurchN(m), apply(ChurchN(n), Succ), Zero)) )).to.equal(m * n) /* @@ -145,7 +146,7 @@ describe('Church encodings', () => { * in the Church numerals simultaneously. */ expect(UnChurch( - reduce(apply(B, ChurchN(m), ChurchN(n), Succ, Zero)) + reduceSKI(apply(B, ChurchN(m), ChurchN(n), Succ, Zero)) )).to.equal(m * n) }) }) @@ -170,13 +171,19 @@ describe('Church encodings', () => { it('computes the predecessor', () => { UpTo(8).forEach(m => { + const expected = Math.max(m - 1, 0) // pred of 0 is 0 + expect( UnChurch( - reduce( + reduceSKI( apply(Cdr, apply(ChurchN(m), pairShiftSucc, pairZeroZero)) ) ) - ).to.equal(Math.max(m - 1, 0)) // in Church numerals, pred of 0 is 0 + ).to.equal(expected) + + expect( + UnChurch(reduceSKI(apply(pred, ChurchN(m)))) + ).to.deep.equal(expected) }) }) }) diff --git a/test/expression.test.ts b/test/ski/expression.test.ts similarity index 69% rename from test/expression.test.ts rename to test/ski/expression.test.ts index 4f99b57..e30e58c 100644 --- a/test/expression.test.ts +++ b/test/ski/expression.test.ts @@ -1,12 +1,17 @@ -import { Expression, generate, prettyPrint, size } from '../lib/expression' -import { nt } from '../lib/nonterminal' -import { K, S } from '../lib/terminal' +import { + SKIExpression, + generate, + prettyPrint, + size +} from '../../lib/ski/expression' +import { nt } from '../../lib/nonterminal' +import { K, S } from '../../lib/ski/terminal' import { assert } from 'chai' import { create, RandomSeed } from 'random-seed' describe('prettyPrint', () => { - const expr = nt(nt(S, K), K) + const expr = nt(nt(S, K), K) const printedExpr = '((SK)K)' it('pretty prints a valid expression', diff --git a/test/packer.test.ts b/test/ski/packer.test.ts similarity index 82% rename from test/packer.test.ts rename to test/ski/packer.test.ts index 4daaec5..4a62617 100644 --- a/test/packer.test.ts +++ b/test/ski/packer.test.ts @@ -1,26 +1,26 @@ -import { parse } from '../lib/parser' import { BinaryHeap, maxHeapIndex, packHeap, unpackHeap -} from '../lib/packer' -import { compute, Expression, size } from '../lib/expression' +} from '../../lib/ski/packer' +import { compute, SKIExpression, size } from '../../lib/ski/expression' import { assert } from 'chai' import { describe, it } from 'mocha' import { hrtime } from 'process' import { create } from 'random-seed' +import { parseSKI } from '../../lib/parser/ski' -const eye = parse('I') -const dos = parse('II') -const tres = parse('III') -const quattro = parse('SKKI') -const toTheRight = parse('(I(S(KI)))') -const zipper = parse('((I(S(KI)))I)') +const eye = parseSKI('I') +const dos = parseSKI('II') +const tres = parseSKI('III') +const quattro = parseSKI('SKKI') +const toTheRight = parseSKI('(I(S(KI)))') +const zipper = parseSKI('((I(S(KI)))I)') describe('packHeap and unpackHeap', () => { - const assertRepack = (expr: Expression): BinaryHeap => { + const assertRepack = (expr: SKIExpression): BinaryHeap => { const packed = packHeap(expr) const unpacked = unpackHeap(packed) assert.deepStrictEqual(expr, unpacked) diff --git a/test/typedLambda.test.ts b/test/typed/typedLambda.test.ts similarity index 88% rename from test/typedLambda.test.ts rename to test/typed/typedLambda.test.ts index 9b141d0..f45a51e 100644 --- a/test/typedLambda.test.ts +++ b/test/typed/typedLambda.test.ts @@ -1,7 +1,7 @@ -import { mkVar } from '../lib/lambda' -import { nt } from '../lib/nonterminal' -import { mkTypedAbs, typecheck } from '../lib/typedLambda' -import { arrow, arrows, mkTypeVar, typesLitEq } from '../lib/types' +import { mkVar } from '../../lib/lambda/lambda' +import { nt } from '../../lib/nonterminal' +import { mkTypedAbs, typecheck } from '../../lib/typed/typedLambda' +import { arrow, arrows, mkTypeVar, typesLitEq } from '../../lib/typed/types' import { expect } from 'chai' import { describe, it } from 'mocha' diff --git a/test/types.test.ts b/test/typed/types.test.ts similarity index 92% rename from test/types.test.ts rename to test/typed/types.test.ts index 3ad2e8b..cf6aa8a 100644 --- a/test/types.test.ts +++ b/test/typed/types.test.ts @@ -4,14 +4,14 @@ import { mkTypeVar, typesLitEq, inferType -} from '../lib/types' +} from '../../lib/typed/types' import { expect } from 'chai' import { describe, it } from 'mocha' -import { UntypedLambda, mkUntypedAbs, mkVar } from '../lib/lambda' -import { nt } from '../lib/nonterminal' -import { typedTermsLitEq } from '../lib/typedLambda' -import { parseType, parseTypedLambda } from '../lib' +import { UntypedLambda, mkUntypedAbs, mkVar } from '../../lib/lambda/lambda' +import { nt } from '../../lib/nonterminal' +import { typedTermsLitEq } from '../../lib/typed/typedLambda' +import { parseType, parseTypedLambda } from '../../lib' describe('type construction and equivalence', () => { const t1 = arrows(mkTypeVar('a'), mkTypeVar('b'), mkTypeVar('c'))