hclang 0.15.1 → 0.16.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 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
- const env = dntShim.Deno.env.toObject();
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
+ };
@@ -32,6 +32,7 @@ export const syntaxFacets = [
32
32
  frame.FrameString.SYNTAX,
33
33
  frame.FrameStringEnd.SYNTAX,
34
34
  frame.FrameSymbol.SYNTAX,
35
+ frame.FrameTime.SYNTAX,
35
36
  frame.FrameURI.SYNTAX,
36
37
  ];
37
38
  const lexicalModes = {
@@ -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}`];
@@ -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.operationError(operator, right);
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);
@@ -12,8 +12,12 @@
12
12
  * promises about a resource identifier still has to hold. A resource prints as
13
13
  * the reference it was written as, compares by that reference, and publishes the
14
14
  * same RFC 3986 components. What it adds is the authority, and the authority is
15
- * private: the store is a TypeScript field, never metadata, so it is not
16
- * reachable by property enumeration.
15
+ * private: the store and the handler table are TypeScript fields, never
16
+ * metadata, so neither is reachable by property enumeration.
17
+ *
18
+ * Two planes, selected by whether the reference publishes a scheme. The path
19
+ * plane extends the root binding and is bounded by normalization. The scheme
20
+ * plane dispatches to a handler table entry, where an empty slot is the refusal.
17
21
  *
18
22
  * @module
19
23
  */
@@ -22,7 +26,8 @@ import { FrameInt } from "./frame-int.js";
22
26
  import { FrameString } from "./frame-string.js";
23
27
  import { FrameURI } from "./frame-uri.js";
24
28
  import { hasCharacterContent } from "./frame-text.js";
25
- import { joinNormalized, normalizeReference, } from "./resource-reference.js";
29
+ import { ResourceHandlers } from "./resource-handlers.js";
30
+ import { decomposeReference, joinNormalized, normalizeReference, } from "./resource-reference.js";
26
31
  /** The reference a root binding prints and normalizes as itself. */
27
32
  const ROOT_REFERENCE = ".";
28
33
  export class FrameResource extends FrameURI {
@@ -32,16 +37,23 @@ export class FrameResource extends FrameURI {
32
37
  * The root is a resource at the empty path, which is what makes extension the
33
38
  * only operation and attenuation structural rather than checked: a child is a
34
39
  * longer path on the same store, and no path is shorter than the empty one.
40
+ *
41
+ * The handler table defaults to empty, so a harness that installs nothing
42
+ * grants nothing: every scheme is an empty slot until one is bound.
35
43
  */
36
- static root(store) {
37
- return new FrameResource(ROOT_REFERENCE, store, { ok: true, path: "" });
44
+ static root(store, handlers = ResourceHandlers.none) {
45
+ return new FrameResource(ROOT_REFERENCE, store, handlers, {
46
+ kind: "path",
47
+ path: "",
48
+ });
38
49
  }
39
50
  /**
40
51
  * @param reference The reference as written, which stays the printed form.
41
- * @param store The host seam this resource and its children share.
42
- * @param location The normalization decided before this frame existed.
52
+ * @param store The host seam this resource and its path children share.
53
+ * @param handlers The scheme table this resource dispatches through.
54
+ * @param placement The plane and location decided before this frame existed.
43
55
  */
44
- constructor(reference, store, location) {
56
+ constructor(reference, store, handlers, placement) {
45
57
  super(reference);
46
58
  Object.defineProperty(this, "store", {
47
59
  enumerable: true,
@@ -49,23 +61,29 @@ export class FrameResource extends FrameURI {
49
61
  writable: true,
50
62
  value: store
51
63
  });
52
- Object.defineProperty(this, "location", {
64
+ Object.defineProperty(this, "handlers", {
65
+ enumerable: true,
66
+ configurable: true,
67
+ writable: true,
68
+ value: handlers
69
+ });
70
+ Object.defineProperty(this, "placement", {
53
71
  enumerable: true,
54
72
  configurable: true,
55
73
  writable: true,
56
- value: location
74
+ value: placement
57
75
  });
58
76
  }
59
77
  /**
60
78
  * Extends this binding by one reference.
61
79
  *
62
- * Normalization runs here, before any dispatch could select a handler, and it
63
- * is pure: a refused reference still yields a resource, carrying its refusal.
64
- * Surfacing the refusal at construction instead would make evaluating
80
+ * Normalization runs here, before dispatch selects a handler, and it is pure:
81
+ * a refused reference still yields a resource, carrying its refusal. Surfacing
82
+ * the refusal at construction instead would make evaluating
65
83
  * `'https://example.com/x'` an error, and a03 requires evaluation to be inert.
66
84
  */
67
85
  extend(reference) {
68
- return new FrameResource(reference, this.store, this.locate(reference));
86
+ return new FrameResource(reference, this.store, this.handlers, this.locate(reference));
69
87
  }
70
88
  /** A resource is a value, and evaluating one performs no access. */
71
89
  in() {
@@ -79,33 +97,46 @@ export class FrameResource extends FrameURI {
79
97
  * masquerade as it.
80
98
  */
81
99
  apply(argument, _parameter = Frame.nil) {
82
- const path = this.contained();
83
- if (path === undefined)
84
- return this.refusal();
100
+ const placement = this.placement;
101
+ if (placement.kind === "refused") {
102
+ return this.storeRefusal(placement.reason);
103
+ }
85
104
  const content = hasCharacterContent(argument)
86
105
  ? argument.characterContent()
87
106
  : argument.toString();
88
- const written = this.store.write(path, content);
107
+ if (placement.kind === "scheme") {
108
+ const written = placement.handler.write(placement.parts, content);
109
+ return written.ok
110
+ ? FrameInt.for(written.count.toString())
111
+ : this.storeRefusal(written.reason);
112
+ }
113
+ const written = this.store.write(placement.path, content);
89
114
  return written.ok
90
115
  ? FrameInt.for(written.content.length.toString())
91
116
  : this.storeRefusal(written.reason);
92
117
  }
93
118
  /**
94
- * Reading pushes characters, because the character is the primitive.
119
+ * Reading pushes elements, and for a store the element is the character.
95
120
  *
96
121
  * No part of the reading is a property of the source: a receiver accepts the
97
122
  * characters and decides whether they are whole content, a list, chunks, or HC
98
- * code. That is why nothing here declares an element type, and why choosing
99
- * lines would have decided downstream composition by accident.
123
+ * code. A scheme handler answers its own elements instead, because a handler
124
+ * is not required to be a byte store — a clock knows the instant it read, and
125
+ * spelling it as characters would only make the reader parse it back.
100
126
  *
101
127
  * A refusal arrives as the single element, so it flows back through the
102
128
  * reduce as an ordinary value rather than raising.
103
129
  */
104
130
  elements() {
105
- const path = this.contained();
106
- if (path === undefined)
107
- return [this.refusal()];
108
- const read = this.store.read(path);
131
+ const placement = this.placement;
132
+ if (placement.kind === "refused") {
133
+ return [this.storeRefusal(placement.reason)];
134
+ }
135
+ if (placement.kind === "scheme") {
136
+ const read = placement.handler.read(placement.parts);
137
+ return read.ok ? [...read.elements] : [this.storeRefusal(read.reason)];
138
+ }
139
+ const read = this.store.read(placement.path);
109
140
  if (!read.ok)
110
141
  return [this.storeRefusal(read.reason)];
111
142
  return [...read.content].map((char) => new FrameString(char));
@@ -125,28 +156,55 @@ export class FrameResource extends FrameURI {
125
156
  describeRoot() {
126
157
  return this.store.describe();
127
158
  }
159
+ /**
160
+ * The schemes this binding can reach, for a harness or a test.
161
+ *
162
+ * Not reachable from HC. The adapter surface is enumerable so a harness can
163
+ * audit it, and a program that could enumerate it could probe its host.
164
+ */
165
+ boundSchemes() {
166
+ return this.handlers.schemes();
167
+ }
128
168
  /**
129
169
  * Locates a child, keeping a refused parent refused.
130
170
  *
131
171
  * A refusal is inherited before the child's own reference is even consulted,
132
172
  * because attenuation has no meaning below a location that was never reached.
133
173
  * Extending a refusal must not launder it into an allowed path.
174
+ *
175
+ * A handler's resource is a leaf: what lies below `'https://host/a'` is the
176
+ * handler's business, not a path this binding may compose, so extending one is
177
+ * refused rather than guessed at.
134
178
  */
135
179
  locate(reference) {
136
- if (!this.location.ok)
137
- return this.location;
180
+ if (this.placement.kind === "refused")
181
+ return this.placement;
182
+ if (this.placement.kind === "scheme") {
183
+ return { kind: "refused", reason: "resource-not-extensible" };
184
+ }
185
+ const parts = decomposeReference(reference);
186
+ if (parts.scheme !== undefined)
187
+ return this.dispatch(parts);
138
188
  const normal = normalizeReference(reference);
139
189
  if (!normal.ok)
140
- return normal;
141
- return { ok: true, path: joinNormalized(this.location.path, normal.path) };
190
+ return { kind: "refused", reason: normal.reason };
191
+ return {
192
+ kind: "path",
193
+ path: joinNormalized(this.placement.path, normal.path),
194
+ };
142
195
  }
143
- /** The normalized path, or undefined when normalization refused. */
144
- contained() {
145
- return this.location.ok ? this.location.path : undefined;
146
- }
147
- refusal() {
148
- const reason = this.location.ok ? "resource-refused" : this.location.reason;
149
- return this.storeRefusal(reason);
196
+ /**
197
+ * Selects the handler a scheme names, or refuses without a host call.
198
+ *
199
+ * The empty slot is the refusal. Nothing here consults a policy, and nothing
200
+ * reaches a host: an unbound scheme is decided by a table read.
201
+ */
202
+ dispatch(parts) {
203
+ const scheme = parts.scheme ?? "";
204
+ const handler = this.handlers.handler(scheme);
205
+ return handler === undefined
206
+ ? { kind: "refused", reason: "resource-scheme-unbound" }
207
+ : { kind: "scheme", handler, parts };
150
208
  }
151
209
  storeRefusal(reason) {
152
210
  return Frame.error(`$!.${reason} ${this.toString()}`);