fusion-lang 0.0.1.alpha2 → 0.0.2
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/.mutant.yml +24 -0
- data/.simplecov +11 -0
- data/CHANGELOG.md +42 -0
- data/README.md +11 -9
- data/Rakefile +8 -0
- data/docs/lang/design.md +289 -51
- data/docs/lang/implementation.md +279 -0
- data/docs/lang/roadmap.md +20 -36
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +145 -15
- data/docs/user/reference.md +365 -140
- data/docs/user/tutorial.md +22 -19
- data/examples/double.fsn +4 -1
- data/examples/factorial.fsn +6 -3
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/gcd.fsn +9 -0
- data/examples/json_test.fsn +4 -0
- data/examples/matrix/OP.fsn +2 -0
- data/examples/matrix/average.fsn +2 -0
- data/examples/matrix/solve.fsn +2 -0
- data/examples/palindrome.fsn +1 -1
- data/exe/fusion +12 -12
- data/lib/fusion/ast.rb +76 -27
- data/lib/fusion/atom.rb +1 -1
- data/lib/fusion/cli/decoder.rb +13 -8
- data/lib/fusion/cli/encoder.rb +2 -2
- data/lib/fusion/cli/options.rb +134 -61
- data/lib/fusion/cli/parser.rb +4 -4
- data/lib/fusion/cli/repl.rb +32 -27
- data/lib/fusion/cli/serializer.rb +11 -11
- data/lib/fusion/cli.rb +120 -49
- data/lib/fusion/interpreter/builtins.rb +298 -160
- data/lib/fusion/interpreter/env.rb +42 -12
- data/lib/fusion/interpreter/error_val.rb +42 -20
- data/lib/fusion/interpreter/thunk.rb +53 -0
- data/lib/fusion/interpreter.rb +263 -98
- data/lib/fusion/lexer.rb +125 -37
- data/lib/fusion/parser.rb +245 -70
- data/lib/fusion/version.rb +3 -1
- data/lib/fusion.rb +0 -1
- data/stdlib/all.fsn +13 -0
- data/stdlib/any.fsn +12 -0
- data/stdlib/chars.fsn +5 -0
- data/stdlib/compact.fsn +5 -0
- data/stdlib/concat.fsn +6 -0
- data/stdlib/entries.fsn +6 -0
- data/stdlib/falsey.fsn +6 -0
- data/stdlib/filter.fsn +12 -0
- data/stdlib/flatten.fsn +7 -0
- data/stdlib/map.fsn +7 -4
- data/stdlib/matrix/Matrix.fsn +8 -0
- data/stdlib/matrix/OP.fsn +18 -0
- data/stdlib/matrix/add.fsn +7 -0
- data/stdlib/matrix/column.fsn +6 -0
- data/stdlib/matrix/determinant.fsn +16 -0
- data/stdlib/matrix/dimensions.fsn +5 -0
- data/stdlib/matrix/identity.fsn +6 -0
- data/stdlib/matrix/invert.fsn +26 -0
- data/stdlib/matrix/minor.fsn +15 -0
- data/stdlib/matrix/multiply.fsn +10 -0
- data/stdlib/matrix/negate.fsn +5 -0
- data/stdlib/matrix/product.fsn +11 -0
- data/stdlib/matrix/rotate.fsn +10 -0
- data/stdlib/matrix/row.fsn +6 -0
- data/stdlib/matrix/scale.fsn +6 -0
- data/stdlib/matrix/subtract.fsn +7 -0
- data/stdlib/matrix/sum.fsn +14 -0
- data/stdlib/matrix/transpose.fsn +5 -0
- data/stdlib/range.fsn +2 -2
- data/stdlib/reduce.fsn +8 -0
- data/stdlib/safe.fsn +7 -0
- data/stdlib/sanitize.fsn +2 -3
- data/stdlib/toObject.fsn +7 -0
- data/stdlib/truthy.fsn +7 -0
- data/stdlib/vector/Vector.fsn +7 -0
- data/stdlib/vector/add.fsn +6 -0
- data/stdlib/vector/cross.fsn +6 -0
- data/stdlib/vector/dot.fsn +6 -0
- data/stdlib/vector/norm.fsn +6 -0
- data/stdlib/vector/scale.fsn +5 -0
- data/stdlib/vector/subtract.fsn +6 -0
- data/stdlib/zip.fsn +6 -0
- metadata +50 -4
- data/lib/fusion/interpreter/file_thunk.rb +0 -39
- data/stdlib/mapValues.fsn +0 -5
- data/stdlib/math/square.fsn +0 -4
|
@@ -22,21 +22,28 @@ module Fusion
|
|
|
22
22
|
attr_reader :parent
|
|
23
23
|
|
|
24
24
|
def initialize(parent = nil)
|
|
25
|
-
@vars = {}
|
|
25
|
+
@vars = {} # pattern bindings, keyed by identifier
|
|
26
|
+
@context = {} # hidden interpreter context, keyed by symbol
|
|
26
27
|
@parent = parent
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@vars[name] = value
|
|
32
|
-
self
|
|
30
|
+
def child
|
|
31
|
+
Env.new(self)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
# The topmost ancestor — the binding-free root a run is built on. The
|
|
35
|
+
# interpreter loads files relative to it, so they stay isolated.
|
|
36
|
+
def root
|
|
37
|
+
@parent ? @parent.root : self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Pattern bindings:
|
|
41
|
+
# - Shadowing a binding from a parent Env is always allowed.
|
|
42
|
+
# - A duplicate identifier in the same Env is usually an error, but allowed on the REPL.
|
|
43
|
+
def bind(name, value, checked: true)
|
|
44
|
+
if checked && @vars.key?(name)
|
|
45
|
+
raise DuplicateBinding, name
|
|
46
|
+
end
|
|
40
47
|
|
|
41
48
|
@vars[name] = value
|
|
42
49
|
end
|
|
@@ -51,8 +58,31 @@ module Fusion
|
|
|
51
58
|
end
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
# Hidden interpreter context:
|
|
62
|
+
# - `:dir`: the directory @-references resolve against (a path String).
|
|
63
|
+
# - `:file`: the current file's absolute path, used for error origins (a
|
|
64
|
+
# String; absent for inline/REPL code, which reports as origin
|
|
65
|
+
# "code" with file "<inline>").
|
|
66
|
+
# - `:self`: the current top-level unit's own Thunk, used for recursion via a bare `@`.
|
|
67
|
+
# - `:jail`: the run's jail root confining @-resolution (an absolute path
|
|
68
|
+
# String, or nil for unconfined). Set once on the root and, unlike
|
|
69
|
+
# the others, never overridden by a descendant.
|
|
70
|
+
# - `:call_site`: the innermost user-code `file` a stdlib body borrows for its
|
|
71
|
+
# errors (a String). Set on a stdlib function's clause env in
|
|
72
|
+
# Interpreter#apply; user/inline code derives its own and omits it.
|
|
73
|
+
def set_context(key, value)
|
|
74
|
+
@context[key] = value
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def context(key)
|
|
79
|
+
if @context.key?(key)
|
|
80
|
+
@context[key]
|
|
81
|
+
elsif @parent
|
|
82
|
+
@parent.context(key)
|
|
83
|
+
else
|
|
84
|
+
:__unbound__
|
|
85
|
+
end
|
|
56
86
|
end
|
|
57
87
|
end
|
|
58
88
|
end
|
|
@@ -9,32 +9,54 @@ module Fusion
|
|
|
9
9
|
class ErrorVal
|
|
10
10
|
attr_reader :payload
|
|
11
11
|
|
|
12
|
-
def initialize(payload)
|
|
12
|
+
def initialize(payload, runtime: false)
|
|
13
13
|
@payload = payload
|
|
14
|
-
@
|
|
14
|
+
@runtime = runtime
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@
|
|
17
|
+
# Attach the call-site `file` to a runtime error. Idempotent.
|
|
18
|
+
def with_call_site(file)
|
|
19
|
+
# Only stamp runtime-produced errors. After this check we are sure that the payload wasn't user-constructed.
|
|
20
|
+
return self unless @runtime
|
|
21
|
+
raise Unreachable, "Unexpected runtime error payload: #{@payload}" unless @payload.is_a?(Hash)
|
|
22
|
+
# Don't double stamp. Idempotency.
|
|
23
|
+
return self if @payload.key?("file")
|
|
24
|
+
# Only stamp certain errors.
|
|
25
|
+
return self unless ["builtin", "stdlib"].include?(@payload["origin"])
|
|
26
|
+
|
|
27
|
+
# Insert "file" after "origin"
|
|
28
|
+
reordered = {}
|
|
29
|
+
@payload.each do |key, value|
|
|
30
|
+
reordered[key] = value
|
|
31
|
+
reordered["file"] = file if key == "origin"
|
|
32
|
+
end
|
|
33
|
+
@payload = reordered
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Was this error runtime-produced (as opposed to user-constructed via `!expr`)?
|
|
38
|
+
# Runtime errors use lenient serialization (docs/user/reference.md §9.3) and
|
|
39
|
+
# get a call-site `file` stamped.
|
|
40
|
+
def runtime?
|
|
41
|
+
@runtime
|
|
21
42
|
end
|
|
22
43
|
|
|
23
|
-
# Build
|
|
44
|
+
# Build a runtime-produced error with the standardized payload shape
|
|
24
45
|
# documented in docs/user/reference.md §6.5.
|
|
25
|
-
def self.
|
|
26
|
-
error
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
def self.from_runtime(kind:, origin:, operation:, input:, file: nil, expected: nil, message: nil)
|
|
47
|
+
raise Unreachable, "an error with `expected` must not also carry a `message`" if expected && message
|
|
48
|
+
|
|
49
|
+
received_error = input.is_a?(ErrorVal)
|
|
50
|
+
|
|
51
|
+
payload = { "kind" => kind, "origin" => origin }
|
|
52
|
+
payload["file"] = file if file
|
|
53
|
+
payload["operation"] = operation
|
|
54
|
+
payload["status"] = received_error ? 1 : 0
|
|
55
|
+
payload["input"] = received_error ? input.payload : input
|
|
56
|
+
payload["expected"] = expected if expected
|
|
57
|
+
payload["message"] = message if message
|
|
58
|
+
|
|
59
|
+
new(payload, runtime: true)
|
|
38
60
|
end
|
|
39
61
|
|
|
40
62
|
def inspect
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
#
|
|
5
|
+
# Lazy, memoized value of a top-level unit (a file, or an inline/REPL entry).
|
|
6
|
+
|
|
7
|
+
module Fusion
|
|
8
|
+
class Interpreter
|
|
9
|
+
class Thunk
|
|
10
|
+
# We use a custom Ruby error to transmit read failures between `Interpreter.evaluate_file`
|
|
11
|
+
# (which runs in the @compute block) and the Thunk to enforce their connection.
|
|
12
|
+
# If `Interpreter.evaluate_file` were to be used outside of a Thunk, the Ruby error would
|
|
13
|
+
# bubble and trigger an `internal_error` later on.
|
|
14
|
+
class ReadFailure < StandardError; end
|
|
15
|
+
|
|
16
|
+
def initialize(&compute)
|
|
17
|
+
@compute = compute
|
|
18
|
+
@state = :unforced # :unforced | :forcing | :done
|
|
19
|
+
@value = nil # memoized result: runtime value/error | ReadFailure
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# `operation`/`input`/`site` describe the @-reference forcing this thunk.
|
|
23
|
+
# They are NOT passed to `@compute`, because they differ when evaluating the same
|
|
24
|
+
# Thunk for different @-references. They MUST NOT become part or the memoized value.
|
|
25
|
+
def force(operation: "loading code", input: NULL, site: { origin: "code", file: nil })
|
|
26
|
+
result = case @state
|
|
27
|
+
when :done
|
|
28
|
+
@value
|
|
29
|
+
when :forcing
|
|
30
|
+
# Re-entering while still computing results in a non-productive data cycle. Not memoized.
|
|
31
|
+
ErrorVal.from_runtime(kind: "reference_error", **site, operation: operation, input: input, message: "non-productive data cycle")
|
|
32
|
+
when :unforced
|
|
33
|
+
@state = :forcing
|
|
34
|
+
begin
|
|
35
|
+
@value = @compute.call
|
|
36
|
+
rescue ReadFailure => e
|
|
37
|
+
# Memoize the Ruby error itself. Turn it into a Fusion runtime error below.
|
|
38
|
+
@value = e
|
|
39
|
+
end
|
|
40
|
+
@state = :done
|
|
41
|
+
@value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
case result
|
|
45
|
+
when ReadFailure
|
|
46
|
+
ErrorVal.from_runtime(kind: "reference_error", **site, operation: operation, input: input, message: result.message)
|
|
47
|
+
else
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|