Skip to content

Commit

Permalink
Incorporate partial handling of arbitrary JSON data
Browse files Browse the repository at this point in the history
  • Loading branch information
robsimmons committed Dec 3, 2023
1 parent 3e3a264 commit 10476a3
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 10 deletions.
91 changes: 89 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Data, TRIV_DATA, expose, hide } from './datastructures/data.js';
import { Data, TRIV_DATA, expose, getRef, hide } from './datastructures/data.js';
import {
ChoiceTree,
ChoiceTreeNode,
Expand Down Expand Up @@ -28,6 +28,7 @@ export type Term =
| bigint // Natural numbers and integers
| string // Strings
| boolean
| { name: null; value: number } // JSON refs
| { name: string } // Constants
| { name: string; args: [Term, ...Term[]] };
export interface Fact {
Expand All @@ -41,6 +42,7 @@ export type InputTerm =
| boolean
| bigint
| string
| { name: null; value: number }
| { name: string; args?: InputTerm[] };
export interface InputFact {
name: string;
Expand All @@ -56,12 +58,15 @@ export class DusaError extends Error {
}
}

export type JsonData = null | number | bigint | string | JsonData[] | { [field: string]: JsonData };

function dataToTerm(d: Data): Term {
const view = expose(d);
if (view.type === 'triv') return null;
if (view.type === 'int') return view.value;
if (view.type === 'bool') return view.value;
if (view.type === 'string') return view.value;
if (view.type === 'ref') return { name: null, value: view.value };
if (view.args.length === 0) return { name: view.name };
const args = view.args.map(dataToTerm) as [Term, ...Term[]];
return { name: view.name, args };
Expand All @@ -73,11 +78,40 @@ function termToData(tm: InputTerm): Data {
if (typeof tm === 'string') return hide({ type: 'string', value: tm });
if (typeof tm === 'bigint') return hide({ type: 'int', value: tm });
if (typeof tm === 'object') {
if (tm.name === null) return hide({ type: 'ref', value: tm.value });
return hide({ type: 'const', name: tm.name, args: tm.args?.map(termToData) ?? [] });
}
return hide({ type: 'int', value: BigInt(tm) });
}

function loadJson(json: JsonData, facts: [Data, Data, Data][]): Data {
if (
json === null ||
typeof json === 'number' ||
typeof json === 'string' ||
typeof json === 'bigint'
) {
return termToData(json);
}
const ref = getRef();
if (Array.isArray(json)) {
for (const [index, value] of json.entries()) {
const dataValue = loadJson(value, facts);
facts.push([ref, hide({ type: 'int', value: BigInt(index) }), dataValue]);
}
} else if (typeof json === 'object') {
for (const [field, value] of Object.entries(json)) {
const dataValue = loadJson(value, facts);
facts.push([ref, hide({ type: 'string', value: field }), dataValue]);
}
} else {
throw new DusaError([
{ type: 'Issue', msg: `Could not load ${typeof json} as JSON data triples` },
]);
}
return ref;
}

export class DusaSolution {
private db: Database;
constructor(db: Database) {
Expand Down Expand Up @@ -136,6 +170,7 @@ function* solutionGenerator(
export class Dusa {
private program: IndexedProgram;
private debug: boolean;
private arities: Map<string, number>;
private db: Database;
private stats: Stats;
private cachedSolution: DusaSolution | null = null;
Expand All @@ -146,21 +181,51 @@ export class Dusa {
throw new DusaError(parsed.errors);
}

const errors = check(parsed.document);
const { errors, arities } = check(parsed.document);
if (errors.length !== 0) {
throw new DusaError(errors);
}

this.debug = debug;
this.arities = arities;
this.program = compile(parsed.document, debug);
this.db = makeInitialDb(this.program);
this.stats = { cycles: 0, deadEnds: 0 };
}

private checkPredicateForm(pred: string, arity: number) {
const expected = this.arities.get(pred);
if (!pred.match(/^[a-z][A-Za-z0-9]*$/)) {
throw new DusaError([
{
type: 'Issue',
msg: `Asserted predicates must start with a lowercase letter and include only alphanumeric characters, '${pred}' does not.`,
},
]);
}
if (expected === undefined) {
this.arities.set(pred, arity);
} else if (arity !== expected) {
throw new DusaError([
{
type: 'Issue',
msg: `Predicate ${pred} should have ${expected} argument${
expected === 1 ? '' : 's'
}, but the asserted fact has ${arity}`,
},
]);
}
}

/**
* Add new facts to the database. These will affect the results of any
* subsequent solutions.
*/
assert(...facts: InputFact[]) {
this.cachedSolution = null;
this.db = { ...this.db };
for (const { name, args, value } of facts) {
this.checkPredicateForm(name, args.length);
insertFact(
name,
args.map(termToData),
Expand All @@ -170,6 +235,28 @@ export class Dusa {
}
}

/**
* Insert the structure of a JSON object into the database. If no two-place
* predicate is provided, these facts will be added with the special built-in
* predicate `->`, which is represented with (left-associative) infix notation
* in Dusa.
*/
load(json: JsonData, pred?: string): Term {
this.cachedSolution = null;
this.db = { ...this.db };

if (pred !== undefined) {
this.checkPredicateForm(pred, 2);
}
const usedPred = pred ?? '->';
const triples: [Data, Data, Data][] = [];
const rep = loadJson(json, triples);
for (const [obj, key, value] of triples) {
insertFact(usedPred, [obj, key], value, this.db);
}
return dataToTerm(rep);
}

get solutions(): IterableIterator<DusaSolution> {
return solutionGenerator(this.program, this.db, this.stats, this.debug);
}
Expand Down
19 changes: 17 additions & 2 deletions src/datastructures/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export type DataView =
| { type: 'int'; value: bigint }
| { type: 'bool'; value: boolean }
| { type: 'string'; value: string }
| { type: 'const'; name: string; args: Data[] };
| { type: 'const'; name: string; args: Data[] }
| { type: 'ref'; value: number };

type ViewsIndex = number;
let nextRef: number = -1;
const views: DataView[] = [
{ type: 'triv' },
{ type: 'bool', value: true },
Expand All @@ -20,7 +22,9 @@ export const BOOL_FALSE = 2;

export function expose(d: Data): DataView {
if (typeof d === 'bigint') return { type: 'int', value: d };
if (d < 0 || d >= views.length) throw new Error(`Internalized value ${d} invalid`);
if (d <= nextRef) throw new Error(`Internalized ref ${-d} too small.`);
if (d < 0) return { type: 'ref', value: -d };
if (d >= views.length) throw new Error(`Internalized value ${d} invalid`);
return views[d];
}

Expand Down Expand Up @@ -75,6 +79,11 @@ export function hide(d: DataView): Data {
return d.value;
case 'bool':
return d.value ? 1 : 2;
case 'ref':
if (-d.value <= nextRef || d.value >= 0) {
throw new Error(`Ref value is invalid`);
}
return -d.value;
case 'string': {
const candidate = strings[d.value];
if (candidate) return candidate;
Expand Down Expand Up @@ -103,6 +112,8 @@ export function dataToString(d: Data, needsParens = true): string {
return `${view.value}`;
case 'bool':
return `#${view.value ? 'tt' : 'ff'}`;
case 'ref':
return `#${view.value}`;
case 'string':
return `"${view.value}"`;
case 'const':
Expand All @@ -113,3 +124,7 @@ export function dataToString(d: Data, needsParens = true): string {
: `${view.name} ${view.args.map((arg) => dataToString(arg)).join(' ')}`;
}
}

export function getRef(): Data {
return nextRef--;
}
2 changes: 1 addition & 1 deletion src/engine/choiceengine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function testExecution(source: string, debug = false) {
throw parsed.errors;
}

const errors = check(parsed.document);
const { errors } = check(parsed.document);
if (errors.length !== 0) {
throw errors;
}
Expand Down
4 changes: 3 additions & 1 deletion src/engine/forwardengine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ Facts known:
${[...db.factValues.entries()]
.map(({ name, keys, value }) =>
value.type === 'is'
? `${name}${keys.map((arg) => ` ${dataToString(arg)}`)} is ${dataToString(value.value)}\n`
? `${name}${keys.map((arg) => ` ${dataToString(arg)}`).join('')} is ${dataToString(
value.value,
)}\n`
: `${name}${keys.map(
(arg) =>
` ${dataToString(arg)} is none of ${value.value
Expand Down
7 changes: 5 additions & 2 deletions src/language/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ export function checkFunctionalPredicatesInDecl(
return issues;
}

export function check(decls: ParsedDeclaration[]): Issue[] {
export function check(decls: ParsedDeclaration[]): {
errors: Issue[];
arities: Map<string, number>;
} {
const arityInfo = checkPropositionArity(decls);
const errors: Issue[] = arityInfo.issues || [];
for (const decl of decls) {
Expand All @@ -336,5 +339,5 @@ export function check(decls: ParsedDeclaration[]): Issue[] {
errors.push(...declErrors);
}

return errors;
return { errors, arities: new Map(Object.entries(arityInfo)) };
}
2 changes: 1 addition & 1 deletion src/web/codemirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function dusaLinter(view: EditorView): readonly Diagnostic[] {
if (parsedIssues.length > 0) {
return issueToDiagnostic(parsedIssues);
}
const errors = check(parsedDecls);
const { errors } = check(parsedDecls);
return issueToDiagnostic(errors);
}

Expand Down
2 changes: 1 addition & 1 deletion src/web/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ class SessionTabs {
if (ast.errors !== null) {
issues = ast.errors;
} else {
issues = check(ast.document);
issues = check(ast.document).errors;
decls = ast.document;
}

Expand Down

0 comments on commit 10476a3

Please sign in to comment.