@graffiticode/basis 1.6.1 → 1.6.3

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/spec/spec.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Graffiticode Core Language Specification
2
2
 
3
3
  ```
4
- Version: 0.1.0
5
- Date: 2025-04-30
4
+ Version: 0.1.2
5
+ Date: 2026-01-26
6
6
  ```
7
7
 
8
8
  # Introduction
@@ -44,6 +44,8 @@ Function application is written in prefix style:
44
44
  add 1 2
45
45
  ```
46
46
 
47
+ Functions have fixed arity, so applications can be parsed unambiguously without grouping syntax. For example, `add 1 mul 2 3` parses as `add(1, mul(2, 3))` because the parser knows `add` takes 2 arguments and `mul` takes 2 arguments.
48
+
47
49
  Parentheses are used to defer application:
48
50
  ```
49
51
  map (double) [1 2 3]
@@ -55,10 +57,53 @@ map (double) [1 2 3]
55
57
  ```
56
58
 
57
59
  ### Records
60
+
61
+ Records may use **shorthand syntax** for fields where the value is a reference to a variable of the same name.
62
+
63
+ ```gc
64
+ let foo = 10..
65
+ {foo} | equivalent to {foo: 10}
66
+ ```
67
+
68
+ This provides a concise way to construct records using in-scope variable names as field keys and values.
69
+
70
+ You can also mix shorthand and explicit fields:
71
+
72
+ ```gc
73
+ let x = 1..
74
+ let y = 2..
75
+ {x y z: 3} | equivalent to {x: 1, y: 2, z: 3}
58
76
  ```
77
+
78
+ ```gc
59
79
  { name: "Alice", age: 30 }
60
80
  ```
61
81
 
82
+ ### Tags
83
+
84
+ A **tag value** is an arity-0 symbolic value used to represent symbolic variants in pattern matching or other symbolic forms.
85
+
86
+ Tag values are **only defined** by using record shorthand syntax. For example:
87
+
88
+ ```gc
89
+ {red blue green} | defines the tag values red, blue, and green
90
+ ```
91
+
92
+ Tag values:
93
+
94
+ - Must be introduced via record shorthand notation
95
+ - Are resolved as symbolic constants with identity semantics
96
+ - Match directly in `case` expressions:
97
+
98
+ ```
99
+ let color = get "red" {red blue green}
100
+ case color of
101
+ red: "warm"
102
+ blue: "cool"
103
+ _: "other"
104
+ end
105
+ ```
106
+
62
107
  ### Lambdas
63
108
  ```
64
109
  <x: add x 1>
@@ -76,6 +121,7 @@ let double = <x: mul 2 x>..
76
121
  ## Pattern Matching
77
122
 
78
123
  Pattern matching is done using `case`:
124
+
79
125
  ```
80
126
  case x of
81
127
  0: "zero"
@@ -92,29 +138,126 @@ Supports:
92
138
 
93
139
  Pattern matching on function arguments is disallowed.
94
140
 
95
- ### Tag Values
141
+ # Type System
142
+
143
+ Graffiticode includes a implicit structural type system. Every expression has
144
+ a statically inferred type, and type errors are detected at compile time.
145
+ Explicit type annotations are not included in the grammar.
146
+
147
+ ## Primitive Types
96
148
 
97
- A **tag value** is an arity-0 symbolic value that can be used in pattern matching or to encode variant types.
149
+ - `number` Represents integers or floating-point numbers.
150
+ - `string` – Represents UTF-8 strings.
151
+ - `bool` – Represents Boolean values: `true` and `false`.
152
+ - `json` – Represents any JSON-compatible value (opaque, untyped).
153
+ - `any` – Used internally to denote an unconstrained type (e.g., during inference).
98
154
 
155
+ ## Composite Types
156
+
157
+ - **Lists** – Written as `[T]` where `T` is any type.
158
+ ```
159
+ [string] | list of strings
160
+ [number] | list of numbers
161
+ [[bool]] | list of lists of booleans
162
+ ```
163
+
164
+ - **Records** – Key-value maps with known keys and types.
165
+ ```
166
+ { name: string, age: number }
167
+ ```
168
+
169
+ - **Tuples** – Ordered, fixed-length collections with heterogeneous types.
170
+ ```
171
+ (number, string, bool)
172
+ ```
173
+
174
+ ## Function Types
175
+
176
+ Functions are written using Graffiticode lambda signature syntax:
99
177
  ```
100
- red
178
+ <number number: number> | function taking two numbers and returning a number
179
+ <string: [string]> | function taking a string and returning a list of strings
180
+ <list record: record> | common signature for structural transformation
101
181
  ```
102
182
 
103
- Tag values:
183
+ Function types are curried by default. That means:
184
+ ```
185
+ <number number: number>
186
+ ```
187
+ is equivalent to a function that returns another function:
188
+ ```
189
+ <number: <number: number>>
190
+ ```
104
191
 
105
- - Are unbound identifiers that appear in expression position.
106
- - May optionally be introduced via implicit enum definitions.
107
- - Match directly in `case` expressions:
192
+ ## Tag Sets and Tag Value Type
193
+
194
+ Tag values are symbolic constants introduced using record shorthand syntax. They have a default type of `tag`, and their identity is preserved across bindings.
195
+
196
+ ### Tag Identity and Type
197
+
198
+ Tag values are symbolic constants identified **solely by their name**. The same tag name (e.g., `A`) refers to the same symbolic value, regardless of where it is defined.
199
+
200
+ Tag values compare equal by name. For example, `A` in `{A B}` and `A` in `{A C}` are equal.
201
+
202
+ ### Tag Sets as Structural Types
203
+
204
+ Although tag values are globally identified by name, the **type system tracks which tags are expected to co-occur**.
205
+
206
+ For example, `{A B}` and `{A C}` are different types:
108
207
 
109
208
  ```
110
- case color of
209
+ {A B} | type: { A: tag, B: tag }
210
+ {A C} | type: { A: tag, C: tag }
211
+ ```
212
+
213
+ This allows the type system to constrain the expected context for pattern matching, record shapes, or enum-style uses of tags.
214
+
215
+ Using tags not part of the expected set results in a type error.
216
+
217
+ ### Default Type: `tag`
218
+
219
+ By default, a tag value has the type:
220
+
221
+ ```gc
222
+ tag
223
+ ```
224
+
225
+ When tags are introduced with:
226
+
227
+ ```
228
+ {A B C}
229
+ ```
230
+
231
+ the resulting type is:
232
+
233
+ ```
234
+ { A: tag, B: tag, C: tag }
235
+ ```
236
+
237
+ This allows for composition and safe comparison.
238
+
239
+ ### Example
240
+
241
+ ```gc
242
+ let colors = {red blue green}..
243
+ case red of
111
244
  red: "warm"
112
245
  blue: "cool"
113
246
  _: "other"
114
247
  end
115
248
  ```
116
249
 
117
- Tags are resolved as special constants with symbolic identity. They are case-sensitive and may be compared for equality using regular pattern match semantics.
250
+ In this case, `red`, `blue`, and `green` are all of type `tag`, and `case` matches by identity.
251
+
252
+ ## Example: Annotated Definitions
253
+
254
+ ```gc
255
+ let inc = <x: add x 1> | type: <number: number>
256
+ let greet = <name: concat ["Hello, " name]> | type: <string: string>
257
+ let zip = <xs ys: zipLists xs ys> | type: <[T] [U]: [(T, U)]>
258
+ ```
259
+
260
+ (Note: actual Graffiticode does not use type annotations in `let` bindings; types are inferred.)
118
261
 
119
262
  # Semantics
120
263
 
@@ -173,6 +316,7 @@ This approach draws inspiration from **Model-View-Update** (MVU) architectures,
173
316
  | `get` | `<string record: any>` | Retrieves a value from a record by key |
174
317
  | `hd` | `<list: any>` | First item of list |
175
318
  | `isEmpty` | `<list: bool>` | Returns true if the list is empty |
319
+ | `log` | `<any: any>` | Logs the value to console and returns it (identity function) |
176
320
  | `map` | `<function list: list>` | Applies function to each item |
177
321
  | `max` | `<number number: number>` | Returns the larger of two numbers |
178
322
  | `min` | `<number number: number>` | Returns the smaller of two numbers |
@@ -266,6 +410,15 @@ Return true if list is empty, otherwise return false
266
410
  isEmpty [] | returns true
267
411
  ```
268
412
 
413
+ ### log
414
+
415
+ Log a value to the console and return the value unchanged
416
+
417
+ ```
418
+ log "Hello" | prints "Hello" to console and returns "Hello"
419
+ log (add 1 2) | prints 3 to console and returns 3
420
+ ```
421
+
269
422
  ### map
270
423
 
271
424
  Apply a function to each element
package/src/compiler.js CHANGED
@@ -73,6 +73,7 @@ class Visitor {
73
73
  case "STR":
74
74
  case "IDENT":
75
75
  case "BOOL":
76
+ case "TAG":
76
77
  elts[0] = n.elts[0];
77
78
  break;
78
79
  default:
@@ -173,6 +174,11 @@ export class Checker extends Visitor {
173
174
  const val = node;
174
175
  resume(err, val);
175
176
  }
177
+ TAG(node, options, resume) {
178
+ const err = [];
179
+ const val = node;
180
+ resume(err, val);
181
+ }
176
182
  STR(node, options, resume) {
177
183
  const err = [];
178
184
  const val = node.elts[0];
@@ -506,6 +512,13 @@ export class Checker extends Visitor {
506
512
  resume(err, val);
507
513
  });
508
514
  }
515
+ LOG(node, options, resume) {
516
+ this.visit(node.elts[0], options, (err1, val1) => {
517
+ const err = [].concat(err1);
518
+ const val = node;
519
+ resume(err, val);
520
+ });
521
+ }
509
522
  }
510
523
 
511
524
  function enterEnv(ctx, name, paramc) {
@@ -602,7 +615,7 @@ export class Transformer extends Visitor {
602
615
  }
603
616
  const patternNid = this.internPattern(pattern);
604
617
  if (patternNid === this.internPattern(node) ||
605
- patternNid === this.internPattern(newNode('_', []))) {
618
+ patternNid === this.internPattern(newNode('TAG', ['_']))) {
606
619
  return true;
607
620
  }
608
621
  if (pattern.tag === node.tag) {
@@ -641,8 +654,9 @@ export class Transformer extends Visitor {
641
654
  return matches;
642
655
  }
643
656
  CATCH_ALL(node, options, resume) {
657
+ // Fallback for unknown node types.
644
658
  const err = [];
645
- const val = node; // Use the node as the tag value.
659
+ const val = node;
646
660
  resume(err, val);
647
661
  }
648
662
  PROG(node, options, resume) {
@@ -735,6 +749,11 @@ export class Transformer extends Visitor {
735
749
  const val = word?.val !== undefined ? word.val : node.elts[0];
736
750
  resume(err, val);
737
751
  }
752
+ TAG(node, options, resume) {
753
+ const err = [];
754
+ const val = { tag: node.elts[0] };
755
+ resume(err, val);
756
+ }
738
757
  STR(node, options, resume) {
739
758
  const err = [];
740
759
  const val = node.elts[0];
@@ -820,6 +839,10 @@ export class Transformer extends Visitor {
820
839
  const ndx = [];
821
840
  for (let elt of node.elts) {
822
841
  this.visit(elt, options, (e0, v0) => {
842
+ console.log(
843
+ "RECORD()",
844
+ "v0=" + JSON.stringify(v0, null, 2),
845
+ );
823
846
  err = err.concat(e0);
824
847
  ndx[elt] = v0;
825
848
  if (++len === node.elts.length) {
@@ -1028,7 +1051,8 @@ export class Transformer extends Visitor {
1028
1051
  const val = `${v0}`;
1029
1052
  const expr = (
1030
1053
  v0 === null && {tag: "NUL", elts: []} ||
1031
- v0.tag !== undefined && v0 || // We've got a tag value.
1054
+ v0?.tag !== undefined && !v0.elts && {tag: "TAG", elts: [v0.tag]} ||
1055
+ v0?.tag !== undefined && v0 || // Already an AST node.
1032
1056
  type === "boolean" && {tag: "BOOL", elts: [val]} ||
1033
1057
  type === "number" && {tag: "NUM", elts: [val]} ||
1034
1058
  {tag: "STR", elts: [val]}
@@ -1364,6 +1388,19 @@ export class Transformer extends Visitor {
1364
1388
  }
1365
1389
  });
1366
1390
  }
1391
+ LOG(node, options, resume) {
1392
+ this.visit(node.elts[0], options, (e0, v0) => {
1393
+ const err = [].concat(e0);
1394
+ try {
1395
+ // Log the value to the console
1396
+ console.log(`LOG: ${v0}`);
1397
+ // Return the value unchanged (identity function)
1398
+ resume(err, v0);
1399
+ } catch (e) {
1400
+ resume([...err, `Error in LOG operation: ${e.message}`], null);
1401
+ }
1402
+ });
1403
+ }
1367
1404
  }
1368
1405
 
1369
1406
  export class Renderer {
package/src/lexicon.js CHANGED
@@ -3,256 +3,256 @@ export const lexicon = {
3
3
  "tk": 1,
4
4
  "name": "PRINT",
5
5
  "cls": "function",
6
- "length": 1,
7
6
  "arity": 1,
7
+ "type": "<any: record>",
8
8
  "description": "Outputs a value to the form."
9
9
  },
10
10
  "get": {
11
11
  "tk": 1,
12
12
  "name": "GET",
13
13
  "cls": "function",
14
- "length": 2,
15
14
  "arity": 2,
15
+ "type": "<integer record|list: any>",
16
16
  "description": "Retrieves a value from a record or list by key or index."
17
17
  },
18
18
  "set": {
19
19
  "tk": 1,
20
20
  "name": "SET",
21
21
  "cls": "function",
22
- "length": 3,
23
22
  "arity": 3,
23
+ "type": "<integer any record|list: record|list>",
24
24
  "description": "Returns a new record or list with the specified key or index updated."
25
25
  },
26
26
  "nth": {
27
27
  "tk": 1,
28
28
  "name": "NTH",
29
29
  "cls": "function",
30
- "length": 2,
31
30
  "arity": 2,
31
+ "type": "<integer list: any>",
32
32
  "description": "Returns the nth element of a list by index."
33
33
  },
34
34
  "sub": {
35
35
  "tk": 1,
36
36
  "name": "SUB",
37
37
  "cls": "function",
38
- "length": 2,
39
38
  "arity": 2,
39
+ "type": "<number number: number>",
40
40
  "description": "Subtracts the second number from the first."
41
41
  },
42
42
  "filter": {
43
43
  "tk": 1,
44
44
  "name": "FILTER",
45
45
  "cls": "function",
46
- "length": 2,
47
46
  "arity": 2,
47
+ "type": "<lambda list: list>",
48
48
  "description": "Returns a list of elements that match a predicate."
49
49
  },
50
50
  "reduce": {
51
51
  "tk": 1,
52
52
  "name": "REDUCE",
53
53
  "cls": "function",
54
- "length": 3,
55
54
  "arity": 3,
55
+ "type": "<lambda any list: any>",
56
56
  "description": "Reduces a list to a single value using a binary function and initial value."
57
57
  },
58
58
  "map": {
59
59
  "tk": 1,
60
60
  "name": "MAP",
61
61
  "cls": "function",
62
- "length": 2,
63
62
  "arity": 2,
63
+ "type": "<lambda list: list>",
64
64
  "description": "Applies a function to each element in a list and returns a new list."
65
65
  },
66
66
  "lt": {
67
67
  "tk": 1,
68
68
  "name": "LT",
69
69
  "cls": "function",
70
- "length": 2,
71
70
  "arity": 2,
71
+ "type": "<number number: boolean>",
72
72
  "description": "Returns true if the first value is less than the second."
73
73
  },
74
74
  "le": {
75
75
  "tk": 1,
76
76
  "name": "LE",
77
77
  "cls": "function",
78
- "length": 2,
79
78
  "arity": 2,
79
+ "type": "<number number: boolean>",
80
80
  "description": "Returns true if the first value is less than or equal to the second."
81
81
  },
82
82
  "gt": {
83
83
  "tk": 1,
84
84
  "name": "GT",
85
85
  "cls": "function",
86
- "length": 2,
87
86
  "arity": 2,
87
+ "type": "<number number: boolean>",
88
88
  "description": "Returns true if the first value is greater than the second."
89
89
  },
90
90
  "ge": {
91
91
  "tk": 1,
92
92
  "name": "GE",
93
93
  "cls": "function",
94
- "length": 2,
95
94
  "arity": 2,
95
+ "type": "<number number: boolean>",
96
96
  "description": "Returns true if the first value is greater than or equal to the second."
97
97
  },
98
98
  "ne": {
99
99
  "tk": 1,
100
100
  "name": "NE",
101
101
  "cls": "function",
102
- "length": 2,
103
102
  "arity": 2,
103
+ "type": "<number number: boolean>",
104
104
  "description": "Returns true if the two values are not equal."
105
105
  },
106
106
  "len": {
107
107
  "tk": 1,
108
108
  "name": "LEN",
109
109
  "cls": "function",
110
- "length": 1,
111
110
  "arity": 1,
111
+ "type": "<list|string: integer>",
112
112
  "description": "Returns the length of a list or string."
113
113
  },
114
114
  "concat": {
115
115
  "tk": 1,
116
116
  "name": "CONCAT",
117
117
  "cls": "function",
118
- "length": 1,
119
118
  "arity": 1,
119
+ "type": "<list: list|string>",
120
120
  "description": "Concatenates a list of strings or nested lists."
121
121
  },
122
122
  "add": {
123
123
  "tk": 1,
124
124
  "name": "ADD",
125
125
  "cls": "function",
126
- "length": 2,
127
126
  "arity": 2,
127
+ "type": "<number number: number>",
128
128
  "description": "Adds two numbers."
129
129
  },
130
130
  "mul": {
131
131
  "tk": 1,
132
132
  "name": "MUL",
133
133
  "cls": "function",
134
- "length": 2,
135
134
  "arity": 2,
135
+ "type": "<number number: number>",
136
136
  "description": "Multiplies two numbers."
137
137
  },
138
138
  "pow": {
139
139
  "tk": 1,
140
140
  "name": "POW",
141
141
  "cls": "function",
142
- "length": 2,
143
142
  "arity": 2,
143
+ "type": "<number number: number>",
144
144
  "description": "Raises the first number to the power of the second."
145
145
  },
146
146
  "apply": {
147
147
  "tk": 1,
148
148
  "name": "APPLY",
149
149
  "cls": "function",
150
- "length": 2,
151
150
  "arity": 2,
151
+ "type": "<number number: number>",
152
152
  "description": "Applies a function to a list of arguments."
153
153
  },
154
154
  "data": {
155
155
  "tk": 1,
156
156
  "name": "DATA",
157
157
  "cls": "function",
158
- "length": 1,
159
158
  "arity": 1,
160
- "description": "Returns the raw data payload from a structured input."
159
+ "type": "<record: record>",
160
+ "description": "Returns the data from the upstream task, or the argument value if no input exists."
161
161
  },
162
162
  "json": {
163
163
  "tk": 1,
164
164
  "name": "JSON",
165
165
  "cls": "function",
166
- "length": 1,
167
166
  "arity": 1,
168
- "description": "Parses a string as JSON or serializes a value to JSON."
167
+ "type": "<string: any>",
168
+ "description": "Parses a string as JSON."
169
169
  },
170
170
  "eq": {
171
171
  "tk": 1,
172
172
  "name": "EQ",
173
173
  "cls": "function",
174
- "length": 2,
175
174
  "arity": 2,
175
+ "type": "<number number: boolean>",
176
176
  "description": "Returns true if the two values are equal."
177
177
  },
178
178
  "mod": {
179
179
  "tk": 1,
180
180
  "name": "MOD",
181
181
  "cls": "function",
182
- "length": 2,
183
182
  "arity": 2,
183
+ "type": "<number number: integer>",
184
184
  "description": "Returns the remainder of dividing the first number by the second."
185
185
  },
186
186
  "min": {
187
187
  "tk": 1,
188
188
  "name": "MIN",
189
189
  "cls": "function",
190
- "length": 2,
191
190
  "arity": 2,
191
+ "type": "<number number: number>",
192
192
  "description": "Returns the smaller of two values."
193
193
  },
194
194
  "max": {
195
195
  "tk": 1,
196
196
  "name": "MAX",
197
197
  "cls": "function",
198
- "length": 2,
199
198
  "arity": 2,
199
+ "type": "<number number: number>",
200
200
  "description": "Returns the larger of two values."
201
201
  },
202
202
  "range": {
203
203
  "tk": 1,
204
204
  "name": "RANGE",
205
205
  "cls": "function",
206
- "length": 3,
207
206
  "arity": 3,
207
+ "type": "<number number number: list>",
208
208
  "description": "Generates a list of numbers from start to end using a step."
209
209
  },
210
210
  "not": {
211
211
  "tk": 1,
212
212
  "name": "NOT",
213
213
  "cls": "function",
214
- "length": 1,
215
214
  "arity": 1,
215
+ "type": "<boolean: boolean>",
216
216
  "description": "Returns the logical negation of a boolean value."
217
217
  },
218
218
  "equiv": {
219
219
  "tk": 1,
220
220
  "name": "EQUIV",
221
221
  "cls": "function",
222
- "length": 2,
223
222
  "arity": 2,
223
+ "type": "<any any: boolean>",
224
224
  "description": "Returns true if the two values are semantically equivalent."
225
225
  },
226
226
  "or": {
227
227
  "tk": 1,
228
228
  "name": "OR",
229
229
  "cls": "function",
230
- "length": 2,
231
230
  "arity": 2,
231
+ "type": "<boolean boolean: boolean>",
232
232
  "description": "Returns true if at least one of the two values is true."
233
233
  },
234
234
  "and": {
235
235
  "tk": 1,
236
236
  "name": "AND",
237
237
  "cls": "function",
238
- "length": 2,
239
238
  "arity": 2,
239
+ "type": "<boolean boolean: boolean>",
240
240
  "description": "Returns true if both values are true."
241
241
  },
242
242
  "hd": {
243
243
  "tk": 1,
244
244
  "name": "HD",
245
245
  "cls": "function",
246
- "length": 1,
247
246
  "arity": 1,
247
+ "type": "<list: any>",
248
248
  "description": "Returns the first element of a list."
249
249
  },
250
250
  "tl": {
251
251
  "tk": 1,
252
252
  "name": "TL",
253
253
  "cls": "function",
254
- "length": 1,
255
254
  "arity": 1,
255
+ "type": "<list: list>",
256
256
  "description": "Returns the list without its first element."
257
257
  },
258
258
  "cons": {
@@ -261,14 +261,23 @@ export const lexicon = {
261
261
  "cls": "function",
262
262
  "length": 2,
263
263
  "arity": 2,
264
+ "type": "<any list: list>",
264
265
  "description": "Prepends an element to the front of a list."
265
266
  },
266
267
  "append": {
267
268
  "tk": 1,
268
269
  "name": "APPEND",
269
270
  "cls": "function",
270
- "length": 2,
271
271
  "arity": 2,
272
+ "type": "<any list: list>",
272
273
  "description": "Appends an element to the end of a list."
274
+ },
275
+ "log": {
276
+ "tk": 1,
277
+ "name": "LOG",
278
+ "cls": "function",
279
+ "arity": 1,
280
+ "type": "<any: any>",
281
+ "description": "Logs the value to the console and returns the value (identity function)."
273
282
  }
274
283
  };