@mark1russell7/coeval 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +40 -2
- package/build/index.d.ts +1600 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3378 -0
- package/build/index.js.map +1 -0
- package/package.json +49 -3
- package/src/classify.ts +54 -0
- package/src/context.ts +193 -0
- package/src/deref.ts +401 -0
- package/src/doc.ts +251 -0
- package/src/errors.ts +141 -0
- package/src/evaluate.ts +1069 -0
- package/src/formula.ts +409 -0
- package/src/index.ts +183 -0
- package/src/ladder.ts +141 -0
- package/src/order.ts +111 -0
- package/src/parts.ts +156 -0
- package/src/static.ts +364 -0
- package/src/stdlib.ts +310 -0
- package/src/world.ts +237 -0
package/src/doc.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/** A JSON value. A document is JSON. */
|
|
2
|
+
export type Json = null | boolean | number | string | readonly Json[] | { readonly [key: string]: Json };
|
|
3
|
+
|
|
4
|
+
/** A JSON record. */
|
|
5
|
+
export type JsonRecord = { readonly [key: string]: Json };
|
|
6
|
+
|
|
7
|
+
/** One step of a path that is a plain key: a name, or an index of a list. */
|
|
8
|
+
export type Key = string | number;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The type of a document is a reference into the world (the registry of kinds).
|
|
12
|
+
*
|
|
13
|
+
* - A text is a base reference with one key: `"add"`.
|
|
14
|
+
* - A list of keys is a base reference with a path: `["git", "push"]`.
|
|
15
|
+
* - A document of type `ref` is the same reference, expanded. Its path starts at the root `world`.
|
|
16
|
+
*
|
|
17
|
+
* The kernel resolves a base reference itself. The expanded form is necessary only to attach more to the
|
|
18
|
+
* reference. Thus the chain of types stops at a base reference.
|
|
19
|
+
*/
|
|
20
|
+
export type TypeRef = string | readonly Key[] | { readonly type: "ref"; readonly value: readonly Json[] };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A typed document: an envelope with a header and a value. The header names the type of the value. The value is
|
|
24
|
+
* not changed by the envelope at any time. Thus any JSON value can have a type: a number, a list, a text or a record.
|
|
25
|
+
*/
|
|
26
|
+
export type Envelope = {
|
|
27
|
+
/** The type: a reference to a kind of the world. */
|
|
28
|
+
readonly type: TypeRef;
|
|
29
|
+
/** The value. */
|
|
30
|
+
readonly value: Json;
|
|
31
|
+
/** The name of a binding. A sequence gives the result of this document to the later steps under this name. */
|
|
32
|
+
readonly name?: string;
|
|
33
|
+
/** The own entries: one evaluator name for each verb. An own entry changes only this document. */
|
|
34
|
+
readonly own?: Readonly<Record<string, string>>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The keys that an envelope can have. */
|
|
38
|
+
export const ENVELOPE_KEYS = ["type", "value", "name", "own"] as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
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.
|
|
43
|
+
*
|
|
44
|
+
* @param value - The value to examine.
|
|
45
|
+
* @returns `true` for a plain record.
|
|
46
|
+
*/
|
|
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;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function reads a field of a record. It reads only the own fields, thus `constructor` is not a field.
|
|
55
|
+
*
|
|
56
|
+
* @param record - The record.
|
|
57
|
+
* @param key - The name of the field.
|
|
58
|
+
* @returns The value of the field, or `undefined` when the record has no such field.
|
|
59
|
+
*/
|
|
60
|
+
export function field(record: JsonRecord, key: string): Json | undefined {
|
|
61
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This function tells if a value is a key of a path: a text, or a whole number from 0.
|
|
66
|
+
*
|
|
67
|
+
* @param value - The value to examine.
|
|
68
|
+
* @returns `true` for a key.
|
|
69
|
+
*/
|
|
70
|
+
export function isKey(value: unknown): value is Key {
|
|
71
|
+
return typeof value === "string" || (typeof value === "number" && Number.isInteger(value) && value >= 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* This function tells if a value is a reference to a type. The reference is a text, a list of keys, or a document
|
|
76
|
+
* of type `ref` whose path starts at the root `world`.
|
|
77
|
+
*
|
|
78
|
+
* @param value - The value to examine.
|
|
79
|
+
* @returns `true` for a reference to a type.
|
|
80
|
+
*/
|
|
81
|
+
export function isTypeRef(value: unknown): value is TypeRef {
|
|
82
|
+
if (typeof value === "string") return value !== "";
|
|
83
|
+
if (Array.isArray(value)) return value.length > 0 && (value as readonly unknown[]).every(isKey);
|
|
84
|
+
if (!isRecord(value)) return false;
|
|
85
|
+
const path = field(value, "value");
|
|
86
|
+
return field(value, "type") === "ref" && Array.isArray(path) && (path as readonly Json[])[0] === "world";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function gives the path of a reference to a type, from the root of the world.
|
|
91
|
+
*
|
|
92
|
+
* @param type - The reference.
|
|
93
|
+
* @returns The keys from the root of the world, or `undefined` for a computed step that has no fixed key.
|
|
94
|
+
*/
|
|
95
|
+
export function typePath(type: TypeRef): readonly Key[] | undefined {
|
|
96
|
+
if (typeof type === "string") return [type];
|
|
97
|
+
if (Array.isArray(type)) return type as readonly Key[];
|
|
98
|
+
const steps = (type as { readonly value: readonly Json[] }).value.slice(1);
|
|
99
|
+
return steps.every(isKey) ? (steps as readonly Key[]) : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the text of a type for a reader: its keys, joined with a dot.
|
|
104
|
+
*
|
|
105
|
+
* @param type - The reference to the type.
|
|
106
|
+
* @returns The text, for example `git.push`.
|
|
107
|
+
*/
|
|
108
|
+
export function typeName(type: TypeRef): string {
|
|
109
|
+
return typePath(type)?.join(".") ?? "(computed)";
|
|
110
|
+
}
|
|
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
|
+
|
|
120
|
+
/**
|
|
121
|
+
* This function tells if a value has the form of an envelope. That form is a record with a reference to a type
|
|
122
|
+
* and a `value`. It has no other keys than the keys of an envelope. A contract decides where an envelope is
|
|
123
|
+
* expected. This function only checks the form.
|
|
124
|
+
*
|
|
125
|
+
* @param value - The value to examine.
|
|
126
|
+
* @returns `true` when the value has the form of an envelope.
|
|
127
|
+
*/
|
|
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 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* This function makes an envelope.
|
|
164
|
+
*
|
|
165
|
+
* @param type - The reference to the kind.
|
|
166
|
+
* @param value - The value.
|
|
167
|
+
* @param header - The optional name and own entries.
|
|
168
|
+
* @returns The envelope.
|
|
169
|
+
*/
|
|
170
|
+
export function typed(type: TypeRef, value: Json, header: { readonly name?: string; readonly own?: Record<string, string> } = {}): Envelope {
|
|
171
|
+
return { type, value, ...(header.name === undefined ? {} : { name: header.name }), ...(header.own === undefined ? {} : { own: header.own }) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* This function makes a reference document: a path from a root of the context.
|
|
176
|
+
*
|
|
177
|
+
* @param path - The steps. The first step is a root: `self`, `names`, `world`, `event` or `caps`.
|
|
178
|
+
* @returns The document of type `ref`.
|
|
179
|
+
*/
|
|
180
|
+
export function ref(...path: readonly Json[]): Envelope {
|
|
181
|
+
return typed("ref", path);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* This function gives an envelope as a JSON record, for a store or for a view.
|
|
186
|
+
*
|
|
187
|
+
* @param doc - The envelope.
|
|
188
|
+
* @returns The same envelope as JSON.
|
|
189
|
+
*/
|
|
190
|
+
export function toJson(doc: Envelope): JsonRecord {
|
|
191
|
+
return {
|
|
192
|
+
type: doc.type as Json,
|
|
193
|
+
value: doc.value,
|
|
194
|
+
...(doc.name === undefined ? {} : { name: doc.name }),
|
|
195
|
+
...(doc.own === undefined ? {} : { own: doc.own }),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* This function reads an envelope from JSON. A value without the form of an envelope gives `undefined`.
|
|
201
|
+
*
|
|
202
|
+
* @param value - The JSON value.
|
|
203
|
+
* @returns The envelope, or `undefined`.
|
|
204
|
+
*/
|
|
205
|
+
export function fromJson(value: unknown): Envelope | undefined {
|
|
206
|
+
if (!isEnvelope(value)) return undefined;
|
|
207
|
+
const own = field(value, "own");
|
|
208
|
+
const ownEntries: Record<string, string> = {};
|
|
209
|
+
if (isRecord(own)) {
|
|
210
|
+
for (const [verb, name] of Object.entries(own)) if (typeof name === "string") ownEntries[verb] = name;
|
|
211
|
+
}
|
|
212
|
+
const name = field(value, "name");
|
|
213
|
+
return typed(field(value, "type") as TypeRef, field(value, "value") ?? null, {
|
|
214
|
+
...(typeof name === "string" ? { name } : {}),
|
|
215
|
+
...(isRecord(own) ? { own: ownEntries } : {}),
|
|
216
|
+
});
|
|
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
|
+
}
|