hclang 0.17.1 → 0.18.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.
@@ -28,7 +28,7 @@ export class EvalPipe extends Frame {
28
28
  }
29
29
  apply(expr, context) {
30
30
  const result = expr.in(EvaluationScope.root(this.fileScope, this.hostNamespace));
31
- this.out.apply(result, context);
31
+ this.out.collect(result, context);
32
32
  return result;
33
33
  }
34
34
  }
@@ -1,4 +1,6 @@
1
1
  import { Frame } from "./frame.js";
2
+ import { BoundMethod } from "./bound-method.js";
3
+ import { methodEffect } from "./effect-marker.js";
2
4
  import { FrameExpr } from "./frame-expr.js";
3
5
  import { FrameList } from "./frame-list.js";
4
6
  import { FrameNote } from "./frame-note.js";
@@ -119,7 +121,7 @@ export class FrameArray extends FrameList {
119
121
  }
120
122
  const clone = this.copy();
121
123
  seen.set(this, clone);
122
- clone.data = this.data.map((item) => item.instanceCopy(seen));
124
+ clone.data = this.itemsFor(clone, (item) => item.instanceCopy(seen));
123
125
  for (const [key, value] of this.meta_pairs()) {
124
126
  clone.set(key, value.instanceCopy(seen));
125
127
  }
@@ -131,9 +133,50 @@ export class FrameArray extends FrameList {
131
133
  }
132
134
  return super.lookup_here(key, origin);
133
135
  }
134
- /** An aggregate collects, which is what makes applying to one a push. */
136
+ /**
137
+ * An aggregate collects, which is what makes applying to one a push, unless
138
+ * it declares its own application. A declared application is an operation,
139
+ * so a refusal applied to one stays a refusal rather than being held.
140
+ */
135
141
  collects() {
136
- return true;
142
+ return this.applicationMethod() === undefined;
143
+ }
144
+ /**
145
+ * The method this aggregate declares as its own application (#375).
146
+ *
147
+ * Applying an aggregate collects, and a receiver that wants another rule
148
+ * declares it as a method named `apply`, or `apply_` when the rule writes the
149
+ * receiver. It is found on the aggregate or its declared parents, the same
150
+ * chain a class's methods come from, and never in the scope around it, so a
151
+ * plain array keeps collecting wherever it is written.
152
+ */
153
+ applicationMethod() {
154
+ for (const key of FrameArray.APPLICATION_KEYS) {
155
+ const seen = new Set([this]);
156
+ let local = this.get_here(key);
157
+ let next = this.hasDeclaredParent() ? this.parent : undefined;
158
+ while (local.is.missing && next && !seen.has(next)) {
159
+ seen.add(next);
160
+ local = next.get_here(key);
161
+ next = next.hasDeclaredParent() ? next.parent : undefined;
162
+ }
163
+ if (local.className() === "FrameLazy")
164
+ return { key, method: local };
165
+ }
166
+ return undefined;
167
+ }
168
+ /**
169
+ * Runs a declared application with the receiver it binds to.
170
+ *
171
+ * The effect axis decides writes, as it does for every method: `apply_`
172
+ * reached mutably writes the receiver and answers it, and reached immutably
173
+ * answers an updated copy. A step that answers nil leaves the receiver as it
174
+ * was, the way an aggregate ignores a nil argument, so a reduce can skip an
175
+ * element without losing what it accumulated.
176
+ */
177
+ static applyDeclared(bound, argument, unchanged) {
178
+ const result = bound.call(argument);
179
+ return result.is.void ? unchanged : result;
137
180
  }
138
181
  /**
139
182
  * A fresh aggregate holding the same elements. Only the outer aggregate is
@@ -141,13 +184,20 @@ export class FrameArray extends FrameList {
141
184
  * copying, and `===` still finds each one.
142
185
  */
143
186
  collectingCopy() {
144
- const result = this.copy();
145
- result.data = [...this.data];
146
- return result;
187
+ return this.copy();
147
188
  }
148
189
  apply(argument, _parameter) {
149
- if (!argument.is.void) {
150
- this.data.push(argument);
190
+ const declared = this.applicationMethod();
191
+ if (declared) {
192
+ const bound = new BoundMethod(declared.method, this, true, methodEffect(declared.key));
193
+ return FrameArray.applyDeclared(bound, argument, this);
194
+ }
195
+ return this.collect(argument);
196
+ }
197
+ /** Pushes a non-nil value, whatever application this aggregate declares. */
198
+ collect(value, _context = Frame.nil) {
199
+ if (!value.is.void) {
200
+ this.data.push(value);
151
201
  }
152
202
  return this;
153
203
  }
@@ -197,3 +247,10 @@ Object.defineProperty(FrameArray, "comparing", {
197
247
  writable: true,
198
248
  value: new Map()
199
249
  });
250
+ /** The keys an aggregate declares its own application under, mutating first. */
251
+ Object.defineProperty(FrameArray, "APPLICATION_KEYS", {
252
+ enumerable: true,
253
+ configurable: true,
254
+ writable: true,
255
+ value: ["apply_", "apply"]
256
+ });
@@ -134,10 +134,20 @@ export class FrameHandle extends Frame {
134
134
  * Applying through a name answers what applying the value would, without
135
135
  * writing through the name. A collect lands on a fresh aggregate, so `xs 3`
136
136
  * answers a new array and `xs` is unchanged, whether or not the name is
137
- * mutable: a write goes through a mutating method or `@`, never application.
137
+ * mutable: a write goes through a mutating method or `@`, never a collect.
138
+ * An aggregate that declares its own application (#375) is the exception
139
+ * by declaration: its `apply_` method writes like any mutating method.
138
140
  */
139
141
  apply(argument, parameter) {
140
- return this.target.collectingCopy().apply(argument, parameter);
142
+ const declared = this.target.applicationMethod();
143
+ if (!declared) {
144
+ return this.target.collectingCopy().apply(argument, parameter);
145
+ }
146
+ // A declared application is a method, so this name's mutability decides
147
+ // whether it writes the target or answers an updated copy, and a nil
148
+ // answer leaves the accumulator as it was (`FrameArray.applyDeclared`).
149
+ const result = new BoundMethod(declared.method, this.target, this.mutable, methodEffect(declared.key), this.copyOnWrite, this.readContext).call(argument, parameter);
150
+ return result.is.void ? this : result;
141
151
  }
142
152
  toString() {
143
153
  return this.target.toString();
@@ -134,9 +134,35 @@ export class FrameList extends Frame {
134
134
  }
135
135
  copy() {
136
136
  const clone = super.copy();
137
- clone.data = [...this.data];
137
+ clone.data = this.itemsFor(clone, (item) => item);
138
138
  return clone;
139
139
  }
140
+ /**
141
+ * This frame's items as a copy holds them.
142
+ *
143
+ * An echo records which frame holds its property, so an echo this frame
144
+ * holds is re-issued for the copy. Otherwise the copy would print and
145
+ * iterate its own declarations as though another frame held them.
146
+ */
147
+ itemsFor(clone, other) {
148
+ return this.data.map((item) => this.holdsAsProperty(item)
149
+ ? FrameList.reownedEcho(item, clone)
150
+ : other(item));
151
+ }
152
+ /**
153
+ * A declaration echo re-issued for `owner`, looking through a statement's
154
+ * wrapper. The echo is read structurally for the same reason as
155
+ * `echoedBinding`.
156
+ */
157
+ static reownedEcho(term, owner) {
158
+ if (term.is.statement === true && term instanceof FrameList) {
159
+ const wrapper = term.copy();
160
+ wrapper.data = term.data.map((inner) => FrameList.reownedEcho(inner, owner));
161
+ return wrapper;
162
+ }
163
+ const echo = term;
164
+ return echo.reownedBy ? echo.reownedBy(owner) : term;
165
+ }
140
166
  /** Evaluate source items with an explicit innermost declaration target. */
141
167
  array_eval(input, out = this) {
142
168
  const scope = EvaluationScope.from(input).withWriteTarget(out, "construction");
@@ -41,6 +41,18 @@ export class FrameLiteral extends FrameAtom {
41
41
  toData() {
42
42
  return this.data;
43
43
  }
44
+ /**
45
+ * The same echo, held by `owner`. A copy of the declaring aggregate holds
46
+ * the copied property, so it needs an echo that names it as the holder.
47
+ */
48
+ reownedBy(owner) {
49
+ if (!this.binding)
50
+ return this;
51
+ return new FrameLiteral(this.data, {
52
+ ...this.binding,
53
+ target: new WeakRef(owner),
54
+ });
55
+ }
44
56
  }
45
57
  export class FrameSymbol extends FrameAtom {
46
58
  static for(symbol) {
@@ -313,6 +313,28 @@ export class Frame extends MetaFrame {
313
313
  collectingCopy() {
314
314
  return this;
315
315
  }
316
+ /**
317
+ * Hands a result to this frame as the interpreter's output, not as an
318
+ * application written in the program.
319
+ *
320
+ * The evaluator's output array is also the file scope, where top-level
321
+ * declarations land, so a program can declare an application on it (#375).
322
+ * Collecting output must not run that declaration, so an aggregate collects
323
+ * here unconditionally. Any other output receiver, such as a test harness,
324
+ * is applied as usual.
325
+ */
326
+ collect(value, context = Frame.nil) {
327
+ return this.apply(value, context);
328
+ }
329
+ /**
330
+ * The method this frame declares as its own application, if any.
331
+ *
332
+ * Only an aggregate can declare one, so the base answers none. See
333
+ * `FrameArray.applicationMethod` for the protocol.
334
+ */
335
+ applicationMethod() {
336
+ return undefined;
337
+ }
316
338
  /**
317
339
  * The values this frame streams when it is iterated.
318
340
  *
@@ -1 +1 @@
1
- export const VERSION = "0.17.1";
1
+ export const VERSION = "0.18.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hclang",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Homoiconic C command-line interpreter and REPL",
5
5
  "keywords": [
6
6
  "language",