Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ jspm_packages

# Optional REPL history
.node_repl_history

*.swp
176 changes: 149 additions & 27 deletions lib/regex.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// http://www.ccs.neu.edu/home/turon/re-deriv.pdf

import { dt, symbolToString, zip, valid, explain, getName } from './util'
import {
dt, symbolToString, zip,
valid, explain, getName,
specize, conform
} from './util'
import { Nilable } from './spec/nilable'
import { invalid } from './symbols'
import * as p from './predicates'
// ops
const amp = Symbol('amp &')
const rep = Symbol('rep *')
const kleene = Symbol('kleene *')
const alt = Symbol('alt |')
const cat = Symbol('cat ·')
const acc = Symbol('accepted')
Expand Down Expand Up @@ -38,15 +43,32 @@ export function isRegex(x) {
* ∂a (r · s) = ∂a r · s + ν(r) · ∂a s
*/
function catDeriv(regex, x) {
const [r, ...s] = regex.ps
const [p0, ...prest] = regex.ps
const [k0, ...krest] = regex.ks

const derivations = [
pcat({
ps: [deriv(p0, x), ...prest],
ks: regex.ks,
ret: regex.ret
})
]

if (acceptsNil(p0)) {
derivations.push(
deriv(
pcat({
ps: prest,
ks: krest,
ret: [...regex.ret, k0 ? {
[k0]: null
} : null]
})))
} else {
}

return palt({
ps: [
pcat({
ps: [deriv(r, x), ...s],
ks: regex.ks,
ret: regex.ret
})
]
ps: derivations
})
}

Expand All @@ -63,6 +85,22 @@ function altDeriv(regex, x) {
})
}

function kleeneDeriv(regex, x) {
const { p1, p2, ret } = regex;

let ps = [
pkleene({ p1: deriv(p1, x), p2, ret })
];

if (acceptsNil(p1)) {
ps = ps.concat([
deriv(pkleene({ p1: p2, p2, ret: ret.concat([getReturn(p1)]) }), x)
]);
}

return ps.length > 1 ? palt({ ps }) : ps[0];
}


/**
* Calculates derivative of regex with respect to x.
Expand All @@ -87,31 +125,54 @@ export function deriv(regex, x) {
return altDeriv(regex, x)
case cat:
return catDeriv(regex, x)
case kleene:
return kleeneDeriv(regex, x)
}
throw new Error(`Cannot derive unknown operation "${symbolToString(op)}"`)
}

default:
throw new Error(`Cannot derive unknown operation "${symbolToString(op)}"`)
function getReturn(regex) {
switch (regex.op) {
case cat:
const catProcessed = regex.ret.reduce((agg, val) => Object.assign(agg, val), {})
// unprocessed values
regex.ks.forEach(k => catProcessed[k] = null)
return catProcessed
case acc:
case alt:
return regex.ret;
case kleene:
const p1ret = accepted(regex.p1) ? getReturn(regex.p1) : null;
return regex.ret.concat(p1ret ? [p1ret] : []);
}
throw new Error(`Return for ${symbolToString(regex.op)} not implemented`)
}

/**
* Conform for regex objects. Returns conformed value when regex
* matches data, invalid otherwise.
*/
export function regexConform(regex, data) {
const [x0, ...xrest] = data
const dx = deriv(regex, x0)
if (accepted(dx)) {
return dx.ret
if (data !== null && !Array.isArray(data)) {
return invalid;
}
if (xrest.length > 0) {
return regexConform(dx, xrest)

const [x0, ...xrest] = data

if (!data.length) {
if (acceptsNil(regex)) {
return getReturn(regex);
} else {
return invalid;
}
} else {
const dx = deriv(regex, x0);
return dx ? regexConform(dx, xrest) : invalid;
}
return invalid
}

export function regexExplain(regex, path, via, value) {
// no value provided, so no problems can occur
//
if (p.nil(value)) {
return null
}
Expand Down Expand Up @@ -155,6 +216,42 @@ export function regexExplain(regex, path, via, value) {
return regex.ps.map((p, i) => explain(p, [...path, i], [...via, getName(regex), regex.ks[i]], value))
case acc:
return null

case kleene:
return value
.reduce((errs, val, i) => {
let r = kleeneDeriv(regex, val);

if (r.p1 === null) {
r = Object.assign({}, r, { i, value: val });
return errs.concat([r]);
}

return errs;
}, [])
.map(regex => ({
predicate: regex.p2,
path: [...path, regex.i],
via,
value: regex.value,
}));
}
}

function acceptsNil(regex) {
regex = regex || {};
switch (regex.op) {
case acc:
return true;
case alt:
return regex.ps.some(p => acceptsNil(p))
case cat:
return regex.ps.every(p => acceptsNil(p))
case kleene:
return regex.p1 === regex.p2 || acceptsNil(regex.p1);
default:
// TODO
return false
}
}

Expand All @@ -176,11 +273,13 @@ function pcat(opts = {}) {
ret: pret
})
}

// there are no more values
// we convert the array of matches to a single map and return that
return accept(pret.reduce((agg, val) => Object.assign(agg, val), {}))
return accept(getReturn(pcat({
ret: pret
})))
}

return {
op: cat,
ps,
Expand Down Expand Up @@ -219,7 +318,6 @@ function palt(opts = {}) {
ks,
ret
}

// if any of the alternatives is accepted, return that
const acceptIdx = ps.findIndex(p => accepted(p))
if (acceptIdx !== -1) {
Expand Down Expand Up @@ -264,9 +362,23 @@ export function altImpl(...predicates) {
})
}

// x*
export function kleeneImpl() {
function pkleene(opts) {
let { p1, p2, ret = [] } = opts;

const r = { op: kleene, p1, p2, ret };

return accepted(p1)
? Object.assign({}, r, { p1: p2, ret: ret.concat([getReturn(p1)]) })
: r;
}

// x*
export function kleeneImpl(predicate) {
return pkleene({
op: kleene,
p1: predicate,
p2: predicate,
});
}

// x+
Expand All @@ -275,8 +387,18 @@ export function plusImpl() {
}

// x?
export function maybeImpl() {

// TODO should there be a name parameter for maybe?
export function maybeImpl(name, predicate) {
if (p.nil(name) || !p.string(name)) {
throw new Error(`Must provide a name to maybe.`)
}
if (p.nil(predicate)) {
throw new Error(`Must provide a predicate to maybe.`)
}
return palt({
ps: [predicate, p.nil],
ks: [name, name]
})
}

//????
Expand Down
2 changes: 1 addition & 1 deletion lib/spec/nilable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { invalid } from '../symbols'
export class Nilable extends Spec {
conform(value) {
if (p.nil(value)) {
return value
return null
} else {
return dt(this.options.spec, value)
}
Expand Down
9 changes: 6 additions & 3 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ export function getName(thing) {
}

function toArray(value) {
return p.array(value) ? value : [value]
return value === undefined
? []
: p.array(value) ? value : [value]
}

export function conform(spec, value) {
if (isRegex(spec)) {
return regexConform(spec, toArray(value))
return regexConform(spec, toArray(value == null ? [] : value))
}
return specize(spec).conform(value)
}
Expand All @@ -101,7 +103,8 @@ export function dt(predicate, value, returnBoolean = false) {
if (returnBoolean) {
return predicate(value)
}
return predicate(value) ? value : invalid
// normalize undefined and null
return predicate(value) ? (value === undefined ? null : value) : invalid
}
throw new Error(`${getName(predicate)} is a ${typeof predicate}, not a function. Expected predicate`)
}
Expand Down
23 changes: 23 additions & 0 deletions test/regex/alt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,33 @@ describe("alt", () => {
})

it("works with too many items provided", () => {
// TODO this diverges from clojure.spec
// @see https://github.com/clojure/clojure/blob/master/test/clojure/test_clojure/spec.clj#L95
expect(conform(ingredient_part, [5, "spoons", "tops"])).to.deep.equal({
quantity: 5
})
})

it("works with null/undefined", () => {
// expect not nullable spec to be invalid for nil
expect(conform(ingredient_part, [])).to.deep.equal(invalid)
expect(conform(ingredient_part)).to.deep.equal(invalid)

// expect nullable spec to be valid for nil
const nullable_alt = alt("value", p.number, "no value", p.nil)
expect(conform(nullable_alt, null)).to.deep.equal({
"no value": null
})

// TODO I don't think we want null and undefined to have the same meaning
expect(conform(nullable_alt)).to.deep.equal({
"no value": null
})
expect(conform(nullable_alt, 5)).to.deep.equal({
"value": 5
})
})

it("works in happy nested case", () => {
expect(conform(ingredient_variation, [5, "spoons"]), "regular").to.deep.equal({
regular: {
Expand Down Expand Up @@ -120,6 +142,7 @@ describe("alt", () => {
})

it("[too many values]", () => {
// TODO again this diverges from clojure.spec
const problems = explainData(ingredient_variation, ["spoons", 5, 5])
expect(problems).to.be.an("array").and.to.have.length(0)
})
Expand Down
Loading