hclang 0.15.1 → 0.17.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/esm/cli/hc.js +24 -3
- package/esm/lib/execute/env-visibility.js +80 -0
- package/esm/lib/execute/syntax.js +1 -0
- package/esm/lib/frames/atom-syntax.js +9 -0
- package/esm/lib/frames/clock.js +177 -0
- package/esm/lib/frames/dimensional-algebra.js +43 -0
- package/esm/lib/frames/evaluation-scope.js +7 -6
- package/esm/lib/frames/frame-array.js +60 -0
- package/esm/lib/frames/frame-handle.js +14 -0
- package/esm/lib/frames/frame-numeric.js +31 -2
- package/esm/lib/frames/frame-resource.js +94 -36
- package/esm/lib/frames/frame-symbol.js +19 -2
- package/esm/lib/frames/frame-time.js +555 -0
- package/esm/lib/frames/frame-typed-number.js +7 -0
- package/esm/lib/frames/frame.js +35 -0
- package/esm/lib/frames/resource-handlers.js +114 -0
- package/esm/lib/frames/resource-reference.js +14 -8
- package/esm/lib/frames.js +5 -1
- package/esm/lib/ops/conditionals.js +6 -1
- package/esm/lib/ops/math.js +6 -4
- package/esm/lib/ops.js +3 -3
- package/esm/lib/version.js +1 -1
- package/package.json +1 -1
package/esm/cli/hc.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "../_dnt.polyfills.js";
|
|
3
3
|
import * as dntShim from "../_dnt.shims.js";
|
|
4
|
+
import { visibleEnvironment } from "../lib/execute/env-visibility.js";
|
|
4
5
|
import { HCEval, make_context } from "../lib/execute/hc-eval.js";
|
|
5
6
|
import { HCLog } from "../lib/execute/hc-log.js";
|
|
6
7
|
import { HCTest } from "../lib/execute/hc-test.js";
|
|
@@ -8,7 +9,7 @@ import { parseArgs } from "../deps/jsr.io/@std/cli/1.0.32/parse_args.js";
|
|
|
8
9
|
import { runfile } from "./runfile.js";
|
|
9
10
|
import { Prompt } from "./prompt.js";
|
|
10
11
|
import { DenoFileStore } from "./resource-store.js";
|
|
11
|
-
import { Frame, FrameResource, RESOURCE_ROOT_KEY, } from "../lib/frames.js";
|
|
12
|
+
import { CLOCK_SCHEME, ClockHandler, Frame, FrameResource, RealClock, RESOURCE_ROOT_KEY, ResourceHandlers, } from "../lib/frames.js";
|
|
12
13
|
/**
|
|
13
14
|
* @module hc
|
|
14
15
|
*
|
|
@@ -71,9 +72,26 @@ export function getHost(context = {}) {
|
|
|
71
72
|
// logger holds the same one: the root binding belongs to the host namespace
|
|
72
73
|
// and nowhere else.
|
|
73
74
|
const host = new Frame({ ...context });
|
|
74
|
-
host.set(RESOURCE_ROOT_KEY, FrameResource.root(new DenoFileStore()));
|
|
75
|
+
host.set(RESOURCE_ROOT_KEY, FrameResource.root(new DenoFileStore(), getHandlers()));
|
|
75
76
|
return host;
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* This harness's scheme table.
|
|
80
|
+
*
|
|
81
|
+
* One entry, and the emptiness of every other slot is the point: `'https://…'`
|
|
82
|
+
* refuses because nothing is bound to `https`, not because a rule forbids it.
|
|
83
|
+
* The CLI installs the real clock, which is the only thing this harness can
|
|
84
|
+
* honestly implement that the root binding does not already cover.
|
|
85
|
+
*
|
|
86
|
+
* Exported so a test can run under the table the CLI actually grants.
|
|
87
|
+
*
|
|
88
|
+
* @returns The scheme handlers reachable from a CLI evaluation.
|
|
89
|
+
*/
|
|
90
|
+
export function getHandlers() {
|
|
91
|
+
return ResourceHandlers.of({
|
|
92
|
+
[CLOCK_SCHEME]: new ClockHandler(new RealClock()),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
77
95
|
/**
|
|
78
96
|
* The main function for the CLI application.
|
|
79
97
|
*
|
|
@@ -147,7 +165,10 @@ export async function main(hcEval, options) {
|
|
|
147
165
|
return lexicalComplete ? test?.exitCode ?? 0 : 1;
|
|
148
166
|
}
|
|
149
167
|
if (globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).main) {
|
|
150
|
-
|
|
168
|
+
// One name at a time, through the dictionary. A wholesale read would hand the
|
|
169
|
+
// whole process environment to `$$` again, and the flags this harness runs
|
|
170
|
+
// under are a derived artifact rather than the bound.
|
|
171
|
+
const env = visibleEnvironment((name) => dntShim.Deno.env.get(name));
|
|
151
172
|
const options = getOptions(dntShim.Deno.args);
|
|
152
173
|
const hc_eval = getEval(env);
|
|
153
174
|
main(hc_eval, options).then((exitCode) => {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard UNIX variables, which describe the invocation rather than the host's
|
|
3
|
+
* secrets. Every one of these is already visible to anything the user runs.
|
|
4
|
+
*/
|
|
5
|
+
const UNIX_VARIABLES = [
|
|
6
|
+
"HOME",
|
|
7
|
+
"HOSTNAME",
|
|
8
|
+
"LANG",
|
|
9
|
+
"LC_ALL",
|
|
10
|
+
"LC_CTYPE",
|
|
11
|
+
"LOGNAME",
|
|
12
|
+
"PATH",
|
|
13
|
+
"PWD",
|
|
14
|
+
"SHELL",
|
|
15
|
+
"SHLVL",
|
|
16
|
+
"TMPDIR",
|
|
17
|
+
"TZ",
|
|
18
|
+
"USER",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Variables open-source tooling probes.
|
|
22
|
+
*
|
|
23
|
+
* This group is load-bearing rather than generous: the CLI's own dependency
|
|
24
|
+
* tree reads the colour-support family, and `DEBUG`/`DEBUG_ENV` are read by the
|
|
25
|
+
* logger and the context builder. Omitting them would leave a dictionary that
|
|
26
|
+
* looks tidy and breaks the harness that uses it.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately absent: the CI-vendor names that same dependency reads to guess
|
|
29
|
+
* at colour support — `TF_BUILD`, `GITHUB_ACTIONS`, `TRAVIS`, and a dozen more.
|
|
30
|
+
* Which CI a build runs under is the harness's business, not a property of the
|
|
31
|
+
* invocation a program should describe itself with, and they reach the dependency
|
|
32
|
+
* through the flag rather than through this dictionary. The vendor-neutral `CI`
|
|
33
|
+
* stays, because that one is a fact about the invocation.
|
|
34
|
+
*/
|
|
35
|
+
const TOOLING_VARIABLES = [
|
|
36
|
+
"CI",
|
|
37
|
+
"COLORTERM",
|
|
38
|
+
"DEBUG",
|
|
39
|
+
"DEBUG_ENV",
|
|
40
|
+
"EDITOR",
|
|
41
|
+
"FORCE_COLOR",
|
|
42
|
+
"NO_COLOR",
|
|
43
|
+
"PAGER",
|
|
44
|
+
"TERM",
|
|
45
|
+
"TERM_PROGRAM",
|
|
46
|
+
"TERM_PROGRAM_VERSION",
|
|
47
|
+
];
|
|
48
|
+
/** The curated default dictionary, which a harness may replace wholesale. */
|
|
49
|
+
export const VISIBLE_ENVIRONMENT = Object.freeze([
|
|
50
|
+
...UNIX_VARIABLES,
|
|
51
|
+
...TOOLING_VARIABLES,
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Reads only the declared variables, one name at a time.
|
|
55
|
+
*
|
|
56
|
+
* Never a wholesale read. Asking for the names the dictionary lists is what
|
|
57
|
+
* makes the dictionary the bound: a variable it omits is never read, so it
|
|
58
|
+
* cannot arrive by accident when the host grows a new one.
|
|
59
|
+
*
|
|
60
|
+
* A read the host refuses is absent for the same reason a read the dictionary
|
|
61
|
+
* omits is absent — the harness's own permission flags must not become
|
|
62
|
+
* something a program can detect.
|
|
63
|
+
*/
|
|
64
|
+
export const visibleEnvironment = (read, visible = VISIBLE_ENVIRONMENT) => {
|
|
65
|
+
const environment = {};
|
|
66
|
+
for (const name of visible) {
|
|
67
|
+
const value = readOrAbsent(read, name);
|
|
68
|
+
if (value !== undefined)
|
|
69
|
+
environment[name] = value;
|
|
70
|
+
}
|
|
71
|
+
return environment;
|
|
72
|
+
};
|
|
73
|
+
const readOrAbsent = (read, name) => {
|
|
74
|
+
try {
|
|
75
|
+
return read(name);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
11
|
import { ScanDisposition } from "../scan.js";
|
|
12
|
+
/**
|
|
13
|
+
* The delimiter that opens and closes a time literal.
|
|
14
|
+
*
|
|
15
|
+
* The time family owns it, but the operator family has to agree about it: `%` is
|
|
16
|
+
* an operator continuation character, so without a shared name an operator would
|
|
17
|
+
* swallow the sigil of a literal abutting it. It lives here, with the other
|
|
18
|
+
* facts both families read, rather than in either one of them.
|
|
19
|
+
*/
|
|
20
|
+
export const TIME_DELIMITER = "%";
|
|
12
21
|
/** Counts non-overlapping occurrences of `token` in `source`. */
|
|
13
22
|
const occurrences = (source, token) => source.split(token).length - 1;
|
|
14
23
|
/** Consume an accepted character; otherwise end the atom before it. */
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clock, as a handler table entry.
|
|
3
|
+
*
|
|
4
|
+
* A time literal is inert data; reading the current instant is not. `now` reads
|
|
5
|
+
* something outside the program, so it cannot be ambient — there is no nameable
|
|
6
|
+
* primitive for it and therefore no way for a program to request one. The clock
|
|
7
|
+
* is a harness grant like any other resource, spelled `'clock:now'`, and a
|
|
8
|
+
* harness that installs no clock leaves the scheme an empty slot.
|
|
9
|
+
*
|
|
10
|
+
* Real, frozen, and scripted clocks are three entries, not three modes. The
|
|
11
|
+
* program is byte-identical across all of them, exactly as it is across any other
|
|
12
|
+
* handler swap, which is what finally lets a `;` source and `#` expected pair say
|
|
13
|
+
* something time-dependent.
|
|
14
|
+
*
|
|
15
|
+
* What holds for a program that is refused: no reading is available, whether the
|
|
16
|
+
* scheme was never bound or the clock declined, and either way the refusal is an
|
|
17
|
+
* ordinary value that flows back through evaluation rather than raising. It does
|
|
18
|
+
* *not* hold that the two are indistinguishable — `$!.resource-scheme-unbound`
|
|
19
|
+
* and `$!.clock-exhausted` are different values, and a program can read them.
|
|
20
|
+
* That is deliberate here and general to the resource primitive, whose refusals
|
|
21
|
+
* are nameable by design; `spec/a12-resource-frames.md` records the tension with
|
|
22
|
+
* a07 §7's stronger claim rather than resolving it in this one handler.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { FrameDateTime } from "./frame-time.js";
|
|
27
|
+
/** The scheme a harness binds a clock to. */
|
|
28
|
+
export const CLOCK_SCHEME = "clock";
|
|
29
|
+
/** The one reading a clock answers to. */
|
|
30
|
+
const NOW = "now";
|
|
31
|
+
/** The host clock, which prod installs and nothing else should. */
|
|
32
|
+
export class RealClock {
|
|
33
|
+
describe() {
|
|
34
|
+
return "real";
|
|
35
|
+
}
|
|
36
|
+
now() {
|
|
37
|
+
return { ok: true, instant: FrameDateTime.fromEpochMillis(Date.now()) };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One instant, answered every time.
|
|
42
|
+
*
|
|
43
|
+
* This is what retires the deterministic-build-timestamp workaround: a
|
|
44
|
+
* reproducible build pins the clock instead of deriving a substitute for one.
|
|
45
|
+
*/
|
|
46
|
+
export class FrozenClock {
|
|
47
|
+
constructor(instant) {
|
|
48
|
+
Object.defineProperty(this, "instant", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: instant
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
describe() {
|
|
56
|
+
return `frozen ${this.instant.canonical()}`;
|
|
57
|
+
}
|
|
58
|
+
now() {
|
|
59
|
+
return { ok: true, instant: this.instant };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A finite script of instants, which refuses once spent.
|
|
64
|
+
*
|
|
65
|
+
* Exhaustion is the clock refusing, which is the general shape a time budget
|
|
66
|
+
* takes: a limit on time is a duration plus a clock, not a limiter subsystem.
|
|
67
|
+
*/
|
|
68
|
+
export class ScriptedClock {
|
|
69
|
+
constructor(readings) {
|
|
70
|
+
Object.defineProperty(this, "readings", {
|
|
71
|
+
enumerable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
value: readings
|
|
75
|
+
});
|
|
76
|
+
Object.defineProperty(this, "next", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
writable: true,
|
|
80
|
+
value: 0
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
describe() {
|
|
84
|
+
return `scripted ${this.readings.length}`;
|
|
85
|
+
}
|
|
86
|
+
now() {
|
|
87
|
+
const reading = this.readings[this.next];
|
|
88
|
+
if (reading === undefined) {
|
|
89
|
+
return { ok: false, reason: "clock-exhausted" };
|
|
90
|
+
}
|
|
91
|
+
this.next += 1;
|
|
92
|
+
return { ok: true, instant: reading };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A clock that stops once a duration has elapsed.
|
|
97
|
+
*
|
|
98
|
+
* The budget is a duration value and the exhaustion is the clock refusing, so
|
|
99
|
+
* nothing here is a scheduler: the first reading starts the budget and a reading
|
|
100
|
+
* past it is `clock-budget-spent`.
|
|
101
|
+
*/
|
|
102
|
+
export class BudgetClock {
|
|
103
|
+
constructor(clock, budget) {
|
|
104
|
+
Object.defineProperty(this, "clock", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: clock
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(this, "budget", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: budget
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(this, "started", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: true,
|
|
119
|
+
writable: true,
|
|
120
|
+
value: void 0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
describe() {
|
|
124
|
+
return `budget ${this.budget.canonical()} on ${this.clock.describe()}`;
|
|
125
|
+
}
|
|
126
|
+
now() {
|
|
127
|
+
const reading = this.clock.now();
|
|
128
|
+
if (!reading.ok)
|
|
129
|
+
return reading;
|
|
130
|
+
this.started ??= reading.instant.nanos;
|
|
131
|
+
return reading.instant.nanos - this.started > this.budget.nanos
|
|
132
|
+
? { ok: false, reason: "clock-budget-spent" }
|
|
133
|
+
: reading;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Binds a clock to the `clock` scheme.
|
|
138
|
+
*
|
|
139
|
+
* Reading answers the instant itself rather than its characters, because the
|
|
140
|
+
* handler already knows the value and spelling it as text would only make every
|
|
141
|
+
* reader parse it back. Writing is refused: a clock is an observation, and a
|
|
142
|
+
* program that could set one could make its own history.
|
|
143
|
+
*/
|
|
144
|
+
export class ClockHandler {
|
|
145
|
+
constructor(clock) {
|
|
146
|
+
Object.defineProperty(this, "clock", {
|
|
147
|
+
enumerable: true,
|
|
148
|
+
configurable: true,
|
|
149
|
+
writable: true,
|
|
150
|
+
value: clock
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
describe() {
|
|
154
|
+
return `${CLOCK_SCHEME}:${this.clock.describe()}`;
|
|
155
|
+
}
|
|
156
|
+
read(reference) {
|
|
157
|
+
if (!ClockHandler.readsNow(reference)) {
|
|
158
|
+
return { ok: false, reason: "clock-unreadable" };
|
|
159
|
+
}
|
|
160
|
+
const reading = this.clock.now();
|
|
161
|
+
return reading.ok
|
|
162
|
+
? { ok: true, elements: [reading.instant] }
|
|
163
|
+
: { ok: false, reason: reading.reason };
|
|
164
|
+
}
|
|
165
|
+
write(_reference, _content) {
|
|
166
|
+
return { ok: false, reason: "clock-read-only" };
|
|
167
|
+
}
|
|
168
|
+
/** `'clock:now'` is the whole vocabulary; anything else names nothing. */
|
|
169
|
+
static readsNow(reference) {
|
|
170
|
+
if (reference.authority !== undefined)
|
|
171
|
+
return false;
|
|
172
|
+
if (reference.query !== undefined || reference.fragment !== undefined) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return (reference.path ?? "").replace(/^\/+/, "") === NOW;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which operand kinds combine, and what they answer.
|
|
3
|
+
*
|
|
4
|
+
* Datetime and duration are a two-element instance of the general dimensional
|
|
5
|
+
* rule, not a special case of time: an instant is a point on a line, a duration
|
|
6
|
+
* is an interval on it, and the algebra that relates them is the affine one.
|
|
7
|
+
* Writing it as a table rather than as branches inside a frame class is what
|
|
8
|
+
* keeps the units work from becoming a parallel implementation of the same
|
|
9
|
+
* rules — a dimensioned quantity is an interval on its own line, and a plain
|
|
10
|
+
* number is the scalar.
|
|
11
|
+
*
|
|
12
|
+
* The table is the whole ruling. A combination it does not name has no meaning,
|
|
13
|
+
* so the caller answers a type error rather than inventing one, which is how
|
|
14
|
+
* `datetime + datetime` becomes an error frame instead of a second datetime.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Every combination that means something, keyed by operator and operand kinds.
|
|
20
|
+
*
|
|
21
|
+
* Read the omissions as deliberately as the entries: `+ point point` has no
|
|
22
|
+
* meaning because adding two positions depends on an origin the values do not
|
|
23
|
+
* carry, and `- interval point` is refused because subtraction is not
|
|
24
|
+
* commutative and a displacement minus a position is not a position.
|
|
25
|
+
*/
|
|
26
|
+
const RESULTS = Object.freeze({
|
|
27
|
+
"+ point interval": "point",
|
|
28
|
+
"+ interval point": "point",
|
|
29
|
+
"+ interval interval": "interval",
|
|
30
|
+
"+ scalar scalar": "scalar",
|
|
31
|
+
"- point point": "interval",
|
|
32
|
+
"- point interval": "point",
|
|
33
|
+
"- interval interval": "interval",
|
|
34
|
+
"- scalar scalar": "scalar",
|
|
35
|
+
"* interval scalar": "interval",
|
|
36
|
+
"* scalar interval": "interval",
|
|
37
|
+
"* scalar scalar": "scalar",
|
|
38
|
+
"/ interval scalar": "interval",
|
|
39
|
+
"/ interval interval": "scalar",
|
|
40
|
+
"/ scalar scalar": "scalar",
|
|
41
|
+
});
|
|
42
|
+
/** The kind this combination answers, or undefined when it has no meaning. */
|
|
43
|
+
export const dimensionalResult = (operator, left, right) => RESULTS[`${operator} ${left} ${right}`];
|
|
@@ -166,15 +166,16 @@ export class EvaluationScope {
|
|
|
166
166
|
* the argument to the argument", which is what made a closure's write target
|
|
167
167
|
* and its argument the same frame and produced a self-referential binding.
|
|
168
168
|
*
|
|
169
|
-
* A handle argument is the one exception, because
|
|
170
|
-
* mutable view
|
|
169
|
+
* A mutable handle argument is the one exception, because the caller asked
|
|
170
|
+
* for a mutable view by spelling the name with the effect marker (`x_`). A
|
|
171
|
+
* plain name also arrives as a handle, but an immutable one, and it grants no
|
|
172
|
+
* write: `f x` must not change `x`.
|
|
171
173
|
*/
|
|
172
174
|
static call(argument, parameter = Frame.nil, closure, enclosing, receiverState) {
|
|
173
175
|
const local = new Frame();
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
: "statement";
|
|
176
|
+
const writable = argument instanceof FrameHandle && argument.isMutable();
|
|
177
|
+
const target = writable ? argument.unwrap() : local;
|
|
178
|
+
const targetRole = writable ? "handle" : "statement";
|
|
178
179
|
const lookupFrames = [local, argument];
|
|
179
180
|
if (!parameter.is.void)
|
|
180
181
|
lookupFrames.push(parameter);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Frame } from "./frame.js";
|
|
1
2
|
import { FrameExpr } from "./frame-expr.js";
|
|
2
3
|
import { FrameList } from "./frame-list.js";
|
|
3
4
|
import { FrameNote } from "./frame-note.js";
|
|
@@ -51,6 +52,58 @@ export class FrameArray extends FrameList {
|
|
|
51
52
|
}
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
55
|
+
/** Aggregates own identity, so `===` compares them by reference. */
|
|
56
|
+
ownsIdentity() {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `=` compares the complete view, the same one `&&` iterates: visible
|
|
61
|
+
* properties in declaration order, then elements by index, each by `=`.
|
|
62
|
+
* Spelling, private fields, the declared parent, and ids are not the value.
|
|
63
|
+
*/
|
|
64
|
+
equals(other) {
|
|
65
|
+
return this.sameMembers(other, (left, right) => left.equals(right));
|
|
66
|
+
}
|
|
67
|
+
/** `==` is the same walk with `==` at every node. */
|
|
68
|
+
typeEquals(other) {
|
|
69
|
+
return this.sameMembers(other, (left, right) => left.typeEquals(right));
|
|
70
|
+
}
|
|
71
|
+
sameMembers(other, same) {
|
|
72
|
+
const right = other.referent();
|
|
73
|
+
if (right === this)
|
|
74
|
+
return Frame.all;
|
|
75
|
+
if (right.constructor !== this.constructor)
|
|
76
|
+
return Frame.nil;
|
|
77
|
+
const inProgress = FrameArray.comparing.get(this) ?? new Set();
|
|
78
|
+
if (inProgress.has(right))
|
|
79
|
+
return Frame.all;
|
|
80
|
+
inProgress.add(right);
|
|
81
|
+
FrameArray.comparing.set(this, inProgress);
|
|
82
|
+
try {
|
|
83
|
+
const keys = this.visibleKeys();
|
|
84
|
+
const rightKeys = right.visibleKeys();
|
|
85
|
+
if (keys.length !== rightKeys.length ||
|
|
86
|
+
keys.some((key, index) => key !== rightKeys[index]))
|
|
87
|
+
return Frame.nil;
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
if (same(this.get_here(key), right.get_here(key)) !== Frame.all) {
|
|
90
|
+
return Frame.nil;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const elements = this.elements();
|
|
94
|
+
const rightElements = right.elements();
|
|
95
|
+
if (elements.length !== rightElements.length)
|
|
96
|
+
return Frame.nil;
|
|
97
|
+
return elements.every((element, index) => same(element, rightElements[index]) === Frame.all)
|
|
98
|
+
? Frame.all
|
|
99
|
+
: Frame.nil;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
inProgress.delete(right);
|
|
103
|
+
if (inProgress.size === 0)
|
|
104
|
+
FrameArray.comparing.delete(this);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
54
107
|
/**
|
|
55
108
|
* Aggregates are where identity matters, so an instance copy isolates them:
|
|
56
109
|
* every nested aggregate, in the data plane and the metadata plane alike, gets
|
|
@@ -127,3 +180,10 @@ Object.defineProperty(FrameArray, "SIGIL_STARTS", {
|
|
|
127
180
|
{ key: FrameArray.END_ARRAY, mode: "pop" },
|
|
128
181
|
]
|
|
129
182
|
});
|
|
183
|
+
/** Pairs under comparison, so a frame reachable from itself terminates. */
|
|
184
|
+
Object.defineProperty(FrameArray, "comparing", {
|
|
185
|
+
enumerable: true,
|
|
186
|
+
configurable: true,
|
|
187
|
+
writable: true,
|
|
188
|
+
value: new Map()
|
|
189
|
+
});
|
|
@@ -60,6 +60,20 @@ export class FrameHandle extends Frame {
|
|
|
60
60
|
unwrap() {
|
|
61
61
|
return this.target;
|
|
62
62
|
}
|
|
63
|
+
/** A handle is transparent to equality: it compares as its target. */
|
|
64
|
+
referent() {
|
|
65
|
+
return this.target.referent();
|
|
66
|
+
}
|
|
67
|
+
equals(other) {
|
|
68
|
+
return this.target.equals(other);
|
|
69
|
+
}
|
|
70
|
+
typeEquals(other) {
|
|
71
|
+
return this.target.typeEquals(other);
|
|
72
|
+
}
|
|
73
|
+
/** Whether the name that produced this handle carried the effect marker. */
|
|
74
|
+
isMutable() {
|
|
75
|
+
return this.mutable;
|
|
76
|
+
}
|
|
63
77
|
/** The lexical fallback selected for this particular read. */
|
|
64
78
|
readContextFrame() {
|
|
65
79
|
return this.readContext;
|
|
@@ -78,7 +78,7 @@ export class FrameNumeric extends FrameAtom {
|
|
|
78
78
|
}
|
|
79
79
|
divide(right) {
|
|
80
80
|
if (this.rank == null || right.rank == null) {
|
|
81
|
-
return this.operationError("/", right);
|
|
81
|
+
return this.reflect("/", right) ?? this.operationError("/", right);
|
|
82
82
|
}
|
|
83
83
|
if (right.isZero())
|
|
84
84
|
return Frame.error("$!.division-by-zero /");
|
|
@@ -171,6 +171,17 @@ export class FrameNumeric extends FrameAtom {
|
|
|
171
171
|
exactInt(_max) {
|
|
172
172
|
return Frame.error(`$!.exact-integer-required ${this.className()}`);
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The exact ratio this frame denotes, or null when it denotes none.
|
|
176
|
+
*
|
|
177
|
+
* Published so a null-ranked peer outside the promotion ladder can scale by an
|
|
178
|
+
* exact scalar without joining the ladder to do it. An inexact value answers
|
|
179
|
+
* null, which is what keeps such a peer's arithmetic exact by construction
|
|
180
|
+
* rather than by rounding.
|
|
181
|
+
*/
|
|
182
|
+
exactRatio() {
|
|
183
|
+
return this.ratioParts();
|
|
184
|
+
}
|
|
174
185
|
operationError(operator, right) {
|
|
175
186
|
return Frame.error(`$!.numeric-domain ${operator} ${this.className()} ${right.className()}`);
|
|
176
187
|
}
|
|
@@ -180,6 +191,17 @@ export class FrameNumeric extends FrameAtom {
|
|
|
180
191
|
repeat(_argument, _parameter) {
|
|
181
192
|
return this.repetitionError();
|
|
182
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* A right operand's chance to define an operation its left cannot.
|
|
196
|
+
*
|
|
197
|
+
* A value outside the promotion ladder carries rules the ladder does not know:
|
|
198
|
+
* a duration times a number is a duration, whichever side the number was
|
|
199
|
+
* written on. Answering `undefined` — which every ranked type does — leaves the
|
|
200
|
+
* domain error exactly as it was, so this adds a case rather than changing one.
|
|
201
|
+
*/
|
|
202
|
+
combineFrom(_operator, _left) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
183
205
|
/**
|
|
184
206
|
* A numeric `up` link is the receiver a property lookup climbed from, not a
|
|
185
207
|
* lexical scope, so it must not answer a later segment. Without this, an
|
|
@@ -201,11 +223,18 @@ export class FrameNumeric extends FrameAtom {
|
|
|
201
223
|
}
|
|
202
224
|
join(right, operator, operation) {
|
|
203
225
|
if (this.rank == null || right.rank == null) {
|
|
204
|
-
return this.
|
|
226
|
+
return this.reflect(operator, right) ??
|
|
227
|
+
this.operationError(operator, right);
|
|
205
228
|
}
|
|
206
229
|
const target = Math.max(this.rank, right.rank);
|
|
207
230
|
return operation(this.promoteTo(target), right.promoteTo(target));
|
|
208
231
|
}
|
|
232
|
+
/** Offers an unranked right operand the operation, once, before refusing. */
|
|
233
|
+
reflect(operator, right) {
|
|
234
|
+
return this.rank != null && right.rank == null
|
|
235
|
+
? right.combineFrom(operator, this)
|
|
236
|
+
: undefined;
|
|
237
|
+
}
|
|
209
238
|
order(right, operator, predicate) {
|
|
210
239
|
if (this.rank == null || right.rank == null) {
|
|
211
240
|
return this.operationError(operator, right);
|