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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Implementation notes — Fusion
|
|
2
|
+
|
|
3
|
+
Companion to `design.md`. That file records *decisions*; this one explains *how* a
|
|
4
|
+
few non-obvious mechanisms in the interpreter actually work.
|
|
5
|
+
|
|
6
|
+
## Thunks: laziness, memoization, and bare `@`
|
|
7
|
+
|
|
8
|
+
A top-level **unit** — a file, an inline (`-e`) program, or a REPL entry — is a
|
|
9
|
+
single expression whose value is computed lazily and at most once. That is a `Thunk`
|
|
10
|
+
(`lib/fusion/interpreter/thunk.rb`): a small state machine over a compute closure.
|
|
11
|
+
|
|
12
|
+
- `:unforced` → on the first `force`, it runs the closure, stores the result, and
|
|
13
|
+
becomes `:done`. The closure takes **no arguments**, so the stored result is the
|
|
14
|
+
same for every reference.
|
|
15
|
+
- `:done` → returns the stored value. **This is the memoization.** It lives on the
|
|
16
|
+
thunk object itself, not in any lookup table.
|
|
17
|
+
- `:forcing` → re-entered while still computing ⇒ a non-productive data cycle,
|
|
18
|
+
reported as a `reference_error` at the point the cycle closes.
|
|
19
|
+
|
|
20
|
+
The subtlety: a unit's *value* is independent of the reference that forced it, but a
|
|
21
|
+
**read failure** is not — a missing file reached as `@a` from one place and `@../a`
|
|
22
|
+
from another must report each reference's own `operation`/`input`/`site`. So the
|
|
23
|
+
closure can't bake those in (it would memoize the *first* reference's error and hand
|
|
24
|
+
it to every later one). Instead, when the unit's own source can't be read the closure
|
|
25
|
+
*raises* `Thunk::ReadFailure` (carrying only the message, no reference). `force`
|
|
26
|
+
catches it and stores the `ReadFailure` itself as the memoized `@value`; then — since
|
|
27
|
+
it alone knows the reference — it builds a fresh `reference_error` from that stored
|
|
28
|
+
failure per call. So the thunk still memoizes once (the read happens once) and caches
|
|
29
|
+
nothing reference-specific. (A parse or evaluation error *is* part of the file's value
|
|
30
|
+
and is memoized and returned as-is.)
|
|
31
|
+
|
|
32
|
+
Raising — rather than returning a half-built error value — does two things. A read
|
|
33
|
+
failure can never *be* a value, so an uncompleted one can never leak into the value
|
|
34
|
+
space. And it couples `evaluate_file` to the Thunk: run outside one, the `ReadFailure`
|
|
35
|
+
bubbles up and becomes an `internal_error`, instead of silently yielding a placeholder.
|
|
36
|
+
|
|
37
|
+
A bare `@` means "the value of the current unit", resolved by forcing that unit's
|
|
38
|
+
own thunk.
|
|
39
|
+
|
|
40
|
+
### Two separate jobs — only one was ever keyed by filename
|
|
41
|
+
|
|
42
|
+
It is tempting to think memoization is keyed by filename. It is not. There are two
|
|
43
|
+
distinct mechanisms:
|
|
44
|
+
|
|
45
|
+
1. **Sharing a file's thunk across references.** `@a` reached from many places must
|
|
46
|
+
load `a.fsn` once. The interpreter keeps `@file_cache` (`abspath → Thunk`); a
|
|
47
|
+
given file always resolves to the same thunk object. *This* is keyed by the
|
|
48
|
+
absolute path.
|
|
49
|
+
2. **Memoizing a unit's value, and detecting cycles.** This is the thunk's own
|
|
50
|
+
`@state`/`@value`. It never consults a filename; it just remembers what it
|
|
51
|
+
computed.
|
|
52
|
+
|
|
53
|
+
So the filename is the key for *finding and sharing* a file's thunk — never for the
|
|
54
|
+
memoization itself.
|
|
55
|
+
|
|
56
|
+
### How a bare `@` reaches its thunk
|
|
57
|
+
|
|
58
|
+
Each unit puts its own thunk into the **interpreter context** of its environment
|
|
59
|
+
(see below) under `:self`, and `@` forces `env.context(:self)`:
|
|
60
|
+
|
|
61
|
+
- **Files**: the `:self` thunk is the very same object held in `@file_cache` for
|
|
62
|
+
that path (`set_context(:self, load_file(abspath))`). So `@` and a sibling `@a`
|
|
63
|
+
naming the same file share one memoized thunk, and a file that references itself
|
|
64
|
+
mid-load is caught as a cycle.
|
|
65
|
+
- **Inline / REPL**: there is no path, so the thunk is *only* reachable through the
|
|
66
|
+
environment binding. A closure captures the env where it was defined, so a bare
|
|
67
|
+
`@` inside a function resolves to that unit's thunk even when the function is
|
|
68
|
+
applied much later — which is what makes recursion work. The older filename-only
|
|
69
|
+
scheme could not express this, which is why inline/REPL had no `@`.
|
|
70
|
+
|
|
71
|
+
The REPL builds a fresh interpreter per entry, but the thunk object lives in the
|
|
72
|
+
captured environment and outlives any single interpreter: once `:done`, forcing it
|
|
73
|
+
from a later entry simply returns the stored value. So `f = (… @ …)` defined in one
|
|
74
|
+
entry and applied by `5 | f` in the next resolves `@` to `f`.
|
|
75
|
+
|
|
76
|
+
### When `@` produces something useful
|
|
77
|
+
|
|
78
|
+
`@` only forces when it is actually evaluated, so the outcome depends on the program,
|
|
79
|
+
not on whether stdin is present:
|
|
80
|
+
|
|
81
|
+
- In a **function** unit, `@` sits in the unevaluated body, so the unit's value is
|
|
82
|
+
just the function. Applying it externally — stdin for `-e`, a later entry in the
|
|
83
|
+
REPL — forces `@` and recurses. With nothing to apply it, the value is the function
|
|
84
|
+
itself (a `serialization_error` on stdout, or a lenient `"<function>"` in the
|
|
85
|
+
REPL). Both are valid; neither is a special case.
|
|
86
|
+
- In a **data** position (`[1, @]`), `@` is forced as the unit loads, while its own
|
|
87
|
+
thunk is still `:forcing` — a non-productive data cycle.
|
|
88
|
+
|
|
89
|
+
## Environment: bindings vs. interpreter context
|
|
90
|
+
|
|
91
|
+
`Env` (`lib/fusion/interpreter/env.rb`) holds two separate maps, both walking the
|
|
92
|
+
parent chain:
|
|
93
|
+
|
|
94
|
+
- **Bindings** (`@vars`) — user-visible identifiers. Pattern binders insert here via
|
|
95
|
+
`bind`; the REPL keeps a name across entries via `bind(…, checked: false)`. A bare
|
|
96
|
+
identifier in an expression is resolved here (`lookup`); unbound ⇒ `binding_error`.
|
|
97
|
+
- **Interpreter context** (`@context`) — ambient state the evaluator needs, written
|
|
98
|
+
with `set_context` and read with `context`, keyed by symbol:
|
|
99
|
+
|
|
100
|
+
| key | contents | set for |
|
|
101
|
+
| ------- | ------------------------------------------------------------ | --------------------------------------- |
|
|
102
|
+
| `:dir` | the directory `@name` / `@../a` resolve against (a `String`) | every unit (file's dir, else `Dir.pwd`) |
|
|
103
|
+
| `:file` | the absolute path, for error origins (a `String`) | files only (absent ⇒ file `"<inline>"`) |
|
|
104
|
+
| `:self` | the unit's own `Thunk`, forced by a bare `@` | every unit |
|
|
105
|
+
| `:jail` | the run's jail root (a `String`, or nil for unconfined) | once, on the run's root env |
|
|
106
|
+
| `:call_site` | the innermost user-code `file`, stamped onto built-in/stdlib errors (a `String`) | a stdlib function's clause env, in `apply` |
|
|
107
|
+
|
|
108
|
+
The two channels are deliberately separate, and a program reads only the first one.
|
|
109
|
+
So the context names are **not** identifiers: `__dir__`, `__file__`, and `__self__`
|
|
110
|
+
are unbound like any other unknown name (a `binding_error`), not readable values.
|
|
111
|
+
|
|
112
|
+
In particular `__self__` is **not** a synonym for `@`. `@` *forces* the `:self`
|
|
113
|
+
thunk down to the unit's value; the thunk object itself is not a Fusion value, and
|
|
114
|
+
exposing it would put a non-value into the value space (where it would crash
|
|
115
|
+
serialization). Keeping interpreter context out of the binding namespace is what
|
|
116
|
+
prevents that.
|
|
117
|
+
|
|
118
|
+
## The jail: confining `@`-resolution
|
|
119
|
+
|
|
120
|
+
The jail root lives in the environment's `:jail` context (set once on the run's root
|
|
121
|
+
env). Every file reached through an `@`-reference is checked with `within_jail?`, which
|
|
122
|
+
reads the jail from the interpreter's `@env` (`@env.context(:jail)`): the target's
|
|
123
|
+
expanded absolute path must be inside the jail root, or inside the stdlib directory
|
|
124
|
+
(always reachable, since it lives outside any project). A target outside both is a
|
|
125
|
+
`reference_error` (`outside the jail`).
|
|
126
|
+
|
|
127
|
+
The time of check differs by reference form, because `@name`/`@dir/a` carry a
|
|
128
|
+
built-in/stdlib fallback and `@../a`/`@load` do not:
|
|
129
|
+
|
|
130
|
+
- `@../a` and `@load` check the jail *before* the file is touched, so an out-of-jail
|
|
131
|
+
target errors whether or not it exists (it is never probed for existence).
|
|
132
|
+
- `@name`/`@dir/a` resolves *sibling-first*, so the sibling is `File.exist?`-ed to
|
|
133
|
+
choose between it and the built-in/stdlib fallback. An existing sibling outside the
|
|
134
|
+
jail then errors (`outside the jail`); a missing one falls through to the fallback.
|
|
135
|
+
|
|
136
|
+
The jail does **not** cover two things:
|
|
137
|
+
|
|
138
|
+
- The top-level program file (given explicitly on the CLI) is loaded directly, not
|
|
139
|
+
through `@`-resolution, so it is never jail-checked — the jail is about what the
|
|
140
|
+
program may *reach*, not whether it may run.
|
|
141
|
+
- stdin — it is decoded as JSON, never evaluated as Fusion source, so it holds no
|
|
142
|
+
`@`-references at all.
|
|
143
|
+
|
|
144
|
+
The check is lexical: `File.expand_path` normalises `..` and existing symlinks are
|
|
145
|
+
followed. The jail confines references to a directory tree; it is not a security
|
|
146
|
+
sandbox. Fusion cannot write files, so no symlink can be planted to escape, but
|
|
147
|
+
existing ones are part of the legitimate project layout. A nil/unset jail means
|
|
148
|
+
unconfined. `CLI.root_environment` defaults it to `Dir.pwd`, and a real CLI run
|
|
149
|
+
instead supplies the program's directory.
|
|
150
|
+
|
|
151
|
+
The jail rides the environment: the CLI builds the root env once (`root_environment`)
|
|
152
|
+
and passes it to both program loading and `apply`, so every interpreter the run builds
|
|
153
|
+
reads the same `:jail` from its `@env`.
|
|
154
|
+
|
|
155
|
+
## The error `file`: the innermost user-code call site
|
|
156
|
+
|
|
157
|
+
A standardized error's `file` is the **innermost user-code file** on the call chain
|
|
158
|
+
when the operation failed — a stdlib frame like `@map` is transparent, so a built-in
|
|
159
|
+
failing inside it reports *your* file, not the stdlib's. It is `Dir.pwd`-relative, or
|
|
160
|
+
`"<inline>"` for an `-e`/REPL entry, or `"<fusion>"` above all user code (a bare
|
|
161
|
+
operation applied straight to a value). Present for `code`/`builtin`/`stdlib` errors;
|
|
162
|
+
absent for the channel/runtime ones (`input`/`output`/`interpreter`), which have no
|
|
163
|
+
call site.
|
|
164
|
+
|
|
165
|
+
It is filled by two complementary mechanisms, split by *where the error is born*.
|
|
166
|
+
|
|
167
|
+
### Born in user code → set at birth, from `code_site`
|
|
168
|
+
|
|
169
|
+
An error raised while evaluating user code (`.name`, `[]`, an unresolved `@`-ref, an
|
|
170
|
+
unbound identifier) is created in `eval_expr` with the env in hand, so it takes its
|
|
171
|
+
`file` straight from `code_site(env)`. This *must* happen at birth: for such an error
|
|
172
|
+
the innermost user file is its own birth location — the deepest user frame, strictly
|
|
173
|
+
below any caller — so no later step could recover it. These errors also routinely
|
|
174
|
+
surface where no `apply` runs (`[1, @undefined]` as a whole program, emitted directly
|
|
175
|
+
with no input applied), so they couldn't be stamped even if we wanted to. (`code_site`
|
|
176
|
+
labels any file under the stdlib directory as `origin: "stdlib"`, so an `origin:
|
|
177
|
+
"code"` error only ever arises in user code — where `code_site`'s file already *is*
|
|
178
|
+
the innermost user file.)
|
|
179
|
+
|
|
180
|
+
### Born outside user code → stamped at `apply`
|
|
181
|
+
|
|
182
|
+
A built-in error is built in Ruby; a stdlib error is a `!{…}` raised deep in a stdlib
|
|
183
|
+
body. Neither has the user's env in hand. But both are always produced *inside* an
|
|
184
|
+
`apply` (a built-in runs as `f.fn.call`; a stdlib body runs in `apply`'s `Func`
|
|
185
|
+
branch), and `apply` knows the call site — so `apply` wraps `dispatch_apply` and
|
|
186
|
+
stamps the result with `ErrorVal#with_call_site(call_site)`. The call site is the
|
|
187
|
+
`:call_site` context (`call_site(env)`); a stdlib function's clause env inherits its
|
|
188
|
+
*caller's* `:call_site`, so a built-in failing several stdlib frames deep still reports
|
|
189
|
+
the user's file. Stamping is idempotent (a no-op once a `file` is present), so an error
|
|
190
|
+
is stamped once — at the innermost `apply` that produced it — and outer applies leave
|
|
191
|
+
it alone. No "already-stamped" flag is needed: in the innermost-user-file model the
|
|
192
|
+
call site is constant all the way up a stdlib chain.
|
|
193
|
+
|
|
194
|
+
### Why the stamp keys on `runtime?`, never `origin`
|
|
195
|
+
|
|
196
|
+
The stamp must fire for interpreter/stdlib errors but never for an arbitrary user
|
|
197
|
+
`!{…}`. Keying on the payload's `origin` is **unsound** — `origin` is just data, so a
|
|
198
|
+
user writing `!{"origin": "builtin"}` would get a spurious `file`. Provenance isn't
|
|
199
|
+
payload; it is interpreter state, recorded by `ErrorVal#runtime?` (the `@runtime`
|
|
200
|
+
flag). So the gate is two parts:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
return self unless @runtime && @payload.is_a?(Hash) && !@payload.key?("file")
|
|
204
|
+
origin = @payload["origin"]
|
|
205
|
+
return self unless origin == "builtin" || origin == "stdlib"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`@runtime` is the **soundness gate** — a user error is never runtime, so a forged
|
|
209
|
+
`origin` can never trigger a stamp. The `origin` check runs only *past* that gate,
|
|
210
|
+
where `origin` is interpreter-set and therefore safe to read; its narrower job is to
|
|
211
|
+
skip runtime errors that have no call site (a JSON-`input` parse `syntax_error` is
|
|
212
|
+
`runtime?` but `origin: "input"`, and rightly gets no `file`).
|
|
213
|
+
|
|
214
|
+
### Why marking stdlib errors `@runtime` is watertight
|
|
215
|
+
|
|
216
|
+
`@runtime` is set true in exactly two places, both keyed on interpreter state, never
|
|
217
|
+
on payload content:
|
|
218
|
+
|
|
219
|
+
1. `ErrorVal.from_runtime` — every interpreter-built error.
|
|
220
|
+
2. An `ErrLit` evaluated **where `code_site(env).origin == "stdlib"`**.
|
|
221
|
+
|
|
222
|
+
For (2), `env`'s `:file` must lie under `@stdlib_dir` (`file_site` checks
|
|
223
|
+
`start_with?`), and `:file` is set there only by `evaluate_file` loading a file
|
|
224
|
+
resolved via `resolve_builtin_or_stdlib`'s `File.join(@stdlib_dir, …)`. Every user
|
|
225
|
+
route (`@name` siblings, `@../a`, `@load`) resolves into the user's own tree;
|
|
226
|
+
inline/REPL has no `:file` at all. So user code always runs under `origin: "code"`;
|
|
227
|
+
the only way to reach `origin: "stdlib"` is for the code to physically live in the
|
|
228
|
+
interpreter's stdlib directory — which a Fusion program cannot arrange (it can't write
|
|
229
|
+
files, and a project isn't installed there), and which *would be* stdlib if it did.
|
|
230
|
+
Because the flag is set from *where the code runs*, a user writing `!{"origin":
|
|
231
|
+
"stdlib", "runtime": true}` changes only payload fields; the `@runtime` ivar is
|
|
232
|
+
untouched, so the error is never stamped.
|
|
233
|
+
|
|
234
|
+
Marking the flag at construction (a constructor argument, not a later mutation) keeps
|
|
235
|
+
`@runtime` write-once. A consequence of stdlib errors being runtime errors is that
|
|
236
|
+
they serialize **leniently** (functions → `"<function>"`, non-finite → `"<Infinity>"`),
|
|
237
|
+
which made the old `| @sanitize` in stdlib error payloads redundant; it was dropped,
|
|
238
|
+
and `sanitize.fsn` remains as a standalone utility.
|
data/docs/lang/roadmap.md
CHANGED
|
@@ -5,43 +5,17 @@ live in [design.md](./design.md); this file is only for things still ahead.
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## 1. Ergonomics
|
|
8
|
+
## 1. Ergonomics
|
|
9
9
|
|
|
10
|
-
**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
10
|
+
**Exposing the current call site** *(use case found; needs a sigil)*. The
|
|
11
|
+
interpreter tracks the current `:file`/`:dir`/`:call_site` as internal context,
|
|
12
|
+
unreadable from a program. User code should be able to mimick our *standardized*
|
|
13
|
+
error payloads. It needs access to `:file` for that.
|
|
20
14
|
|
|
21
15
|
---
|
|
22
16
|
|
|
23
17
|
## 2. Error model
|
|
24
18
|
|
|
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
19
|
**Stack traces** *(deferred)*. A propagated error tells you what happened, but
|
|
46
20
|
not the chain of function applications it passed through. A capped trace
|
|
47
21
|
(last N frames, accessible as an extra payload field, opt-in via env) would
|
|
@@ -49,33 +23,26 @@ help in deep pipelines.
|
|
|
49
23
|
|
|
50
24
|
---
|
|
51
25
|
|
|
52
|
-
## 3.
|
|
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
|
|
26
|
+
## 3. Runtime and tooling
|
|
64
27
|
|
|
65
|
-
- **Streaming I/O** (NDJSON): map a program over a stream of JSON values.
|
|
66
28
|
- **A faster implementation** once semantics are frozen.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
29
|
+
- **`fusion --stdlib-path`** *(planned)*. Print the absolute path of the bundled
|
|
30
|
+
standard library so a user can find, read, copy, or symlink its `.fsn` files —
|
|
31
|
+
e.g. to make a derived helper like `@range` follow a local `@OP` override:
|
|
32
|
+
`cp "$(fusion --stdlib-path)/range.fsn" .`. This is the one ergonomics gap in the
|
|
33
|
+
"reskin the operators" workflow (see how-to-guides). Needs a stable path: on a
|
|
34
|
+
versioned install the returned directory changes across upgrades, which dangles
|
|
35
|
+
any symlink made against it — copies are unaffected.
|
|
36
|
+
- **`fusion vendor <name>…`** *(planned)*. Scaffold command: copy the named stdlib
|
|
37
|
+
files into the current directory as real, editable files. Ergonomic front-end
|
|
38
|
+
over `--stdlib-path` + `cp` for when a directory reskins `@OP` and wants several
|
|
39
|
+
helpers that derive from `@OP` (`range`, …) to follow the local override
|
|
40
|
+
at once. Copies (portable, frozen) rather than symlinks, so it survives upgrades;
|
|
41
|
+
the copied one-liners can then be hand-edited.
|
|
75
42
|
|
|
76
43
|
---
|
|
77
44
|
|
|
78
|
-
##
|
|
45
|
+
## 4. Bigger experiments
|
|
79
46
|
|
|
80
47
|
**Destructuring functions (homoiconicity).** Treat a function as a list of
|
|
81
48
|
`(pattern, output)` clause-pairs and pattern-match on it, enabling macros and
|
|
@@ -91,7 +58,3 @@ invertible functions; hopeless for many-to-one. Would change Fusion from
|
|
|
91
58
|
functional to relational and needs backtracking search. The most exciting and
|
|
92
59
|
most disruptive possible direction; best pursued as a separate mode or sibling
|
|
93
60
|
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.
|
data/docs/user/explanation.md
CHANGED
|
@@ -47,12 +47,12 @@ everything else line up. With exactly one input and one output:
|
|
|
47
47
|
- **Application has one shape:** `value | function`. There is no argument list, no
|
|
48
48
|
call syntax, no arity to track. A pipeline `a | f | g | h` reads like a sentence.
|
|
49
49
|
- **Multi-argument needs are met by data:** pass an array `[a, b]` or an object
|
|
50
|
-
`{"f": ..., "
|
|
50
|
+
`{"f": ..., "c": ...}`. The "arguments" become a value you can also store, inspect,
|
|
51
51
|
and destructure — there's no separate notion of "argument tuple."
|
|
52
52
|
- **Pattern matching has one job:** match the single input. A function's clauses are
|
|
53
53
|
just alternative shapes that one input might have.
|
|
54
54
|
|
|
55
|
-
The cost is verbosity in arithmetic (`[a, b] | @
|
|
55
|
+
The cost is verbosity in arithmetic (`[a, b] | @OP.sum` instead of `a + b`) and a little
|
|
56
56
|
ceremony for multi-argument library functions. The first is a candidate for later
|
|
57
57
|
syntactic sugar; the second is mild. In exchange, the evaluation model is almost
|
|
58
58
|
trivially simple, which is exactly what you want in a language meant to be small.
|
|
@@ -110,7 +110,7 @@ This also resolved the module system for free. If a file is a value, then refere
|
|
|
110
110
|
a file *is* importing a value — no separate `import` construct, no namespace syntax.
|
|
111
111
|
The directory tree becomes the namespace. The standard library is just a folder of
|
|
112
112
|
files. One mechanism (`@`-references) now does top-level structure, modules, and
|
|
113
|
-
library delivery — and, in the current design, built-in access too: `@
|
|
113
|
+
library delivery — and, in the current design, built-in access too: `@OP` and
|
|
114
114
|
`@Integer` are looked up through the very same `@name` machinery as files. A bare
|
|
115
115
|
`@name` checks for a sibling file, then a built-in, then a standard-library file, so
|
|
116
116
|
your own files can locally shadow a built-in or a stdlib function without affecting
|
|
@@ -137,14 +137,9 @@ productive structure surrounds it.
|
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
140
|
-
## The roads not taken
|
|
140
|
+
## The roads not taken
|
|
141
141
|
|
|
142
|
-
|
|
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.
|
|
142
|
+
Some ideas were explored and set aside for future experiments or a different language.
|
|
148
143
|
|
|
149
144
|
**Destructuring functions.** The tantalizing one. Since a function literal is visibly
|
|
150
145
|
a list of `pattern => output` clauses, could we pattern-match *on a function*, taking
|
data/docs/user/how-to-guides.md
CHANGED
|
@@ -8,18 +8,13 @@ basics. Scan for the problem you have and copy the solution.*
|
|
|
8
8
|
## Diagnose a program that returns an error unexpectedly
|
|
9
9
|
|
|
10
10
|
When you run a program and see an error payload on stderr, the payload itself
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
tells you what went wrong. Interpreter errors carry a standardized object whose
|
|
12
|
+
fields (`kind`, `origin`, `file`, `operation`, `status`, `input`, `expected`,
|
|
13
|
+
`message`) are documented in
|
|
14
|
+
[reference §6.5](./reference.md#65-the-standardized-error-payload).
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
16
|
+
For a missing file or a parse error in an `@`-referenced file, the `file` and
|
|
17
|
+
`input` fields name the path that failed.
|
|
23
18
|
|
|
24
19
|
---
|
|
25
20
|
|
|
@@ -31,7 +26,7 @@ Compute a boolean, then pipe it into a two-clause function:
|
|
|
31
26
|
|
|
32
27
|
```fusion
|
|
33
28
|
(n =>
|
|
34
|
-
[n, 0] | @
|
|
29
|
+
[n, 0] | @OP.compare | @lt | (
|
|
35
30
|
true => "negative",
|
|
36
31
|
false => "non-negative"
|
|
37
32
|
)
|
|
@@ -45,8 +40,8 @@ Here's an elegant way of writing FizzBuzz:
|
|
|
45
40
|
(
|
|
46
41
|
n =>
|
|
47
42
|
[
|
|
48
|
-
[n, 3] | @
|
|
49
|
-
[n, 5] | @
|
|
43
|
+
[n, 3] | @OP.modulo,
|
|
44
|
+
[n, 5] | @OP.modulo,
|
|
50
45
|
]
|
|
51
46
|
|
|
|
52
47
|
(
|
|
@@ -66,7 +61,7 @@ You could write `sum` like this:
|
|
|
66
61
|
```fusion
|
|
67
62
|
(
|
|
68
63
|
[] => 0,
|
|
69
|
-
[x, ...rest] => [x, rest | @] | @
|
|
64
|
+
[x, ...rest] => [x, rest | @] | @OP.sum
|
|
70
65
|
)
|
|
71
66
|
```
|
|
72
67
|
|
|
@@ -83,7 +78,7 @@ A Fusion function takes exactly one input. Functions that require multiple input
|
|
|
83
78
|
them into an array (or object) and destructure that in the pattern:
|
|
84
79
|
|
|
85
80
|
```fusion
|
|
86
|
-
([a, b] => [a, b] | @
|
|
81
|
+
([a, b] => [a, b] | @OP.sum)
|
|
87
82
|
```
|
|
88
83
|
|
|
89
84
|
Call it as `[3, 4] | @thatFunction`
|
|
@@ -94,7 +89,7 @@ needs the remaining arguments (`y` in the example below). Each `=>` consumes one
|
|
|
94
89
|
and hands back a function waiting for the next:
|
|
95
90
|
|
|
96
91
|
```fusion
|
|
97
|
-
(x => (y => [x, y] | @
|
|
92
|
+
(x => (y => [x, y] | @OP.sum))
|
|
98
93
|
```
|
|
99
94
|
|
|
100
95
|
Call it as `4 | (3 | @thatFunction)`. `3 | f` yields a one-argument function that adds 3,
|
|
@@ -119,14 +114,14 @@ so guard the whole array instead:
|
|
|
119
114
|
(
|
|
120
115
|
[] => "palindrome",
|
|
121
116
|
[_] => "palindrome",
|
|
122
|
-
[_, ...rest, _] ? ([first, ..., last] => [first, last] | @
|
|
117
|
+
[_, ...rest, _] ? ([first, ..., last] => [first, last] | @OP.equal) => rest | @,
|
|
123
118
|
_ => "not a palindrome"
|
|
124
119
|
)
|
|
125
120
|
```
|
|
126
121
|
|
|
127
122
|
The outer pattern `[_, ...rest, _]` is what you destructure for retrieving the middle
|
|
128
123
|
of the array which you need to continue your recursion. The inline predicate
|
|
129
|
-
`([first, ..., last] => [first, last] | @
|
|
124
|
+
`([first, ..., last] => [first, last] | @OP.equal)` independently destructures the same
|
|
130
125
|
array again to compare its two ends.
|
|
131
126
|
|
|
132
127
|
---
|
|
@@ -135,8 +130,52 @@ array again to compare its two ends.
|
|
|
135
130
|
|
|
136
131
|
Because a sibling file wins over a built-in or the standard library, you can override
|
|
137
132
|
either — but only for files in the same directory, so you can't break things
|
|
138
|
-
globally. Put
|
|
139
|
-
uses yours; files elsewhere still get the
|
|
133
|
+
globally. Put a `map.fsn` next to your program and every `@map` *in that directory*
|
|
134
|
+
uses yours; files elsewhere still get the standard one.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Reskin the operators (`@OP`) for a directory
|
|
139
|
+
|
|
140
|
+
The arithmetic, comparison, and boolean operators live in one built-in object,
|
|
141
|
+
`@OP` (`@OP.sum`, `@OP.compare`, `@OP.and`, …), and — like any `@`-name — it is
|
|
142
|
+
resolved per directory. To change what the operators mean for the files in one
|
|
143
|
+
directory (complex numbers, matrices, …), drop an `OP.fsn` there that overrides the
|
|
144
|
+
members you want, reaching the originals with `@@`:
|
|
145
|
+
|
|
146
|
+
```fusion
|
|
147
|
+
# OP.fsn — this directory's arithmetic
|
|
148
|
+
{ ...@@, "sum": (p => "my sum"), "product": (p => "my product") }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Only files in *that* directory are affected; everything else keeps the defaults. To
|
|
152
|
+
check whether a directory changed the operators, look for an `OP.fsn` — there is no
|
|
153
|
+
other way to change them.
|
|
154
|
+
|
|
155
|
+
### Making a named derived helper follow your override
|
|
156
|
+
|
|
157
|
+
Most stdlib helpers are deliberately **immune** to your override, so a reskin can't
|
|
158
|
+
break them by accident: `@truthy`/`@falsey` decide truthiness by pattern matching,
|
|
159
|
+
`@compact` drops nulls by pattern matching too, and the comparison helpers
|
|
160
|
+
`@lt`/`@gt`/`@lte`/`@gte` interpret an `@OP.compare` *result* (you write
|
|
161
|
+
`[a, b] | @OP.compare | @lt`, so the compare step already follows your override while
|
|
162
|
+
the interpretation stays fixed).
|
|
163
|
+
|
|
164
|
+
One helper still calls `@OP` internally — `@range` uses `@OP.sum` and `@OP.compare` —
|
|
165
|
+
and, like `@`-names everywhere, it resolves `@OP` in *its own* directory (the stdlib),
|
|
166
|
+
so it keeps the default even where you overrode `@OP`. To make it follow your override,
|
|
167
|
+
copy the stdlib file to the directory containing your other overrides. It then
|
|
168
|
+
resolves `@OP` locally:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
# CAUTION: `fusion --stdlib-path` not implemented yet, determine manually
|
|
172
|
+
|
|
173
|
+
# copy — portable, a frozen snapshot
|
|
174
|
+
cp "$(fusion --stdlib-path)/range.fsn" .
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Your copy of `range.fsn` now resolves `@OP` in its own directory first and will find
|
|
178
|
+
your overrides before the original builtin implementations.
|
|
140
179
|
|
|
141
180
|
---
|
|
142
181
|
|
|
@@ -185,7 +224,7 @@ to be called) instead:
|
|
|
185
224
|
|
|
186
225
|
```fusion
|
|
187
226
|
# factorial
|
|
188
|
-
(0 => 1, n ? @Integer => [n, [n, 1] | @
|
|
227
|
+
(0 => 1, n ? @Integer => [n, [n, -1] | @OP.sum | @] | @OP.product)
|
|
189
228
|
```
|
|
190
229
|
|
|
191
230
|
If the file evaluates to an **object** and a recursive *helper* lives inside it as a
|
|
@@ -196,7 +235,7 @@ by a `.member` access — rather than `@filename.helper`:
|
|
|
196
235
|
{
|
|
197
236
|
"sumTo": (
|
|
198
237
|
0 => 0,
|
|
199
|
-
n ? @Integer => [n, [n, 1] | @
|
|
238
|
+
n ? @Integer => [n, [n, -1] | @OP.sum | @.sumTo] | @OP.sum
|
|
200
239
|
)
|
|
201
240
|
}
|
|
202
241
|
```
|