├── LICENSE
├── README.md
├── TODO.md
├── demo
├── 0-multi-web.html
├── 0-single-web.html
├── 1-node.js
└── 1-web.html
├── dist
├── roomdb-web.js
└── roomdb-web.min.js
├── minify
├── package.json
└── src
├── AbstractClient.js
├── Fact.js
├── LocalClient.js
├── RemoteClient.js
├── RoomDB.js
├── listen.js
├── parse.js
├── roomdb-node.js
├── roomdb-web.js
├── server.js
└── terms.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Alessandro Warth
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RoomDB
2 |
3 | RoomDB is a Datalog-style database inspired by the implementation of the [Dynamicland](https://dynamicland.org/) project. It enables programmers to represent facts using natural language, with a syntax adapted from my [NL-Datalog project](https://github.com/harc/nl-datalog).
4 |
5 | ## TODO
6 |
7 | * Add documentation, examples, etc.
8 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | TODO
2 | ====
3 |
4 | * samples vs. wishes
5 |
6 | * evidence
7 |
8 | * include "world time" in samples
9 |
10 | * factor common code from LocalClient and RemoteClient into AbstractClient
11 |
12 | * be more careful with client IDs
13 | (e.g., to prevent 2 clients from having the same ID)
14 |
15 | * consider space-insensitive matching for facts
16 | (would need a canonical representation to use as keys for factMap)
17 |
18 | * think about "primary keys"?
19 |
--------------------------------------------------------------------------------
/demo/0-multi-web.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/demo/0-single-web.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
201 |
202 |
203 |
--------------------------------------------------------------------------------
/demo/1-node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const roomdb = require('../src/roomdb-node');
4 |
5 | const db = roomdb.create().listen(8080);
6 | const localClient = db.client('Lola');
7 | const remoteClient = roomdb.client('localhost', 8080, 'MrRemoto');
8 |
9 | async function main() {
10 | console.log('facts:\n' + db + '\n');
11 |
12 | remoteClient.assert(`#obj1 is a "circle" at (300, 400)`);
13 | remoteClient.assert(`the answer is 42`);
14 | await remoteClient.flushChanges();
15 | console.log('\nfacts:\n' + db + '\n');
16 |
17 | for (let n = 1; n <= 10; n++) {
18 | remoteClient.assert(`_ is a number`, n);
19 | }
20 | await remoteClient.flushChanges();
21 | console.log('\nfacts:\n' + db + '\n');
22 |
23 | await localClient.select(`$n is a number`).do(({n}) => {
24 | if (n % 2 === 1) {
25 | localClient.retract(`_ is a number`, n);
26 | }
27 | });
28 | localClient.flushChanges();
29 | console.log('\nfacts:\n' + db + '\n');
30 |
31 | await remoteClient.select(`$o is a "circle" at ($x, $y)`).do(vars => {
32 | console.log(Object.keys(vars).map(key => key + '=' + vars[key]).join(', '));
33 | });
34 | await remoteClient.select(`the answer is $ans`).do(vars => {
35 | console.log(Object.keys(vars).map(key => key + '=' + vars[key]).join(', '));
36 | });
37 |
38 | localClient.assert('blah blah blah');
39 | await localClient.flushChanges();
40 | await localClient.immediatelyRetractEverythingAbout('obj1');
41 | console.log('\nfacts:\n' + db + '\n');
42 | }
43 |
44 | main();
45 |
--------------------------------------------------------------------------------
/demo/1-web.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/minify:
--------------------------------------------------------------------------------
1 | node_modules/uglify-es/bin/uglifyjs src/RoomDB.js src/parser.js > dist/roomdb.min.js
2 |
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "roomdb",
3 | "version": "0.0.3",
4 | "description": "A Datalog-style database that enables programmers to represent facts in natural language.",
5 | "main": "src/roomdb-node.js",
6 | "dependencies": {
7 | "browserify": "^15.2.0",
8 | "node-fetch": "^1.7.3",
9 | "ohm-js": "^0.14.0",
10 | "restify": "^6.3.4",
11 | "restify-cors-middleware": "^1.1.0",
12 | "restify-errors": "^5.0.0",
13 | "uglify-js": "^3.17.4"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/alexwarth/roomdb.git"
18 | },
19 | "keywords": [
20 | "datalog",
21 | "natural",
22 | "language",
23 | "dynamicland",
24 | "harc"
25 | ],
26 | "author": "Alex Warth (http://alexwarth.com)",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/alexwarth/roomdb/issues"
30 | },
31 | "homepage": "https://github.com/alexwarth/roomdb#readme",
32 | "scripts": {
33 | "clean": "rm dist/*",
34 | "build-web": "node node_modules/browserify/bin/cmd.js src/roomdb-web.js -s roomdb -o dist/roomdb-web.js && node node_modules/uglify-js/bin/uglifyjs dist/roomdb-web.js > dist/roomdb-web.min.js",
35 | "server": "node src/server.js"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/AbstractClient.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {Term} = require('./terms');
4 | const parse = require('./parse');
5 |
6 | const MAX_PARSE_CACHE_SIZE = 1000;
7 |
8 | class AbstractClient {
9 | constructor(id) {
10 | this._id = id;
11 | this._parseCache = new Map();
12 | this._asserts = [];
13 | this._retracts = [];
14 | }
15 |
16 | assert(factString, ...fillerValues) {
17 | const fact = this._toJSONFactOrPattern(factString, ...fillerValues);
18 | this._asserts.push(fact);
19 | return this;
20 | }
21 |
22 | retract(patternString, ...fillerValues) {
23 | const pattern = this._toJSONFactOrPattern(patternString, ...fillerValues);
24 | this._retracts.push(pattern);
25 | return this;
26 | }
27 |
28 | async flushChanges() {
29 | throw new Error('subclass responsibility');
30 | }
31 |
32 | async immediatelyAssert(factString, ...fillerValues) {
33 | this.assert(factString, ...fillerValues);
34 | await this.flushChanges();
35 | return this;
36 | }
37 |
38 | async immediatelyRetract(patternString, ...fillerValues) {
39 | this.retract(patternString, ...fillerValues);
40 | await this.flushChanges();
41 | return this;
42 | }
43 |
44 | async immediatelyRetractEverythingAbout(name) {
45 | throw new Error('subclass responsibility');
46 | }
47 |
48 | async immediatelyRetractEverythingAssertedByMe() {
49 | throw new Error('subclass responsibility');
50 | }
51 |
52 | async getAllFacts() {
53 | throw new Error('subclass responsibility');
54 | }
55 |
56 | _toJSONFactOrPattern(factOrPatternString, ...fillerValues) {
57 | if (arguments.length === 0) {
58 | throw new Error('not enough arguments!');
59 | }
60 | if (typeof factOrPatternString !== 'string') {
61 | throw new Error('factOrPatternString must be a string!');
62 | }
63 | let terms = this._parse(factOrPatternString);
64 | if (fillerValues.length > 0) {
65 | terms = terms.slice();
66 | }
67 | for (let idx = 0; idx < terms.length; idx++) {
68 | const term = terms[idx];
69 | if (term.hasOwnProperty('hole')) {
70 | if (fillerValues.length === 0) {
71 | throw new Error('not enough filler values!');
72 | }
73 | terms[idx] = this._toJSONTerm(fillerValues.shift());
74 | }
75 | }
76 | if (fillerValues.length > 0) {
77 | throw new Error('too many filler values!');
78 | }
79 | return terms;
80 | }
81 |
82 | _toJSONTerm(value) {
83 | return value instanceof Term ? value.toJSON() : {value: value};
84 | }
85 |
86 | _parse(factOrPatternString) {
87 | if (this._parseCache.has(factOrPatternString)) {
88 | return this._parseCache.get(factOrPatternString);
89 | } else {
90 | this._clearParseCacheIfTooBig();
91 | const terms = parse(factOrPatternString);
92 | this._parseCache.set(factOrPatternString, terms);
93 | return terms;
94 | }
95 | }
96 |
97 | _clearParseCacheIfTooBig() {
98 | if (this._parseCache.size > MAX_PARSE_CACHE_SIZE) {
99 | this.clearParseCache();
100 | }
101 | }
102 |
103 | clearParseCache() {
104 | this._parseCache.clear();
105 | return this;
106 | }
107 | }
108 |
109 | module.exports = AbstractClient;
110 |
--------------------------------------------------------------------------------
/src/Fact.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {Term, Variable, Wildcard} = require('./terms');
4 |
5 | class Fact {
6 | constructor(terms) {
7 | this.terms = terms;
8 | }
9 |
10 | hasVariablesOrWildcards() {
11 | return this.terms.some(term =>
12 | term instanceof Variable ||
13 | term instanceof Wildcard);
14 | }
15 |
16 | match(that, env) {
17 | if (this.terms.length !== that.terms.length) {
18 | return null;
19 | }
20 | for (let idx = 0; idx < this.terms.length; idx++) {
21 | const thisTerm = this.terms[idx];
22 | const thatTerm = that.terms[idx];
23 | if (!thisTerm.match(thatTerm, env)) {
24 | return null;
25 | }
26 | }
27 | return env;
28 | }
29 |
30 | toString() {
31 | return this.terms.map(term => term.toString()).join('');
32 | }
33 | }
34 |
35 | Fact.fromJSON =
36 | jsonTerms => new Fact(jsonTerms.map(jsonTerm => Term.fromJSON(jsonTerm)));
37 |
38 | module.exports = Fact;
39 |
--------------------------------------------------------------------------------
/src/LocalClient.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const AbstractClient = require('./AbstractClient');
4 | const {Term} = require('./terms');
5 |
6 | class LocalClient extends AbstractClient {
7 | constructor(db, id) {
8 | super(id);
9 | this._db = db;
10 | }
11 |
12 | select(...patternStrings) {
13 | const patterns = patternStrings.map(p =>
14 | p instanceof Array ?
15 | this._toJSONFactOrPattern(...p) :
16 | this._toJSONFactOrPattern(p));
17 | const solutions = this._db.select(...patterns);
18 | const results = {
19 | async do(callbackFn) {
20 | for (let solution of solutions) {
21 | for (let name in solution) {
22 | // force serialization and deserialization to simulate going over the network
23 | const json = JSON.parse(JSON.stringify(solution[name]));
24 | solution[name] = Term.fromJSON(json).toRawValue();
25 | }
26 | await callbackFn(solution);
27 | }
28 | return results;
29 | },
30 | async count() {
31 | return solutions.length;
32 | },
33 | async isEmpty() {
34 | return solutions.length === 0;
35 | },
36 | async isNotEmpty() {
37 | return solutions.length > 0;
38 | }
39 | };
40 | return results;
41 | }
42 |
43 | async flushChanges() {
44 | this._retracts.forEach(pattern => this._db.retract(this._id, pattern));
45 | this._retracts = [];
46 | this._asserts.forEach(fact => this._db.assert(this._id, fact));
47 | this._asserts = [];
48 | return this;
49 | }
50 |
51 | async immediatelyRetractEverythingAbout(name) {
52 | return this._db.retractEverythingAbout(this._id, name);
53 | }
54 |
55 | async immediatelyRetractEverythingAssertedByMe() {
56 | return this._db.retractEverythingAssertedBy(this._id);
57 | }
58 |
59 | async getAllFacts() {
60 | return this._db.getAllFacts();
61 | }
62 |
63 | toString() {
64 | return `[LocalClient ${this._id}]`;
65 | }
66 | }
67 |
68 | module.exports = LocalClient;
69 |
--------------------------------------------------------------------------------
/src/RemoteClient.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const AbstractClient = require('./AbstractClient');
4 | const {Term} = require('./terms');
5 | const parse = require('./parse');
6 |
7 | // If fetch is not declared, load it from the node-fetch module.
8 | // (This makes it possible to run RemoteClient in the browser and in node-js.)
9 | const fetch = (() => {
10 | try {
11 | return fetch;
12 | } catch (e) {
13 | return require('node-fetch');
14 | }
15 | })();
16 |
17 | class RemoteClient extends AbstractClient {
18 | constructor(address, port, id) {
19 | super(id);
20 | this._address = address;
21 | this._port = port;
22 | }
23 |
24 | select(...patternStrings) {
25 | const patterns = patternStrings.map(p =>
26 | p instanceof Array ?
27 | this._toJSONFactOrPattern(...p) :
28 | this._toJSONFactOrPattern(p));
29 | const solutions = async () => {
30 | const params = `query=${JSON.stringify(patterns)}`;
31 | const response = await fetch(`http://${this._address}:${this._port}/facts?${params}`);
32 | return await response.json();
33 | };
34 | const results = {
35 | async do(callbackFn) {
36 | for (let solution of await solutions()) {
37 | for (let name in solution) {
38 | // force serialization and deserialization to simulate going over the network
39 | const json = JSON.parse(JSON.stringify(solution[name]));
40 | solution[name] = Term.fromJSON(json).toRawValue();
41 | }
42 | await callbackFn(solution);
43 | }
44 | return results;
45 | },
46 | async count() {
47 | return (await solutions()).length;
48 | },
49 | async isEmpty() {
50 | return (await solutions()).length === 0;
51 | },
52 | async isNotEmpty() {
53 | return (await solutions()).length > 0;
54 | }
55 | };
56 | return results;
57 | }
58 |
59 | async flushChanges() {
60 | const retractions = this._retracts;
61 | const assertions = this._asserts;
62 | this._retracts = [];
63 | this._asserts = [];
64 | const params =
65 | 'clientId=' + this._id + '&' +
66 | 'retractions=' + JSON.stringify(retractions) + '&' +
67 | 'assertions=' + JSON.stringify(assertions);
68 | const response = await fetch(
69 | `http://${this._address}:${this._port}/facts?${params}`,
70 | {method: 'PUT'});
71 | return await response.json();
72 | }
73 |
74 | async immediatelyRetractEverythingAbout(name) {
75 | const response = await fetch(
76 | `http://${this._address}:${this._port}/facts?clientId=${this._id}&name=${name}`,
77 | {method: 'DELETE'});
78 | return await response.json();
79 | }
80 |
81 | async immediatelyRetractEverythingAssertedByMe() {
82 | const response = await fetch(
83 | `http://${this._address}:${this._port}/facts?clientId=${this._id}`,
84 | {method: 'DELETE'});
85 | return await response.json();
86 | }
87 |
88 | async getAllFacts() {
89 | const response = await fetch(`http://${this._address}:${this._port}/facts`);
90 | return await response.json();
91 | }
92 |
93 | toString() {
94 | return `[RemoteClient ${this._address}:${this._port}, ${this._id}]`;
95 | }
96 | }
97 |
98 | module.exports = RemoteClient;
99 |
--------------------------------------------------------------------------------
/src/RoomDB.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const LocalClient = require('./LocalClient');
4 | const Fact = require('./Fact');
5 | const {Id} = require('./terms');
6 |
7 | function flatten(obj) {
8 | for (let prop in obj) {
9 | obj[prop] = obj[prop];
10 | }
11 | return obj;
12 | }
13 |
14 | class RoomDB {
15 | constructor() {
16 | this._factMap = new Map();
17 | }
18 |
19 | select(...jsonPatterns) {
20 | const patterns = jsonPatterns.map(jsonPattern => Fact.fromJSON(jsonPattern));
21 | const solutions = [];
22 | this._collectSolutions(patterns, Object.create(null), solutions);
23 | return solutions.map(flatten);
24 | }
25 |
26 | _collectSolutions(patterns, env, solutions) {
27 | if (patterns.length === 0) {
28 | solutions.push(env);
29 | } else {
30 | const pattern = patterns[0];
31 | for (let fact of this._facts) {
32 | const newEnv = Object.create(env);
33 | if (pattern.match(fact, newEnv)) {
34 | this._collectSolutions(patterns.slice(1), newEnv, solutions);
35 | }
36 | }
37 | }
38 | }
39 |
40 | assert(clientId, factJSON) {
41 | const fact = Fact.fromJSON(factJSON);
42 | if (fact.hasVariablesOrWildcards()) {
43 | throw new Error('cannot assert a fact that has variables or wildcards!');
44 | }
45 | fact.asserter = clientId;
46 | this._factMap.set(fact.toString(), fact);
47 | }
48 |
49 | retract(clientId, factJSON) {
50 | const pattern = Fact.fromJSON(factJSON);
51 | if (pattern.hasVariablesOrWildcards()) {
52 | const factsToRetract =
53 | this._facts.filter(fact => pattern.match(fact, Object.create(null)));
54 | factsToRetract.forEach(fact => this._factMap.delete(fact.toString()));
55 | return factsToRetract.length;
56 | } else {
57 | return this._factMap.delete(pattern.toString()) ? 1 : 0;
58 | }
59 | }
60 |
61 | retractEverythingAbout(clientId, name) {
62 | const id = Id.get(name);
63 | const emptyEnv = Object.create(null);
64 | const factsToRetract =
65 | this._facts.filter(fact => fact.terms.some(term => id.match(term, emptyEnv)));
66 | factsToRetract.forEach(fact => this._factMap.delete(fact.toString()));
67 | return factsToRetract.length;
68 | }
69 |
70 | retractEverythingAssertedBy(clientId) {
71 | const factsToRetract = this._facts.filter(fact => fact.asserter === clientId);
72 | factsToRetract.forEach(fact => this._factMap.delete(fact.toString()));
73 | return factsToRetract.length;
74 | }
75 |
76 | get _facts() {
77 | return Array.from(this._factMap.values());
78 | }
79 |
80 | getAllFacts() {
81 | return this._facts.map(fact => fact.toString());
82 | }
83 |
84 | toString() {
85 | return this._facts.map(fact => '<' + fact.asserter + '> ' + fact.toString()).join('\n');
86 | }
87 |
88 | client(id = 'local-client') {
89 | return new LocalClient(this, id);
90 | }
91 | }
92 |
93 | module.exports = RoomDB;
94 |
--------------------------------------------------------------------------------
/src/listen.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const restify = require('restify');
4 | const errors = require('restify-errors');
5 | const corsMiddleware = require('restify-cors-middleware');
6 |
7 | function listen(port = 8080) {
8 | const db = this;
9 | const server = restify.createServer({name: 'RoomDB'});
10 | server.use(restify.plugins.queryParser());
11 |
12 | const cors = corsMiddleware({origins: ['*']});
13 | server.pre(cors.preflight);
14 | server.use(cors.actual);
15 |
16 | server.get('/facts', (req, res, next) => {
17 | try {
18 | if (req.query.query !== undefined) {
19 | const patterns = JSON.parse(req.query.query);
20 | const solutions = db.select(...patterns);
21 | res.send(solutions);
22 | next();
23 | } else {
24 | res.send(db.getAllFacts());
25 | next();
26 | }
27 | } catch (e) {
28 | console.error('uh-oh:', e);
29 | next(e instanceof SyntaxError ?
30 | new errors.BadRequestError(e.message) :
31 | new errors.InternalServerError(e.message));
32 | }
33 | });
34 |
35 | server.put('/facts', (req, res, next) => {
36 | try {
37 | const retractions = req.query.retractions !== undefined ?
38 | JSON.parse(req.query.retractions) :
39 | [];
40 | const assertions = req.query.assertions !== undefined ?
41 | JSON.parse(req.query.assertions) :
42 | [];
43 | retractions.forEach(pattern => db.retract(req.query.clientId, pattern));
44 | assertions.forEach(fact => db.assert(req.query.clientId, fact));
45 | res.send('ok');
46 | next();
47 | } catch (e) {
48 | next(e instanceof SyntaxError ?
49 | new errors.BadRequestError(e.message) :
50 | new errors.InternalServerError(e.message));
51 | }
52 | });
53 |
54 | server.del('/facts', (req, res, next) => {
55 | if (req.query.name !== undefined) {
56 | db.retractEverythingAbout(req.query.clientId, req.query.name);
57 | } else {
58 | db.retractEverythingAssertedBy(req.query.clientId);
59 | }
60 | res.send('ok');
61 | next();
62 | });
63 |
64 | server.listen(port, () => {
65 | console.log('%s listening at %s', server.name, server.url);
66 | });
67 |
68 | return db;
69 | }
70 |
71 | module.exports = listen;
72 |
--------------------------------------------------------------------------------
/src/parse.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ohm = require('ohm-js');
4 |
5 | const grammar = ohm.grammar(`
6 | G {
7 |
8 | factOrPattern
9 | = term*
10 |
11 | term
12 | = id
13 | | word
14 | | value
15 | | variable
16 | | wildcard
17 | | hole
18 |
19 | id
20 | = upper alnum*
21 |
22 | value
23 | = keyword<"true"> -- true
24 | | keyword<"false"> -- false
25 | | keyword<"null"> -- null
26 | | number
27 | | string
28 |
29 | variable
30 | = "$" alnum+
31 |
32 | wildcard
33 | = "*"
34 |
35 | hole
36 | = "_"
37 |
38 | word
39 | = (~special any)+ -- nonspace
40 | | space+ -- space
41 |
42 | keyword
43 | = k ~alnum
44 |
45 | number
46 | = float ("e" float)?
47 |
48 | float
49 | = integer ("." digit+)?
50 |
51 | integer
52 | = ("+" | "-")? digit+
53 |
54 | string
55 | = "\\"" (~"\\"" ~"\\n" any)* "\\""
56 |
57 | special
58 | = id | value | variable | wildcard | hole | space
59 |
60 | }
61 | `);
62 |
63 | const semantics = grammar.createSemantics().addOperation('parse', {
64 | factOrPattern(terms) {
65 | return terms.parse();
66 | },
67 | id(_1, _2) {
68 | return {id: this.sourceString};
69 | },
70 | value_true(_) {
71 | return {value: true};
72 | },
73 | value_false(_) {
74 | return {value: false};
75 | },
76 | value_null(_) {
77 | return {value: null};
78 | },
79 | variable(_, cs) {
80 | return {variable: cs.sourceString};
81 | },
82 | wildcard(_) {
83 | return {wildcard: true};
84 | },
85 | hole(_) {
86 | return {hole: true};
87 | },
88 | word_nonspace(_) {
89 | return {word: this.sourceString};
90 | },
91 | word_space(_) {
92 | return {word: ' '};
93 | },
94 | number(_1, _2, _3) {
95 | return {value: parseFloat(this.sourceString)};
96 | },
97 | string(_oq, cs, _cq) {
98 | const chars = [];
99 | let idx = 0;
100 | cs = cs.parse();
101 | while (idx < cs.length) {
102 | let c = cs[idx++];
103 | if (c === '\\' && idx < cs.length) {
104 | c = cs[idx++];
105 | switch (c) {
106 | case 'n': c = '\n'; break;
107 | case 't': c = '\t'; break;
108 | default: idx--;
109 | }
110 | }
111 | chars.push(c);
112 | }
113 | return {value: chars.join('')};
114 | },
115 | _terminal() {
116 | return this.sourceString;
117 | }
118 | });
119 |
120 | function parse(str, optRule) {
121 | const rule = optRule || 'factOrPattern';
122 | const matchResult = grammar.match(str.trim(), rule);
123 | if (matchResult.succeeded()) {
124 | return semantics(matchResult).parse();
125 | } else {
126 | console.log(str.trim());
127 | console.log(matchResult.message);
128 | throw new Error(`invalid ${rule}: ${str}`);
129 | }
130 | };
131 |
132 | module.exports = parse;
133 |
--------------------------------------------------------------------------------
/src/roomdb-node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const RoomDB = require('./RoomDB');
4 | RoomDB.prototype.listen = require('./listen');
5 |
6 | module.exports = require('./roomdb-web');
7 |
--------------------------------------------------------------------------------
/src/roomdb-web.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const RoomDB = require('./RoomDB');
4 | const RemoteClient = require('./RemoteClient');
5 |
6 | module.exports = {
7 | create() {
8 | return new RoomDB();
9 | },
10 | client(address, port, id = 'remote-client') {
11 | return new RemoteClient(address, port, id);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const roomdb = require('./roomdb-node');
4 |
5 | let port;
6 |
7 | if (process.argv.length === 3) {
8 | port = parseInt(process.argv[2]);
9 | } else if (process.argv.length < 3) {
10 | port = 8080;
11 | } else {
12 | console.error('usage:', process.argv[0], process.argv[1], '[portNumber]');
13 | process.exit(1);
14 | }
15 |
16 | roomdb.create().listen(port);
17 |
--------------------------------------------------------------------------------
/src/terms.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Term {
4 | toString() {
5 | throw new Error('subclass responsibility');
6 | }
7 |
8 | toJSON() {
9 | throw new Error('subclass responsibility');
10 | }
11 |
12 | toRawValue() {
13 | throw new Error('subclass responsibility');
14 | }
15 |
16 | match(that, env) {
17 | throw new Error('subclass responsibility');
18 | }
19 | }
20 |
21 | Term.fromJSON = json => {
22 | if (json.hasOwnProperty('id')) {
23 | return Id.get(json.id);
24 | } else if (json.hasOwnProperty('word')) {
25 | return Word.get(json.word);
26 | } else if (json.hasOwnProperty('value')) {
27 | return new Value(json.value);
28 | } else if (json.hasOwnProperty('blobRef')) {
29 | return new BlobRef(json.blobRef);
30 | } else if (json.hasOwnProperty('variable')) {
31 | return new Variable(json.variable);
32 | } else if (json.hasOwnProperty('wildcard')) {
33 | return new Wildcard();
34 | } else if (json.hasOwnProperty('hole')) {
35 | return new Hole();
36 | } else {
37 | throw new Error('unrecognized JSON term: ' + JSON.stringify(json));
38 | }
39 | };
40 |
41 | class Id extends Term {
42 | static internedIds = new Map();
43 | static get(name) {
44 | let id = this.internedIds.get(name);
45 | if (id == null) {
46 | id = new Id(name);
47 | this.internedIds.set(name, id);
48 | }
49 | return id;
50 | }
51 |
52 | constructor(name) {
53 | super();
54 | this.name = name;
55 | }
56 |
57 | toString() {
58 | return this.name;
59 | }
60 |
61 | toJSON() {
62 | return {id: this.name};
63 | }
64 |
65 | toRawValue() {
66 | return this;
67 | }
68 |
69 | match(that, env) {
70 | return that instanceof Id && this.name === that.name ?
71 | env :
72 | null;
73 | }
74 | }
75 |
76 | class Word extends Term {
77 | static internedWords = new Map();
78 | static get(value) {
79 | let word = this.internedWords.get(value);
80 | if (word == null) {
81 | word = new Word(value);
82 | this.internedWords.set(value, word);
83 | }
84 | return word;
85 | }
86 |
87 | constructor(value) {
88 | super();
89 | this.value = value;
90 | }
91 |
92 | toString() {
93 | return this.value;
94 | }
95 |
96 | toJSON() {
97 | return {word: this.value};
98 | }
99 |
100 | toRawValue() {
101 | return this;
102 | }
103 |
104 | match(that, env) {
105 | return that instanceof Word && this.value === that.value ?
106 | env :
107 | null;
108 | }
109 | }
110 |
111 | class Value extends Term {
112 | constructor(value) {
113 | super();
114 | this.value = value;
115 | }
116 |
117 | toString() {
118 | return JSON.stringify(this.value);
119 | }
120 |
121 | toJSON() {
122 | return {value: this.value};
123 | }
124 |
125 | toRawValue() {
126 | return this.value;
127 | }
128 |
129 | match(that, env) {
130 | return that instanceof Value && this.value === that.value ?
131 | env :
132 | null;
133 | }
134 | }
135 |
136 | class BlobRef extends Term {
137 | constructor(id) {
138 | super();
139 | this.id = id;
140 | }
141 |
142 | toString() {
143 | return '@' + this.id;
144 | }
145 |
146 | toJSON() {
147 | return {blobRef: this.id};
148 | }
149 |
150 | toRawValue() {
151 | return this;
152 | }
153 |
154 | match(that, env) {
155 | return that instanceof BlobRef && this.id === that.id ?
156 | env :
157 | null;
158 | }
159 | }
160 |
161 | class Variable extends Term {
162 | constructor(name) {
163 | super();
164 | this.name = name;
165 | }
166 |
167 | toString() {
168 | return '$' + this.name;
169 | }
170 |
171 | toJSON() {
172 | return {variable: this.name};
173 | }
174 |
175 | toRawValue() {
176 | throw new Error('Variable\'s toRawValue() should never be called!');
177 | }
178 |
179 | match(that, env) {
180 | if (env[this.name] === undefined) {
181 | env[this.name] = that;
182 | return env;
183 | } else {
184 | return env[this.name].match(that, env);
185 | }
186 | }
187 | }
188 |
189 | class Wildcard extends Term {
190 | constructor() {
191 | super();
192 | // no-op
193 | }
194 |
195 | toString() {
196 | return '$';
197 | }
198 |
199 | toJSON() {
200 | return {wildcard: true};
201 | }
202 |
203 | toRawValue() {
204 | throw new Error('Wildcard\'s toRawValue() should never be called!');
205 | }
206 |
207 | match(that, env) {
208 | return env;
209 | }
210 | }
211 |
212 | class Hole extends Term {
213 | constructor() {
214 | super();
215 | // no-op
216 | }
217 |
218 | toString() {
219 | return '_';
220 | }
221 |
222 | toJSON() {
223 | return {hole: true};
224 | }
225 |
226 | toRawValue() {
227 | throw new Error('Hole\'s toRawValue() should never be called!');
228 | }
229 |
230 | match(that, env) {
231 | throw new Error('Hole\'s match() should never be called!');
232 | }
233 | }
234 |
235 | module.exports = {
236 | Term,
237 | Id,
238 | Word,
239 | Value,
240 | BlobRef,
241 | Variable,
242 | Wildcard,
243 | Hole
244 | };
245 |
--------------------------------------------------------------------------------