fusion-lang 0.0.1.alpha1

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.
@@ -0,0 +1,97 @@
1
+ # Fusion roadmap
2
+
3
+ Tracked future work and open questions. Decisions that have already been made
4
+ live in [design.md](./design.md); this file is only for things still ahead.
5
+
6
+ ---
7
+
8
+ ## 1. Ergonomics: the most-wanted improvements
9
+
10
+ **Operator sugar (planned).** Reintroduce infix `+ - * / % == != < <= > >= && || !`
11
+ and string `++`, desugaring to the existing built-ins over pairs. Pure ergonomics,
12
+ no semantic change. This is the single biggest readability win available and was
13
+ always intended. Open question: exact precedence table and how it interleaves with
14
+ `|` and `=>`.
15
+
16
+ **`@`-namespace resolution polish.** Decide on project-root confinement
17
+ (sandboxing) for `@../` escapes; consider a configurable standard-library search
18
+ path; consider tooling to surface *which* target a given `@name` resolves to in
19
+ a directory, since shadowing is invisible at the call site.
20
+
21
+ ---
22
+
23
+ ## 2. Error model
24
+
25
+ **Payload shape consistency** *(open)*. The payload *shape* is inconsistent:
26
+ built-ins use bare strings (`"divide: division by zero"`) while runtime
27
+ mechanics use structured objects (`{"kind":"missing_key","key":"foo"}`). The
28
+ string form is human-friendlier; the structured form is machine-friendlier
29
+ (catchable via `!{"kind": k}`). Three plausible resolutions:
30
+
31
+ - (a) all errors get structured payloads with a `kind` and an optional `message`;
32
+ - (b) all errors get human strings, and structured matching is left to user code;
33
+ - (c) keep both, document the rule that built-in operations use strings and
34
+ runtime mechanics use objects.
35
+
36
+ This is a small but irreversible decision (it shapes how catch clauses are
37
+ written) and should be decided before any external code grows that depends on
38
+ the current shapes.
39
+
40
+ **Better diagnostics.** `FUSION_DEBUG` exists for file/parse errors; extend
41
+ principled diagnostics to runtime error origins (where did this error first
42
+ arise?). One option: attaching a source position to the payload (an extra
43
+ `"at": "file.fsn:L:C"` field) when `FUSION_DEBUG` is set.
44
+
45
+ **Stack traces** *(deferred)*. A propagated error tells you what happened, but
46
+ not the chain of function applications it passed through. A capped trace
47
+ (last N frames, accessible as an extra payload field, opt-in via env) would
48
+ help in deep pipelines.
49
+
50
+ ---
51
+
52
+ ## 3. Standard library completion
53
+
54
+ Populate Tier 1 (written in Fusion): `filter`, `reduce`/`fold`, `reverse`,
55
+ `head`, `tail`, `last`, `init`, `take`, `drop`, `zip`, `flatten`, `member`,
56
+ `find`, `all`, `any`, `count`; comparison derivatives `lessEq`, `greaterThan`,
57
+ `greaterEq`, `notEquals`; object helpers `entries`, `get`, `set`, `merge`; an
58
+ `if` helper. This is also the best stress test of whether the language is
59
+ pleasant to *write* in, not just to implement.
60
+
61
+ ---
62
+
63
+ ## 4. Runtime and tooling
64
+
65
+ - **Streaming I/O** (NDJSON): map a program over a stream of JSON values.
66
+ - **A faster implementation** once semantics are frozen.
67
+
68
+ ## 5. Open semantic questions to settle
69
+
70
+ - Numeric tower: keep int/float split, or move to a single number type /
71
+ arbitrary precision? Affects `divide`, `floor`, `equals`, and the
72
+ `Integer`/`Float` predicates.
73
+ - Function equality: `equals` on two functions — always `false`, or an error?
74
+ (Function equality is undecidable beyond trivial identity.)
75
+
76
+ ---
77
+
78
+ ## 6. Bigger experiments
79
+
80
+ **Destructuring functions (homoiconicity).** Treat a function as a list of
81
+ `(pattern, output)` clause-pairs and pattern-match on it, enabling macros and
82
+ function transformers with the same matching machinery. The clean path is
83
+ explicit, opt-in reflection (`reflect : function → data`,
84
+ `reify : data → function`) representing patterns as reflective AST objects, so
85
+ normal code keeps functions opaque and "three ingredients" intact. High payoff
86
+ (metaprogramming), moderate disruption.
87
+
88
+ **Running functions backwards (relational mode).** Given an output, find an
89
+ input — unification and search, à la Prolog/miniKanren. Clean only for
90
+ invertible functions; hopeless for many-to-one. Would change Fusion from
91
+ functional to relational and needs backtracking search. The most exciting and
92
+ most disruptive possible direction; best pursued as a separate mode or sibling
93
+ project rather than folded into the core.
94
+
95
+ **A static checker.** Because "types" are predicates, an optional static layer
96
+ could attempt to verify predicate-guarded clauses and exhaustiveness without
97
+ changing the dynamic semantics. Speculative.
@@ -0,0 +1,157 @@
1
+ # Explanation
2
+
3
+ *This is an understanding-oriented discussion: it provides context and answers "why?".
4
+ This article may hold opinions and approach the subject from several angles. For the
5
+ formal decision ledger (who decided what, alternatives, trade-offs) see the
6
+ [Design documentation](../lang/design.md).*
7
+
8
+ ---
9
+
10
+ ## What Fusion is, in one breath
11
+
12
+ Fusion is "functional JSON": take JSON — its atoms, arrays, and objects — and add one
13
+ more ingredient, the function. Keep the count of concepts brutally small. From those
14
+ pieces, recover everything a small language needs: branching, looping, types,
15
+ modules, and error handling. The whole design is an exercise in *not adding things*
16
+ and discovering that the pieces you already have are enough.
17
+
18
+ ---
19
+
20
+ ## Why only three ingredients
21
+
22
+ Most languages accrete features: statements, loops, conditionals, classes,
23
+ exceptions, a type system, a module system, each with its own syntax. Fusion's bet is
24
+ that almost all of that is unnecessary if your three composite ingredients — arrays,
25
+ objects, functions — are chosen well and made to compose.
26
+
27
+ The discipline pays a recurring dividend. Again and again, a feature that would
28
+ normally be its own language construct turns out to be expressible with what already
29
+ exists:
30
+
31
+ - An **`if`** is a function with `true` and `false` clauses.
32
+ - A **`for` loop** is recursion that pattern-matches a list down to `[]`.
33
+ - A **type** is a predicate function attached with `?`.
34
+ - A **module** is a file, and an import is a reference to that file's value.
35
+ - **Error propagation** is just how application treats the error value.
36
+
37
+ None of these required new syntax. That is the aesthetic core of the language: when
38
+ you feel the urge to add a construct, look harder at the three you have.
39
+
40
+ ---
41
+
42
+ ## Why functions take exactly one argument
43
+
44
+ This looks like a severe restriction and is actually a simplification that makes
45
+ everything else line up. With exactly one input and one output:
46
+
47
+ - **Application has one shape:** `value | function`. There is no argument list, no
48
+ call syntax, no arity to track. A pipeline `a | f | g | h` reads like a sentence.
49
+ - **Multi-argument needs are met by data:** pass an array `[a, b]` or an object
50
+ `{"f": ..., "xs": ...}`. The "arguments" become a value you can also store, inspect,
51
+ and destructure — there's no separate notion of "argument tuple."
52
+ - **Pattern matching has one job:** match the single input. A function's clauses are
53
+ just alternative shapes that one input might have.
54
+
55
+ The cost is verbosity in arithmetic (`[a, b] | @add` instead of `a + b`) and a little
56
+ ceremony for multi-argument library functions. The first is a candidate for later
57
+ syntactic sugar; the second is mild. In exchange, the evaluation model is almost
58
+ trivially simple, which is exactly what you want in a language meant to be small.
59
+
60
+ ---
61
+
62
+ ## Why bare words are "holes"
63
+
64
+ The keystone trick is that a bare identifier means *bind* in a pattern and *read* in
65
+ an expression. This works because JSON has no bare identifiers — strings are quoted,
66
+ so an unquoted word is an unused syntactic slot. Fusion claims that slot for local
67
+ variables.
68
+
69
+ The consequence is a pleasing symmetry: a pattern and a result are mirror images.
70
+ `[a, b] => [b, a]` reads almost pictorially — the same names appear on both sides,
71
+ filled from the input on the left and poured into the output on the right. You never
72
+ write "get element 0, get element 1, now build a new array"; you draw the shape you
73
+ have and the shape you want, and the names connect them. Destructuring stops being an
74
+ operation and becomes a *correspondence*.
75
+
76
+ ---
77
+
78
+ ## Why pattern matching is the only control flow
79
+
80
+ Because functions dispatch by matching their single input against ordered clauses,
81
+ "choosing what to do" and "taking apart the data" are the same act. A clause is
82
+ simultaneously a condition (does this shape match?) and a binding (here are the
83
+ pieces). This collapses two things most languages keep separate — `switch` and
84
+ destructuring assignment — into one.
85
+
86
+ Once control flow *is* matching, recursion naturally absorbs iteration. A list is
87
+ either `[]` or `[head, ...tail]`; those two shapes are the two clauses of almost every
88
+ list function; the recursive call shrinks the input until the base shape matches. The
89
+ "loop" is invisible because it isn't a loop — it's a function rediscovering a smaller
90
+ version of its own input.
91
+
92
+ The one wrinkle this introduces: what should happen when *nothing* matches? Fusion's
93
+ answer is `null` by default. If you want to make a function total, simply append
94
+ `_ => default_value` as a final clause. If you want to make it strict, append
95
+ `_ => !error` instead.
96
+
97
+ ---
98
+
99
+ ## Why a file is exactly one value
100
+
101
+ Earlier drafts had a program be a list of top-level `name = value` bindings plus a
102
+ "main" expression — essentially a second little language stacked on top of the
103
+ expression language, with its own scoping rules (the bindings had to be mutually
104
+ recursive, a `letrec`). The "one value per file" rule deletes all of that. The
105
+ outermost layer of a program is now the same kind of thing as every inner layer: a
106
+ value. A program is just a file whose value happens to be a function, and running it
107
+ is `STDIN | thatFunction`.
108
+
109
+ This also resolved the module system for free. If a file is a value, then referencing
110
+ a file *is* importing a value — no separate `import` construct, no namespace syntax.
111
+ The directory tree becomes the namespace. The standard library is just a folder of
112
+ files. One mechanism (`@`-references) now does top-level structure, modules, and
113
+ library delivery — and, in the current design, built-in access too: `@add` and
114
+ `@Integer` are looked up through the very same `@name` machinery as files. A bare
115
+ `@name` checks for a sibling file, then a built-in, then a standard-library file, so
116
+ your own files can locally shadow a built-in or a stdlib function without affecting
117
+ any other directory. The cost of folding built-ins into `@` is that a bare word like
118
+ `add` is no longer the built-in — it is only ever a pattern hole — so built-ins must
119
+ always be written with `@`.
120
+
121
+ ---
122
+
123
+ ## Why references are lazy
124
+
125
+ If `@`-references resolved eagerly (when a file loads), then a file referencing itself
126
+ would loop forever before it could ever run. Lazy resolution — resolve a reference
127
+ only when it is actually used — is what makes self-recursion and mutual recursion
128
+ possible at all. It is not a performance nicety; it is load-bearing for the whole
129
+ recursion story. Memoization on top means a referenced file is evaluated once and
130
+ shared, so diamond-shaped dependencies are cheap and a referenced-but-unused file is
131
+ never loaded.
132
+
133
+ A side effect is that *data* cycles (files whose values point at each other directly,
134
+ not through a function) are non-productive — there's no pattern match to bottom out
135
+ on. The runtime detects the cycle and yields `!` at that point, while keeping whatever
136
+ productive structure surrounds it.
137
+
138
+ ---
139
+
140
+ ## The roads not taken (and one we're still tempted by)
141
+
142
+ Two ideas were explored and deliberately set aside, both documented in the design doc.
143
+
144
+ **Operator sugar.** We could write `a + b` and desugar it to `[a, b] | @add`. We rolled
145
+ this back early to keep the core honest, with the explicit intent to reintroduce it
146
+ once the semantics were settled. It is a pure ergonomics layer; it changes nothing
147
+ underneath.
148
+
149
+ **Destructuring functions.** The tantalizing one. Since a function literal is visibly
150
+ a list of `pattern => output` clauses, could we pattern-match *on a function*, taking
151
+ it apart as data? That would give Lisp-like metaprogramming with the same matching
152
+ machinery — and, in its wildest form, the ability to run functions *backwards* (given
153
+ an output, find an input), which is logic programming. We kept it out of the grammar
154
+ because the clean version requires representing patterns as data (a function's pattern
155
+ contains unbound binders, so it isn't an ordinary value), and the wild version would
156
+ turn Fusion from a functional language into a relational one. It remains the most
157
+ interesting possible future for the language, and the most disruptive.
@@ -0,0 +1,205 @@
1
+ # How-to guides
2
+
3
+ *These are recipes for specific real-world tasks and assume you already know the
4
+ basics. Scan for the problem you have and copy the solution.*
5
+
6
+ ---
7
+
8
+ ## Diagnose a program that returns an error unexpectedly
9
+
10
+ When you run a program and see an error payload on stderr, the payload itself
11
+ usually tells you what went wrong (e.g. `"divide: division by zero"` or
12
+ `{"kind":"missing_key","key":"email"}`). For errors arising *during reference
13
+ resolution* — typically a missing file or a parse error in an `@`-referenced file —
14
+ enable extra diagnostics:
15
+
16
+ ```sh
17
+ FUSION_DEBUG=1 fusion program.fsn '...'
18
+ ```
19
+
20
+ With `FUSION_DEBUG` set, the interpreter prints to stderr the exact path it failed
21
+ to find or the parse error it hit. The most common cause is that the `stdlib/`
22
+ folder is not where the interpreter expects it, so `@`-references can't be resolved.
23
+
24
+ ---
25
+
26
+ ## Emulate control structures from other programming languages
27
+
28
+ 1. Branch on a condition (emulate `if`)
29
+
30
+ Compute a boolean, then pipe it into a two-clause function:
31
+
32
+ ```fusion
33
+ (n =>
34
+ [n, 0] | @lessThan | (
35
+ true => "negative",
36
+ false => "non-negative"
37
+ )
38
+ )
39
+ ```
40
+
41
+ You don't need to restrict yourself to 2 branches and the intermediate values `true` and `false`.
42
+ Here's an elegant way of writing FizzBuzz:
43
+
44
+ ```fusion
45
+ (
46
+ n =>
47
+ [
48
+ [n, 3] | @mod,
49
+ [n, 5] | @mod,
50
+ ]
51
+ |
52
+ (
53
+ [0, 0] => "FizzBuzz",
54
+ [0, _] => "Fizz",
55
+ [_, 0] => "Buzz",
56
+ _ => n,
57
+ )
58
+ )
59
+ ```
60
+
61
+ 2. Loop over a list (emulate `for`)
62
+
63
+ Use recursion on lists with the base case `[]` and the recursive case `[x, ...rest]`.
64
+ You could write `sum` like this:
65
+
66
+ ```fusion
67
+ (
68
+ [] => 0,
69
+ [x, ...rest] => [x, rest | @] | @add
70
+ )
71
+ ```
72
+
73
+ If you don't have a list yet, but want to repeat something `n` times, use the
74
+ standard library function `range` to construct a list. `5 | @range` will evaluate
75
+ to `[0,1,2,3,4]`.
76
+
77
+ If you need further inputs in addition to your list, e.g. a function, use an object:
78
+ `{"function": f, "list": [1, 2, 3]}`.
79
+
80
+ 3. Apply a function partially (only provide a subset of required inputs and emulate `currying`)
81
+
82
+ A Fusion function takes exactly one input. Functions that require multiple inputs bundle
83
+ them into an array (or object) and destructure that in the pattern:
84
+
85
+ ```fusion
86
+ ([a, b] => [a, b] | @add)
87
+ ```
88
+
89
+ Call it as `[3, 4] | @thatFunction`
90
+
91
+ To curry a function call (that means to supply arguments one at a time) write a function that
92
+ takes a single argument (`x` in the example below) and returns a simpler function that only
93
+ needs the remaining arguments (`y` in the example below). Each `=>` consumes one argument
94
+ and hands back a function waiting for the next:
95
+
96
+ ```fusion
97
+ (x => (y => [x, y] | @add))
98
+ ```
99
+
100
+ Call it as `4 | (3 | @thatFunction)`. `3 | f` yields a one-argument function that adds 3,
101
+ and piping a second value into that finishes the sum.
102
+
103
+ With this, you can partially apply functions. Apply the first argument now and then use
104
+ the resulting function later.
105
+
106
+ ---
107
+
108
+ ## Match on a condition involving several captured values
109
+
110
+ A `?` predicate sees **only** the value its own pattern matched — it cannot see
111
+ sibling bindings. To compare two captured values, attach the predicate to the
112
+ **parent container** so it receives the whole thing.
113
+
114
+ For example, to recognise a three-element palindrome you have to compare the first
115
+ and last elements. A predicate guarding `first` on its own could never see `last`,
116
+ so guard the whole array instead:
117
+
118
+ ```fusion
119
+ (
120
+ [] => "palindrome",
121
+ [_] => "palindrome",
122
+ [_, ...rest, _] ? ([first, ..., last] => [first, last] | @equals) => rest | @,
123
+ _ => "not a palindrome"
124
+ )
125
+ ```
126
+
127
+ The outer pattern `[_, ...rest, _]` is what you destructure for retrieving the middle
128
+ of the array which you need to continue your recursion. The inline predicate
129
+ `([first, ..., last] => [first, last] | @equals)` independently destructures the same
130
+ array again to compare its two ends.
131
+
132
+ ---
133
+
134
+ ## Shadow a built-in or stdlib function locally
135
+
136
+ Because a sibling file wins over a built-in or the standard library, you can override
137
+ either — but only for files in the same directory, so you can't break things
138
+ globally. Put an `add.fsn` next to your program and every `@add` *in that directory*
139
+ uses yours; files elsewhere still get the built-in.
140
+
141
+ ---
142
+
143
+ ## Read environment variables
144
+
145
+ `@ENV` evaluates to an object of all environment variables (every value is a string;
146
+ nothing is auto-parsed). Read one with member access:
147
+
148
+ ```fusion
149
+ # with CI=1 in the environment, yields the string "1"
150
+ (_ => @ENV.CI)
151
+ ```
152
+
153
+ A missing variable (`@ENV.NOPE`) yields `!`. Note `@ENV` is itself shadowable: a
154
+ sibling `ENV.fsn` replaces it for that directory.
155
+
156
+ ---
157
+
158
+ ## Load a file by a runtime filename
159
+
160
+ `@map` and friends require the name at parse time and imply the `.fsn` extension.
161
+ When you need to load a file whose name is computed at runtime, or whose name isn't a
162
+ plain identifier (e.g. contains a `.`), use the `@load` built-in. It takes a filename
163
+ **verbatim** — no extension is added — resolved relative to the current file:
164
+
165
+ ```fusion
166
+ # Load a file with exactly the given name. Don't append ".fsn".
167
+ ("data.config.json" | @load)
168
+ ```
169
+
170
+ ```fusion
171
+ # Load a file chosen at runtime.
172
+ (name => name | @load)
173
+ ```
174
+
175
+ A missing file yields an error.
176
+
177
+ ---
178
+
179
+ ## Keep a recursive file independent of its name and location
180
+
181
+ A recursive function should never call itself by its own filename. Writing `@fact`
182
+ inside `fact.fsn` works until someone renames or moves the file — then the reference
183
+ dangles. Refer to the current file with a bare `@` ("this file", whatever it happens
184
+ to be called) instead:
185
+
186
+ ```fusion
187
+ # factorial
188
+ (0 => 1, n ? @Integer => [n, [n, 1] | @subtract | @] | @multiply)
189
+ ```
190
+
191
+ If the file evaluates to an **object** and a recursive *helper* lives inside it as a
192
+ member, reach that member with `@.helper` — the current file's value (`@`) followed
193
+ by a `.member` access — rather than `@filename.helper`:
194
+
195
+ ```fusion
196
+ {
197
+ "sumTo": (
198
+ 0 => 0,
199
+ n ? @Integer => [n, [n, 1] | @subtract | @.sumTo] | @add
200
+ )
201
+ }
202
+ ```
203
+
204
+ Both forms move and rename along with the file, so nothing dangles when you
205
+ reorganise your directory.