@mark1russell7/coeval 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/build/index.d.ts +832 -148
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1898 -376
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +25 -21
- package/src/deref.ts +165 -54
- package/src/doc.ts +86 -17
- package/src/errors.ts +141 -0
- package/src/evaluate.ts +797 -281
- package/src/formula.ts +409 -0
- package/src/index.ts +72 -7
- package/src/parts.ts +140 -27
- package/src/static.ts +364 -0
- package/src/stdlib.ts +60 -33
- package/src/world.ts +96 -22
package/src/doc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A JSON value. */
|
|
1
|
+
/** A JSON value. A document is JSON. */
|
|
2
2
|
export type Json = null | boolean | number | string | readonly Json[] | { readonly [key: string]: Json };
|
|
3
3
|
|
|
4
4
|
/** A JSON record. */
|
|
@@ -38,13 +38,16 @@ export type Envelope = {
|
|
|
38
38
|
export const ENVELOPE_KEYS = ["type", "value", "name", "own"] as const;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* This function tells if a value is a
|
|
41
|
+
* This function tells if a value is a plain record: an object with the prototype of a plain object, or with no
|
|
42
|
+
* prototype. A list, `null`, a date, a map and an instance of a class are not plain records.
|
|
42
43
|
*
|
|
43
44
|
* @param value - The value to examine.
|
|
44
|
-
* @returns `true` for a record.
|
|
45
|
+
* @returns `true` for a plain record.
|
|
45
46
|
*/
|
|
46
|
-
export function isRecord(value:
|
|
47
|
-
|
|
47
|
+
export function isRecord(value: unknown): value is JsonRecord {
|
|
48
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
49
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
50
|
+
return proto === Object.prototype || proto === null;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -64,7 +67,7 @@ export function field(record: JsonRecord, key: string): Json | undefined {
|
|
|
64
67
|
* @param value - The value to examine.
|
|
65
68
|
* @returns `true` for a key.
|
|
66
69
|
*/
|
|
67
|
-
export function isKey(value:
|
|
70
|
+
export function isKey(value: unknown): value is Key {
|
|
68
71
|
return typeof value === "string" || (typeof value === "number" && Number.isInteger(value) && value >= 0);
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -75,9 +78,9 @@ export function isKey(value: Json | undefined): value is Key {
|
|
|
75
78
|
* @param value - The value to examine.
|
|
76
79
|
* @returns `true` for a reference to a type.
|
|
77
80
|
*/
|
|
78
|
-
export function isTypeRef(value:
|
|
81
|
+
export function isTypeRef(value: unknown): value is TypeRef {
|
|
79
82
|
if (typeof value === "string") return value !== "";
|
|
80
|
-
if (Array.isArray(value)) return value.length > 0 && (value as readonly
|
|
83
|
+
if (Array.isArray(value)) return value.length > 0 && (value as readonly unknown[]).every(isKey);
|
|
81
84
|
if (!isRecord(value)) return false;
|
|
82
85
|
const path = field(value, "value");
|
|
83
86
|
return field(value, "type") === "ref" && Array.isArray(path) && (path as readonly Json[])[0] === "world";
|
|
@@ -106,6 +109,14 @@ export function typeName(type: TypeRef): string {
|
|
|
106
109
|
return typePath(type)?.join(".") ?? "(computed)";
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
function headerFits(value: JsonRecord): boolean {
|
|
113
|
+
if (!isTypeRef(field(value, "type")) || !Object.hasOwn(value, "value")) return false;
|
|
114
|
+
const name = field(value, "name");
|
|
115
|
+
if (name !== undefined && typeof name !== "string") return false;
|
|
116
|
+
const own = field(value, "own");
|
|
117
|
+
return own === undefined || isRecord(own);
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
/**
|
|
110
121
|
* This function tells if a value has the form of an envelope. That form is a record with a reference to a type
|
|
111
122
|
* and a `value`. It has no other keys than the keys of an envelope. A contract decides where an envelope is
|
|
@@ -114,14 +125,38 @@ export function typeName(type: TypeRef): string {
|
|
|
114
125
|
* @param value - The value to examine.
|
|
115
126
|
* @returns `true` when the value has the form of an envelope.
|
|
116
127
|
*/
|
|
117
|
-
export function isEnvelope(value:
|
|
118
|
-
if (!isRecord(value)) return false;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
export function isEnvelope(value: unknown): value is JsonRecord & Envelope {
|
|
129
|
+
if (!isRecord(value) || !headerFits(value)) return false;
|
|
130
|
+
for (const key in value) if (Object.hasOwn(value, key) && !(ENVELOPE_KEYS as readonly string[]).includes(key)) return false;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A stored document: an envelope, and the keys of its store beside it, for example `_id`. */
|
|
135
|
+
export interface Stored {
|
|
136
|
+
readonly envelope: Envelope;
|
|
137
|
+
/** The keys of the store, with their values. */
|
|
138
|
+
readonly header: Readonly<Record<string, Json>>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* This function reads a stored document: an envelope that also has the keys of a store, for example `{ _id, type,
|
|
143
|
+
* value }`. Only the keys in the list can be beside the keys of the envelope.
|
|
144
|
+
*
|
|
145
|
+
* @param value - The value to examine.
|
|
146
|
+
* @param header - The keys of the store that the document can have.
|
|
147
|
+
* @returns The envelope and the keys of the store, or `undefined` when the value has another form.
|
|
148
|
+
*/
|
|
149
|
+
export function stored(value: unknown, header: readonly string[]): Stored | undefined {
|
|
150
|
+
if (!isRecord(value) || !headerFits(value)) return undefined;
|
|
151
|
+
const keys: Record<string, Json> = {};
|
|
152
|
+
const own: Record<string, Json> = {};
|
|
153
|
+
for (const key in value) {
|
|
154
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
155
|
+
if ((ENVELOPE_KEYS as readonly string[]).includes(key)) own[key] = value[key] as Json;
|
|
156
|
+
else if (header.includes(key)) keys[key] = value[key] as Json;
|
|
157
|
+
else return undefined;
|
|
158
|
+
}
|
|
159
|
+
return { envelope: fromJson(own) as Envelope, header: keys };
|
|
125
160
|
}
|
|
126
161
|
|
|
127
162
|
/**
|
|
@@ -167,7 +202,7 @@ export function toJson(doc: Envelope): JsonRecord {
|
|
|
167
202
|
* @param value - The JSON value.
|
|
168
203
|
* @returns The envelope, or `undefined`.
|
|
169
204
|
*/
|
|
170
|
-
export function fromJson(value:
|
|
205
|
+
export function fromJson(value: unknown): Envelope | undefined {
|
|
171
206
|
if (!isEnvelope(value)) return undefined;
|
|
172
207
|
const own = field(value, "own");
|
|
173
208
|
const ownEntries: Record<string, string> = {};
|
|
@@ -180,3 +215,37 @@ export function fromJson(value: Json | undefined): Envelope | undefined {
|
|
|
180
215
|
...(isRecord(own) ? { own: ownEntries } : {}),
|
|
181
216
|
});
|
|
182
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* This function tells if two JSON values are equal. Equal values have the same texts, numbers and truth values,
|
|
221
|
+
* the same items in the same order, and the same fields. The order of the fields does not matter. It does not use the stack for the
|
|
222
|
+
* depth, thus a deep document cannot overflow it.
|
|
223
|
+
*
|
|
224
|
+
* @param a - The first value.
|
|
225
|
+
* @param b - The second value.
|
|
226
|
+
* @returns `true` when the values are equal.
|
|
227
|
+
*/
|
|
228
|
+
export function docEquals(a: unknown, b: unknown): boolean {
|
|
229
|
+
const stack: [unknown, unknown][] = [[a, b]];
|
|
230
|
+
while (stack.length > 0) {
|
|
231
|
+
const [x, y] = stack.pop() as [unknown, unknown];
|
|
232
|
+
if (Object.is(x, y)) continue;
|
|
233
|
+
if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) return false;
|
|
234
|
+
if (Array.isArray(x) !== Array.isArray(y)) return false;
|
|
235
|
+
if (Array.isArray(x)) {
|
|
236
|
+
const ys = y as readonly unknown[];
|
|
237
|
+
if (x.length !== ys.length) return false;
|
|
238
|
+
for (let index = 0; index < x.length; index += 1) stack.push([x[index], ys[index]]);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const xr = x as Readonly<Record<string, unknown>>;
|
|
242
|
+
const yr = y as Readonly<Record<string, unknown>>;
|
|
243
|
+
const keys = Object.keys(xr);
|
|
244
|
+
if (keys.length !== Object.keys(yr).length) return false;
|
|
245
|
+
for (const key of keys) {
|
|
246
|
+
if (!Object.hasOwn(yr, key)) return false;
|
|
247
|
+
stack.push([xr[key], yr[key]]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Key } from "./doc.ts";
|
|
2
|
+
|
|
3
|
+
/** The codes that the kernel gives. */
|
|
4
|
+
export type KernelCode =
|
|
5
|
+
| "#KIND?"
|
|
6
|
+
| "#NOEVAL"
|
|
7
|
+
| "#PART?"
|
|
8
|
+
| "#NAME?"
|
|
9
|
+
| "#REF!"
|
|
10
|
+
| "#N/A"
|
|
11
|
+
| "#CYCLE!"
|
|
12
|
+
| "#STEP?"
|
|
13
|
+
| "#VALUE!"
|
|
14
|
+
| "#NUM!"
|
|
15
|
+
| "#ARGS"
|
|
16
|
+
| "#CAP!"
|
|
17
|
+
| "#CALC!"
|
|
18
|
+
| "#DEPTH!"
|
|
19
|
+
| "#ABORT";
|
|
20
|
+
|
|
21
|
+
/** The code of an error value. A language can add its own codes. Each code starts with `#`. */
|
|
22
|
+
export type ErrorCode = KernelCode | `#${string}`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An error value: a code and a reason. An evaluation gives an error value, and it does not throw.
|
|
26
|
+
*
|
|
27
|
+
* - `place`: the keys from the root document to the document that failed.
|
|
28
|
+
* - `cause`: the error of the host that caused this error, for example the exception of an effect.
|
|
29
|
+
*/
|
|
30
|
+
export interface EvalError {
|
|
31
|
+
readonly code: ErrorCode;
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
readonly place?: readonly Key[];
|
|
34
|
+
readonly cause?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The result of an evaluation: a value, or an error value. A value can be any value of the host. */
|
|
38
|
+
export type Result<V = unknown> = { readonly ok: true; readonly value: V } | { readonly ok: false; readonly error: EvalError };
|
|
39
|
+
|
|
40
|
+
/** A result with an error value. */
|
|
41
|
+
export type Failed = { readonly ok: false; readonly error: EvalError };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function makes a result with a value.
|
|
45
|
+
*
|
|
46
|
+
* @param value - The value.
|
|
47
|
+
* @returns The result.
|
|
48
|
+
*/
|
|
49
|
+
export function ok<V>(value: V): Result<V> {
|
|
50
|
+
return { ok: true, value };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function makes a result with an error value.
|
|
55
|
+
*
|
|
56
|
+
* @param code - The code of the error.
|
|
57
|
+
* @param reason - One sentence that tells the cause.
|
|
58
|
+
* @param more - The place and the cause of the error, when they are known.
|
|
59
|
+
* @returns The result.
|
|
60
|
+
*/
|
|
61
|
+
export function fail(code: ErrorCode, reason: string, more: { readonly place?: readonly Key[]; readonly cause?: unknown } = {}): Failed {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: { code, reason, ...(more.place === undefined ? {} : { place: more.place }), ...(more.cause === undefined ? {} : { cause: more.cause }) },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function gives a failed result its place, when it has none. A result with a value, or an error with a
|
|
70
|
+
* place, stays the same object.
|
|
71
|
+
*
|
|
72
|
+
* @param result - The result.
|
|
73
|
+
* @param place - The keys from the root document to the document that failed.
|
|
74
|
+
* @returns The result with a place.
|
|
75
|
+
*/
|
|
76
|
+
export function placed<V>(result: Result<V>, place: readonly Key[]): Result<V> {
|
|
77
|
+
if (result.ok || result.error.place !== undefined) return result;
|
|
78
|
+
return { ok: false, error: { ...result.error, place } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* An exception that carries an error value through code of the host. A closure of a formula throws it when the
|
|
83
|
+
* host calls the closure and the closure fails. The kernel catches it and gives its error value again.
|
|
84
|
+
*/
|
|
85
|
+
export class Failure extends Error {
|
|
86
|
+
readonly error: EvalError;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* This constructor makes the exception.
|
|
90
|
+
*
|
|
91
|
+
* @param error - The error value that the exception carries.
|
|
92
|
+
*/
|
|
93
|
+
constructor(error: EvalError) {
|
|
94
|
+
super(`${error.code} ${error.reason}`);
|
|
95
|
+
this.name = "Failure";
|
|
96
|
+
this.error = error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* This function tells if an exception is the overflow of the stack of the host.
|
|
102
|
+
*
|
|
103
|
+
* @param error - The exception.
|
|
104
|
+
* @returns `true` for a stack overflow.
|
|
105
|
+
*/
|
|
106
|
+
export function isStackOverflow(error: unknown): boolean {
|
|
107
|
+
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This function changes an exception into an error value. A `Failure` gives its error value. A stack overflow
|
|
112
|
+
* gives `#DEPTH!`. Each other exception gives `#CALC!`, with the exception as the cause.
|
|
113
|
+
*
|
|
114
|
+
* @param error - The exception.
|
|
115
|
+
* @param who - A short text that names the code that threw, for example `the evaluator math.add`.
|
|
116
|
+
* @returns The failed result.
|
|
117
|
+
*/
|
|
118
|
+
export function thrown(error: unknown, who: string): Failed {
|
|
119
|
+
if (error instanceof Failure) return { ok: false, error: error.error };
|
|
120
|
+
if (isStackOverflow(error)) return fail("#DEPTH!", `The nesting of ${who} is too deep for the stack of the host.`, { cause: error });
|
|
121
|
+
return fail("#CALC!", `${upper(who)} threw: ${describe(error)}`, { cause: error });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function upper(text: string): string {
|
|
125
|
+
return text.length === 0 ? text : `${text[0]?.toUpperCase() ?? ""}${text.slice(1)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* This function gives a short text for an exception or any value. It does not throw.
|
|
130
|
+
*
|
|
131
|
+
* @param value - The value.
|
|
132
|
+
* @returns The text.
|
|
133
|
+
*/
|
|
134
|
+
export function describe(value: unknown): string {
|
|
135
|
+
try {
|
|
136
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
137
|
+
return String(value);
|
|
138
|
+
} catch {
|
|
139
|
+
return "(a value without a text)";
|
|
140
|
+
}
|
|
141
|
+
}
|