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,338 @@
1
+ # Tutorial: Your first hour with Fusion
2
+
3
+ *This is a guided lesson: follow it start to finish, type the code yourself, and run
4
+ every example. By the end you will have written a recursive program and understood how
5
+ Fusion's pieces fit together.*
6
+
7
+ > **What you need:** Ruby installed and the `fusion` interpreter available on your
8
+ > `PATH` (along with its bundled `stdlib/` folder). All commands below can be run
9
+ > from any directory containing your `.fsn` files.
10
+
11
+ ---
12
+
13
+ ## Step 1 — Run something
14
+
15
+ Fusion programs read JSON on standard input and write JSON on standard output. Let's
16
+ create our first program. Create a file `lesson.fsn` containing exactly:
17
+
18
+ ```fusion
19
+ (n => [n, 2] | @multiply)
20
+ ```
21
+
22
+ Now run it:
23
+
24
+ ```sh
25
+ echo '21' | fusion lesson.fsn
26
+ ```
27
+
28
+ You should see `42`. Take a moment to notice three things you just used without
29
+ being told:
30
+
31
+ - The whole file is **one value** — here, a function. There is no `main`, no list of
32
+ statements. The file *is* the program.
33
+ - The input `21` was piped *into* the function. That is what `|` means: **`value |
34
+ function`** applies the function to the value.
35
+ - `[n, 2] | @multiply` built a two-element array and piped it into the built-in
36
+ `@multiply`. Fusion has no `*` operator (yet); arithmetic is done by piping a pair
37
+ into a named function. **Built-ins are reached with an `@` prefix**, just like files
38
+ — `@multiply`, `@add`, and so on. (You'll see why `@` is used for both in Step 8.)
39
+
40
+ Note: on the right side of `=>` you can use regular parentheses `()` to group
41
+ expressions and influence execution order.
42
+
43
+ You are now able to compute arithmetic expressions.
44
+
45
+ ---
46
+
47
+ ## Step 2 — A function is a list of patterns
48
+
49
+ A Fusion function is a comma-separated list of `pattern => result` clauses, wrapped
50
+ in parentheses. When you apply it, the clauses are tried top to bottom and the
51
+ **first one that matches** wins. Replace your file with:
52
+
53
+ ```fusion
54
+ (
55
+ 0 => "zero",
56
+ 1 => "one",
57
+ 2 => "two"
58
+ )
59
+ ```
60
+
61
+ Run `echo '1' | fusion lesson.fsn` and you get `"one"`. The pattern `1`
62
+ is a *literal* — it matches only the value `1`.
63
+
64
+ Now try an input no clause matches: `echo '5' | fusion lesson.fsn`. You get
65
+ `null`. **A function with no matching clause returns `null`.** Remember this; it is a
66
+ deliberate, important rule.
67
+
68
+ ---
69
+
70
+ ## Step 3 — Capturing values with holes
71
+
72
+ Literals are not very useful on their own. The power comes from *binding*. A bare
73
+ word in a pattern is a **hole**: it matches anything and captures it under that name.
74
+ The same bare word in the result **reads** that captured value back out. Try:
75
+
76
+ ```fusion
77
+ ([a, b] => [b, a])
78
+ ```
79
+
80
+ ```sh
81
+ echo '[1, 2]' | fusion lesson.fsn
82
+ ```
83
+
84
+ You get `[2, 1]`. The pattern `[a, b]` matches a two-element array and binds its
85
+ elements to `a` and `b`; the result `[b, a]` constructs a new array and fills in
86
+ `a` and `b` in swapped order.
87
+
88
+ This single idea — *bare words bind in patterns, read in results* — is the heart of the
89
+ whole language. Patterns and results are mirror images of each other.
90
+
91
+ Try passing in a three-element array (`echo '[1, 2, 3]'`). You get `null`, because
92
+ `[a, b]` only matches arrays of length exactly two.
93
+
94
+ ---
95
+
96
+ ## Step 4 — Matching the *shape* of data
97
+
98
+ Patterns can dig into nested structure. Objects work just like arrays. Replace your
99
+ file with a function that pulls a name out of a person object:
100
+
101
+ ```fusion
102
+ ({"name": n, "age": _} => n)
103
+ ```
104
+
105
+ ```sh
106
+ echo '{"name": "Ada", "age": 36}' | fusion lesson.fsn
107
+ ```
108
+
109
+ You get `"Ada"`. Two new things here:
110
+
111
+ - `{"name": n, ...}` matches an object that has a `name` key and binds its value.
112
+ - `_` is the **wildcard**: it matches anything but binds nothing. We required an
113
+ `age` key to exist but didn't care about its value.
114
+
115
+ This is *destructuring*: the pattern describes the shape you expect. Pattern matching
116
+ both *checks the shape* AND *extracts values* in one step.
117
+
118
+ ---
119
+
120
+ ## Step 5 — Making decisions (there is no `if`)
121
+
122
+ Fusion has no `if` statement. You don't need one, because choosing between cases is
123
+ exactly what pattern matching does. To branch on a condition, compute the condition
124
+ as a boolean and match on `true`/`false`. Let's write a function that computes a
125
+ number's absolute value:
126
+
127
+ ```fusion
128
+ (n =>
129
+ [n, 0] | @lessThan | (
130
+ true => [0, n] | @subtract,
131
+ false => n
132
+ )
133
+ )
134
+ ```
135
+
136
+ Run it on `-5` and on `5`:
137
+
138
+ ```sh
139
+ echo '-5' | fusion lesson.fsn # => 5
140
+ echo '5' | fusion lesson.fsn # => 5
141
+ ```
142
+
143
+ Read the middle line carefully: `[n, 0] | @lessThan` produces `true` or `false`. That
144
+ boolean is then piped into a *second, inline function* whose two clauses are the two
145
+ branches. **An `if` is just a function with a `true` clause and a `false` clause.**
146
+
147
+ Note: you don't need to restrict yourself to the two values `true` and `false` as
148
+ an intermediate result. Don't use it purely as an `if / else`. Use it like a `case`
149
+ statement. You can have as many cases as you need.
150
+
151
+ ---
152
+
153
+ ## Step 6 — Repeating things (there is no loop, either)
154
+
155
+ There are no loops in Fusion. Repetition is done with recursion, and recursion is
156
+ done by pattern-matching on structure, usually on arrays.
157
+
158
+ To make recursion easier, `@` always means *the current file*. Create a new file
159
+ `sum.fsn`:
160
+
161
+ ```fusion
162
+ (
163
+ [] => 0,
164
+ [x, ...rest] => [x, rest | @ ] | @add
165
+ )
166
+ ```
167
+
168
+ ```sh
169
+ echo '[1, 2, 3, 4]' | fusion sum.fsn # => 10
170
+ ```
171
+
172
+ Walk through what happened:
173
+ - As long as our input list is non-empty, the first pattern doesn't match.
174
+ - The pattern `[x, ...rest]` matches non-empty lists and binds the first element
175
+ to `x` and *the remaining elements* to `rest` (that's what `...` does — it
176
+ captures "the rest").
177
+ - The expression on the right side of `=>` then adds `x` to the "sum of `rest`".
178
+ - This "sum of `rest`" gets computed recursively by piping `rest` back into the
179
+ same function via `@`.
180
+ - Eventually `rest` becomes `[]`, the first clause matches and the recursion bottoms
181
+ out at `0`.
182
+ - Then, all the additions unwind.
183
+
184
+ This is how you emulate `for` loops via recursion in Fusion.
185
+
186
+ ---
187
+
188
+ ## Step 7 — Refining a match with a predicate
189
+
190
+ Sometimes pattern matching on the input's *structure* isn't enough. An integer and a
191
+ string are both atomic values. They have the same *structure*. You also can't express
192
+ conditions between bindings with structure alone.
193
+
194
+ To express such distinctions, you can attach a predicate to any pattern with `?`. The
195
+ clause will match only if the structure matches *and* the predicate returns `true`.
196
+ Let's compute the factorial:
197
+
198
+ ```fusion
199
+ (
200
+ 0 => 1,
201
+ n ? @Integer => [n, [n, 1] | @subtract | @] | @multiply
202
+ )
203
+ ```
204
+
205
+ Save it as `fact.fsn` and run `echo '5' | fusion fact.fsn` → `120`.
206
+
207
+ `n ? @Integer` reads as "bind `n`, but only if `n` is an integer." And here is the
208
+ beautiful part: `@Integer` is not a keyword. It is just a built-in function that
209
+ returns `true` for integers. Fusion's "type system" is a dynamic runtime type system.
210
+ It is nothing more than ordinary functions you attach with `?`. You can write your own
211
+ and use them exactly the same way.
212
+
213
+ Create a function that sorts a pair of values:
214
+
215
+ ```fusion
216
+ (
217
+ [a, b] ? @lessThan => [a, b],
218
+ [a, b] => [b, a]
219
+ )
220
+ ```
221
+
222
+ The first case only matches, if the values are already in the correct order. It
223
+ simply returns the input unmodified. The second case matches without restrictions.
224
+ It swaps the two elements.
225
+
226
+ ---
227
+
228
+ ## Step 8 — Using the standard library (and the one `@` namespace)
229
+
230
+ You don't have to write `sum` and friends from scratch; common helpers live in the
231
+ standard library and are reached with a plain `@name` — the same `@map` you'd use for
232
+ a sibling file. The classic `map` is in the standard library. Create `doubler.fsn`:
233
+
234
+ ```fusion
235
+ (xs => {"f": (n => [n, 2] | @multiply), "xs": xs} | @map)
236
+ ```
237
+
238
+ ```sh
239
+ echo '[1, 2, 3]' | fusion doubler.fsn # => [2, 4, 6]
240
+ ```
241
+
242
+ Because every Fusion function takes exactly one argument, `map` takes an *object*
243
+ bundling the function `f` and the list `xs`. You just passed a function as a value
244
+ nested within an object — functions are values like any other.
245
+
246
+ Now the payoff for using `@` everywhere. A bare `@name` is resolved in the following
247
+ order:
248
+ 1. A **sibling file** `name.fsn` next to the current file.
249
+ 2. A **built-in** called `name`.
250
+ 3. A **standard-library** file `name.fsn`.
251
+
252
+ The first match wins. So `@multiply` finds the built-in and `@map` falls through to
253
+ the standard library. And if *you* put a `map.fsn` next to your program, *your* `map`
254
+ shadows the standard one — but only for files in that directory.
255
+
256
+ Built-ins and the standard library share the same `@` namespace as your own files
257
+ and you can locally override either, without ever affecting other directories.
258
+
259
+ ---
260
+
261
+ ## Step 9 — When things go wrong: errors with payloads
262
+
263
+ So far you have written programs that succeed. What happens when something goes
264
+ wrong? Try dividing by zero. Save as `boom.fsn`:
265
+
266
+ ```fusion
267
+ (n => [n, 0] | @divide)
268
+ ```
269
+
270
+ ```sh
271
+ echo '5' | fusion boom.fsn
272
+ ```
273
+
274
+ You will see no output on stdout, a message like `"divide: division by zero"` on
275
+ stderr, and the process will exit with status `1`. The result *was* an error, and
276
+ the interpreter knows the difference: it routes the error's **payload** to
277
+ stderr, leaves stdout empty, and signals failure. That makes Fusion programs
278
+ well-behaved Unix filters.
279
+
280
+ An error is always written as `!` followed by a **payload** — any regular JSON value.
281
+ The built-ins above produced `!"divide: division by zero"` (an error whose payload
282
+ is a string). You can construct your own:
283
+
284
+ | Example | Meaning |
285
+ | ----------------------------------- | ------------------------ |
286
+ | `!42` | Error with payload 42 |
287
+ | `!"could not parse"` | Error carrying a message |
288
+ | `!{"kind": "bad_input", "got": 99}` | Structured error |
289
+ | `!` | Shorthand for `!null` |
290
+
291
+ Errors **propagate** through pipelines automatically. If any step in
292
+ `a | f | g | h` produces an error, the rest is skipped and the error becomes the
293
+ final result — *unless* you write a clause that explicitly catches an error.
294
+ Catching is done with an error pattern:
295
+
296
+ ```fusion
297
+ # Matches any error, returns "recovered"
298
+ (! => "recovered", x => x)
299
+ ```
300
+
301
+ ```fusion
302
+ # Binds the payload to "msg" for further processing
303
+ (!msg => msg, x => "ok")
304
+ ```
305
+
306
+ Here's `safeDivide` that returns `null` instead of failing:
307
+
308
+ ```fusion
309
+ (p => p | @divide | (! => null, n => n))
310
+ ```
311
+
312
+ Run `echo '[10, 0]' | fusion safeDivide.fsn` and you get `null` rather than an
313
+ error. Notice how matching `!` is symmetric with constructing it: in expression
314
+ position, `!42` *builds* an error with payload 42; in pattern position, `!42`
315
+ *matches* an error with payload 42. The same syntax means "construct" on the right
316
+ of `=>` and "destructure" on the left, just like every other pattern in Fusion.
317
+
318
+ ---
319
+
320
+ ## What you have learned
321
+
322
+ In about an hour you have used every major feature of the language:
323
+
324
+ - A file is one value; a program is a function; `value | function` applies it.
325
+ - Functions are ordered `pattern => result` clauses; the first match wins; no match
326
+ gives `null`; unmatched errors propagate.
327
+ - Bare words are holes: they bind in patterns and read in results.
328
+ - Destructuring matches and extracts nested array/object shape at once; `_` ignores,
329
+ `...rest` captures the remainder.
330
+ - `if` is a function matching `true`/`false`; a loop is recursion via `@` (a bare
331
+ `@` means "this file").
332
+ - `?` attaches a predicate to refine a match, and predicates double as types.
333
+ - Everything reachable lives in one `@` namespace: built-ins (`@add`, `@Integer`),
334
+ the standard library (`@map`), sibling files (`@helper`), and the current file
335
+ (`@`). A bare `@name` checks sibling → built-in → standard library, so you can
336
+ locally shadow a built-in or stdlib function per directory.
337
+
338
+ You are ready to solve real problems with Fusion.
@@ -0,0 +1,4 @@
1
+ (
2
+ [] => [],
3
+ [first, ...rest] => [[first, first] | @add, ...rest | @]
4
+ )
@@ -0,0 +1,6 @@
1
+ # Factorial: n! for a non-negative integer.
2
+ (
3
+ _ ? (i ? @Integer => [i, 0] | @lessThan, _ => true) => !"Only non-negative integers, please!",
4
+ 0 => 1,
5
+ n => [n, [n, 1] | @subtract | @] | @multiply
6
+ )
@@ -0,0 +1,4 @@
1
+ (
2
+ [] => !,
3
+ [first, ...rest] => first
4
+ )
@@ -0,0 +1,15 @@
1
+ # FizzBuzz for a single integer
2
+ (
3
+ n =>
4
+ [
5
+ [n, 3] | @mod,
6
+ [n, 5] | @mod,
7
+ ]
8
+ |
9
+ (
10
+ [0, 0] => "FizzBuzz",
11
+ [0, _] => "Fizz",
12
+ [_, 0] => "Buzz",
13
+ _ => n,
14
+ )
15
+ )
@@ -0,0 +1,8 @@
1
+ # Check, whether input string or array is a palindrome
2
+ (
3
+ s ? @String => s | @chars | @,
4
+ [] => "palindrome",
5
+ [_] => "palindrome",
6
+ [_, ...rest, _] ? ([first, ..., last] => [first, last] | @equals) => rest | @,
7
+ _ => "not a palindrome"
8
+ )
data/exe/fusion ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Fusion CLI entrypoint.
5
+ #
6
+ # A file contains exactly one value. A file is "executable" if that value is a
7
+ # function; the runtime computes STDIN | thatFunction and prints the result.
8
+ #
9
+ # Usage:
10
+ # echo '[1,2,3]' | fusion path/to/main.fsn
11
+ # fusion path/to/main.fsn '<json-input>' # input as an argument
12
+ # fusion -e '(n => [n,2] | @multiply)' '21' # inline program
13
+
14
+ require_relative "../lib/fusion"
15
+
16
+ args = ARGV.dup
17
+ inline = nil
18
+ if args[0] == "-e"
19
+ inline = args[1]
20
+ program_path = nil
21
+ explicit_input = args[2]
22
+ else
23
+ program_path = args[0]
24
+ explicit_input = args[1]
25
+ end
26
+
27
+ stdlib = File.expand_path("../stdlib", __dir__)
28
+ interp = Fusion::Interpreter.new(stdlib_dir: (Dir.exist?(stdlib) ? stdlib : nil))
29
+
30
+ # Determine the program function value.
31
+ program_value =
32
+ if inline
33
+ ast = Fusion::Parser.parse_file(inline)
34
+ env = interp.root_env.child
35
+ env.define("__dir__", Dir.pwd)
36
+ interp.eval_expr(ast, env)
37
+ else
38
+ abort("usage: fusion <file.fsn> [json-input] or fusion -e '<src>' [json-input]") unless program_path
39
+ interp.load_file(File.expand_path(program_path)).force
40
+ end
41
+
42
+ # Read input: explicit arg wins, else stdin.
43
+ input_text = explicit_input || ($stdin.tty? ? "" : $stdin.read)
44
+ input_text = "null" if input_text.nil? || input_text.strip.empty?
45
+ input_value = Fusion::JsonInput.parse(input_text)
46
+
47
+ result = interp.apply(program_value, input_value)
48
+
49
+ if Fusion.error?(result)
50
+ # Print only the payload (as JSON) to stderr; stdout stays empty so that
51
+ # shell pipelines can rely on "stdout has the result, or there isn't one".
52
+ $stderr.puts Fusion::Serializer.to_json(result.payload)
53
+ exit 1
54
+ else
55
+ puts Fusion::Serializer.to_json(result)
56
+ exit 0
57
+ end
@@ -0,0 +1,3 @@
1
+ module Fusion
2
+ VERSION = "0.0.1.alpha1"
3
+ end