@mark1russell7/coeval 0.8.0 → 0.10.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 +2 -2
- package/build/index.d.ts +100 -16
- package/build/index.d.ts.map +1 -1
- package/build/index.js +295 -47
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/conform.ts +15 -7
- package/src/evaluate.ts +2 -0
- package/src/facts.ts +17 -3
- package/src/formula.ts +151 -19
- package/src/index.ts +4 -0
- package/src/providers.ts +4 -3
- package/src/reference.ts +7 -4
- package/src/static.ts +103 -17
- package/src/stdlib.ts +20 -7
- package/src/types.ts +4 -2
- package/src/world.ts +35 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/coeval",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./build/index.js",
|
package/src/conform.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
|
|
|
32
32
|
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
33
33
|
import { sameValue } from "./agree.ts";
|
|
34
34
|
import { type Random, random } from "./generate.ts";
|
|
35
|
-
import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
35
|
+
import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
36
36
|
|
|
37
37
|
/*
|
|
38
38
|
* The conformance tests of one evaluator (K92, R7 P3, D36). `conforms` calls the code of one evaluator or runner
|
|
@@ -85,8 +85,9 @@ export interface Finding {
|
|
|
85
85
|
/** The options of `conforms`. */
|
|
86
86
|
export interface ConformOptions {
|
|
87
87
|
/**
|
|
88
|
-
* The verb that the evaluator serves. The default is `eval`. The facts
|
|
89
|
-
*
|
|
88
|
+
* The verb that the evaluator serves. The default is `eval`. The facts `pure` and `lazy` of a contract describe only
|
|
89
|
+
* `eval`, thus `conforms` checks C3 and C4 only for `eval` (D23). `conforms` checks C2 for each verb that the
|
|
90
|
+
* contract declares effects for: `eval`, and each verb with an entry in `verbEffects` (D49).
|
|
90
91
|
*/
|
|
91
92
|
readonly verb?: string;
|
|
92
93
|
/**
|
|
@@ -276,6 +277,8 @@ interface Contract {
|
|
|
276
277
|
readonly lazy: readonly LazyRule[] | undefined;
|
|
277
278
|
/** `true` when the facts describe the verb: they describe only `eval`. */
|
|
278
279
|
readonly describes: boolean;
|
|
280
|
+
/** `true` when the contract declares the effects of the verb: for `eval`, and for a verb in `verbEffects`. */
|
|
281
|
+
readonly declares: boolean;
|
|
279
282
|
readonly caps: readonly Cap[];
|
|
280
283
|
}
|
|
281
284
|
|
|
@@ -487,13 +490,16 @@ function typesNaming(world: World, name: string, verb: string): readonly TypeRef
|
|
|
487
490
|
/** This function gives the facts of the kind of a type. The hook `facts` of the machine wins over the contract. */
|
|
488
491
|
function contractOf(env: Env, kind: Kind, typePath: readonly Key[], caps: readonly Cap[] | undefined): Contract {
|
|
489
492
|
const facts = env.machine.facts?.(typePath, env.name);
|
|
490
|
-
|
|
493
|
+
// A verb without a declaration keeps the effects of eval for the capabilities of the probes, as before D49.
|
|
494
|
+
const own = effectsOfVerb(kind, env.verb);
|
|
495
|
+
const effects = own ?? kind.effects;
|
|
491
496
|
const declared: readonly Cap[] = effects === undefined ? [] : effects === "type" ? [typePath] : effects;
|
|
492
497
|
return {
|
|
493
498
|
declared,
|
|
494
499
|
pure: (facts?.pure ?? kind.pure) === true,
|
|
495
500
|
lazy: facts?.lazy ?? kind.lazy,
|
|
496
501
|
describes: env.verb === "eval",
|
|
502
|
+
declares: own !== undefined,
|
|
497
503
|
caps: caps ?? [...declared, "probe"],
|
|
498
504
|
};
|
|
499
505
|
}
|
|
@@ -872,13 +878,15 @@ function checkShot(env: Env, sample: Sample, contract: Contract, scenario: Scena
|
|
|
872
878
|
report(env, "C1", `The code gives its effect ${effect.kind} ${capsText(effect.context.caps)}, and its document has ${capsText(docCaps)}.`, probe);
|
|
873
879
|
}
|
|
874
880
|
}
|
|
875
|
-
if (contract.
|
|
876
|
-
// C2: only the declared effects.
|
|
881
|
+
if (contract.declares) {
|
|
882
|
+
// C2: only the declared effects of the verb (VERB.EFFECTS).
|
|
877
883
|
for (const effect of shot.caused) {
|
|
878
884
|
if (!permits(contract.declared, effect.kind)) {
|
|
879
|
-
report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}.`, probe);
|
|
885
|
+
report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}${env.verb === "eval" ? "" : ` for the verb ${env.verb}`}.`, probe);
|
|
880
886
|
}
|
|
881
887
|
}
|
|
888
|
+
}
|
|
889
|
+
if (contract.describes) {
|
|
882
890
|
// C4: a pure kind causes no effect.
|
|
883
891
|
const first = shot.caused[0];
|
|
884
892
|
if (contract.pure && first !== undefined) report(env, "C4", `The kind ${sample.type} is pure, and the code causes the effect ${first.kind}.`, probe);
|
package/src/evaluate.ts
CHANGED
|
@@ -913,6 +913,8 @@ class Session implements Runtime {
|
|
|
913
913
|
const free = isFree(reading.root, ctx, path);
|
|
914
914
|
// A kind that sets another self can give the old self on, as the parent of a node of a tree does (READS.KEPT).
|
|
915
915
|
if (!free && path[0] === "self" && !reading.self) this.keep(reading, ["self"]);
|
|
916
|
+
// A free read of the whole root self already holds what a kept read of self holds, as the root of a tree reads it.
|
|
917
|
+
else if (free && path.length === 1 && path[0] === "self") reading.self = true;
|
|
916
918
|
let found: Resolution;
|
|
917
919
|
try {
|
|
918
920
|
found = this.find(ctx, path, compiled, hops);
|
package/src/facts.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { type DocVisit, declaredEffects, walk } from "./static.ts";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* One read of a document: a free reference, with its marks (FACTS.READS). A plan resolves the path of the reference
|
|
11
|
-
* when it evaluates the reference. The static read covers each path that the plan resolves there (FACTS.SOUND).
|
|
11
|
+
* when it evaluates the reference. The static read covers each path that the plan resolves there (FACTS.SOUND). A
|
|
12
|
+
* root that a kind declares in `reads` is a read too, with the path of the root alone.
|
|
12
13
|
*/
|
|
13
14
|
export interface StaticRead {
|
|
14
15
|
/** The root of the path. */
|
|
@@ -34,7 +35,10 @@ export interface StaticRead {
|
|
|
34
35
|
|
|
35
36
|
/** The static facts of one document (D24). They depend only on the document and the machine, not on a context. */
|
|
36
37
|
export interface DocFacts {
|
|
37
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The free references, in the order of the document (FACTS.READS). A reference to a name that a kind binds is not
|
|
40
|
+
* here. The roots that a kind declares in `reads` are here too, each one at the place of its document.
|
|
41
|
+
*/
|
|
38
42
|
readonly reads: readonly StaticRead[];
|
|
39
43
|
/** `"run"` when one kind of the document is on the run side (FACTS.SIDE). */
|
|
40
44
|
readonly side: Side;
|
|
@@ -146,7 +150,8 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
|
|
|
146
150
|
* by the contracts of its kinds:
|
|
147
151
|
*
|
|
148
152
|
* - `reads`: each free reference, with the root and the keys of its path. A read under a lazy part has the mark
|
|
149
|
-
* `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`.
|
|
153
|
+
* `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`. A kind that
|
|
154
|
+
* declares `reads` adds a read of each of its roots at each document of the kind.
|
|
150
155
|
* - `side` and `effects`: from the contract of each kind.
|
|
151
156
|
* - `pure`: each kind declares `pure`, and the document has no effect and is not volatile.
|
|
152
157
|
* - `volatile`: a kind, an operation or a step document declares `volatile`, or the host marks a read.
|
|
@@ -200,13 +205,22 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
|
|
|
200
205
|
}
|
|
201
206
|
const code = codeOf(world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
|
|
202
207
|
const answer = code.name === undefined ? undefined : hook?.(v.typePath ?? [], code.name);
|
|
208
|
+
let roots: readonly Root[] | undefined;
|
|
203
209
|
if (code.described) {
|
|
204
210
|
note(answer?.pure ?? kind.pure, answer?.volatile ?? kind.volatile);
|
|
205
211
|
above.set(v, { lazy, rules: answer?.lazy ?? kind.lazy });
|
|
212
|
+
roots = answer?.reads ?? kind.reads;
|
|
206
213
|
} else {
|
|
207
214
|
// The contract of the kind does not describe this code. Only the hook can.
|
|
208
215
|
note(answer?.pure, answer?.volatile);
|
|
209
216
|
above.set(v, { lazy, rules: answer?.lazy ?? EACH });
|
|
217
|
+
roots = answer?.reads;
|
|
218
|
+
}
|
|
219
|
+
// The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
|
|
220
|
+
for (const root of roots ?? NONE) {
|
|
221
|
+
const read: StaticRead = { root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(v.place), at: tight(v.at) };
|
|
222
|
+
reads.push(read);
|
|
223
|
+
if (options.volatile?.(read) === true) volatile = true;
|
|
210
224
|
}
|
|
211
225
|
if (kind.side === "run") side = "run";
|
|
212
226
|
// check and effectsOf read the declared effects with the same function (D27).
|
package/src/formula.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfT
|
|
|
4
4
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
|
|
5
5
|
import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
|
|
6
6
|
import { type MachinePart, mergeMachines } from "./merge.ts";
|
|
7
|
-
import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, stepType, typeText } from "./types.ts";
|
|
7
|
+
import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeText } from "./types.ts";
|
|
8
8
|
import type { TypeInput, TypeRule } from "./typing.ts";
|
|
9
9
|
import { decodeDomain, domainTypeOf } from "./codec.ts";
|
|
10
10
|
|
|
@@ -42,6 +42,8 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
|
|
|
42
42
|
* evaluates, and one failed part gives its own error. Without it, the first error stops the evaluation.
|
|
43
43
|
* - `pure`: the pure operations, for `factsOf` (D24). `true` names each operation that is not volatile. The default
|
|
44
44
|
* names no operation.
|
|
45
|
+
* - `pureExt`: the pure extensions of `ext`, by their name, for `factsOf` and the folds of a plan (PLAN.FOLD, D47).
|
|
46
|
+
* The default names no extension. A standard extension is pure without a name here.
|
|
45
47
|
* - `volatile`: the operations whose results can change while their arguments stay the same, for example a clock.
|
|
46
48
|
* - `sigs`: the signatures of the operations, by their name, for a check of types (TYPE.SIGNATURE). A list gives
|
|
47
49
|
* overloads. An operation without a signature takes and gives `unknown` (TYPE.GRADUAL).
|
|
@@ -61,13 +63,14 @@ export interface FormulaProfile {
|
|
|
61
63
|
readonly check?: (value: unknown, op: string) => Result | undefined;
|
|
62
64
|
readonly combine?: (errors: readonly EvalError[]) => EvalError;
|
|
63
65
|
readonly pure?: true | readonly string[];
|
|
66
|
+
readonly pureExt?: readonly string[];
|
|
64
67
|
readonly volatile?: readonly string[];
|
|
65
68
|
readonly sigs?: Readonly<Record<string, Signature | readonly Signature[]>> | ((name: string) => Signature | readonly Signature[] | undefined);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
|
-
* The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter
|
|
70
|
-
* the
|
|
72
|
+
* The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter at
|
|
73
|
+
* the end of `params` is optional when `undefined` is a member of its type, for example `unknown`. A later version can add a key, for example the type that a
|
|
71
74
|
* true result proves.
|
|
72
75
|
*/
|
|
73
76
|
export interface Signature {
|
|
@@ -209,9 +212,13 @@ export function formulaKinds(): Readonly<Record<string, WorldEntry>> {
|
|
|
209
212
|
return kinds as Readonly<Record<string, WorldEntry>>;
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* This function gives the type of a value of a profile: the value itself as `one`. `undefined` gives the base type
|
|
217
|
+
* `undefined`, because JSON and thus `one` cannot hold it (D48). A function gives `unknown`.
|
|
218
|
+
*/
|
|
213
219
|
function valueType(value: unknown): ValueType {
|
|
214
|
-
|
|
220
|
+
if (value === undefined) return "undefined";
|
|
221
|
+
return typeof value === "function" ? "unknown" : { one: [value as Json] };
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
/** This function tells if a value has the form of a signature: a record with `params` as a list, and `to`. */
|
|
@@ -236,14 +243,14 @@ function listText(names: readonly string[]): string {
|
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
/**
|
|
239
|
-
* This function tells if a signature takes a count of arguments. A parameter
|
|
240
|
-
*
|
|
246
|
+
* This function tells if a signature takes a count of arguments. A parameter at the end is optional when its type
|
|
247
|
+
* holds `undefined`, because a missing argument is `undefined`. `unknown` and the base type `undefined` hold it.
|
|
241
248
|
*
|
|
242
249
|
* @returns `undefined` when the number is correct, or else the text of the numbers that the signature takes.
|
|
243
250
|
*/
|
|
244
251
|
function arityOf(sig: Signature, count: number, domains: Machine["domains"]): string | undefined {
|
|
245
252
|
let least = sig.params.length;
|
|
246
|
-
while (least > 0 &&
|
|
253
|
+
while (least > 0 && member(undefined, sig.params[least - 1] as ValueType, domains)) least -= 1;
|
|
247
254
|
const most = sig.rest === undefined ? sig.params.length : Number.POSITIVE_INFINITY;
|
|
248
255
|
if (count >= least && count <= most) return undefined;
|
|
249
256
|
if (most === Number.POSITIVE_INFINITY) return `at least ${argumentsText(least)}`;
|
|
@@ -487,6 +494,8 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
|
|
|
487
494
|
const name = nameOf(input);
|
|
488
495
|
const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
|
|
489
496
|
if (own) return "unknown";
|
|
497
|
+
// The standard extensions give the values that JSON cannot write, thus each has a base type (D48).
|
|
498
|
+
if (name === "undefined") return "undefined";
|
|
490
499
|
if (name === "number") return "number";
|
|
491
500
|
return name === "domain" ? domainTypeOf(input.doc.value, input.machine.domains) : "unknown";
|
|
492
501
|
},
|
|
@@ -498,19 +507,119 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
|
|
|
498
507
|
* This function makes the evaluators of the formula kit for one language. Each evaluator compiles its document
|
|
499
508
|
* one time into code. Each evaluator also holds its type rule, for a check of types (TYPE.SYNTH).
|
|
500
509
|
*
|
|
510
|
+
* Equal profiles give the same evaluator objects (COMPOSE.UNIFY, D46). Thus two machines of one profile merge without
|
|
511
|
+
* a conflict. Each call gives a new record. Each evaluator object is frozen, because many machines can share it.
|
|
512
|
+
*
|
|
501
513
|
* @param profile - The operations and the rules of the language.
|
|
502
514
|
* @returns The evaluators, by their name.
|
|
503
515
|
*/
|
|
504
516
|
export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record<string, Evaluator>> {
|
|
517
|
+
return { ...kitOf(profile).evaluators };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** This function makes new evaluators of the formula kit for the fields of one profile. */
|
|
521
|
+
function makeEvaluators(profile: FormulaProfile): Readonly<Record<string, Evaluator>> {
|
|
505
522
|
const evaluators = formulaCode(profile);
|
|
506
523
|
const types = formulaTypes(profile);
|
|
507
524
|
const out: Record<string, Evaluator> = {};
|
|
508
525
|
for (const name of Object.keys(evaluators)) {
|
|
509
526
|
const evaluator = evaluators[name] as CompiledEvaluator;
|
|
510
527
|
const type = Object.hasOwn(types, name) ? types[name] : undefined;
|
|
511
|
-
out[name] = type === undefined ? evaluator : { compile: evaluator.compile, type };
|
|
528
|
+
out[name] = Object.freeze(type === undefined ? evaluator : { compile: evaluator.compile, type });
|
|
529
|
+
}
|
|
530
|
+
return Object.freeze(out);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** The keys of a profile that the kit reads. The kit ignores each other key. */
|
|
534
|
+
const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
|
|
535
|
+
"ops",
|
|
536
|
+
"lazy",
|
|
537
|
+
"ext",
|
|
538
|
+
"truthy",
|
|
539
|
+
"otherwise",
|
|
540
|
+
"logic",
|
|
541
|
+
"omit",
|
|
542
|
+
"missing",
|
|
543
|
+
"check",
|
|
544
|
+
"combine",
|
|
545
|
+
"pure",
|
|
546
|
+
"pureExt",
|
|
547
|
+
"volatile",
|
|
548
|
+
"sigs",
|
|
549
|
+
] as const satisfies readonly (keyof FormulaProfile)[]);
|
|
550
|
+
|
|
551
|
+
/** The evaluators and the hooks of the formula kit for one profile, with the fields that made them. */
|
|
552
|
+
interface Kit {
|
|
553
|
+
readonly fields: FormulaProfile;
|
|
554
|
+
readonly evaluators: Readonly<Record<string, Evaluator>>;
|
|
555
|
+
readonly known: (path: readonly Key[], evaluator: string) => true | string;
|
|
556
|
+
readonly facts: (path: readonly Key[], code: string) => KindFacts | undefined;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The number of recent kits that a profile in a new object can find by its fields. */
|
|
560
|
+
const RECENT_KITS = 16;
|
|
561
|
+
|
|
562
|
+
/** The kit of each profile object. A kit goes when its profile object goes. */
|
|
563
|
+
const kitsByProfile = /* @__PURE__ */ new WeakMap<object, Kit>();
|
|
564
|
+
|
|
565
|
+
/** The recent kits, the most recent first. The list holds `RECENT_KITS` kits or fewer. */
|
|
566
|
+
const recentKits: Kit[] = [];
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* This function gives the canonical form of a profile: a frozen record of the fields that the kit reads. A field
|
|
570
|
+
* with the value `undefined` is absent, but `otherwise` is present when the profile has it as an own key. A list
|
|
571
|
+
* is a frozen copy. Each other value stays the same object.
|
|
572
|
+
*/
|
|
573
|
+
function fieldsOf(profile: FormulaProfile): FormulaProfile {
|
|
574
|
+
const out: Record<string, unknown> = {};
|
|
575
|
+
for (const key of PROFILE_KEYS) {
|
|
576
|
+
if (key === "otherwise" ? !Object.hasOwn(profile, key) : profile[key] === undefined) continue;
|
|
577
|
+
const value: unknown = profile[key];
|
|
578
|
+
out[key] = Array.isArray(value) ? Object.freeze([...(value as unknown[])]) : value;
|
|
579
|
+
}
|
|
580
|
+
return Object.freeze(out);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* This function tells if two canonical forms are equal. They must have the same keys. A function, a record and a host
|
|
585
|
+
* value are equal only to themselves. Two lists are equal when their items are the same, in order.
|
|
586
|
+
*/
|
|
587
|
+
function sameFields(a: FormulaProfile, b: FormulaProfile): boolean {
|
|
588
|
+
const left = a as Readonly<Record<string, unknown>>;
|
|
589
|
+
const right = b as Readonly<Record<string, unknown>>;
|
|
590
|
+
const keys = Object.keys(left);
|
|
591
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
592
|
+
return keys.every((key) => {
|
|
593
|
+
if (!Object.hasOwn(right, key)) return false;
|
|
594
|
+
const x = left[key];
|
|
595
|
+
const y = right[key];
|
|
596
|
+
if (Object.is(x, y)) return true;
|
|
597
|
+
return Array.isArray(x) && Array.isArray(y) && x.length === y.length && x.every((item, index) => Object.is(item, y[index]));
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* This function gives the kit of a profile (D46). A profile object that the kit knows, with the same fields, gives its
|
|
603
|
+
* kit. Then a recent kit with equal fields serves a new profile object. Else the function makes a new kit from the
|
|
604
|
+
* canonical form. Thus a later change of a field of the profile object gives a new kit, and an old kit does not change.
|
|
605
|
+
*/
|
|
606
|
+
function kitOf(profile: FormulaProfile): Kit {
|
|
607
|
+
const fields = fieldsOf(profile);
|
|
608
|
+
const known = kitsByProfile.get(profile);
|
|
609
|
+
if (known !== undefined && sameFields(known.fields, fields)) return known;
|
|
610
|
+
const index = recentKits.findIndex((kit) => sameFields(kit.fields, fields));
|
|
611
|
+
let kit: Kit;
|
|
612
|
+
if (index >= 0) {
|
|
613
|
+
kit = recentKits[index] as Kit;
|
|
614
|
+
recentKits.splice(index, 1);
|
|
615
|
+
} else {
|
|
616
|
+
const hook = makeKnown(fields);
|
|
617
|
+
kit = { fields, evaluators: makeEvaluators(fields), known: hook, facts: makeFacts(fields, hook) };
|
|
512
618
|
}
|
|
513
|
-
|
|
619
|
+
recentKits.unshift(kit);
|
|
620
|
+
if (recentKits.length > RECENT_KITS) recentKits.length = RECENT_KITS;
|
|
621
|
+
kitsByProfile.set(profile, kit);
|
|
622
|
+
return kit;
|
|
514
623
|
}
|
|
515
624
|
|
|
516
625
|
/** This function makes the code of the evaluators of the formula kit, without their type rules. */
|
|
@@ -848,10 +957,17 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
|
|
|
848
957
|
* `formula.ext` knows an extension of `ext` and each standard extension. The hook knows each name of the other
|
|
849
958
|
* evaluators.
|
|
850
959
|
*
|
|
960
|
+
* Equal profiles give the same hook (D46).
|
|
961
|
+
*
|
|
851
962
|
* @param profile - The operations and the extensions of the language.
|
|
852
963
|
* @returns The hook: `true`, or the reason of the `#NAME?` that the evaluator gives in a run.
|
|
853
964
|
*/
|
|
854
965
|
export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[], evaluator: string) => true | string {
|
|
966
|
+
return kitOf(profile).known;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** This function makes a new hook `known` for the fields of one profile. */
|
|
970
|
+
function makeKnown(profile: FormulaProfile): (path: readonly Key[], evaluator: string) => true | string {
|
|
855
971
|
const hasOp = (name: string): boolean => {
|
|
856
972
|
if (profile.lazy !== undefined && Object.hasOwn(profile.lazy, name)) return true;
|
|
857
973
|
const ops = profile.ops;
|
|
@@ -873,10 +989,13 @@ export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[
|
|
|
873
989
|
};
|
|
874
990
|
}
|
|
875
991
|
|
|
876
|
-
/**
|
|
992
|
+
/**
|
|
993
|
+
* The facts of an extension of the profile that `pureExt` does not name. It gets the context, thus it can read outside
|
|
994
|
+
* `resolve`.
|
|
995
|
+
*/
|
|
877
996
|
const NOT_PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: false });
|
|
878
997
|
|
|
879
|
-
/** The facts of a standard extension. */
|
|
998
|
+
/** The facts of a standard extension, and of an extension that `pureExt` names. */
|
|
880
999
|
const PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: true });
|
|
881
1000
|
|
|
882
1001
|
/** The lazy rule of a lazy operation: it covers each argument. */
|
|
@@ -888,19 +1007,30 @@ const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE
|
|
|
888
1007
|
*
|
|
889
1008
|
* - An operation of `lazy` has a lazy rule for each argument.
|
|
890
1009
|
* - An operation is pure when the profile names it in `pure`, or when `pure` is `true`. A volatile operation is not pure.
|
|
891
|
-
* - A standard extension is pure. An extension of the profile
|
|
1010
|
+
* - A standard extension is pure. An extension of the profile is pure only when the profile names it in `pureExt` (D47).
|
|
1011
|
+
* Such an extension keeps C4: it reads the context only through `resolve`.
|
|
1012
|
+
*
|
|
1013
|
+
* Equal profiles give the same hook (D46).
|
|
892
1014
|
*
|
|
893
1015
|
* @param profile - The operations, the extensions and their facts.
|
|
894
1016
|
* @returns The hook: the facts of a name, or `undefined` for the facts of the contract.
|
|
895
1017
|
*/
|
|
896
1018
|
export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[], code: string) => KindFacts | undefined {
|
|
1019
|
+
return kitOf(profile).facts;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** This function makes a new hook `facts` for the fields of one profile, with the hook `known` of the same fields. */
|
|
1023
|
+
function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evaluator: string) => true | string): (path: readonly Key[], code: string) => KindFacts | undefined {
|
|
897
1024
|
const volatile = new Set(profile.volatile ?? []);
|
|
898
1025
|
const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
|
|
899
|
-
const
|
|
1026
|
+
const pureExt = new Set(profile.pureExt ?? []);
|
|
900
1027
|
return (path, code) => {
|
|
901
1028
|
if (code !== "formula.op" && code !== "formula.ext") return undefined;
|
|
902
1029
|
const name = path.slice(1).join(".");
|
|
903
|
-
if (code === "formula.ext")
|
|
1030
|
+
if (code === "formula.ext") {
|
|
1031
|
+
if (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) return pureExt.has(name) ? PURE : NOT_PURE;
|
|
1032
|
+
return Object.hasOwn(STD_EXT, name) ? PURE : undefined;
|
|
1033
|
+
}
|
|
904
1034
|
// An operation that the language does not have gives #NAME?. The kit gives it no facts.
|
|
905
1035
|
if (known(path, code) !== true) return undefined;
|
|
906
1036
|
const isVolatile = volatile.has(name);
|
|
@@ -922,14 +1052,16 @@ export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[
|
|
|
922
1052
|
* The kit gives the domains of the kernel (`STD_DOMAINS`). Thus a domain of the language with the name `number` is a
|
|
923
1053
|
* conflict. The language names `["domains", "number"]` in `replaces` to replace it.
|
|
924
1054
|
*
|
|
1055
|
+
* Equal profiles give the same evaluators and the same hooks (COMPOSE.UNIFY, D46). Thus two machines of one profile
|
|
1056
|
+
* merge, also when each adds its own language. Two profiles with different fields give different evaluators, and
|
|
1057
|
+
* their machines conflict at each evaluator name of the kit.
|
|
1058
|
+
*
|
|
925
1059
|
* @param profile - The operations and the rules of the language.
|
|
926
1060
|
* @param more - The parts of a machine that the language adds, with the paths of the entries that it replaces.
|
|
927
1061
|
* @returns The machine.
|
|
928
1062
|
* @throws {MergeError} The language and the kit have a conflict.
|
|
929
1063
|
*/
|
|
930
1064
|
export function formulaMachine(profile: FormulaProfile = {}, more: MachinePart = {}): Machine {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
more,
|
|
934
|
-
);
|
|
1065
|
+
const kit = kitOf(profile);
|
|
1066
|
+
return mergeMachines({ world: { kinds: formulaKinds() }, evaluators: { ...kit.evaluators }, domains: STD_DOMAINS, known: kit.known, facts: kit.facts }, more);
|
|
935
1067
|
}
|
package/src/index.ts
CHANGED
|
@@ -49,8 +49,10 @@ export {
|
|
|
49
49
|
type Shape as PartShape,
|
|
50
50
|
type Side,
|
|
51
51
|
type TreeContract,
|
|
52
|
+
type VerbEffects,
|
|
52
53
|
type World,
|
|
53
54
|
type WorldEntry,
|
|
55
|
+
effectsOfVerb,
|
|
54
56
|
entriesAsJson,
|
|
55
57
|
equationType,
|
|
56
58
|
isAlias,
|
|
@@ -202,7 +204,9 @@ export {
|
|
|
202
204
|
type Typing,
|
|
203
205
|
type WalkOptions,
|
|
204
206
|
type WalkProblem,
|
|
207
|
+
type WorldIssue,
|
|
205
208
|
check,
|
|
209
|
+
checkWorld,
|
|
206
210
|
mapRefs,
|
|
207
211
|
refsOf,
|
|
208
212
|
typeCheck,
|
package/src/providers.ts
CHANGED
|
@@ -235,6 +235,7 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
|
|
|
235
235
|
let lists = false;
|
|
236
236
|
for (const value of values) {
|
|
237
237
|
if (value === null) bases.add("null");
|
|
238
|
+
else if (value === undefined) bases.add("undefined");
|
|
238
239
|
else if (typeof value === "boolean") bases.add("bool");
|
|
239
240
|
else if (typeof value === "number") bases.add("number");
|
|
240
241
|
else if (typeof value === "string") bases.add("text");
|
|
@@ -243,10 +244,10 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
|
|
|
243
244
|
// A hole of a list holds undefined, thus a loop by index reads it too.
|
|
244
245
|
for (let index = 0; index < value.length; index += 1) items.push(value[index]);
|
|
245
246
|
} else if (isRecord(value)) records.push(value);
|
|
246
|
-
// A value without a type of the kernel, for example a function
|
|
247
|
+
// A value without a type of the kernel, for example a function, has each type.
|
|
247
248
|
else return "unknown";
|
|
248
249
|
}
|
|
249
|
-
for (const base of ["null", "bool", "number", "text"]) if (bases.has(base)) types.push(base);
|
|
250
|
+
for (const base of ["null", "undefined", "bool", "number", "text"]) if (bases.has(base)) types.push(base);
|
|
250
251
|
if (lists) types.push({ list: samplesAt(items, closed, depth + 1) });
|
|
251
252
|
if (records.length > 0) {
|
|
252
253
|
// All records join into one record. A field that a record does not have is optional.
|
|
@@ -268,7 +269,7 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
|
|
|
268
269
|
* This function infers a value type from sample values (TYPE.SAMPLES), as a type provider of F# does. A host uses it
|
|
269
270
|
* for the type of a root, for example from some events.
|
|
270
271
|
*
|
|
271
|
-
* - `null`, a truth value, a number and a text give their base types, not their values.
|
|
272
|
+
* - `null`, `undefined`, a truth value, a number and a text give their base types, not their values.
|
|
272
273
|
* - The items of each list join into one type of items.
|
|
273
274
|
* - All records join into one record with each key of the samples. A key that a record does not have is optional.
|
|
274
275
|
* - Values of different forms give a union, as `join` gives. No samples give `never`.
|
package/src/reference.ts
CHANGED
|
@@ -634,10 +634,13 @@ function plainValue(value: unknown, open: readonly object[]): Result {
|
|
|
634
634
|
|
|
635
635
|
/**
|
|
636
636
|
* TREE.LAZY, TREE.PARENT, TREE.SIBLING and TREE.ERROR: a node of an open tree gives its thing, and the root of a new
|
|
637
|
-
* tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error.
|
|
637
|
+
* tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
|
|
638
|
+
* reads `self` only through `resolve` (C4, TREE.SELF).
|
|
638
639
|
*/
|
|
639
|
-
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath }) => {
|
|
640
|
-
const
|
|
640
|
+
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
|
|
641
|
+
const reached = walk(["self"]);
|
|
642
|
+
const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
|
|
643
|
+
const outer = refNodeOf(parent);
|
|
641
644
|
const nested = outer !== undefined && outer.tree.open;
|
|
642
645
|
const found = machine === undefined || typePath.length === 0 ? undefined : lookupKind(machine.world, typePath);
|
|
643
646
|
const contract = found?.ok === true ? found.kind.tree : undefined;
|
|
@@ -661,7 +664,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
661
664
|
type: kind,
|
|
662
665
|
label: `node ${kind}`,
|
|
663
666
|
deref: (key): Thing | undefined => {
|
|
664
|
-
if (key === "parent") return
|
|
667
|
+
if (key === "parent") return parent;
|
|
665
668
|
if (key === "type") return kind;
|
|
666
669
|
const result = attributeOf(node, String(key));
|
|
667
670
|
if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
|