@mark1russell7/coeval 0.23.0 → 0.24.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 -1
- package/build/index.d.ts +84 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +551 -11
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/conform.ts +1 -1
- package/src/effects.ts +1 -1
- package/src/evaluate.ts +133 -3
- package/src/index.ts +5 -0
- package/src/reference.ts +55 -0
- package/src/static.ts +13 -2
- package/src/stdlib.ts +61 -1
- package/src/syntax.ts +208 -1
- package/src/world.ts +7 -1
package/src/syntax.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Json, type JsonRecord, type Key, isEnvelope, isRecord, typePath } from "./doc.ts";
|
|
1
|
+
import { type Json, type JsonRecord, type Key, docEquals, isEnvelope, isRecord, typePath } from "./doc.ts";
|
|
2
2
|
import { type Result, describe, fail, isStackOverflow, ok } from "./errors.ts";
|
|
3
3
|
import type { EvalFn, Part } from "./evaluate.ts";
|
|
4
4
|
import type { MachinePart } from "./merge.ts";
|
|
@@ -1531,3 +1531,210 @@ export function syntaxFormat(profile: SyntaxProfile): MachinePart {
|
|
|
1531
1531
|
};
|
|
1532
1532
|
return { world: { defaults: { format: "syntax.format" } }, evaluators: { "syntax.format": code } };
|
|
1533
1533
|
}
|
|
1534
|
+
|
|
1535
|
+
// === The check of a profile ===
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* The name of a finding of `checkSyntax` (D140). Each name holds a rule of the parser or the printer.
|
|
1539
|
+
*
|
|
1540
|
+
* - `invalid`: `parse` throws a `TypeError` for the profile. The reason is its message.
|
|
1541
|
+
* - `operator`: the document of one operator does not come back. A negative precedence stops an infix operator,
|
|
1542
|
+
* because an expression starts at the precedence `0`. The kinds `ref`, `lit` and `ext.number` have their own text.
|
|
1543
|
+
* - `grouping`: an operator with a second operator in a term does not come back. The parser reads a term from the
|
|
1544
|
+
* precedence plus `1`, and the printer compares precedences, thus a gap under `1` loses the tree. A postfix operator
|
|
1545
|
+
* at the precedence of a `right` infix operator loses it too.
|
|
1546
|
+
* - `longest-match`: the printer writes two postfix operators without a space, and a longer symbol reads their text.
|
|
1547
|
+
* - `word`: a word of `literals.words` does not read as its value. A word operator reads first, and a word that is
|
|
1548
|
+
* not a plain name has no token.
|
|
1549
|
+
* - `root`: a root does not come back. A word operator reads first, and a literal word reads a root without steps.
|
|
1550
|
+
* - `pattern`: a pattern of references reads a text that has another meaning, because the parser tries patterns first.
|
|
1551
|
+
* - `form`: the document of a call form does not come back. A word operator reads first, the last form of a name
|
|
1552
|
+
* hides the others, and a `min` above the fields permits no text.
|
|
1553
|
+
* - `call`: a name of `calls.ops` does not come back. A form of the name wins, a word operator reads first, and a name
|
|
1554
|
+
* with dots needs the field steps.
|
|
1555
|
+
* - `separator`: the separator does not end an argument, and a neutral separator keeps the same document. An operator
|
|
1556
|
+
* reads first, the parser skips a space before it, or a name or a bracket at its start changes the term.
|
|
1557
|
+
* - `construct`: the document of a construct form does not come back, for example two forms have one kind.
|
|
1558
|
+
*/
|
|
1559
|
+
export type SyntaxFindingCode = "invalid" | "operator" | "grouping" | "longest-match" | "word" | "root" | "pattern" | "form" | "call" | "separator" | "construct";
|
|
1560
|
+
|
|
1561
|
+
/** One fault of a syntax profile: its name, its place in the profile and the reason (D140). */
|
|
1562
|
+
export interface SyntaxFinding {
|
|
1563
|
+
/** The name of the fault. */
|
|
1564
|
+
readonly code: SyntaxFindingCode;
|
|
1565
|
+
/** The keys from the root of the profile to the entry, for example `["operators", 3]`. A default list has keys too. */
|
|
1566
|
+
readonly place: readonly Key[];
|
|
1567
|
+
/** The rule that the entry breaks, and the text that shows it. */
|
|
1568
|
+
readonly reason: string;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** The state of one check: the profile, its compiled form, a leaf document and the two functions that record a fault. */
|
|
1572
|
+
interface ProfileCheck {
|
|
1573
|
+
readonly profile: SyntaxProfile;
|
|
1574
|
+
readonly syn: Compiled;
|
|
1575
|
+
readonly leaf: Json;
|
|
1576
|
+
/** This function adds a finding. A pattern that reads the start of `text` takes the code and the place. */
|
|
1577
|
+
readonly report: (code: SyntaxFindingCode, place: readonly Key[], text: string, reason: string) => void;
|
|
1578
|
+
/** This function reports a document that breaks the law, and it tells if the law holds. `start` stands for a refused text. */
|
|
1579
|
+
readonly keeps: (code: SyntaxFindingCode, place: readonly Key[], rule: string, doc: Json, start?: string) => boolean;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/** This function tells how a document breaks `parse(print(doc))`. It gives `undefined` when the document comes back. */
|
|
1583
|
+
function lawBreak(profile: SyntaxProfile, doc: Json): { readonly text: string | undefined; readonly why: string } | undefined {
|
|
1584
|
+
const printed = print(profile, doc);
|
|
1585
|
+
if (!printed.ok) return { text: undefined, why: printed.error.reason };
|
|
1586
|
+
const text = printed.value;
|
|
1587
|
+
const back = parse(profile, text);
|
|
1588
|
+
if (back.ok) return docEquals(back.doc, doc) ? undefined : { text, why: `The text ${text} reads as ${JSON.stringify(back.doc)}.` };
|
|
1589
|
+
return { text, why: `The text ${text} gives the issue: ${back.issues[0]?.message ?? ""}` };
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** This function gives the index of the first pattern that `parse` uses at the start of a text, or `-1`. */
|
|
1593
|
+
function patternAt(syn: Compiled, text: string): number {
|
|
1594
|
+
return syn.patterns.findIndex(({ sticky }) => {
|
|
1595
|
+
sticky.lastIndex = 0;
|
|
1596
|
+
const match = sticky.exec(text)?.[0];
|
|
1597
|
+
return match !== undefined && match !== "" && !/[A-Za-z0-9_$(]/.test(text[match.length] ?? "");
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** This function gives each small document of an operator with `term` in one term. A `flat` operator also has three terms. */
|
|
1602
|
+
function operatorDocs(op: Op, term: Json, leaf: Json): readonly Json[] {
|
|
1603
|
+
if (op.fixity !== "infix") return [opDoc(op, [term])];
|
|
1604
|
+
const pairs = [[term, leaf], [leaf, term]];
|
|
1605
|
+
return (op.assoc === "flat" ? [...pairs, [term, leaf, leaf], [leaf, term, leaf]] : pairs).map((terms) => opDoc(op, terms));
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/** This function gives the longer symbol that reads two postfix operators without a space, or `undefined`. */
|
|
1609
|
+
function gluedSymbol(syn: Compiled, outer: Op, inner: Op): string | undefined {
|
|
1610
|
+
if (outer.word || inner.word || outer.fixity !== "postfix" || inner.fixity !== "postfix") return undefined;
|
|
1611
|
+
const glued = inner.symbol + outer.symbol;
|
|
1612
|
+
const read = syn.binarySymbols.find((symbol) => glued.startsWith(symbol));
|
|
1613
|
+
return read === inner.symbol ? undefined : read;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function checkOperators({ profile, syn, leaf, keeps }: ProfileCheck): void {
|
|
1617
|
+
const ops = (profile.operators ?? []).map((given) => (given.fixity === "prefix" ? syn.prefix : syn.binary).get(given.symbol) as Op);
|
|
1618
|
+
const sound = ops.filter((op, index) => operatorDocs(op, leaf, leaf).every((doc) => keeps("operator", ["operators", index], `The operator ${op.symbol} does not come back.`, doc)));
|
|
1619
|
+
for (const outer of sound) {
|
|
1620
|
+
const place = ["operators", ops.indexOf(outer)];
|
|
1621
|
+
for (const inner of sound) {
|
|
1622
|
+
const glued = gluedSymbol(syn, outer, inner);
|
|
1623
|
+
const code = glued === undefined ? "grouping" : "longest-match";
|
|
1624
|
+
const rule = glued === undefined ? `The operator ${outer.symbol} with ${inner.symbol} in a term loses the tree.` : `The symbol ${glued} reads the postfix operators ${inner.symbol} and ${outer.symbol}.`;
|
|
1625
|
+
for (const term of operatorDocs(inner, leaf, leaf)) for (const doc of operatorDocs(outer, term, leaf)) keeps(code, place, rule, doc);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function checkLiterals({ profile, syn, report }: ProfileCheck): void {
|
|
1631
|
+
for (const [word, value] of syn.words) {
|
|
1632
|
+
const back = parse(profile, word);
|
|
1633
|
+
if (!back.ok || !docEquals(back.doc, envelop(syn, literalDoc(value)))) report("word", ["literals", "words", word], word, `The word ${word} does not read as its value.`);
|
|
1634
|
+
}
|
|
1635
|
+
for (const value of [0, -1, "a"]) {
|
|
1636
|
+
const shape = literalShape(syn, value);
|
|
1637
|
+
if (shape.tag === "text" && patternAt(syn, shape.text) >= 0) report("pattern", [], shape.text, `The literal ${shape.text} reads as a reference.`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function checkRoots({ profile, syn, keeps }: ProfileCheck): void {
|
|
1642
|
+
(profile.refs?.roots ?? DEFAULT_ROOTS).forEach((root, index) => {
|
|
1643
|
+
for (const path of syn.fields ? [[root], [root, "x"]] : [[root]]) {
|
|
1644
|
+
keeps("root", ["refs", "roots", index], `The root ${root} does not come back.`, { type: "ref", value: path }, path.join("."));
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/** This function gives a value that a call form writes: the fewest arguments, and at least one. */
|
|
1650
|
+
function formValue(given: SyntaxForm, leaf: Json): Json {
|
|
1651
|
+
const args = given.args ?? (given.fields === undefined ? "list" : "fields");
|
|
1652
|
+
const least = Math.max(given.min ?? given.fields?.length ?? 0, 1);
|
|
1653
|
+
if (args === "literal") return 1;
|
|
1654
|
+
if (args === "list") return Array.from({ length: least }, () => leaf);
|
|
1655
|
+
const keys = (given.fields ?? ["x"]).slice(0, args === "named" ? 1 : least);
|
|
1656
|
+
return Object.fromEntries(keys.map((key) => [key, leaf]));
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function checkCalls({ profile, syn, leaf, keeps }: ProfileCheck): void {
|
|
1660
|
+
const calls = profile.calls ?? {};
|
|
1661
|
+
(calls.forms ?? []).forEach((form, index) => {
|
|
1662
|
+
keeps("form", ["calls", "forms", index], `The form ${form.name} does not come back.`, { type: typeOfKind(form.kind).type, value: formValue(form, leaf) });
|
|
1663
|
+
});
|
|
1664
|
+
// The dotted text of a call name needs the field steps. A profile with `ops: true` and no field steps has no dotted call.
|
|
1665
|
+
if (syn.ops === true && syn.fields) keeps("call", ["calls", "ops"], "A call name with a dot does not come back.", { type: ["ops", "m", "p"], value: [leaf] });
|
|
1666
|
+
if (typeof calls.ops === "boolean" || calls.ops === undefined) return;
|
|
1667
|
+
calls.ops.forEach((name, index) => {
|
|
1668
|
+
keeps("call", ["calls", "ops", index], `The call ${name} does not come back.`, { type: ["ops", ...name.split(".")], value: [leaf] });
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/** This function writes two terms with the separator. It blames the separator only when a neutral separator keeps the document. */
|
|
1673
|
+
function checkSeparator({ profile, syn, leaf, report }: ProfileCheck): void {
|
|
1674
|
+
if (syn.ops !== true && syn.ops.size === 0 && syn.forms.size === 0 && syn.constructKinds.size === 0) return;
|
|
1675
|
+
const open: SyntaxProfile = { ...profile, calls: { ...profile.calls, ops: true } };
|
|
1676
|
+
const neutral: SyntaxProfile = { ...profile, calls: { ...open.calls, separator: "\u0001" } };
|
|
1677
|
+
const postfix = [...syn.binary.values()].filter((op) => op.fixity === "postfix").map((op) => opDoc(op, [leaf]));
|
|
1678
|
+
const terms: Json[] = [leaf, { type: "ref", value: [syn.bare, "b c"] }, "a", 1, ...postfix];
|
|
1679
|
+
const docs = terms.map((term): Json => ({ type: ["ops", "probe__"], value: [term, leaf] }));
|
|
1680
|
+
const entries = { x: leaf, y: leaf };
|
|
1681
|
+
const { record, let: bind } = syn.constructs;
|
|
1682
|
+
if (record !== undefined) docs.push({ type: record, value: entries });
|
|
1683
|
+
if (bind !== undefined) docs.push({ type: bind, value: { bind: entries, body: leaf } });
|
|
1684
|
+
for (const doc of docs) {
|
|
1685
|
+
const broken = lawBreak(neutral, doc) === undefined ? lawBreak(open, doc) : undefined;
|
|
1686
|
+
if (broken !== undefined) report("separator", ["calls", "separator"], "", `The separator "${syn.separator}" does not end an argument. ${broken.why}`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function checkConstructs({ syn, leaf, keeps }: ProfileCheck): void {
|
|
1691
|
+
const entries = { x: leaf };
|
|
1692
|
+
const values: Readonly<Record<keyof SyntaxConstructs, Json>> = { record: entries, let: { bind: entries, body: leaf }, fn: { params: ["x"], body: leaf }, call: { fn: leaf, args: [leaf] } };
|
|
1693
|
+
for (const name of CONSTRUCTS) {
|
|
1694
|
+
const type = syn.constructs[name];
|
|
1695
|
+
if (type !== undefined) keeps("construct", ["constructs", name], `The form ${name} does not come back.`, { type, value: values[name] });
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* This function finds the faults of a syntax profile before a host uses it (SYNTAX.PROFILE-CHECK, D140). A fault is a
|
|
1701
|
+
* document of an entry that breaks `parse(print(doc))`, or a text of an entry that reads another way. The function
|
|
1702
|
+
* writes and reads one small document for each entry, for each pair of operators and for each root. Thus an empty list
|
|
1703
|
+
* tells that these small documents come back. It does not prove the law for each document. `parse` and `print` do not
|
|
1704
|
+
* call this function, thus it adds no cost to them.
|
|
1705
|
+
*
|
|
1706
|
+
* @param profile - The syntax profile.
|
|
1707
|
+
* @returns At most one finding for each code and place, in the order of the operators, the literals, the references, the calls and the constructs.
|
|
1708
|
+
* @throws An error of a `read` or a `write` function of a pattern.
|
|
1709
|
+
*/
|
|
1710
|
+
export function checkSyntax(profile: SyntaxProfile): readonly SyntaxFinding[] {
|
|
1711
|
+
// A small `maxDepth` is a limit that the host chose, not a fault, thus each probe has the default limit.
|
|
1712
|
+
const open: SyntaxProfile = { ...profile, maxDepth: 256 };
|
|
1713
|
+
let syn: Compiled;
|
|
1714
|
+
try {
|
|
1715
|
+
syn = compileProfile(open);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
if (error instanceof TypeError) return [{ code: "invalid", place: [], reason: error.message }];
|
|
1718
|
+
throw error;
|
|
1719
|
+
}
|
|
1720
|
+
const found = new Map<string, SyntaxFinding>();
|
|
1721
|
+
const report: ProfileCheck["report"] = (code, place, text, reason) => {
|
|
1722
|
+
const index = patternAt(syn, text);
|
|
1723
|
+
const finding: SyntaxFinding = index < 0 ? { code, place, reason } : { code: "pattern", place: ["refs", "patterns", index], reason: `The pattern reads the start of the text ${text}. ${reason}` };
|
|
1724
|
+
const key = JSON.stringify([finding.code, finding.place]);
|
|
1725
|
+
if (!found.has(key)) found.set(key, finding);
|
|
1726
|
+
};
|
|
1727
|
+
const keeps: ProfileCheck["keeps"] = (code, place, rule, doc, start = "") => {
|
|
1728
|
+
const broken = lawBreak(open, doc);
|
|
1729
|
+
if (broken !== undefined) report(code, place, broken.text ?? start, `${rule} ${broken.why}`);
|
|
1730
|
+
return broken === undefined;
|
|
1731
|
+
};
|
|
1732
|
+
const check: ProfileCheck = { profile: open, syn, leaf: { type: "ref", value: [syn.bare, "a"] }, report, keeps };
|
|
1733
|
+
checkOperators(check);
|
|
1734
|
+
checkLiterals(check);
|
|
1735
|
+
checkRoots(check);
|
|
1736
|
+
checkCalls(check);
|
|
1737
|
+
checkSeparator(check);
|
|
1738
|
+
checkConstructs(check);
|
|
1739
|
+
return [...found.values()];
|
|
1740
|
+
}
|
package/src/world.ts
CHANGED
|
@@ -89,7 +89,13 @@ export interface ScopeRule {
|
|
|
89
89
|
export type CapsRule =
|
|
90
90
|
| { readonly rule: "same"; readonly in?: readonly (readonly Key[])[] }
|
|
91
91
|
| { readonly rule: "mask"; readonly in?: readonly (readonly Key[])[] }
|
|
92
|
-
| { readonly rule: "only"; readonly caps: readonly Cap[]; readonly in?: readonly (readonly Key[])[] }
|
|
92
|
+
| { readonly rule: "only"; readonly caps: readonly Cap[]; readonly in?: readonly (readonly Key[])[] }
|
|
93
|
+
/**
|
|
94
|
+
* The parts that the rule covers get each capability that the value of the document holds at the path `from`. The
|
|
95
|
+
* code of the kind answers these effects with its own clauses (`RunPart.handled`), thus they do not reach the host.
|
|
96
|
+
* The key `"*"` in `from` steps into each item (CAP.GRANT, D146).
|
|
97
|
+
*/
|
|
98
|
+
| { readonly rule: "grant"; readonly from: readonly Key[]; readonly in?: readonly (readonly Key[])[] };
|
|
93
99
|
|
|
94
100
|
/**
|
|
95
101
|
* A lazy rule of a kind names the parts that an evaluation can skip and still give a value. An example is the
|