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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -6
  3. data/Rakefile +9 -0
  4. data/docs/lang/design.md +418 -28
  5. data/docs/lang/implementation.md +238 -0
  6. data/docs/lang/roadmap.md +20 -57
  7. data/docs/user/explanation.md +5 -10
  8. data/docs/user/how-to-guides.md +62 -23
  9. data/docs/user/reference.md +596 -168
  10. data/docs/user/tutorial.md +32 -29
  11. data/examples/double.fsn +1 -1
  12. data/examples/ends.fsn +4 -0
  13. data/examples/factorial.fsn +2 -2
  14. data/examples/fizzbuzz.fsn +1 -4
  15. data/examples/json_test.fsn +4 -0
  16. data/examples/palindrome.fsn +2 -1
  17. data/exe/fusion +17 -44
  18. data/lib/fusion/ast.rb +97 -0
  19. data/lib/fusion/atom.rb +17 -0
  20. data/lib/fusion/cli/decoder.rb +84 -0
  21. data/lib/fusion/cli/encoder.rb +28 -0
  22. data/lib/fusion/cli/options.rb +212 -0
  23. data/lib/fusion/cli/parser.rb +38 -0
  24. data/lib/fusion/cli/repl.rb +78 -0
  25. data/lib/fusion/cli/serializer.rb +70 -0
  26. data/lib/fusion/cli.rb +207 -0
  27. data/lib/fusion/interpreter/builtins.rb +465 -0
  28. data/lib/fusion/interpreter/env.rb +89 -0
  29. data/lib/fusion/interpreter/error_val.rb +71 -0
  30. data/lib/fusion/interpreter/func.rb +22 -0
  31. data/lib/fusion/interpreter/native_func.rb +22 -0
  32. data/lib/fusion/interpreter/thunk.rb +53 -0
  33. data/lib/fusion/interpreter.rb +752 -0
  34. data/lib/fusion/lexer.rb +249 -0
  35. data/lib/fusion/null.rb +9 -0
  36. data/lib/fusion/parser.rb +542 -0
  37. data/lib/fusion/token.rb +22 -0
  38. data/lib/fusion/typed_data.rb +23 -0
  39. data/lib/fusion/version.rb +1 -1
  40. data/lib/fusion/wire_pair.rb +11 -0
  41. data/lib/fusion.rb +11 -1122
  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 +6 -0
  46. data/stdlib/concat.fsn +5 -0
  47. data/stdlib/falsey.fsn +6 -0
  48. data/stdlib/filter.fsn +12 -0
  49. data/stdlib/flatten.fsn +7 -0
  50. data/stdlib/gt.fsn +9 -0
  51. data/stdlib/gte.fsn +9 -0
  52. data/stdlib/lt.fsn +9 -0
  53. data/stdlib/lte.fsn +9 -0
  54. data/stdlib/map.fsn +6 -2
  55. data/stdlib/range.fsn +2 -1
  56. data/stdlib/reduce.fsn +8 -0
  57. data/stdlib/sanitize.fsn +12 -0
  58. data/stdlib/truthy.fsn +7 -0
  59. metadata +41 -2
  60. data/stdlib/math/square.fsn +0 -1
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ # === Transformation ===
4
+ #
5
+ # Input: String (source code)
6
+ # Output: Array<Token>
7
+
8
+ require_relative "token"
9
+ require_relative "null"
10
+
11
+ module Fusion
12
+ class Lexer
13
+ PUNCT = {
14
+ "(" => :lparen, ")" => :rparen,
15
+ "[" => :lbracket, "]" => :rbracket,
16
+ "{" => :lbrace, "}" => :rbrace,
17
+ "," => :comma, ":" => :colon,
18
+ "?" => :question, "." => :dot,
19
+ "@" => :at, "/" => :slash,
20
+ "=" => :equals,
21
+ "+" => :plus, "-" => :minus, "*" => :star,
22
+ "%" => :percent, "~" => :tilde,
23
+ }.freeze
24
+
25
+ def initialize(src)
26
+ @src = src
27
+ @i = 0
28
+ @n = src.length
29
+ end
30
+
31
+ def tokens
32
+ out = []
33
+ loop do
34
+ t = next_token
35
+ out << t
36
+ # A file-reference path is one tight token, lexed only right after `@`/`@@`.
37
+ if t.type == :at || t.type == :atat
38
+ p = try_lex_path
39
+ out << p unless p.nil?
40
+ end
41
+ break if t.type == :eof
42
+ end
43
+ out
44
+ end
45
+
46
+ private
47
+
48
+ def peek(o = 0) = @i + o < @n ? @src[@i + o] : nil
49
+
50
+ def next_token
51
+ skip_trivia
52
+ start = @i
53
+ c = peek
54
+ return Token.new(type: :eof, value: nil, pos: start) if c.nil?
55
+
56
+ # "=>" and "..." handled specially ("#" line comments handled in skip_trivia)
57
+ if c == "=" && peek(1) == ">"
58
+ @i += 2
59
+ return Token.new(type: :arrow, value: "=>", pos: start)
60
+ end
61
+ if c == "." && peek(1) == "." && peek(2) == "."
62
+ @i += 3
63
+ return Token.new(type: :spread, value: "...", pos: start)
64
+ end
65
+ if c == "@" && peek(1) == "@"
66
+ @i += 2
67
+ return Token.new(type: :atat, value: "@@", pos: start)
68
+ end
69
+ # Two-character operators, matched before their single-char prefixes.
70
+ if c == "=" && peek(1) == "="
71
+ @i += 2
72
+ return Token.new(type: :eqeq, value: "==", pos: start)
73
+ end
74
+ if c == "/" && peek(1) == "/"
75
+ @i += 2
76
+ return Token.new(type: :slashslash, value: "//", pos: start)
77
+ end
78
+ if c == "?" && peek(1) == "?"
79
+ @i += 2
80
+ return Token.new(type: :qq, value: "??", pos: start)
81
+ end
82
+ if c == "&" && peek(1) == "&"
83
+ @i += 2
84
+ return Token.new(type: :andand, value: "&&", pos: start)
85
+ end
86
+ if c == "|"
87
+ case peek(1)
88
+ when "|" then @i += 2; return Token.new(type: :oror, value: "||", pos: start)
89
+ when ":" then @i += 2; return Token.new(type: :pipemap, value: "|:", pos: start)
90
+ when "?" then @i += 2; return Token.new(type: :pipefilter, value: "|?", pos: start)
91
+ when "+" then @i += 2; return Token.new(type: :pipereduce, value: "|+", pos: start)
92
+ else @i += 1; return Token.new(type: :pipe, value: "|", pos: start)
93
+ end
94
+ end
95
+ if c == "!"
96
+ @i += 1
97
+ return Token.new(type: :bang, value: "!", pos: start)
98
+ end
99
+ if c == '"'
100
+ return lex_string(start)
101
+ end
102
+ if digit?(c)
103
+ return lex_number(start)
104
+ end
105
+ if ident_start?(c)
106
+ return lex_word(start)
107
+ end
108
+ if (type = PUNCT[c])
109
+ @i += 1
110
+ return Token.new(type: type, value: c, pos: start)
111
+ end
112
+ raise ParseError, "Unexpected character #{c.inspect} at #{start}"
113
+ end
114
+
115
+ def skip_trivia
116
+ loop do
117
+ c = peek
118
+ if c == " " || c == "\t" || c == "\n" || c == "\r"
119
+ @i += 1
120
+ elsif c == "#" && at_line_start?
121
+ # A line is a comment iff its first non-whitespace char is "#".
122
+ # This also covers shebang lines (#!) for free.
123
+ @i += 1 until peek.nil? || peek == "\n"
124
+ else
125
+ break
126
+ end
127
+ end
128
+ end
129
+
130
+ # True when only whitespace precedes @i on the current physical line.
131
+ def at_line_start?
132
+ j = @i - 1
133
+ j -= 1 while j >= 0 && (@src[j] == " " || @src[j] == "\t")
134
+ j < 0 || @src[j] == "\n" || @src[j] == "\r"
135
+ end
136
+
137
+ def lex_string(start)
138
+ @i += 1 # opening quote
139
+ buf = +""
140
+ while (c = peek)
141
+ if c == '"'
142
+ @i += 1
143
+ return Token.new(type: :string, value: buf, pos: start)
144
+ elsif c == "\\"
145
+ @i += 1
146
+ e = peek
147
+ buf << case e
148
+ when '"' then '"'
149
+ when "\\" then "\\"
150
+ when "/" then "/"
151
+ when "n" then "\n"
152
+ when "t" then "\t"
153
+ when "r" then "\r"
154
+ when "b" then "\b"
155
+ when "f" then "\f"
156
+ when "u"
157
+ hex = @src[@i + 1, 4]
158
+ @i += 4
159
+ code_point = hex.to_i(16)
160
+ # Reject surrogates and out-of-range code points up front:
161
+ # pack("U") would otherwise build an invalid-encoding string
162
+ # whose malformed-UTF-8 error surfaces far downstream.
163
+ if code_point.between?(0xD800, 0xDFFF) || code_point > 0x10FFFF
164
+ raise ParseError, "Invalid unicode escape \\u#{hex}"
165
+ end
166
+ [code_point].pack("U")
167
+ else
168
+ raise ParseError, "Bad escape \\#{e}"
169
+ end
170
+ @i += 1
171
+ elsif c == "\n" || c == "\r"
172
+ raise ParseError, "Raw newline in string starting at #{start}; use \\n"
173
+ else
174
+ buf << c
175
+ @i += 1
176
+ end
177
+ end
178
+ raise ParseError, "Unterminated string starting at #{start}"
179
+ end
180
+
181
+ def lex_number(start)
182
+ j = @i
183
+ j += 1 while j < @n && digit?(@src[j])
184
+ is_float = false
185
+ if @src[j] == "." && digit?(@src[j + 1])
186
+ is_float = true
187
+ j += 1
188
+ j += 1 while j < @n && digit?(@src[j])
189
+ end
190
+ if (@src[j] == "e" || @src[j] == "E")
191
+ is_float = true
192
+ j += 1
193
+ j += 1 if (@src[j] == "+" || @src[j] == "-")
194
+ j += 1 while j < @n && digit?(@src[j])
195
+ end
196
+ text = @src[@i...j]
197
+ @i = j
198
+ val = is_float ? text.to_f : text.to_i
199
+ Token.new(type: :number, value: val, pos: start)
200
+ end
201
+
202
+ def lex_word(start)
203
+ j = @i
204
+ j += 1 while j < @n && ident_part?(@src[j])
205
+ text = @src[@i...j]
206
+ @i = j
207
+ case text
208
+ when "true" then Token.new(type: :true_kw, value: true, pos: start)
209
+ when "false" then Token.new(type: :false_kw, value: false, pos: start)
210
+ when "null" then Token.new(type: :null_kw, value: NULL, pos: start)
211
+ else Token.new(type: :ident, value: text, pos: start)
212
+ end
213
+ end
214
+
215
+ # Path lexing — see `tokens`. Only called immediately after `@`/`@@`, with no
216
+ # whitespace skipped, so the path must be tight. Returns nil (position restored)
217
+ # when nothing path-like follows, i.e. a bare `@`/`@@`.
218
+ def try_lex_path
219
+ start = @i
220
+ buf = +""
221
+ while peek == "." && peek(1) == "." && peek(2) == "/"
222
+ buf << "../"
223
+ @i += 3
224
+ end
225
+ unless ident_start?(peek)
226
+ @i = start
227
+ return nil
228
+ end
229
+ buf << lex_ident_text
230
+ while peek == "/" && ident_start?(peek(1))
231
+ @i += 1
232
+ buf << "/" << lex_ident_text
233
+ end
234
+ Token.new(type: :path, value: buf, pos: start)
235
+ end
236
+
237
+ def lex_ident_text
238
+ j = @i
239
+ j += 1 while j < @n && ident_part?(@src[j])
240
+ text = @src[@i...j]
241
+ @i = j
242
+ text
243
+ end
244
+
245
+ def digit?(c) = c && c >= "0" && c <= "9"
246
+ def ident_start?(c) = c && (c =~ /[A-Za-z_]/)
247
+ def ident_part?(c) = c && (c =~ /[A-Za-z0-9_]/)
248
+ end
249
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # === Value ===
4
+ #
5
+ # Runtime null value.
6
+
7
+ module Fusion
8
+ NULL = :null
9
+ end