hclang 0.16.0 → 0.17.1

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.
@@ -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 a handle is explicitly a
170
- * mutable view requested by the caller for exactly this purpose.
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 target = argument instanceof FrameHandle ? argument.unwrap() : local;
175
- const targetRole = argument instanceof FrameHandle
176
- ? "handle"
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
@@ -82,6 +135,16 @@ export class FrameArray extends FrameList {
82
135
  collects() {
83
136
  return true;
84
137
  }
138
+ /**
139
+ * A fresh aggregate holding the same elements. Only the outer aggregate is
140
+ * new: the elements are shared, so collecting reuses them rather than
141
+ * copying, and `===` still finds each one.
142
+ */
143
+ collectingCopy() {
144
+ const result = this.copy();
145
+ result.data = [...this.data];
146
+ return result;
147
+ }
85
148
  apply(argument, _parameter) {
86
149
  if (!argument.is.void) {
87
150
  this.data.push(argument);
@@ -127,3 +190,10 @@ Object.defineProperty(FrameArray, "SIGIL_STARTS", {
127
190
  { key: FrameArray.END_ARRAY, mode: "pop" },
128
191
  ]
129
192
  });
193
+ /** Pairs under comparison, so a frame reachable from itself terminates. */
194
+ Object.defineProperty(FrameArray, "comparing", {
195
+ enumerable: true,
196
+ configurable: true,
197
+ writable: true,
198
+ value: new Map()
199
+ });
@@ -7,10 +7,11 @@ import { methodEffect } from "./effect-marker.js";
7
7
  *
8
8
  * The handle's charter is that reference and nothing more. Two rules define it:
9
9
  *
10
- * - **Transparency.** Wrapping must not change what a value prints, equals, or
11
- * exposes, so rendering, data, metadata, and array views all delegate to the
12
- * target. A handle is invisible to comparison, and assignment unwraps it, so
13
- * a mutation lands on the same frame it would have without the wrapper.
10
+ * - **Transparency.** Wrapping must not change what a value prints, equals,
11
+ * exposes, or answers when applied, so rendering, data, metadata, array
12
+ * views, and application all delegate to the target. A handle is invisible to
13
+ * comparison, and assignment unwraps it, so a mutation lands on the same
14
+ * frame it would have without the wrapper.
14
15
  * - **Caller-scoped lookup.** A handle created by symbol lookup carries that
15
16
  * read's lexical context separately from the shared target. Local and
16
17
  * declared-parent lookup remains live on the target, then falls back through
@@ -60,6 +61,20 @@ export class FrameHandle extends Frame {
60
61
  unwrap() {
61
62
  return this.target;
62
63
  }
64
+ /** A handle is transparent to equality: it compares as its target. */
65
+ referent() {
66
+ return this.target.referent();
67
+ }
68
+ equals(other) {
69
+ return this.target.equals(other);
70
+ }
71
+ typeEquals(other) {
72
+ return this.target.typeEquals(other);
73
+ }
74
+ /** Whether the name that produced this handle carried the effect marker. */
75
+ isMutable() {
76
+ return this.mutable;
77
+ }
63
78
  /** The lexical fallback selected for this particular read. */
64
79
  readContextFrame() {
65
80
  return this.readContext;
@@ -111,6 +126,19 @@ export class FrameHandle extends Frame {
111
126
  called_by(context, parameter) {
112
127
  return context.apply(this.target, parameter);
113
128
  }
129
+ /** Transparency covers application: a named aggregate collects like its literal. */
130
+ collects() {
131
+ return this.target.collects();
132
+ }
133
+ /**
134
+ * Applying through a name answers what applying the value would, without
135
+ * writing through the name. A collect lands on a fresh aggregate, so `xs 3`
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.
138
+ */
139
+ apply(argument, parameter) {
140
+ return this.target.collectingCopy().apply(argument, parameter);
141
+ }
114
142
  toString() {
115
143
  return this.target.toString();
116
144
  }
@@ -66,6 +66,13 @@ export class FrameTypedNumber extends FrameNumeric {
66
66
  }
67
67
  return super.lookup_here(key, origin);
68
68
  }
69
+ /** `==` also asks the magnitude's rung: `3.m == 3.0.m` is nil. */
70
+ typeEquals(right) {
71
+ if (!(right instanceof FrameTypedNumber) || this.unit !== right.unit) {
72
+ return Frame.nil;
73
+ }
74
+ return this.magnitude.typeEquals(right.magnitude);
75
+ }
69
76
  equals(right) {
70
77
  if (!(right instanceof FrameTypedNumber) || this.unit !== right.unit) {
71
78
  return Frame.nil;
@@ -179,6 +179,41 @@ export class Frame extends MetaFrame {
179
179
  equals(other) {
180
180
  return this.toString() === other.toString() ? Frame.all : Frame.nil;
181
181
  }
182
+ /**
183
+ * typeEquals is `==`: the same value, and the same type at every node.
184
+ * It is strictly stronger than `equals`, so `3 == 3.0` is nil while
185
+ * `3 = 3.0` holds.
186
+ */
187
+ typeEquals(other) {
188
+ const left = this.referent();
189
+ const right = other.referent();
190
+ return left.constructor === right.constructor
191
+ ? left.equals(right)
192
+ : Frame.nil;
193
+ }
194
+ /**
195
+ * identical is `===`: whether both references reach the same frame.
196
+ *
197
+ * Only a frame that owns identity can be told apart from an equal copy.
198
+ * Atoms and closure bodies are shared by the copy contract, so sharing them
199
+ * is unobservable and identity falls back to `typeEquals`, which keeps the
200
+ * ladder monotone: `===` implies `==` implies `=`. No id is ever exposed.
201
+ */
202
+ identical(other) {
203
+ const left = this.referent();
204
+ const right = other.referent();
205
+ if (left.ownsIdentity())
206
+ return left === right ? Frame.all : Frame.nil;
207
+ return left.typeEquals(right);
208
+ }
209
+ /** The frame a reference reaches. A handle answers its target. */
210
+ referent() {
211
+ return this;
212
+ }
213
+ /** Whether a write can land on this frame, so that identity is observable. */
214
+ ownsIdentity() {
215
+ return false;
216
+ }
182
217
  /** Compare only this frame's data plane, excluding metadata. */
183
218
  dataEquals(other) {
184
219
  return this.dataString() === other.dataString() ? Frame.all : Frame.nil;
@@ -267,6 +302,17 @@ export class Frame extends MetaFrame {
267
302
  collects() {
268
303
  return false;
269
304
  }
305
+ /**
306
+ * The receiver a collect lands on when it must not write this frame.
307
+ *
308
+ * A literal is a fresh aggregate each time it is evaluated, so collecting
309
+ * into it in place is unobservable. A value reached through a name is not,
310
+ * so the name asks for this instead. Anything that does not collect is its
311
+ * own receiver.
312
+ */
313
+ collectingCopy() {
314
+ return this;
315
+ }
270
316
  /**
271
317
  * The values this frame streams when it is iterated.
272
318
  *
@@ -5,9 +5,14 @@ export const IfThen = (source, block, receiverState) => {
5
5
  }
6
6
  return Frame.nil;
7
7
  };
8
+ /**
9
+ * `:` is "or else": a value that is not nil answers itself, and only nil calls
10
+ * the fallback. Composed after `?`, that makes `c ? t : e` answer `t` or `e`,
11
+ * because the taken branch's value reaches `:` as its source.
12
+ */
8
13
  export const IfElse = (source, block, receiverState) => {
9
14
  if (source === Frame.nil) {
10
15
  return block.call(Frame.nil, Frame.nil, receiverState);
11
16
  }
12
- return Frame.nil;
17
+ return source;
13
18
  };
@@ -13,11 +13,13 @@ export const Power = (source, block) => numeric(source, block, (left, right) =>
13
13
  export const Equals = (source, block) => {
14
14
  return source.equals(block);
15
15
  };
16
- export const DataEquals = (source, block) => {
17
- return source.dataEquals(block);
16
+ /** `==`: the same value and the same type at every node. */
17
+ export const TypeEquals = (source, block) => {
18
+ return source.typeEquals(block);
18
19
  };
19
- export const MetadataEquals = (source, block) => {
20
- return source.metadataEquals(block);
20
+ /** `===`: the same frame. Atoms, which own no identity, fall back to `==`. */
21
+ export const Identical = (source, block) => {
22
+ return source.identical(block);
21
23
  };
22
24
  export const GreaterThan = (source, block) => numeric(source, block, (left, right) => left.greaterThan(right));
23
25
  export const GreaterThanOrEqual = (source, block) => numeric(source, block, (left, right) => left.greaterThanOrEqual(right));
package/esm/lib/ops.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { IfElse, IfThen } from "./ops/conditionals.js";
2
2
  import { FrameOps } from "./ops/frame-ops.js";
3
3
  import { MapElements, MapMembers, ReduceElements, ReduceMembers, } from "./ops/iterators.js";
4
- import { Add, DataEquals, Divide, Equals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, MetadataEquals, Modulo, Multiply, Power, Subtract, } from "./ops/math.js";
4
+ import { Add, Divide, Equals, GreaterThan, GreaterThanOrEqual, Identical, LessThan, LessThanOrEqual, Modulo, Multiply, Power, Subtract, TypeEquals, } from "./ops/math.js";
5
5
  import { BindType, HasType } from "./ops/type-operations.js";
6
6
  export { FrameCurry } from "./ops/frame-curry.js";
7
7
  export const Ops = new FrameOps({
@@ -18,8 +18,8 @@ export const Ops = new FrameOps({
18
18
  "%%": Modulo,
19
19
  "**": Power,
20
20
  "=": Equals,
21
- "==": DataEquals,
22
- "===": MetadataEquals,
21
+ "==": TypeEquals,
22
+ "===": Identical,
23
23
  ">": GreaterThan,
24
24
  ">>": GreaterThan,
25
25
  ">=": GreaterThanOrEqual,
@@ -1 +1 @@
1
- export const VERSION = "0.16.0";
1
+ export const VERSION = "0.17.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hclang",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Homoiconic C command-line interpreter and REPL",
5
5
  "keywords": [
6
6
  "language",