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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/Rakefile +8 -0
- data/docs/index.md +34 -0
- data/docs/lang/design.md +674 -0
- data/docs/lang/roadmap.md +97 -0
- data/docs/user/explanation.md +157 -0
- data/docs/user/how-to-guides.md +205 -0
- data/docs/user/reference.md +505 -0
- data/docs/user/tutorial.md +338 -0
- data/examples/double.fsn +4 -0
- data/examples/factorial.fsn +6 -0
- data/examples/first.fsn +4 -0
- data/examples/fizzbuzz.fsn +15 -0
- data/examples/palindrome.fsn +8 -0
- data/exe/fusion +57 -0
- data/lib/fusion/version.rb +3 -0
- data/lib/fusion.rb +1140 -0
- data/stdlib/map.fsn +4 -0
- data/stdlib/math/square.fsn +1 -0
- data/stdlib/range.fsn +4 -0
- metadata +67 -0
|
@@ -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.
|
data/examples/double.fsn
ADDED
data/examples/first.fsn
ADDED
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
|