fusion-lang 0.0.1.alpha1 β 0.0.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.
- checksums.yaml +4 -4
- data/README.md +19 -6
- data/Rakefile +9 -0
- data/docs/lang/design.md +418 -28
- data/docs/lang/implementation.md +238 -0
- data/docs/lang/roadmap.md +20 -57
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +62 -23
- data/docs/user/reference.md +596 -168
- data/docs/user/tutorial.md +32 -29
- data/examples/double.fsn +1 -1
- data/examples/ends.fsn +4 -0
- data/examples/factorial.fsn +2 -2
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/json_test.fsn +4 -0
- data/examples/palindrome.fsn +2 -1
- data/exe/fusion +17 -44
- data/lib/fusion/ast.rb +97 -0
- data/lib/fusion/atom.rb +17 -0
- data/lib/fusion/cli/decoder.rb +84 -0
- data/lib/fusion/cli/encoder.rb +28 -0
- data/lib/fusion/cli/options.rb +212 -0
- data/lib/fusion/cli/parser.rb +38 -0
- data/lib/fusion/cli/repl.rb +78 -0
- data/lib/fusion/cli/serializer.rb +70 -0
- data/lib/fusion/cli.rb +207 -0
- data/lib/fusion/interpreter/builtins.rb +465 -0
- data/lib/fusion/interpreter/env.rb +89 -0
- data/lib/fusion/interpreter/error_val.rb +71 -0
- data/lib/fusion/interpreter/func.rb +22 -0
- data/lib/fusion/interpreter/native_func.rb +22 -0
- data/lib/fusion/interpreter/thunk.rb +53 -0
- data/lib/fusion/interpreter.rb +752 -0
- data/lib/fusion/lexer.rb +249 -0
- data/lib/fusion/null.rb +9 -0
- data/lib/fusion/parser.rb +542 -0
- data/lib/fusion/token.rb +22 -0
- data/lib/fusion/typed_data.rb +23 -0
- data/lib/fusion/version.rb +1 -1
- data/lib/fusion/wire_pair.rb +11 -0
- data/lib/fusion.rb +11 -1122
- data/stdlib/all.fsn +13 -0
- data/stdlib/any.fsn +12 -0
- data/stdlib/chars.fsn +5 -0
- data/stdlib/compact.fsn +6 -0
- data/stdlib/concat.fsn +5 -0
- data/stdlib/falsey.fsn +6 -0
- data/stdlib/filter.fsn +12 -0
- data/stdlib/flatten.fsn +7 -0
- data/stdlib/gt.fsn +9 -0
- data/stdlib/gte.fsn +9 -0
- data/stdlib/lt.fsn +9 -0
- data/stdlib/lte.fsn +9 -0
- data/stdlib/map.fsn +6 -2
- data/stdlib/range.fsn +2 -1
- data/stdlib/reduce.fsn +8 -0
- data/stdlib/sanitize.fsn +12 -0
- data/stdlib/truthy.fsn +7 -0
- metadata +41 -2
- data/stdlib/math/square.fsn +0 -1
data/docs/lang/design.md
CHANGED
|
@@ -79,9 +79,9 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
79
79
|
### Decisions
|
|
80
80
|
|
|
81
81
|
- π§ β
Integers and floats are distinct kinds.
|
|
82
|
-
- π€
|
|
83
|
-
- π€ β
|
|
84
|
-
- π€ β
`
|
|
82
|
+
- π€ βͺ `@OP.divide` returned an integer when evenly divisible, a float otherwise. Reverted in Β§5.5. It is now always a float, with integer division split out to `@OP.quotient` (Β§5.5).
|
|
83
|
+
- π€ β
`@math.floor` returns an integer.
|
|
84
|
+
- π€ β
`@OP.equal` is exact.
|
|
85
85
|
|
|
86
86
|
### Alternatives
|
|
87
87
|
|
|
@@ -101,11 +101,11 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
-
## 1.4 Member/index access failures yield
|
|
104
|
+
## 1.4 Member/index access failures yield an error
|
|
105
105
|
|
|
106
106
|
### Decisions
|
|
107
107
|
|
|
108
|
-
- π€ β
`x.key` on a missing key or non-object, and `x[i]` out of range or on a wrong type, yield
|
|
108
|
+
- π€ β
`x.key` on a missing key or non-object, and `x[i]` out of range or on a wrong type, yield an error (instead of `null`).
|
|
109
109
|
|
|
110
110
|
### Why the implementer decided this
|
|
111
111
|
|
|
@@ -137,6 +137,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
137
137
|
- π§ β
Every function takes exactly one argument and returns one value.
|
|
138
138
|
- π§ β
A function literal is `(pattern => result, pattern => result, ...)`.
|
|
139
139
|
- π§ β
Clauses are tried top to bottom; the first match wins.
|
|
140
|
+
- π§ β
The clause list may be empty: `()` is the empty function (not an empty/invalid grouping).
|
|
140
141
|
|
|
141
142
|
### Alternatives
|
|
142
143
|
|
|
@@ -161,7 +162,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
161
162
|
|
|
162
163
|
### Decisions
|
|
163
164
|
|
|
164
|
-
- π§ β
Patterns and results are mirror images using the same names
|
|
165
|
+
- π§ β
Patterns and results are mirror images using the same names. Values captured by a pattern are re-inserted in the result.
|
|
165
166
|
- π€ β
A bare (unquoted) identifier is the binder/hole: it binds in a pattern and reads in an expression (Claude's choice to use JSON's one unused syntactic slot, rather than a sigil).
|
|
166
167
|
|
|
167
168
|
### Alternatives
|
|
@@ -216,6 +217,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
216
217
|
- π§ β
Predicate functions double as a runtime type system: the built-in "types" (`Integer`, `String`, β¦) are ordinary predicate functions, so `a ? @Integer` matches only integers (inspired by the Ruby gem "literal").
|
|
217
218
|
- π’ β
The `?` refinement may follow *any* pattern, not just a lone binder; the clause then matches iff the pattern matches structurally **and** the matched value piped into the predicate yields `true` (Claude offered a restrictive per-binder form vs. this permissive any-pattern form; the designer chose permissive).
|
|
218
219
|
- π§ β
The predicate is any function.
|
|
220
|
+
- π§ β
A predicate may be a `|` chain of functions, the matched value flowing in from the left: `a ? b | c` matches when `a` matches and `a | b | c` is truthy. The grammar's `predicate` is a full `pipe` (was a single `prefix`).
|
|
219
221
|
|
|
220
222
|
### Alternatives
|
|
221
223
|
|
|
@@ -223,12 +225,14 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
223
225
|
- π€ β Typed pattern keywords (`n: int`).
|
|
224
226
|
- π€ π A separate static type system.
|
|
225
227
|
- π€ β `if`-style guards with arbitrary boolean expressions.
|
|
228
|
+
- π€ π Keep predicates single-function and compose via an inline `(x => x | b | c)`. Rejected: pure noise next to `b | c`.
|
|
226
229
|
|
|
227
230
|
### Pros
|
|
228
231
|
|
|
229
232
|
- Unifies three things (structural matching, type checks, value guards) into one mechanism.
|
|
230
233
|
- The "type system" is user-extensible with ordinary functions.
|
|
231
234
|
- Nothing new to learn beyond `?`.
|
|
235
|
+
- Predicates compose directly (`a ? @ends | @OP.equal`) instead of requiring a wrapping function (`a ? (x => x | @ends | @OP.equal)`).
|
|
232
236
|
|
|
233
237
|
### Cons
|
|
234
238
|
|
|
@@ -270,7 +274,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
270
274
|
- π§ β
Introduce a distinct error mode `!`, separate from `null`.
|
|
271
275
|
- π§ β
`null` = legitimate absence; `!` = failure.
|
|
272
276
|
- π§ β
A function is made *strict* by ending with `_ => !` (error on no match) and is otherwise *lenient* (returns `null` on no match).
|
|
273
|
-
- π§
|
|
277
|
+
- π§ βͺ Total predicates end with `_ => false`. Obsoleted by Β§2.12. Predicates no longer need a `_ => false` clause, because `null` became equivalent to `false`.
|
|
274
278
|
- π€ β
Built-in operations return `!` on bad input; built-in predicates return `false`.
|
|
275
279
|
- π§ β
The form of `!` (always carrying a payload) is fixed by 2.8.
|
|
276
280
|
- π€ β
`!` matches **only** error patterns (not `_`, not a binder).
|
|
@@ -292,7 +296,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
292
296
|
### Cons
|
|
293
297
|
|
|
294
298
|
- `_` does not mean "literally anything" (it excludes `!`), a subtle asymmetry.
|
|
295
|
-
- π©Ή An uncaught error can travel far from its origin, which can make debugging harder
|
|
299
|
+
- π©Ή An uncaught error can travel far from its origin, which can make debugging harder. Partially mitigated by more detailed error payloads in 2.9. Could be improved further with stack traces, see roadmap.
|
|
296
300
|
|
|
297
301
|
---
|
|
298
302
|
|
|
@@ -332,7 +336,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
332
336
|
- π§ β
In expression position, `!expr` is a prefix operator that wraps a value as an error; in pattern position, `!pat` matches an error and destructures its payload (bare `!` matches any error without binding; `!_` does the same but admits a `?` predicate).
|
|
333
337
|
- π§ β
Propagation preserves the *same* error (payload intact), so by the time you reach a catch site you still know what happened.
|
|
334
338
|
- π§ β
The CLI prints the payload (as JSON) to stderr on failure and exits `1`, leaving stdout empty.
|
|
335
|
-
- π§ β
**Errors propagate uniformly β they are not values.** At any moment of execution there is either a value or an error in motion, never both. Built-ins (including `@
|
|
339
|
+
- π§ β
**Errors propagate uniformly β they are not values.** At any moment of execution there is either a value or an error in motion, never both. Built-ins (including `@OP.equal` and the type predicates), array and object literals, `?` predicates, and the function-value position of a pipe all propagate an encountered error. The only way to do anything with an error besides letting it propagate is to catch it in an `!pat` clause, which yields a normal value (the payload).
|
|
336
340
|
- π§ β
**No nested errors.** When `!expr` is evaluated and `expr` itself produces an error, that inner error propagates and the outer `!` is a no-op. This preserves the "never more than one error simultaneously" invariant β there is no `!!` value, ever.
|
|
337
341
|
- π§ β
**Partial matching propagates the unmatched error.** A function with error clauses that match *some* error shapes (e.g. `!42 => ...`) but not the one it receives (e.g. `!"oops"`) propagates the unmatched error rather than turning it into `null`. The "no match β null" lenient default from 2.7 applies only to non-error inputs.
|
|
338
342
|
- π§ β
**`!pat` is a top-level prefix in the clause grammar.** `!` is a prefix on the *clause pattern*, not on any sub-pattern: array elements, object members, and the payload of another `!` all recurse into the non-`!` pattern production. This grammar shape simultaneously enforces two things with no special-case parsing flag: (i) nested error patterns (`[!a, b]`, `{"err": !x}`, `!!42`, `!{"k": !v}`) are syntax errors, matching the runtime invariant that errors never sit inside other values; and (ii) `!pat ? pred` parses as `!(pat ? pred)`, so the `?` binds *inside* the `!`. The runtime payoff is that the predicate of `!a ? pred` naturally sees the payload, with no special case needed in `PGuard`.
|
|
@@ -343,7 +347,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
343
347
|
- π§ βͺ Keep `!` opaque with no payload (the original error model, superseded because it was too hard to debug).
|
|
344
348
|
- π€ π Use a `Result`-style two-variant `Ok | Err` (hypothetical: needs new machinery; payloaded errors with propagation give the same ergonomics with two rules).
|
|
345
349
|
- π€ β Make payloads always strings (Claude's first payload sketch, e.g. `!"divide by zero"`; rejected because built-in mechanics like missing keys benefit from structured payloads).
|
|
346
|
-
- π€ βͺ Make errors first-class values that can be stored in collections, compared with `@
|
|
350
|
+
- π€ βͺ Make errors first-class values that can be stored in collections, compared with `@OP.equal`, and inspected by predicates (Claude implemented this reading; the designer corrected it because it creates carve-outs in propagation that contradict the "never more than one error" invariant).
|
|
347
351
|
- π€ βͺ Parse `!pat ? pred` as `(!pat) ? pred` so the predicate refines the whole error rather than the payload (Claude parsed it this way; the designer corrected it to `!(pat ? pred)` so the predicate sees the payload, matching its sibling binders).
|
|
348
352
|
|
|
349
353
|
### Pros
|
|
@@ -352,16 +356,162 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
352
356
|
- The catch site can dispatch on the error kind (`(!{"kind":"missing_key"} => ..., !msg => !msg)`).
|
|
353
357
|
- Construction and matching are syntactically symmetric (`!42` builds on the right of `=>`, matches on the left).
|
|
354
358
|
- Propagation remains uniform, with no carve-outs to remember.
|
|
359
|
+
- The previous error model with the single error value `!` can still be emulated:
|
|
360
|
+
- The expression `!` is equivalent to `!null`.
|
|
361
|
+
- The pattern `!` is equivalent to `!_`.
|
|
355
362
|
|
|
356
363
|
### Cons
|
|
357
364
|
|
|
358
|
-
- The payload format is part of the language's surface
|
|
365
|
+
- π©Ή The payload format is part of the language's surface. Builtins used string payloads (`"divide: division by zero"`) while the standard library used structured object payloads (`{"kind": ...}`). This inconsistency was resolved in 2.9.
|
|
359
366
|
- A payload that itself contains sensitive data becomes part of the program's stderr stream.
|
|
360
367
|
- The bare-`!`-means-`!null` rule preserves a simple expression form but means a careless `_ => !` clause gives a maximally unhelpful error.
|
|
361
368
|
- Inspecting an error's payload requires a small catch-and-rebind (`(!a => a)`) rather than direct comparison.
|
|
362
369
|
|
|
363
370
|
---
|
|
364
371
|
|
|
372
|
+
## 2.9 Standardized error payloads
|
|
373
|
+
|
|
374
|
+
### Decisions
|
|
375
|
+
|
|
376
|
+
- π§ β
Every error payload produced by "the runtime" (the interpreter or a built-in function) has the **same shape**. This shape is enforced by constructing "runtime errors" via `ErrorVal.from_runtime`. The full schema is documented in [reference Β§6.5](../user/reference.md#65-the-standardized-error-payload).
|
|
377
|
+
- π€ βͺ The stdlib is "ordinary unpriviledged Fusion code". It didn't produce runtime errors. Reverted by Β§2.13. The stdlib now constructs regular `!expr` user errors, but they get marked as runtime errors afterwards.
|
|
378
|
+
- π§ β
All stdlib functions mirror the built-in error shape.
|
|
379
|
+
- π’ βͺ During function application we differentiated `argument_error` (bad input *shape*, expressible as a pattern without `?`) from `type_error` (bad input *type*). Reverted in Β§2.13. Both errors got unified into a single `argument_error`.
|
|
380
|
+
- π§ β
Member/index access reserves `access_error` for exactly `missing key` and `index out of range`:
|
|
381
|
+
- Accessing a member of a non-object or indexing with a wrong-typed key is a `type_error` instead.
|
|
382
|
+
- File-system access failures ("missing file", "directory instead of file", "permission denied") are a `reference_error` instead.
|
|
383
|
+
|
|
384
|
+
### Alternatives
|
|
385
|
+
|
|
386
|
+
- π’ βͺ Keep the previous split between builtin errors (string payload) and stdlib errors (object payloads). Only document the rule.
|
|
387
|
+
- π§ π Don't standardize the error payloads at all.
|
|
388
|
+
|
|
389
|
+
### Pros
|
|
390
|
+
|
|
391
|
+
- Every catch site (`!` pattern) can rely on this default shape.
|
|
392
|
+
- The structured fields make errors self-describing (what failed, where, on what input).
|
|
393
|
+
|
|
394
|
+
### Cons
|
|
395
|
+
|
|
396
|
+
- The new structured payloads are more verbose than the old bare strings.
|
|
397
|
+
- Some converted Ruby messages still leak host detail (e.g. an `Errno` message). Pending per-case cleanup.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 2.10 Disallow duplicate binders
|
|
402
|
+
|
|
403
|
+
### Decisions
|
|
404
|
+
|
|
405
|
+
- π§ β
Binding the same identifier twice in one clause is a `binding_error`.
|
|
406
|
+
- π€ β
It is detected in `match` as the binding is produced, so it surfaces only when the clause's shape otherwise matches. A non-matching shape just means the clause does not apply.
|
|
407
|
+
|
|
408
|
+
### Alternatives
|
|
409
|
+
|
|
410
|
+
- π§ β Binding the same identifier twice is a non-linear "must be equal". The pattern only matches, if all parts with the same identifier have the same value.
|
|
411
|
+
|
|
412
|
+
### Pros
|
|
413
|
+
|
|
414
|
+
- Less implementation effort than "equal identifiers mean equal value". Already solved by fully generic `?` predicates.
|
|
415
|
+
- Runtime detection fits to this language's dynamic nature.
|
|
416
|
+
|
|
417
|
+
### Cons
|
|
418
|
+
|
|
419
|
+
- Slightly less expressive pattern language.
|
|
420
|
+
- Invalid patterns will only raise an error as soon as a value actually matches. No static guarantees.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 2.11 Stricter objects: unique keys, rest last, closed by default
|
|
425
|
+
|
|
426
|
+
### Decisions
|
|
427
|
+
|
|
428
|
+
- π§ β
A fixed object key may not repeat (in expressions and patterns). Keys arriving via `...spread` / `...rest` are dynamic and not checked.
|
|
429
|
+
- π§ β
In an object pattern, `...rest` must come last.
|
|
430
|
+
- π§ β
An object pattern without a `...rest` is *closed*. It matches only an object whose keys are *exactly* the pattern's
|
|
431
|
+
|
|
432
|
+
### Alternatives
|
|
433
|
+
|
|
434
|
+
- π€ π Last-write-wins for duplicate literal keys (JSON behavior). Rejected: silently dropping a written key hides mistakes.
|
|
435
|
+
- π§ βͺ Object patterns always open β extra keys ignored regardless of a rest. Superseded: it gave no way to assert "exactly these keys".
|
|
436
|
+
|
|
437
|
+
### Pros
|
|
438
|
+
|
|
439
|
+
- Duplicate keys and misplaced rests are caught statically with a precise message.
|
|
440
|
+
- Closed matching regains full-shape matching, with `...` the explicit opt-in to extra keys β symmetric with arrays.
|
|
441
|
+
|
|
442
|
+
### Cons
|
|
443
|
+
|
|
444
|
+
- More verbose common case: ignoring extra keys now needs a trailing `...`.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## 2.12 Switching to Ruby's truthiness model
|
|
449
|
+
|
|
450
|
+
### Decisions
|
|
451
|
+
|
|
452
|
+
- π§ β
Truthiness is Ruby-style: every value is truthy except `false` and `null`. This applies to the `@and`/`@or`/`@not` built-ins and `?` predicates.
|
|
453
|
+
|
|
454
|
+
### Alternatives
|
|
455
|
+
|
|
456
|
+
- π§ βͺ The builtins `@and`/`@or`/`@not` are strict and return a `type_error` for non booleans. Predicates match only on exactly `true`.
|
|
457
|
+
|
|
458
|
+
### Pros
|
|
459
|
+
|
|
460
|
+
- Booleans operations return `type_error`s less frequently and are more useful. A lot more functions work as predicates.
|
|
461
|
+
|
|
462
|
+
### Cons
|
|
463
|
+
|
|
464
|
+
- If you really wanted "strict booleans", you'd now need to build them yourselves.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## 2.13 Refining the error payload
|
|
469
|
+
|
|
470
|
+
Refines Β§2.9: same general shape, more orthogonal fields, field values easier to match on, field values contain smarter contents
|
|
471
|
+
|
|
472
|
+
### Decisions
|
|
473
|
+
|
|
474
|
+
- π§ β
The error payload fields are now: `kind`, `origin`, `file` (opt), `operation`, `status`, `input`, `expected` (opt), `message` (opt).
|
|
475
|
+
- π§ β
Split `location` into `origin` (where the operation is *defined*) and an optional `file` (the **innermost user-code file** on the call chain).
|
|
476
|
+
- π§ β
`file` is `Dir.pwd`-relative, so it reads as the route from the location where `fusion` was called to the offending source code.
|
|
477
|
+
- π§ β
Split `status` out from `input`. `status` is `0` (a value) or `1` (an error). On `1`, `input` carries the error's bare payload, so `input` is always valid JSON.
|
|
478
|
+
- π§ β
`operation` now contains the failing operation's own **`@`-reference** (`@`, `@@`, `@lt`, `@math.round`, `@../mod`, `@load`) or for Built-in *syntax* its own form (`|`, `.key`, `[]`, `parsing code`). Loading the top-level program file is `loading code` (not an `@`-reference).
|
|
479
|
+
- π§ β
An `@`-reference takes no argument, so its `input` is `null` and its `status` is always `0`. `@load` is the exception: it's a function taking a filename.
|
|
480
|
+
- π§ β
For *access errors* the "key" appears only once:
|
|
481
|
+
- `.name` carries the static key in `operation` and the object alone in `input`
|
|
482
|
+
- `[]` is generic in `operation` and echoes the key in the `input` = `[collection, key]`
|
|
483
|
+
- `[=]` is generic in `operation` and echoes the key in the `input` = `[collection, key, newValue]`
|
|
484
|
+
- π§ β
A failure to read a file (missing file, directory given, access denied) is reported as `"operation": <the literal @-reference>`, `"input": null`, `"file": <the referring call site>`.
|
|
485
|
+
- π§ β
`type_error` is merged into `argument_error`. The distinction between *wrong shape* and *wrong type* didn't fit Fusion's runtime type system.
|
|
486
|
+
- π§ β
`expected` lists the acceptable inputs as Fusion patterns (the input matched none); an error with `expected` never also carries a `message`.
|
|
487
|
+
- π§ β
`internal_error` is the new catch-all for an unexpected host/interpreter failure. It's a Ruby error the engine caught rather than letting it crash the process (`origin` `interpreter` or `builtin`). It's an interpreter bug.
|
|
488
|
+
- π§ β
A runtime resource limit being exceeded is a separate `limit_error` (currently a stack overflow, `"stack level too deep"`): the runtime gave up because a space/time budget ran out β not an engine defect. The general name (vs `stack_error`) lets future runtime resource limits share the kind.
|
|
489
|
+
- π§ β
stdlib functions preemptively handle all *argument* errors. They appear atomic. No input should be able to trigger e.g. an error in a `|` operation. *Argument* errors refer to the stdlib function itself (`origin: "stdlib"`, `operation` = its `@`-reference).
|
|
490
|
+
- π§ β
stdlib functions are *transparent* for *inner errors*. They can't catch every possible error from inner operations, so an inner error bubbles through unchanged. The purest example is `@map`, which knows nothing about the given `f`: an error from `f` originates from `f` and simply bubbles through `map`.
|
|
491
|
+
- π§ β
stdlib higher-order functions (`@all`/`@map`) guard `f ? @Function` in every clause β a non-function `f` errors even on an empty collection β and `expected` shows the guard.
|
|
492
|
+
- π§ β
`@all` short-circuits: the first falsey item yields `false`, the rest go untested.
|
|
493
|
+
|
|
494
|
+
### Alternatives
|
|
495
|
+
|
|
496
|
+
- π’ βͺ A variable `location` string embedding the file/builtin name, with the error marker living inside `input` β split into the fixed `origin` + `file`, and the `status` field.
|
|
497
|
+
- π’ β Over-approximating `expected` patterns for `@join`/`@toObject` (e.g. `[_ ? @Array, _ ? @String]`) β they would match inputs that still fail, breaking "matches β acceptable"; `@all` keeps them exact.
|
|
498
|
+
- π’ β `operation` = the *literal source text* of the `|`'s right-hand side. Not implementable: the text isn't available where the error is born; stamping it at `apply` would relabel inner errors bubbling *through* a function (violating Β§2.9 transparency); and an indirect RHS like `f` is uninformative. The producer's own `@`-reference gives the same result for a direct call and stays correct otherwise.
|
|
499
|
+
- π§ β Drop `conversion_error` and have a failed conversion (e.g. `@parseNumber` of `"abc"`) return `null` (a "Maybe", as Ruby's `to_i` does). Rejected: the error payload carries more information, `| (! => null)` recovers the lenient form in one token, and forcing a catch keeps errors local β which matters with no backtraces. (A `null` would slip downstream and surface far from its cause.)
|
|
500
|
+
- π§ β Base the payload path on the jail / program directory instead of `Dir.pwd`. Rejected: `Dir.pwd` gives a path usable straight from your shell; for an installed/shebang tool invoked from elsewhere, a jail-relative path would describe the program's internal layout, which you'd then have to rebase onto your own location.
|
|
501
|
+
|
|
502
|
+
### Pros
|
|
503
|
+
|
|
504
|
+
- `input` is always valid JSON as the `status` now lives in its own field
|
|
505
|
+
- `expected` documents the acceptable inputs as patterns a caller can reuse.
|
|
506
|
+
- `origin` is directly dispatchable as the variable filename now lives in its own `file` field.
|
|
507
|
+
- The roles of `file` and `operation` have been clarified and are much more helpful now.
|
|
508
|
+
|
|
509
|
+
### Cons
|
|
510
|
+
|
|
511
|
+
- A few `expected` patterns must reference the `@all` stdlib helper, so they aren't purely structural.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
365
515
|
# 3. @ references
|
|
366
516
|
|
|
367
517
|
## 3.1 A file contains exactly one value
|
|
@@ -500,10 +650,10 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
|
|
|
500
650
|
|
|
501
651
|
All access goes through `@`:
|
|
502
652
|
|
|
503
|
-
- π§ β
**Built-ins require `@`.** `@
|
|
653
|
+
- π§ β
**Built-ins require `@`.** `@OP.sum`, `@Integer`, etc. A bare identifier is *only* a pattern hole; it never denotes a built-in. Built-ins cannot be shadowed by a clause's bindings β they live in a different namespace entirely.
|
|
504
654
|
- π§ β
**The standard library has no prefix.** `@map` reaches it directly.
|
|
505
655
|
- π§ β
**An `@name` reference (without leading `../`) resolves: sibling file β built-in β stdlib file β error.** First match wins. Consequently siblings can shadow a built-in or a stdlib function, but only for files in that directory (never globally).
|
|
506
|
-
- π§ β
**Built-in/stdlib fallback is gated on `../`, not on `/`.** Downward paths (`@dir/a`, `@
|
|
656
|
+
- π§ β
**Built-in/stdlib fallback is gated on `../`, not on `/`.** Downward paths (`@dir/a`, `@util/helper`) remain eligible for the built-in/stdlib fallback; only upward paths (`@../a`) are file-only and never fall back.
|
|
507
657
|
- π§ β
**A bare `@`** (nothing after it) means the current file β recursion is written this way rather than by repeating the file's own name.
|
|
508
658
|
- π§ β
**`@ENV`** is a built-in evaluating to an object of environment variables (all string values, no parsing); read with member access (`@ENV.CI`).
|
|
509
659
|
- π§ β
**`@load`** is a built-in taking a filename **verbatim** (no `.fsn` appended), resolved relative to the referencing file, for runtime/non-identifier filenames. Both `@ENV` and `@load` resolve in the `@name` chain, so both are shadowable by a sibling file of that name.
|
|
@@ -512,7 +662,7 @@ All access goes through `@`:
|
|
|
512
662
|
|
|
513
663
|
- Every bare `@name` follows one precedence order (sibling β builtin β stdlib), so there are no reserved names to remember and no parser carve-outs.
|
|
514
664
|
- `ENV` and `load` live in the builtin tier like everything else.
|
|
515
|
-
- Gating fallback on `../` rather than on the presence of any `/` keeps downward paths (`@
|
|
665
|
+
- Gating fallback on `../` rather than on the presence of any `/` keeps downward paths (`@util/helper`) eligible for the stdlib, which is what stdlib subpackaging needs.
|
|
516
666
|
|
|
517
667
|
### Alternatives
|
|
518
668
|
|
|
@@ -530,12 +680,86 @@ All access goes through `@`:
|
|
|
530
680
|
|
|
531
681
|
### Cons
|
|
532
682
|
|
|
533
|
-
- Built-ins are verbose (`@
|
|
683
|
+
- Built-ins are verbose (`@OP.sum` everywhere).
|
|
534
684
|
- Shadowing is invisible at the call site (whether `@map` is yours or the stdlib's depends on directory contents).
|
|
535
685
|
- A bare word that *looks* like a function reference is silently just a hole (reading an unbound one yields an error).
|
|
536
686
|
|
|
537
687
|
---
|
|
538
688
|
|
|
689
|
+
## 3.7 Bare `@` also works for inline code, not only for files
|
|
690
|
+
|
|
691
|
+
### Decisions
|
|
692
|
+
|
|
693
|
+
- π§ β
A bare `@` is the value of the current top-level **unit**: a file (previously the only case), an inline (`-e`) program, or a REPL entry.Self-recursion works in all three cases.
|
|
694
|
+
- π§ β
Interpreter context is not part of the identifier namespace. `:dir`/`:file`/`:self` are hidden values and not exposed as `__dir__`/`__file__`/`__self__`.
|
|
695
|
+
|
|
696
|
+
### Alternatives
|
|
697
|
+
|
|
698
|
+
- π€ β Model inline/REPL code as a synthetic "fake file" so the existing file machinery applies β needs temp-file lifecycle or a special-cased reader, and leaves the REPL with no coherent "which file" answer.
|
|
699
|
+
- π§ βͺ "Bare `@` = the current file" (3.2/3.6); the "no current file for self-reference" error is gone, as it can no longer occur.
|
|
700
|
+
- π€ π©Ή Claude's first cut kept the self-value as an ordinary binding, so reading `__self__` returned an internal thunk and crashed serialization with a raw Ruby error; the designer caught it. Interpreter context now lives in its own channel, off the binding namespace.
|
|
701
|
+
|
|
702
|
+
### Pros
|
|
703
|
+
|
|
704
|
+
- One self-reference rule across files, inline source, and the REPL. The file path is incidental.
|
|
705
|
+
- No identifier-namespace pollution. Internals stay internal.
|
|
706
|
+
|
|
707
|
+
### Cons
|
|
708
|
+
|
|
709
|
+
- `__dir__` is no longer exposed.
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## 3.8 The jail: confining `@`-resolution to a directory
|
|
714
|
+
|
|
715
|
+
### Decisions
|
|
716
|
+
|
|
717
|
+
- π§ β
`-j/--jail DIR` confines `@`-resolution to `DIR` and its subtree. It defaults to the program's directory (or cwd for `-e` and the REPL). Available in every use case, the REPL included.
|
|
718
|
+
- π§ β
A relative `--jail` resolves against the default jail, so `-j ..` widens to the parent; `--jail '*'` disables confinement entirely.
|
|
719
|
+
- π§ β
An out-of-jail target is a `reference_error` (`outside the jail`).
|
|
720
|
+
- π§ β
The stdlib is unaffected by the jail. However, an existing file outside the jail raises an error and prevents falling through to a built-in or the stdlib.
|
|
721
|
+
- π§ β
`@`-references still resolve relative to the **referencing file**; the jail only filters the resolved target, it does not move the resolution base.
|
|
722
|
+
- π’ β
Containment is lexical (`expand_path` normalises `..`) and confines references to a directory tree. It is **not** a security sandbox and follows existing symlinks. Fusion cannot write files, so no symlink can be planted to escape. Any encountered symlink is part of the legitimate project layout.
|
|
723
|
+
|
|
724
|
+
### Alternatives
|
|
725
|
+
|
|
726
|
+
- π§ π Resolve `@`-references relative to the **jail root** instead of the referencing file (a `--relative-to-jail` mode). Rejected: it would make `@name` mean `<jail>/name` everywhere and turn per-directory sibling-shadowing (3.6) into jail-global shadowing (project-rooted imports).
|
|
727
|
+
- π€ β Resolve symlinks (`realpath`) to make the jail a hard boundary. Declined: it buys nothing here (a program that cannot write files cannot plant an escaping symlink) and a real security sandbox is tricky to build.
|
|
728
|
+
|
|
729
|
+
### Pros
|
|
730
|
+
|
|
731
|
+
- A `.fsn` program is sandboxed to its own directory by default; reaching out is explicit (`-j ..`) or an opt-out (`-j '*'`).
|
|
732
|
+
- The stdlib and stdin are untouched, so confinement never breaks an ordinary program.
|
|
733
|
+
|
|
734
|
+
### Cons
|
|
735
|
+
|
|
736
|
+
- Safe symlink-following rests on Fusion being unable to write files; adding a file-writing capability would mean revisiting it.
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## 3.9 Super-reference `@@`
|
|
741
|
+
|
|
742
|
+
### Decisions
|
|
743
|
+
|
|
744
|
+
- π§ β
`@@name` (and downward `@@dir/name`) resolves `@name` while *skipping its sibling files*. It always references a builtin/stdlib and is immune to local shadowing. Bare `@@` is the special case `@@<current-file>`.
|
|
745
|
+
- π§ β
Inline/REPL `@@` is a `reference_error` (`no enclosing file`). There is no filename to take "super" of.
|
|
746
|
+
|
|
747
|
+
### Alternatives
|
|
748
|
+
|
|
749
|
+
- π€ β Make `@name` never refer to its own file (so `@range` inside `range.fsn` means the stdlib function). Rejected: the override would name itself (breaking relocatability) and it adds a per-file carve-out to the resolution chain.
|
|
750
|
+
|
|
751
|
+
### Pros
|
|
752
|
+
|
|
753
|
+
- An override delegates to the original without a separate handle, and `@name` semantics are untouched.
|
|
754
|
+
- Relocatable: no file names itself.
|
|
755
|
+
- `@@name` provides a stable mechanism for error-payload patterns that must stay canonical regardless of a caller's shadows.
|
|
756
|
+
|
|
757
|
+
### Cons
|
|
758
|
+
|
|
759
|
+
- Reaches only what you shadow under your *own* name, not an arbitrary shadowed built-in.
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
539
763
|
# 4. Runtime and CLI
|
|
540
764
|
|
|
541
765
|
## 4.1 Runtime I/O contract
|
|
@@ -544,7 +768,7 @@ All access goes through `@`:
|
|
|
544
768
|
|
|
545
769
|
- π§ β
Read stdin as JSON β value `v`; compute `v | program`; print the result as JSON.
|
|
546
770
|
- π§ β
A final `!` produces a nonzero exit code.
|
|
547
|
-
- π€
|
|
771
|
+
- π€ βͺ Empty stdin was treated as `null`. Superseded in 4.4: empty stdin means *no input*.
|
|
548
772
|
- π€ β
Non-JSON stdin yields `!`.
|
|
549
773
|
|
|
550
774
|
### Alternatives
|
|
@@ -560,7 +784,119 @@ All access goes through `@`:
|
|
|
560
784
|
### Cons
|
|
561
785
|
|
|
562
786
|
- No streaming; whole input must be buffered and parsed.
|
|
563
|
-
- π©Ή A bare `!` with nonzero exit gives little diagnostic detail
|
|
787
|
+
- π©Ή A bare `!` with nonzero exit gives little diagnostic detail. Mitigated for runtime errors by more detailed error payloads in 2.9.
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## 4.2 No raw Ruby errors reach the user
|
|
792
|
+
|
|
793
|
+
### Decisions
|
|
794
|
+
|
|
795
|
+
- π§ β
The CLI contract already spends stdout (the result) and stderr (the error payload). There is **no third channel**, so a raw Ruby backtrace on stderr would corrupt the contract. Fusion therefore **catches every Ruby error a program can trigger and converts it to a standardized payload**.
|
|
796
|
+
- π’ β
Conversion happens *both* **deep** (so an error becomes catchable by other code as soon as possible) *and* at a **top-level net** (so nothing escapes). Notably `SystemStackError` becomes a regular error value `stack_error` on stderr.
|
|
797
|
+
- π§ β
The internal "assertions" (`raise Unreachable`) are a deliberate exception. Reaching them is an interpreter bug. Interpreter bugs should surface and are allowed to violate our CLI contract.
|
|
798
|
+
- π§ β
The old `FUSION_DEBUG` env var (which wrote to stderr via `warn`) is removed entirely.
|
|
799
|
+
|
|
800
|
+
### Alternatives
|
|
801
|
+
|
|
802
|
+
- π€ β A top-level net only β rejected because a mid-program Ruby error would then become a final result rather than a catchable `!`.
|
|
803
|
+
- π€ π An `internal_error` kind for the invariant asserts. Rejected, because interpreter bugs shouldn't be catchable and shouldn't be a seemingly regular final result.
|
|
804
|
+
|
|
805
|
+
### Pros
|
|
806
|
+
|
|
807
|
+
- The CLI contract holds unconditionally. A program can even crash the interpreter's host language via infinite recursion and the user still gets a clean JSON payload and exit `1`.
|
|
808
|
+
|
|
809
|
+
### Cons
|
|
810
|
+
|
|
811
|
+
- Implementation effort.
|
|
812
|
+
- Might hide valuable debugging information. However, the "top-level net" could be made opt-out.
|
|
813
|
+
- The `location` for a stack overflow is only `"interpreter"`. No single owning file is knowable at that point.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## 4.3 Runtime errors and lenient JSON serialization
|
|
818
|
+
|
|
819
|
+
### Decisions
|
|
820
|
+
|
|
821
|
+
- π’ β
To serialize values without a valid JSON representation, we introduce "lenient serialization". Values without JSON representation get turned into string representations (`"<function>"` and `"<Infinity>"`/`"<-Infinity>"`/`"<NaN>"`).
|
|
822
|
+
- π§ β
The remaining data structure gets preserved.
|
|
823
|
+
- π’ β
Runtime errors (`ErrorVal#runtime?`) get serialized leniently by default, so their info isn't obscured by a `serialization_error`.
|
|
824
|
+
- π€ βͺ The stdlib doesn't produce runtime errors. Reverted in Β§2.13. stdlib errors now get marked as runtime errors.
|
|
825
|
+
- π§ βͺ All stdlib functions use `@sanitize` to mimick the lenient JSON serialization and preserve as much info as possible. Obsoleted by Β§2.13. However, `@sanitize` is kept as a utility.
|
|
826
|
+
- π§ β
Ordinary values and user errors are serialized strictly to avoid surprising type conversions. If they fail to serialize, they get turned into a runtime `serialization_error` and will subsequently get serialized leniently.
|
|
827
|
+
|
|
828
|
+
### Alternatives
|
|
829
|
+
|
|
830
|
+
- π€ β The standard library could create real "runtime errors" via a dedicated `@raise` primitive. Declined: it offers little over the `!` prefix and would let *any* code create runtime errors. Instead a stdlib `!{β¦}` is marked runtime by its construction *location* β the same benefit (lenient serialization, a consistent shape, call-site `file`), confined to stdlib source.
|
|
831
|
+
- π§ β Runtime errors also serialize strictly by default. Rejected, because this would turn too many errors into a `serialization_error` and would lose too much information.
|
|
832
|
+
- π§ π All errors serialize leniently by default. Rejected, because this hides real errors (e.g. `NaN` or a function as a result) behind an automatic type conversion.
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## 4.4 CLI use cases and I/O modes
|
|
837
|
+
|
|
838
|
+
Extends the single runtime contract of 4.1 into three use cases and four ways an
|
|
839
|
+
error value can cross the boundary.
|
|
840
|
+
|
|
841
|
+
### Decisions
|
|
842
|
+
|
|
843
|
+
**Use cases:**
|
|
844
|
+
|
|
845
|
+
- π§ β
The CLI supports three use cases: **pipe** (apply the program to one input, Β§4.1), **stream** (apply it to each line of an NDJSON stream), **repl** (interactive).
|
|
846
|
+
- π§ β
**pipe**: Compute `stdin | program`. Empty or whitespace-only stdin means *no input*: return `program` (evaluated on load). So a `.fsn` file doubles as enriched JSON (computations, `@ENV`, `@`-references).
|
|
847
|
+
- π§ β
**stream**: Conforms to NDJSON. Keeps errors in-band and continues the stream. Always exits `0`.
|
|
848
|
+
- π§ β
A blank **stream** input line is echoed as a blank output line (no computation); `--skip-blank-lines` drops it instead.
|
|
849
|
+
- π§ β
**repl**: Can evaluate expressions. Also allows an assignment statement: `identifier = expression`.
|
|
850
|
+
- π€ β
A command-line misuse (unknown flag, more than one use case, conflicting input/output modes, unsupported mode combination, missing program, `-!` with empty stdin) is plain usage text on stderr with exit `1`, never a payloaded error. Most are caught during option parsing, `-!` with empty-stdin while reading input.
|
|
851
|
+
|
|
852
|
+
**I/O modes** β how an error is marked crossing the boundary; `--input` and `--output` are independent:
|
|
853
|
+
|
|
854
|
+
TODO: Under `-!` the input is the error payload, so empty stdin is a usage error (nothing to mark).
|
|
855
|
+
|
|
856
|
+
- π§ β
Four modes: **unix** (asymmetric, Unix filter) and **bang** / **array** (`[0, value]` / `[1, payload]`), **object** (`{"value": _}` / `{"error": _}`).
|
|
857
|
+
- π§ β
**unix**: input is `stdin` + `-!` flag, output is `value β stdout` + `exit 0` OR `error β stderr` + `exit 1`. stdin/stdout/stderr are always pure JSON.
|
|
858
|
+
- π§ β
**bang**: shortest encoding, errors are simply a `!` prefix and thus not valid JSON.
|
|
859
|
+
- π§ β
**array**: always valid JSON, error encoded via envelope: `[0, value]` / `[1, payload]`.
|
|
860
|
+
- π§ β
**object**: always valid JSON, error encoded via envelope: `{"value": _}` / `{"error": _}`.
|
|
861
|
+
- π§ β
`-!` requires stdin. With absent stdin, it's a usage error.
|
|
862
|
+
- π§ β
pipe supports all four modes; stream all but unix (so errors stay "in-band"); repl has none.
|
|
863
|
+
- π§ β
Defaults: pipe = unix/unix, stream = array/array.
|
|
864
|
+
- π€ β
A malformed `array`/`object` input envelope is a catchable `argument_error` at `location: "input"` (the array tag must be exactly the integer `0`/`1`), flowing into the program like any input error.
|
|
865
|
+
|
|
866
|
+
**REPL:**
|
|
867
|
+
|
|
868
|
+
- π§ β
An entry is an **expression** (evaluated and printed) or an **assignment statement** `identifier = expression` (evaluated, printed, and bound for later entries).
|
|
869
|
+
- π§ β
An entry is evaluated only once it parses as a whole statement/expression. An incomplete or invalid buffer keeps the entry open to finish or correct.
|
|
870
|
+
- π§ β
Error results can also get bound to an identifier via the **assignment statement**. When accessing them, they'll propagate regularly.
|
|
871
|
+
- π€ β
Results print leniently (a function as `"<function>"`, etc.); entries report errors at `location: "code <inline>"`, like `-e`.
|
|
872
|
+
- π€ β
Results go to stdout; the prompt and echoed input go to stderr (like a shell prompt), so stdout is a clean stream of results.
|
|
873
|
+
- π§ β
`stderr` decorations are styled. Styling never touches `stdout`.
|
|
874
|
+
|
|
875
|
+
### Alternatives
|
|
876
|
+
|
|
877
|
+
- π§ βͺ pipe was the unconditional default β now repl on a bare `fusion`, otherwise pipe.
|
|
878
|
+
- π€ βͺ stream defaulted to `bang`/`bang` β now `array`/`array`, since `bang` lines aren't valid JSON.
|
|
879
|
+
- π€ βͺ Empty input was the value `null`, and input could also come from an inline `[json-input]` argument β now empty means *no input* and input is stdin-only.
|
|
880
|
+
- π§ βͺ Empty stdin under `-!` supplied `!null` β now a usage error, since there is no payload to mark.
|
|
881
|
+
- π€ βͺ Blank `stream` lines were silently skipped β now echoed by default, dropped with `--skip-blank-lines`.
|
|
882
|
+
- π§ βͺ REPL accepts only statements (the original "introduces a single statement" sketch); widened so a bare expression is also an entry.
|
|
883
|
+
- π§ βͺ Statements terminated by `;` (so several could share a line); dropped β completeness is decided by parsing, so a line that parses is submitted and no terminator is needed.
|
|
884
|
+
- π€ βͺ A statement does **not** bind an error result (mirroring pattern binders, which never capture an error). Flipped: a statement is an assignment, not a pattern match; binding an error is harmless and needs no special case.
|
|
885
|
+
- π€ β unix mode for stream β one exit code and one stdout/stderr pair cannot mark errors per record.
|
|
886
|
+
- π€ π A single `--mode` controlling both directions β input and output modes are deliberately independent.
|
|
887
|
+
|
|
888
|
+
### Pros
|
|
889
|
+
|
|
890
|
+
- One small flag surface spans first-class Unix filters (pipe), bulk processing (stream), and interactive exploration (repl).
|
|
891
|
+
- Errors cross the boundary in whatever shape the surrounding tool wants β exit code, `!` sentinel, or envelope β with input and output chosen independently.
|
|
892
|
+
- The REPL reuses the whole language: an entry is just an expression, with assignment the one addition, and a bound error propagates like any other value-or-error (no carve-out).
|
|
893
|
+
- A program with no stdin doubles as enriched JSON (computation, `@ENV`, `@`-references), and `--stream` emits valid NDJSON.
|
|
894
|
+
|
|
895
|
+
### Cons
|
|
896
|
+
|
|
897
|
+
- Four error-marking modes are more surface than the original single unix contract.
|
|
898
|
+
- Completeness-by-parsing submits a complete-but-maybe-unintended line (e.g. `x = 5` when more was meant) as-is.
|
|
899
|
+
- Empty stdin no longer means the value `null`; to feed `null` you pipe the literal `null`.
|
|
564
900
|
|
|
565
901
|
---
|
|
566
902
|
|
|
@@ -570,9 +906,8 @@ All access goes through `@`:
|
|
|
570
906
|
|
|
571
907
|
### Decisions
|
|
572
908
|
|
|
573
|
-
- π§
|
|
574
|
-
- π§ β
Arithmetic, comparison, and boolean operations are built-in functions
|
|
575
|
-
- π§ β
Sugar is explicitly deferred, not rejected.
|
|
909
|
+
- π§ βͺ No infix `+ - * / == && β¦` until the core semantics are settled. Superseded by Β§5.6. Syntax sugar is now implemented.
|
|
910
|
+
- π§ β
Arithmetic, comparison, and boolean operations are built-in functions reached via an `@-reference`, e.g. `[a, b] | @OP.sum`.
|
|
576
911
|
|
|
577
912
|
### Alternatives
|
|
578
913
|
|
|
@@ -585,7 +920,7 @@ All access goes through `@`:
|
|
|
585
920
|
|
|
586
921
|
### Cons
|
|
587
922
|
|
|
588
|
-
- Arithmetic-heavy code is verbose and harder to read (`[n, [n, 1] | @
|
|
923
|
+
- Arithmetic-heavy code is verbose and harder to read (`[n, [n, -1] | @OP.sum | @fact] | @OP.product` vs. `n * fact(n-1)`).
|
|
589
924
|
|
|
590
925
|
---
|
|
591
926
|
|
|
@@ -618,17 +953,10 @@ All access goes through `@`:
|
|
|
618
953
|
### Decisions
|
|
619
954
|
|
|
620
955
|
- π€ β
Only things that can't be built in Fusion itself become a builtin. Other frequently used functions become part of the standard library.
|
|
621
|
-
- π€ β
The interpreter provides these builtins:
|
|
622
|
-
- arithmetic (`add`, `subtract`, `multiply`, `divide`, `mod`, `negate`, `floor`);
|
|
623
|
-
- comparison (`equals`, `lessThan`);
|
|
624
|
-
- boolean (`and`, `or`, `not`);
|
|
625
|
-
- bridges (`length`, `concat`, `chars`, `join`, `toString`, `parseNumber`, `keys`, `values`);
|
|
626
|
-
- predicates (`Integer`, `Float`, `Number`, `String`, `Boolean`, `Array`, `Object`, `Null`).
|
|
627
956
|
- π€ β
`keys` must be a builtin: pattern matching can pull *known* object keys but cannot enumerate *unknown* ones, so iterating an object of unknown shape is impossible without it.
|
|
628
957
|
|
|
629
958
|
### Alternatives
|
|
630
959
|
|
|
631
|
-
- π€ β Derive `lessThan`'s siblings as built-ins too (chose to leave `lessEq`/`greaterThan`/etc. to the library).
|
|
632
960
|
- π€ β Omit `values` (derivable from `keys`).
|
|
633
961
|
|
|
634
962
|
### Pros
|
|
@@ -672,3 +1000,65 @@ All access goes through `@`:
|
|
|
672
1000
|
|
|
673
1001
|
- No way to annotate a single token mid-line; an explanatory comment must occupy its own line above the code.
|
|
674
1002
|
- A breaking change from the earlier `//` / `/* */` syntax (acceptable at this Alpha stage).
|
|
1003
|
+
|
|
1004
|
+
---
|
|
1005
|
+
|
|
1006
|
+
## 5.5 Reworking the builtins and standard library
|
|
1007
|
+
|
|
1008
|
+
### Decisions
|
|
1009
|
+
|
|
1010
|
+
- π§ β
Bundle the most important arithmetic and logic operations together into a single `@OP` reference.
|
|
1011
|
+
- π§ β
Make the `stdlib` as orthogonal as possible to `@OP`.
|
|
1012
|
+
- π§ β
Higher-order helpers (`map`, `filter`, `reduce`, `compact`, `flatten`, `any`, `all`) are implemented via recursion in Fusion, not hidden in Ruby. They work for both arrays and objects where possible.
|
|
1013
|
+
- π§ β
Provide access to more advanced mathematical operations in `@math`.
|
|
1014
|
+
- π§ β
Where possible, builtins and stdlib functions are n-ary instead of binary.
|
|
1015
|
+
|
|
1016
|
+
### Alternatives
|
|
1017
|
+
|
|
1018
|
+
- π€ βͺ Keep every operator as its own builtin (the Β§5.3 set). Superseded: bundling into `@OP` gives a directory one place to shadow them all.
|
|
1019
|
+
- π€ β Resolve `@OP` **dynamically** (at the call site) so stdlib helpers follow a foreign override. Rejected: it breaks per-directory confinement.
|
|
1020
|
+
|
|
1021
|
+
### Pros
|
|
1022
|
+
|
|
1023
|
+
- By bundling operators into `@OP`, the presence of an `OP.fsn` file will immediately become a warning sign that core operations might behave differently in a given directory.
|
|
1024
|
+
- By making the `stdlib` orthogonal to `@OP`, a shadowed `@OP` will not create footguns where `stdlib` functions still refer to the original implementation.
|
|
1025
|
+
|
|
1026
|
+
### Cons
|
|
1027
|
+
|
|
1028
|
+
- Not all operators with syntax sugar have been grouped into `@OP`. Exceptions are the structural operators `@map`, `@filter`, `@reduce`.
|
|
1029
|
+
- The few helpers that still reference `@OP` (currently only `@range`) will ignore `@OP` overrides. To make them aware of `@OP` overrides, create a copy of their `stdlib` source code next to your `OP.fsn`.
|
|
1030
|
+
- The native way of writing division `[a, b | @OP.negate] | @OP.product` is numerically incorrect and might produce a double rounding error. For the numerically correct division you have to use `@math.divide`.
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
## 5.6 Syntax sugar
|
|
1035
|
+
|
|
1036
|
+
Implements the infix-operator sugar deferred in Β§5.1. Additionally adds `map` / `filter` / `reduce`
|
|
1037
|
+
pipe operators. Promotes `[]`/`[=]` to core syntax. Desugaring is purely syntactic. The only new
|
|
1038
|
+
runtime node is the `[=]` setter.
|
|
1039
|
+
|
|
1040
|
+
### Decisions
|
|
1041
|
+
|
|
1042
|
+
- π’ β
Precedence, tightestβloosest: postfix (`.` `[]` `[=]`) β unary prefix (`! - / ~`) β pipe (`| |: |? |+`) β multiplicative (`* / % //`) β additive (`+ -`) β `??` β `==` β `&&` β `||`. Pipe binds just under unary, tighter than every value operator.
|
|
1043
|
+
- π§ β
The array/object setter gets promoted to core syntax `container[key = value]`. The `@set` builtin is removed.
|
|
1044
|
+
- π§ β
The getter `container[key]` stays core syntax instead of becoming syntax sugar for `@get`. The `@get` builtin is removed.
|
|
1045
|
+
- π§ β
A maximal run of `+`/`-` folds to one `[terms] | @OP.sum`; a run of `*`/`/` to one `[terms] | @OP.product`. `-` operands are negated, `/` operands inverted.
|
|
1046
|
+
- π§ β
`-` is always an operator; the lexer emits no negative-number literals.
|
|
1047
|
+
- π§ β
A `-` before a bare numeric literal folds into a negative literal (in expressions and patterns alike); any other operand becomes `operand | @OP.negate`. In a pattern, `-` may only precede a numeric literal.
|
|
1048
|
+
- π§ β
`/` never folds to a literal: `a / 2` β `[a, 2|@OP.invert] | @OP.product`, so `/0` stays a runtime `math_error`.
|
|
1049
|
+
- π§ β
A single `/` is a path separator only inside a file-reference path; elsewhere it is division/invert. `@a/b` is the path `a/b`; `@a / b` (any space around the slash) is `@a` divided by `b`.
|
|
1050
|
+
- π§ β
The lexer emits a `path` token only immediately after `@`/`@@`, tight: no space after `@`/`@@`, interior slashes abutting their segments. A path starts with an identifier or `..`, never a lone `.` (so `@.map` is `.map` access on bare `@`). Bare `@`/`@@` when no tight path follows.
|
|
1051
|
+
- π§ β
Whitespace between `@`/`@@` and its path is no longer allowed (`@ name` is a syntax error).
|
|
1052
|
+
- π§ β
Runs of `==`/`&&`/`||` fold n-ary to `@OP.equal`/`@OP.and`/`@OP.or`.
|
|
1053
|
+
- π§ β
`??` is its own precedence level, tighter than `==` (compare produces an ordinal that `==` then tests; a boolean can't be compared).
|
|
1054
|
+
- π§ β
`xs |: f`, `xs |? f`, `xs |+ f` desugar to `{"c": xs, "f": f}` piped into `@map`/`@filter`/`@reduce`.
|
|
1055
|
+
|
|
1056
|
+
### Alternatives
|
|
1057
|
+
|
|
1058
|
+
- π€ βͺ Lex negative-number literals (JSON-style). Rewound: `a-3` would be ambiguous between the literal `-3` and subtraction.
|
|
1059
|
+
- π€ β Desugar every `-x` to `x | @OP.negate`, dropping literal negatives. Rejected: `-5` should stay a plain literal, not an `@OP`-routed computation.
|
|
1060
|
+
- π€ β Assemble the path in the parser so `@a / b` is also the path `a/b` (divide via `(@a) / b`). Rejected: a spaced `@a / b` should read as division like every other operator.
|
|
1061
|
+
|
|
1062
|
+
### Pros
|
|
1063
|
+
|
|
1064
|
+
- Giving `pipe` the tightest precedence keeps the "useful reading" paren-free: a pipe's RHS has to always be a function and arithmetic never yields one, so `x|@f + 1` can only sensibly mean `(x|@f) + 1`.
|