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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +24 -0
  3. data/.simplecov +11 -0
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +11 -9
  6. data/Rakefile +8 -0
  7. data/docs/lang/design.md +289 -51
  8. data/docs/lang/implementation.md +279 -0
  9. data/docs/lang/roadmap.md +20 -36
  10. data/docs/user/explanation.md +5 -10
  11. data/docs/user/how-to-guides.md +145 -15
  12. data/docs/user/reference.md +365 -140
  13. data/docs/user/tutorial.md +22 -19
  14. data/examples/double.fsn +4 -1
  15. data/examples/factorial.fsn +6 -3
  16. data/examples/fizzbuzz.fsn +1 -4
  17. data/examples/gcd.fsn +9 -0
  18. data/examples/json_test.fsn +4 -0
  19. data/examples/matrix/OP.fsn +2 -0
  20. data/examples/matrix/average.fsn +2 -0
  21. data/examples/matrix/solve.fsn +2 -0
  22. data/examples/palindrome.fsn +1 -1
  23. data/exe/fusion +12 -12
  24. data/lib/fusion/ast.rb +76 -27
  25. data/lib/fusion/atom.rb +1 -1
  26. data/lib/fusion/cli/decoder.rb +13 -8
  27. data/lib/fusion/cli/encoder.rb +2 -2
  28. data/lib/fusion/cli/options.rb +134 -61
  29. data/lib/fusion/cli/parser.rb +4 -4
  30. data/lib/fusion/cli/repl.rb +32 -27
  31. data/lib/fusion/cli/serializer.rb +11 -11
  32. data/lib/fusion/cli.rb +120 -49
  33. data/lib/fusion/interpreter/builtins.rb +298 -160
  34. data/lib/fusion/interpreter/env.rb +42 -12
  35. data/lib/fusion/interpreter/error_val.rb +42 -20
  36. data/lib/fusion/interpreter/thunk.rb +53 -0
  37. data/lib/fusion/interpreter.rb +263 -98
  38. data/lib/fusion/lexer.rb +125 -37
  39. data/lib/fusion/parser.rb +245 -70
  40. data/lib/fusion/version.rb +3 -1
  41. data/lib/fusion.rb +0 -1
  42. data/stdlib/all.fsn +13 -0
  43. data/stdlib/any.fsn +12 -0
  44. data/stdlib/chars.fsn +5 -0
  45. data/stdlib/compact.fsn +5 -0
  46. data/stdlib/concat.fsn +6 -0
  47. data/stdlib/entries.fsn +6 -0
  48. data/stdlib/falsey.fsn +6 -0
  49. data/stdlib/filter.fsn +12 -0
  50. data/stdlib/flatten.fsn +7 -0
  51. data/stdlib/map.fsn +7 -4
  52. data/stdlib/matrix/Matrix.fsn +8 -0
  53. data/stdlib/matrix/OP.fsn +18 -0
  54. data/stdlib/matrix/add.fsn +7 -0
  55. data/stdlib/matrix/column.fsn +6 -0
  56. data/stdlib/matrix/determinant.fsn +16 -0
  57. data/stdlib/matrix/dimensions.fsn +5 -0
  58. data/stdlib/matrix/identity.fsn +6 -0
  59. data/stdlib/matrix/invert.fsn +26 -0
  60. data/stdlib/matrix/minor.fsn +15 -0
  61. data/stdlib/matrix/multiply.fsn +10 -0
  62. data/stdlib/matrix/negate.fsn +5 -0
  63. data/stdlib/matrix/product.fsn +11 -0
  64. data/stdlib/matrix/rotate.fsn +10 -0
  65. data/stdlib/matrix/row.fsn +6 -0
  66. data/stdlib/matrix/scale.fsn +6 -0
  67. data/stdlib/matrix/subtract.fsn +7 -0
  68. data/stdlib/matrix/sum.fsn +14 -0
  69. data/stdlib/matrix/transpose.fsn +5 -0
  70. data/stdlib/range.fsn +2 -2
  71. data/stdlib/reduce.fsn +8 -0
  72. data/stdlib/safe.fsn +7 -0
  73. data/stdlib/sanitize.fsn +2 -3
  74. data/stdlib/toObject.fsn +7 -0
  75. data/stdlib/truthy.fsn +7 -0
  76. data/stdlib/vector/Vector.fsn +7 -0
  77. data/stdlib/vector/add.fsn +6 -0
  78. data/stdlib/vector/cross.fsn +6 -0
  79. data/stdlib/vector/dot.fsn +6 -0
  80. data/stdlib/vector/norm.fsn +6 -0
  81. data/stdlib/vector/scale.fsn +5 -0
  82. data/stdlib/vector/subtract.fsn +6 -0
  83. data/stdlib/zip.fsn +6 -0
  84. metadata +50 -4
  85. data/lib/fusion/interpreter/file_thunk.rb +0 -39
  86. data/stdlib/mapValues.fsn +0 -5
  87. 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
- # Unchecked insert, for interpreter-internal names (__dir__, built-ins, …).
30
- def define(name, value)
31
- @vars[name] = value
32
- self
30
+ def child
31
+ Env.new(self)
33
32
  end
34
33
 
35
- # Insert a pattern binding, rejecting a duplicate binder. Only this env's
36
- # own scope is checked: a binder may shadow a name from a parent env, but
37
- # must be unique within one pattern/clause.
38
- def bind(name, value)
39
- raise DuplicateBinding, name if @vars.key?(name)
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
- def child
55
- Env.new(self)
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
- @internal = false
14
+ @runtime = runtime
15
15
  end
16
16
 
17
- # Whether this is an interpreter-produced error (vs. a user-constructed
18
- # `!expr`). Governs serialization — see docs/user/reference.md §9.3.
19
- def internal_error?
20
- @internal
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 an interpreter-produced error with the standardized payload shape
44
+ # Build a runtime-produced error with the standardized payload shape
24
45
  # documented in docs/user/reference.md §6.5.
25
- def self.internal(kind:, location:, operation:, input:, message: nil)
26
- error = new(
27
- "kind" => kind,
28
- "location" => location,
29
- "operation" => operation,
30
- "input" => input,
31
- **(message ? { "message" => message } : {})
32
- )
33
-
34
- # Mark as "@internal" to activate lenient serialization.
35
- error.instance_variable_set(:@internal, true)
36
-
37
- error
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