@graffiticode/basis 1.5.18 → 1.5.20
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/package.json +3 -2
- package/spec/spec.html +7 -4
- package/spec/spec.md +12 -4
- package/src/compiler.js +141 -18
- package/src/lexicon.js +32 -2
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graffiticode/basis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.20",
|
|
5
5
|
"description": "The basis library for creating Graffiticode languages",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest",
|
|
9
9
|
"build-spec": "npx spec-md ./spec/spec.md > ./spec/spec.html",
|
|
10
|
-
"watch-spec": "npx nodemon --exec 'npx spec-md > ./spec/spec.html' ./spec/spec.md"
|
|
10
|
+
"watch-spec": "npx nodemon --exec 'npx spec-md > ./spec/spec.html' ./spec/spec.md",
|
|
11
|
+
"publish-spec": "npm run build-spec && for dir in ../l0*/packages/api/public; do cp ./spec/spec.html \"$dir/graffiticode-language-spec.html\"; echo \"Copied to $dir/graffiticode-language-spec.html\"; done"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": "22.x"
|
package/spec/spec.html
CHANGED
|
@@ -174,6 +174,9 @@ end
|
|
|
174
174
|
<li><strong>Strict evaluation</strong>: arguments evaluated before function application</li>
|
|
175
175
|
<li><strong>Immutable data</strong>: all values are immutable</li>
|
|
176
176
|
</ul>
|
|
177
|
+
<p>Many built-in functions in Graffiticode follow a model-threading pattern. In this pattern, functions are defined to take one or more arguments followed by a model, which represents the current state of the program or view. The function uses the earlier arguments to compute an update to the model and returns a new model as its result.</p>
|
|
178
|
+
<p>This style enables a declarative and order-independent composition of functions. Since each function call returns a new model, multiple calls can be reordered without changing the final result, provided the functional dependencies are preserved.</p>
|
|
179
|
+
<p>This approach draws inspiration from <strong>Model-View-Update</strong> (MVU) architectures, in which the model represents the application state and functions describe pure, deterministic transformations of that state.</p>
|
|
177
180
|
</section>
|
|
178
181
|
<section id="sec-Functions" secid="4.2">
|
|
179
182
|
<h2><span class="spec-secid" title="link to this section"><a href="#sec-Functions">4.2</a></span>Functions</h2>
|
|
@@ -305,12 +308,12 @@ end
|
|
|
305
308
|
</tr>
|
|
306
309
|
<tr>
|
|
307
310
|
<td align="left"><code>get</code></td>
|
|
308
|
-
<td align="left"><code><record
|
|
311
|
+
<td align="left"><code><string record: any></code></td>
|
|
309
312
|
<td align="left">Retrieves a value from a record by key</td>
|
|
310
313
|
</tr>
|
|
311
314
|
<tr>
|
|
312
315
|
<td align="left"><code>set</code></td>
|
|
313
|
-
<td align="left"><code><
|
|
316
|
+
<td align="left"><code><string any record: record></code></td>
|
|
314
317
|
<td align="left">Returns a new record with a key set to a value</td>
|
|
315
318
|
</tr>
|
|
316
319
|
</tbody>
|
|
@@ -414,13 +417,13 @@ end
|
|
|
414
417
|
<section id="sec-get" secid="5.2.17">
|
|
415
418
|
<h3><span class="spec-secid" title="link to this section"><a href="#sec-get">5.2.17</a></span>get</h3>
|
|
416
419
|
<p>Retrieve a record field</p>
|
|
417
|
-
<pre><code>get {a: 1, b: 2}
|
|
420
|
+
<pre><code>get "b" {a: 1, b: 2} | returns 2
|
|
418
421
|
</code></pre>
|
|
419
422
|
</section>
|
|
420
423
|
<section id="sec-set" secid="5.2.18">
|
|
421
424
|
<h3><span class="spec-secid" title="link to this section"><a href="#sec-set">5.2.18</a></span>set</h3>
|
|
422
425
|
<p>Return a new record with an updated field</p>
|
|
423
|
-
<pre><code>set {a: 1}
|
|
426
|
+
<pre><code>set "a" 2 {a: 1} | returns {a: 2}
|
|
424
427
|
</code></pre>
|
|
425
428
|
</section>
|
|
426
429
|
</section>
|
package/spec/spec.md
CHANGED
|
@@ -119,6 +119,14 @@ Tags are resolved as special constants with symbolic identity. They are case-sen
|
|
|
119
119
|
- **Strict evaluation**: arguments evaluated before function application
|
|
120
120
|
- **Immutable data**: all values are immutable
|
|
121
121
|
|
|
122
|
+
Many built-in functions in Graffiticode follow a model-threading pattern. In this pattern, functions are defined to take one or more arguments followed by a model, which represents the current state of the program or view. The function uses the earlier arguments to compute an update to the model and returns a new model as its result.
|
|
123
|
+
|
|
124
|
+
This style enables a declarative and order-independent composition of functions. Since each function call returns a new model, multiple calls can be reordered without changing the final result, provided the functional dependencies are preserved.
|
|
125
|
+
|
|
126
|
+
This approach draws inspiration from **Model-View-Update** (MVU) architectures, in which the model represents the application state and functions describe pure, deterministic transformations of that state.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
122
130
|
## Functions
|
|
123
131
|
|
|
124
132
|
- **Fixed arity**: every function has a known number of parameters
|
|
@@ -167,8 +175,8 @@ Tags are resolved as special constants with symbolic identity. They are case-sen
|
|
|
167
175
|
| `nth` | `<number list: any>` | Nth element of list |
|
|
168
176
|
| `apply` | `<function list: any>` | Applies a function to a list of arguments |
|
|
169
177
|
| `isEmpty` | `<list: bool>` | Returns true if the list is empty |
|
|
170
|
-
| `get` | `<record
|
|
171
|
-
| `set` | `<
|
|
178
|
+
| `get` | `<string record: any>` | Retrieves a value from a record by key |
|
|
179
|
+
| `set` | `<string any record: record>` | Returns a new record with a key set to a value |
|
|
172
180
|
|
|
173
181
|
### add
|
|
174
182
|
|
|
@@ -303,7 +311,7 @@ isEmpty [] | returns true
|
|
|
303
311
|
Retrieve a record field
|
|
304
312
|
|
|
305
313
|
```
|
|
306
|
-
get {a: 1, b: 2}
|
|
314
|
+
get "b" {a: 1, b: 2} | returns 2
|
|
307
315
|
```
|
|
308
316
|
|
|
309
317
|
### set
|
|
@@ -311,7 +319,7 @@ get {a: 1, b: 2} "b" | returns 2
|
|
|
311
319
|
Return a new record with an updated field
|
|
312
320
|
|
|
313
321
|
```
|
|
314
|
-
set {a: 1}
|
|
322
|
+
set "a" 2 {a: 1} | returns {a: 2}
|
|
315
323
|
```
|
|
316
324
|
|
|
317
325
|
# Program Examples
|
package/src/compiler.js
CHANGED
|
@@ -27,7 +27,7 @@ class Visitor {
|
|
|
27
27
|
}
|
|
28
28
|
visit(nid, options, resume) {
|
|
29
29
|
try {
|
|
30
|
-
assert(nid);
|
|
30
|
+
assert(nid, "Invalid nid=" + nid);
|
|
31
31
|
let node;
|
|
32
32
|
if (typeof nid === "object") {
|
|
33
33
|
node = nid;
|
|
@@ -425,6 +425,62 @@ export class Checker extends Visitor {
|
|
|
425
425
|
});
|
|
426
426
|
});
|
|
427
427
|
}
|
|
428
|
+
NOT(node, options, resume) {
|
|
429
|
+
this.visit(node.elts[0], options, (err1, val1) => {
|
|
430
|
+
let err = [].concat(err1);
|
|
431
|
+
if (typeof val1 !== "boolean" && val1 !== null && val1 !== undefined && val1 !== 0 && val1 !== "" && val1 !== false) {
|
|
432
|
+
err.push(`NOT operation requires a boolean argument, got ${typeof val1}`);
|
|
433
|
+
}
|
|
434
|
+
const val = node;
|
|
435
|
+
resume(err, val);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
EQUIV(node, options, resume) {
|
|
439
|
+
this.visit(node.elts[0], options, (err1, val1) => {
|
|
440
|
+
this.visit(node.elts[1], options, (err2, val2) => {
|
|
441
|
+
let err = [].concat(err1).concat(err2);
|
|
442
|
+
const validTypes = ["boolean", "string", "number"];
|
|
443
|
+
if (!validTypes.includes(typeof val1) && val1 !== null) {
|
|
444
|
+
err.push(`EQUIV operation requires primitive arguments, got ${typeof val1} for first argument`);
|
|
445
|
+
}
|
|
446
|
+
if (!validTypes.includes(typeof val2) && val2 !== null) {
|
|
447
|
+
err.push(`EQUIV operation requires primitive arguments, got ${typeof val2} for second argument`);
|
|
448
|
+
}
|
|
449
|
+
const val = node;
|
|
450
|
+
resume(err, val);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
OR(node, options, resume) {
|
|
455
|
+
this.visit(node.elts[0], options, (err1, val1) => {
|
|
456
|
+
this.visit(node.elts[1], options, (err2, val2) => {
|
|
457
|
+
let err = [].concat(err1).concat(err2);
|
|
458
|
+
if (typeof val1 !== "boolean" && val1 !== null && val1 !== undefined && val1 !== 0 && val1 !== "" && val1 !== false) {
|
|
459
|
+
err.push(`OR operation requires boolean arguments, got ${typeof val1} for first argument`);
|
|
460
|
+
}
|
|
461
|
+
if (typeof val2 !== "boolean" && val2 !== null && val2 !== undefined && val2 !== 0 && val2 !== "" && val2 !== false) {
|
|
462
|
+
err.push(`OR operation requires boolean arguments, got ${typeof val2} for second argument`);
|
|
463
|
+
}
|
|
464
|
+
const val = node;
|
|
465
|
+
resume(err, val);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
AND(node, options, resume) {
|
|
470
|
+
this.visit(node.elts[0], options, (err1, val1) => {
|
|
471
|
+
this.visit(node.elts[1], options, (err2, val2) => {
|
|
472
|
+
let err = [].concat(err1).concat(err2);
|
|
473
|
+
if (typeof val1 !== "boolean" && val1 !== null && val1 !== undefined && val1 !== 0 && val1 !== "" && val1 !== false) {
|
|
474
|
+
err.push(`AND operation requires boolean arguments, got ${typeof val1} for first argument`);
|
|
475
|
+
}
|
|
476
|
+
if (typeof val2 !== "boolean" && val2 !== null && val2 !== undefined && val2 !== 0 && val2 !== "" && val2 !== false) {
|
|
477
|
+
err.push(`AND operation requires boolean arguments, got ${typeof val2} for second argument`);
|
|
478
|
+
}
|
|
479
|
+
const val = node;
|
|
480
|
+
resume(err, val);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
428
484
|
}
|
|
429
485
|
|
|
430
486
|
function enterEnv(ctx, name, paramc) {
|
|
@@ -724,7 +780,6 @@ export class Transformer extends Visitor {
|
|
|
724
780
|
}
|
|
725
781
|
BINDING(node, options, resume) {
|
|
726
782
|
const err = [];
|
|
727
|
-
const val = node;
|
|
728
783
|
this.visit(node.elts[0], options, (err1, val1) => {
|
|
729
784
|
this.visit(node.elts[1], options, (err2, val2) => {
|
|
730
785
|
resume([].concat(err1).concat(err2), {key: val1, val: val2});
|
|
@@ -825,9 +880,11 @@ export class Transformer extends Visitor {
|
|
|
825
880
|
resume(err, val);
|
|
826
881
|
}
|
|
827
882
|
LEN(node, options, resume) {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
883
|
+
this.visit(node.elts[0], options, (e0, v0) => {
|
|
884
|
+
const err = e0;
|
|
885
|
+
const val = Array.isArray(v0) && v0.length;
|
|
886
|
+
resume(err, val);
|
|
887
|
+
});
|
|
831
888
|
}
|
|
832
889
|
ARG(node, options, resume) {
|
|
833
890
|
const err = [];
|
|
@@ -869,11 +926,8 @@ export class Transformer extends Visitor {
|
|
|
869
926
|
this.visit(node.elts[1], options, (e1, v1) => {
|
|
870
927
|
let err = [];
|
|
871
928
|
let val = [];
|
|
872
|
-
console.log(
|
|
873
|
-
"MAP()",
|
|
874
|
-
"v1=" + JSON.stringify(v1),
|
|
875
|
-
);
|
|
876
929
|
v1.forEach(args => {
|
|
930
|
+
options.SYNC = true;
|
|
877
931
|
options.args = args;
|
|
878
932
|
options = JSON.parse(JSON.stringify(options)); // Copy option arg support async.
|
|
879
933
|
this.visit(node.elts[0], options, (e0, v0) => {
|
|
@@ -921,6 +975,7 @@ export class Transformer extends Visitor {
|
|
|
921
975
|
let err = [];
|
|
922
976
|
let val = v1;
|
|
923
977
|
v2.forEach((args, index) => {
|
|
978
|
+
options.SYNC = true;
|
|
924
979
|
options.args = [val, args];
|
|
925
980
|
options = JSON.parse(JSON.stringify(options)); // Copy option arg support async.
|
|
926
981
|
this.visit(node.elts[0], options, (e0, v0) => {
|
|
@@ -1007,9 +1062,9 @@ export class Transformer extends Visitor {
|
|
|
1007
1062
|
this.visit(node.elts[0], options, (e0, v0) => {
|
|
1008
1063
|
this.visit(node.elts[1], options, (e1, v1) => {
|
|
1009
1064
|
const err = [...e0, ...e1];
|
|
1010
|
-
assert(typeof v0 === "
|
|
1011
|
-
assert(typeof v1 === "
|
|
1012
|
-
const val = v0
|
|
1065
|
+
assert(typeof v0 === "string", "Type Error: expected v0 to be a string.");
|
|
1066
|
+
assert(typeof v1 === "object", "Type Error: expected v1 to be an object. Got " + JSON.stringify(v1));
|
|
1067
|
+
const val = v1[v0];
|
|
1013
1068
|
resume(err, val);
|
|
1014
1069
|
});
|
|
1015
1070
|
});
|
|
@@ -1018,12 +1073,12 @@ export class Transformer extends Visitor {
|
|
|
1018
1073
|
this.visit(node.elts[0], options, (e0, v0) => {
|
|
1019
1074
|
this.visit(node.elts[1], options, (e1, v1) => {
|
|
1020
1075
|
this.visit(node.elts[2], options, (e2, v2) => {
|
|
1021
|
-
const err = [...e0, ...e1];
|
|
1022
|
-
assert(typeof v0 === "
|
|
1023
|
-
assert(typeof
|
|
1076
|
+
const err = [...e0, ...e1, ...e2];
|
|
1077
|
+
assert(typeof v0 === "string", "Type Error: expected v0 to be a string.");
|
|
1078
|
+
assert(typeof v2 === "object", "Type Error: expected v2 to be an object.");
|
|
1024
1079
|
const val = {
|
|
1025
|
-
...
|
|
1026
|
-
[
|
|
1080
|
+
...v2,
|
|
1081
|
+
[v0]: v1,
|
|
1027
1082
|
};
|
|
1028
1083
|
resume(err, val);
|
|
1029
1084
|
});
|
|
@@ -1150,7 +1205,6 @@ export class Transformer extends Visitor {
|
|
|
1150
1205
|
const start = new Decimal(v0);
|
|
1151
1206
|
const end = new Decimal(v1);
|
|
1152
1207
|
const step = new Decimal(v2);
|
|
1153
|
-
|
|
1154
1208
|
if (step.isZero()) {
|
|
1155
1209
|
resume([...err, 'Error in RANGE operation: step cannot be zero'], []);
|
|
1156
1210
|
return;
|
|
@@ -1176,6 +1230,75 @@ export class Transformer extends Visitor {
|
|
|
1176
1230
|
});
|
|
1177
1231
|
});
|
|
1178
1232
|
}
|
|
1233
|
+
NOT(node, options, resume) {
|
|
1234
|
+
this.visit(node.elts[0], options, (e0, v0) => {
|
|
1235
|
+
const err = [].concat(e0);
|
|
1236
|
+
try {
|
|
1237
|
+
// Handle various falsy values explicitly
|
|
1238
|
+
if (v0 === null || v0 === undefined || v0 === 0 || v0 === "" || v0 === false) {
|
|
1239
|
+
resume(err, true);
|
|
1240
|
+
} else {
|
|
1241
|
+
resume(err, !v0);
|
|
1242
|
+
}
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
resume([...err, `Error in NOT operation: ${e.message}`], false);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
EQUIV(node, options, resume) {
|
|
1249
|
+
this.visit(node.elts[0], options, (e0, v0) => {
|
|
1250
|
+
this.visit(node.elts[1], options, (e1, v1) => {
|
|
1251
|
+
const err = [].concat(e0).concat(e1);
|
|
1252
|
+
try {
|
|
1253
|
+
// Use strict equality for primitive comparison
|
|
1254
|
+
const val = v0 === v1;
|
|
1255
|
+
resume(err, val);
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
resume([...err, `Error in EQUIV operation: ${e.message}`], false);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
OR(node, options, resume) {
|
|
1263
|
+
this.visit(node.elts[0], options, (e0, v0) => {
|
|
1264
|
+
// Short-circuit evaluation - if first argument is truthy, return true immediately
|
|
1265
|
+
if (v0) {
|
|
1266
|
+
resume(e0, true);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
this.visit(node.elts[1], options, (e1, v1) => {
|
|
1271
|
+
const err = [].concat(e0).concat(e1);
|
|
1272
|
+
try {
|
|
1273
|
+
// Standard boolean OR operation
|
|
1274
|
+
const val = Boolean(v0) || Boolean(v1);
|
|
1275
|
+
resume(err, val);
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
resume([...err, `Error in OR operation: ${e.message}`], false);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
AND(node, options, resume) {
|
|
1283
|
+
this.visit(node.elts[0], options, (e0, v0) => {
|
|
1284
|
+
// Short-circuit evaluation - if first argument is falsy, return false immediately
|
|
1285
|
+
if (!v0) {
|
|
1286
|
+
resume(e0, false);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
this.visit(node.elts[1], options, (e1, v1) => {
|
|
1291
|
+
const err = [].concat(e0).concat(e1);
|
|
1292
|
+
try {
|
|
1293
|
+
// Standard boolean AND operation
|
|
1294
|
+
const val = Boolean(v0) && Boolean(v1);
|
|
1295
|
+
resume(err, val);
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
resume([...err, `Error in AND operation: ${e.message}`], false);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1179
1302
|
}
|
|
1180
1303
|
|
|
1181
1304
|
export class Renderer {
|
package/src/lexicon.js
CHANGED
|
@@ -11,14 +11,16 @@ export const lexicon = {
|
|
|
11
11
|
"name": "GET",
|
|
12
12
|
"cls": "function",
|
|
13
13
|
"length": 2,
|
|
14
|
-
"arity": 2
|
|
14
|
+
"arity": 2,
|
|
15
|
+
"args": ["key", "record"]
|
|
15
16
|
},
|
|
16
17
|
"set" : {
|
|
17
18
|
"tk": 1,
|
|
18
19
|
"name": "SET",
|
|
19
20
|
"cls": "function",
|
|
20
21
|
"length": 3,
|
|
21
|
-
"arity": 3
|
|
22
|
+
"arity": 3,
|
|
23
|
+
"args": ["key", "value", "record"]
|
|
22
24
|
},
|
|
23
25
|
"nth" : {
|
|
24
26
|
"tk": 1,
|
|
@@ -187,5 +189,33 @@ export const lexicon = {
|
|
|
187
189
|
"cls": "function",
|
|
188
190
|
"length": 3,
|
|
189
191
|
"arity": 3
|
|
192
|
+
},
|
|
193
|
+
"not" : {
|
|
194
|
+
"tk": 1,
|
|
195
|
+
"name": "NOT",
|
|
196
|
+
"cls": "function",
|
|
197
|
+
"length": 1,
|
|
198
|
+
"arity": 1
|
|
199
|
+
},
|
|
200
|
+
"equiv" : {
|
|
201
|
+
"tk": 1,
|
|
202
|
+
"name": "EQUIV",
|
|
203
|
+
"cls": "function",
|
|
204
|
+
"length": 2,
|
|
205
|
+
"arity": 2
|
|
206
|
+
},
|
|
207
|
+
"or" : {
|
|
208
|
+
"tk": 1,
|
|
209
|
+
"name": "OR",
|
|
210
|
+
"cls": "function",
|
|
211
|
+
"length": 2,
|
|
212
|
+
"arity": 2
|
|
213
|
+
},
|
|
214
|
+
"and" : {
|
|
215
|
+
"tk": 1,
|
|
216
|
+
"name": "AND",
|
|
217
|
+
"cls": "function",
|
|
218
|
+
"length": 2,
|
|
219
|
+
"arity": 2
|
|
190
220
|
}
|
|
191
221
|
}
|