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
data/lib/fusion/lexer.rb
ADDED
|
@@ -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
|