gemite 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +519 -0
- data/examples/comments/README.md +13 -0
- data/examples/comments/index.gmt +65 -0
- data/examples/comments/public/css/style.css +44 -0
- data/exe/gemite +6 -0
- data/lib/gemite/ast.rb +42 -0
- data/lib/gemite/cli.rb +81 -0
- data/lib/gemite/database.rb +101 -0
- data/lib/gemite/environment.rb +73 -0
- data/lib/gemite/errors.rb +39 -0
- data/lib/gemite/function.rb +21 -0
- data/lib/gemite/http_server.rb +181 -0
- data/lib/gemite/interpreter.rb +435 -0
- data/lib/gemite/lexer.rb +289 -0
- data/lib/gemite/parser.rb +433 -0
- data/lib/gemite/request.rb +49 -0
- data/lib/gemite/router.rb +98 -0
- data/lib/gemite/sqlite3_ffi.rb +223 -0
- data/lib/gemite/template_parser.rb +305 -0
- data/lib/gemite/token.rb +11 -0
- data/lib/gemite/version.rb +5 -0
- data/lib/gemite.rb +21 -0
- metadata +69 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast"
|
|
4
|
+
require_relative "environment"
|
|
5
|
+
require_relative "function"
|
|
6
|
+
require_relative "database"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
|
|
9
|
+
module Gemite
|
|
10
|
+
# テンプレートノード木(TemplateParserの出力)と文AST(Parserの出力)を評価する
|
|
11
|
+
# ツリーウォーク型インタプリタ。
|
|
12
|
+
#
|
|
13
|
+
# 1リクエスト = 1つのInterpreterインスタンム。 render を1回呼ぶと、その中で
|
|
14
|
+
# ファイル全体を通してグローバル環境(仕様25: グローバル変数はファイル単位)を共有する。
|
|
15
|
+
class Interpreter
|
|
16
|
+
include AST
|
|
17
|
+
|
|
18
|
+
def initialize(get_params: {}, post_params: {}, base_dir: Dir.pwd)
|
|
19
|
+
@base_dir = base_dir
|
|
20
|
+
@global_env = Environment.new
|
|
21
|
+
@global_env.set_local("get", to_gemite_hash(get_params))
|
|
22
|
+
@global_env.set_local("post", to_gemite_hash(post_params))
|
|
23
|
+
@open_databases = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# テンプレートノード列を実行し、出力HTML文字列を返す。
|
|
27
|
+
# redirect() が呼ばれた場合は Gemite::RedirectSignal が、
|
|
28
|
+
# 未捕捉のエラーが発生した場合は Gemite::RuntimeError 等がそのまま送出される
|
|
29
|
+
# (呼び出し元のHTTPサーバ層で捕捉して適切なレスポンスに変換する)。
|
|
30
|
+
def render(template_nodes)
|
|
31
|
+
@output = +""
|
|
32
|
+
exec_tpl_nodes(template_nodes, @global_env)
|
|
33
|
+
@output
|
|
34
|
+
ensure
|
|
35
|
+
close_databases
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def close_databases
|
|
41
|
+
@open_databases.each { |db| db.close rescue nil }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_gemite_hash(h)
|
|
45
|
+
result = {}
|
|
46
|
+
(h || {}).each { |k, v| result[k.to_s.to_sym] = v }
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ============ テンプレートノードの実行 ============
|
|
51
|
+
|
|
52
|
+
def exec_tpl_nodes(nodes, env)
|
|
53
|
+
nodes.each { |n| exec_tpl_node(n, env) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def exec_tpl_node(node, env)
|
|
57
|
+
case node
|
|
58
|
+
when TplText
|
|
59
|
+
@output << node.text
|
|
60
|
+
when TplExpr
|
|
61
|
+
@output << stringify(eval_expr(node.expr, env))
|
|
62
|
+
when TplCode
|
|
63
|
+
exec_statements(node.statements, env)
|
|
64
|
+
when TplIf
|
|
65
|
+
exec_tpl_if(node, env)
|
|
66
|
+
when TplFor
|
|
67
|
+
exec_tpl_for(node, env)
|
|
68
|
+
else
|
|
69
|
+
raise "internal error: unknown template node #{node.class}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def exec_tpl_if(node, env)
|
|
74
|
+
branch = node.branches.find { |cond, _body| truthy?(eval_expr(cond, env)) }
|
|
75
|
+
if branch
|
|
76
|
+
exec_tpl_nodes(branch[1], env)
|
|
77
|
+
elsif node.else_body
|
|
78
|
+
exec_tpl_nodes(node.else_body, env)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def exec_tpl_for(node, env)
|
|
83
|
+
items = to_enumerable(eval_expr(node.iterable, env))
|
|
84
|
+
with_loop_var(env, node.var_name, items) do |item|
|
|
85
|
+
env.set_local(node.var_name, item)
|
|
86
|
+
exec_tpl_nodes(node.body, env)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ============ 文の実行 ============
|
|
91
|
+
# 戻り値は「最後に実行した文の値」(関数の暗黙の戻り値に使う)。
|
|
92
|
+
|
|
93
|
+
def exec_statements(stmts, env)
|
|
94
|
+
result = nil
|
|
95
|
+
stmts.each { |s| result = exec_statement(s, env) }
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def exec_statement(stmt, env)
|
|
100
|
+
case stmt
|
|
101
|
+
when Assignment
|
|
102
|
+
val = eval_expr(stmt.value, env)
|
|
103
|
+
env.assign(stmt.name, val)
|
|
104
|
+
val
|
|
105
|
+
when FunctionDef
|
|
106
|
+
env.assign(stmt.name, Function.new(stmt.name, stmt.params, stmt.body, env))
|
|
107
|
+
nil
|
|
108
|
+
when ReturnStmt
|
|
109
|
+
raise Gemite::ReturnSignal, eval_expr(stmt.value, env)
|
|
110
|
+
when ExprStmt
|
|
111
|
+
eval_expr(stmt.expr, env)
|
|
112
|
+
when IfStmt
|
|
113
|
+
exec_if_stmt(stmt, env)
|
|
114
|
+
when ForStmt
|
|
115
|
+
exec_for_stmt(stmt, env)
|
|
116
|
+
nil
|
|
117
|
+
when BeginCatchStmt
|
|
118
|
+
exec_begin_catch(stmt, env)
|
|
119
|
+
else
|
|
120
|
+
raise "internal error: unknown statement #{stmt.class}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def exec_if_stmt(stmt, env)
|
|
125
|
+
branch = stmt.branches.find { |cond, _body| truthy?(eval_expr(cond, env)) }
|
|
126
|
+
if branch
|
|
127
|
+
exec_statements(branch[1], env)
|
|
128
|
+
elsif stmt.else_body
|
|
129
|
+
exec_statements(stmt.else_body, env)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def exec_for_stmt(stmt, env)
|
|
134
|
+
items = to_enumerable(eval_expr(stmt.iterable, env))
|
|
135
|
+
with_loop_var(env, stmt.var_name, items) do |item|
|
|
136
|
+
env.set_local(stmt.var_name, item)
|
|
137
|
+
exec_statements(stmt.body, env)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @forのループ変数だけを退避/復元するヘルパー(仕様25: @for変数はループ内のみ有効)。
|
|
142
|
+
# ループ変数以外への代入は通常通り現在のスコープに作用する。
|
|
143
|
+
def with_loop_var(env, var_name, items)
|
|
144
|
+
had_prior = env.has_local?(var_name)
|
|
145
|
+
prior_val = env.get_local(var_name)
|
|
146
|
+
items.each { |item| yield item }
|
|
147
|
+
ensure
|
|
148
|
+
if had_prior
|
|
149
|
+
env.set_local(var_name, prior_val)
|
|
150
|
+
else
|
|
151
|
+
env.delete_local(var_name)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def exec_begin_catch(stmt, env)
|
|
156
|
+
exec_statements(stmt.begin_body, env)
|
|
157
|
+
rescue Gemite::ReturnSignal, Gemite::RedirectSignal
|
|
158
|
+
raise
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
env.assign(stmt.err_name, e.message)
|
|
161
|
+
exec_statements(stmt.catch_body, env)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_enumerable(val)
|
|
165
|
+
case val
|
|
166
|
+
when Array then val
|
|
167
|
+
when nil then []
|
|
168
|
+
else
|
|
169
|
+
raise Gemite::RuntimeError, "@for でループできない値です: #{type_name(val)}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# ============ 式の評価 ============
|
|
174
|
+
|
|
175
|
+
def eval_expr(expr, env)
|
|
176
|
+
case expr
|
|
177
|
+
when IntLiteral, FloatLiteral, BoolLiteral, NilLiteral
|
|
178
|
+
expr.value
|
|
179
|
+
when StringLiteral
|
|
180
|
+
eval_string_literal(expr, env)
|
|
181
|
+
when ArrayLiteral
|
|
182
|
+
expr.elements.map { |e| eval_expr(e, env) }
|
|
183
|
+
when HashLiteral
|
|
184
|
+
h = {}
|
|
185
|
+
expr.pairs.each { |k, v| h[k] = eval_expr(v, env) }
|
|
186
|
+
h
|
|
187
|
+
when Identifier
|
|
188
|
+
env.get(expr.name)
|
|
189
|
+
when BinaryOp
|
|
190
|
+
eval_binary_op(expr, env)
|
|
191
|
+
when UnaryOp
|
|
192
|
+
eval_unary_op(expr, env)
|
|
193
|
+
when LogicalOp
|
|
194
|
+
eval_logical_op(expr, env)
|
|
195
|
+
when MemberAccess
|
|
196
|
+
member_access(eval_expr(expr.receiver, env), expr.name)
|
|
197
|
+
when IndexAccess
|
|
198
|
+
index_access(eval_expr(expr.receiver, env), eval_expr(expr.index, env))
|
|
199
|
+
when CallExpr
|
|
200
|
+
eval_call(expr, env)
|
|
201
|
+
when MethodCallExpr
|
|
202
|
+
eval_method_call(expr, env)
|
|
203
|
+
else
|
|
204
|
+
raise "internal error: unknown expression #{expr.class}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def eval_string_literal(expr, env)
|
|
209
|
+
expr.parts.map do |(kind, val)|
|
|
210
|
+
kind == :str ? val : stringify(eval_expr(val, env))
|
|
211
|
+
end.join
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def eval_binary_op(expr, env)
|
|
215
|
+
left = eval_expr(expr.left, env)
|
|
216
|
+
right = eval_expr(expr.right, env)
|
|
217
|
+
op = expr.op
|
|
218
|
+
case op
|
|
219
|
+
when :+
|
|
220
|
+
eval_plus(left, right)
|
|
221
|
+
when :-, :*, :/, :%
|
|
222
|
+
eval_arith(op, left, right)
|
|
223
|
+
when :==
|
|
224
|
+
values_equal?(left, right)
|
|
225
|
+
when :!=
|
|
226
|
+
!values_equal?(left, right)
|
|
227
|
+
when :<, :<=, :>, :>=
|
|
228
|
+
eval_compare(op, left, right)
|
|
229
|
+
else
|
|
230
|
+
raise "internal error: unknown operator #{op}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def eval_plus(left, right)
|
|
235
|
+
return left + right if left.is_a?(String) && right.is_a?(String)
|
|
236
|
+
return left + right if numeric?(left) && numeric?(right)
|
|
237
|
+
|
|
238
|
+
raise Gemite::RuntimeError, "'+' は #{type_name(left)} と #{type_name(right)} には使えません"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def eval_arith(op, left, right)
|
|
242
|
+
unless numeric?(left) && numeric?(right)
|
|
243
|
+
raise Gemite::RuntimeError, "'#{op}' 演算子は数値同士にのみ使えます (#{type_name(left)}, #{type_name(right)})"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
left.public_send(op, right)
|
|
247
|
+
rescue ZeroDivisionError
|
|
248
|
+
raise Gemite::RuntimeError, "ゼロ除算です"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def eval_compare(op, left, right)
|
|
252
|
+
comparable = (numeric?(left) && numeric?(right)) || (left.is_a?(String) && right.is_a?(String))
|
|
253
|
+
raise Gemite::RuntimeError, "'#{op}' で #{type_name(left)} と #{type_name(right)} を比較できません" unless comparable
|
|
254
|
+
|
|
255
|
+
left.public_send(op, right)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def eval_unary_op(expr, env)
|
|
259
|
+
val = eval_expr(expr.operand, env)
|
|
260
|
+
case expr.op
|
|
261
|
+
when :-@
|
|
262
|
+
raise Gemite::RuntimeError, "単項 '-' は数値にのみ使えます (#{type_name(val)})" unless numeric?(val)
|
|
263
|
+
|
|
264
|
+
-val
|
|
265
|
+
when :not
|
|
266
|
+
!truthy?(val)
|
|
267
|
+
else
|
|
268
|
+
raise "internal error: unknown unary operator #{expr.op}"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def eval_logical_op(expr, env)
|
|
273
|
+
left = eval_expr(expr.left, env)
|
|
274
|
+
case expr.op
|
|
275
|
+
when :and
|
|
276
|
+
truthy?(left) ? eval_expr(expr.right, env) : left
|
|
277
|
+
when :or
|
|
278
|
+
truthy?(left) ? left : eval_expr(expr.right, env)
|
|
279
|
+
else
|
|
280
|
+
raise "internal error: unknown logical operator #{expr.op}"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def member_access(receiver, name)
|
|
285
|
+
return receiver[name.to_sym] if receiver.is_a?(Hash)
|
|
286
|
+
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def index_access(receiver, index)
|
|
291
|
+
case receiver
|
|
292
|
+
when Array
|
|
293
|
+
index.is_a?(Integer) ? receiver[index] : nil
|
|
294
|
+
when Hash
|
|
295
|
+
key = index.is_a?(String) ? index.to_sym : index
|
|
296
|
+
receiver[key]
|
|
297
|
+
else
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
BUILTIN_FUNCTIONS = %w[sqlite redirect puts].freeze
|
|
303
|
+
|
|
304
|
+
def eval_call(expr, env)
|
|
305
|
+
args = expr.args.map { |a| eval_expr(a, env) }
|
|
306
|
+
if BUILTIN_FUNCTIONS.include?(expr.callee_name)
|
|
307
|
+
call_builtin(expr.callee_name, args)
|
|
308
|
+
else
|
|
309
|
+
fn = env.get(expr.callee_name)
|
|
310
|
+
raise Gemite::RuntimeError, "未定義の関数です: #{expr.callee_name}" unless fn.is_a?(Gemite::Function)
|
|
311
|
+
|
|
312
|
+
call_function(fn, args)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def call_builtin(name, args)
|
|
317
|
+
case name
|
|
318
|
+
when "sqlite"
|
|
319
|
+
path = args[0]
|
|
320
|
+
raise Gemite::RuntimeError, "sqlite() にはファイルパス(文字列)を渡してください" unless path.is_a?(String)
|
|
321
|
+
|
|
322
|
+
db = Gemite::Database.new(resolve_db_path(path))
|
|
323
|
+
@open_databases << db
|
|
324
|
+
db
|
|
325
|
+
when "redirect"
|
|
326
|
+
raise Gemite::RedirectSignal, args[0].to_s
|
|
327
|
+
when "puts"
|
|
328
|
+
$stdout.puts(args.map { |a| stringify(a) }.join(" "))
|
|
329
|
+
$stdout.flush
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def resolve_db_path(path)
|
|
335
|
+
return path if path.start_with?("/")
|
|
336
|
+
|
|
337
|
+
File.join(@base_dir, path)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def call_function(fn, args)
|
|
341
|
+
call_env = Environment.new(parent: fn.closure_env, boundary: true)
|
|
342
|
+
fn.params.each_with_index { |param, i| call_env.set_local(param, args[i]) }
|
|
343
|
+
exec_statements(fn.body, call_env)
|
|
344
|
+
rescue Gemite::ReturnSignal => e
|
|
345
|
+
e.value
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def eval_method_call(expr, env)
|
|
349
|
+
receiver = eval_expr(expr.receiver, env)
|
|
350
|
+
args = expr.args.map { |a| eval_expr(a, env) }
|
|
351
|
+
case receiver
|
|
352
|
+
when Gemite::Database
|
|
353
|
+
call_database_method(receiver, expr.method_name, args)
|
|
354
|
+
else
|
|
355
|
+
raise Gemite::RuntimeError, "#{type_name(receiver)} に対して '#{expr.method_name}' は呼び出せません"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def call_database_method(db, name, args)
|
|
360
|
+
case name
|
|
361
|
+
when "select"
|
|
362
|
+
db.select(args[0], args[1] || {})
|
|
363
|
+
when "insert"
|
|
364
|
+
db.insert(args[0], args[1] || {})
|
|
365
|
+
when "update"
|
|
366
|
+
db.update(args[0], args[1] || {}, args[2] || {})
|
|
367
|
+
when "delete"
|
|
368
|
+
db.delete(args[0], args[1] || {})
|
|
369
|
+
when "exec"
|
|
370
|
+
db.exec(args[0])
|
|
371
|
+
else
|
|
372
|
+
raise Gemite::RuntimeError, "データベースに '#{name}' メソッドはありません"
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# ============ 値のユーティリティ ============
|
|
377
|
+
|
|
378
|
+
def numeric?(v)
|
|
379
|
+
v.is_a?(Integer) || v.is_a?(Float)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def values_equal?(a, b)
|
|
383
|
+
a == b
|
|
384
|
+
rescue StandardError
|
|
385
|
+
false
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# 仕様24: false / nil / 0 / "" / [] / {} が偽、それ以外は真。
|
|
389
|
+
def truthy?(val)
|
|
390
|
+
return false if val.nil? || val == false
|
|
391
|
+
return false if val.is_a?(Numeric) && val.zero?
|
|
392
|
+
return false if val.is_a?(String) && val.empty?
|
|
393
|
+
return false if val.is_a?(Array) && val.empty?
|
|
394
|
+
return false if val.is_a?(Hash) && val.empty?
|
|
395
|
+
|
|
396
|
+
true
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def type_name(v)
|
|
400
|
+
case v
|
|
401
|
+
when nil then "nil"
|
|
402
|
+
when true, false then "真偽値"
|
|
403
|
+
when Integer, Float then "数値"
|
|
404
|
+
when String then "文字列"
|
|
405
|
+
when Array then "配列"
|
|
406
|
+
when Hash then "連想配列"
|
|
407
|
+
when Gemite::Function then "関数"
|
|
408
|
+
when Gemite::Database then "データベース"
|
|
409
|
+
else v.class.name
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def stringify(val)
|
|
414
|
+
case val
|
|
415
|
+
when nil then ""
|
|
416
|
+
when String then val
|
|
417
|
+
when Integer, Float then val.to_s
|
|
418
|
+
when true then "true"
|
|
419
|
+
when false then "false"
|
|
420
|
+
when Array
|
|
421
|
+
"[" + val.map { |v| stringify_debug(v) }.join(", ") + "]"
|
|
422
|
+
when Hash
|
|
423
|
+
"{" + val.map { |k, v| "#{k}: #{stringify_debug(v)}" }.join(", ") + "}"
|
|
424
|
+
when Gemite::Function, Gemite::Database
|
|
425
|
+
val.to_s
|
|
426
|
+
else
|
|
427
|
+
val.to_s
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def stringify_debug(val)
|
|
432
|
+
val.is_a?(String) ? val.inspect : stringify(val)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
data/lib/gemite/lexer.rb
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "token"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Gemite
|
|
7
|
+
# Gemiteの「コード」部分(<?gemite ... ?> の中身や、{ ... } 補間の中身)を
|
|
8
|
+
# トークン列に変換する。HTMLの生テキストは扱わない(それは TemplateParser の仕事)。
|
|
9
|
+
class Lexer
|
|
10
|
+
KEYWORDS = {
|
|
11
|
+
"def" => :DEF,
|
|
12
|
+
"end" => :END,
|
|
13
|
+
"return" => :RETURN,
|
|
14
|
+
"true" => :TRUE,
|
|
15
|
+
"false" => :FALSE,
|
|
16
|
+
"nil" => :NIL,
|
|
17
|
+
"and" => :AND,
|
|
18
|
+
"or" => :OR,
|
|
19
|
+
"not" => :NOT,
|
|
20
|
+
"begin" => :BEGIN,
|
|
21
|
+
"catch" => :CATCH,
|
|
22
|
+
"in" => :IN
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# @if / @for などの "@"付きディレクティブ。コードブロック内でも仕様通り
|
|
26
|
+
# @endif / @end を使う(defやbeginの素のendとは別体系)。
|
|
27
|
+
AT_KEYWORDS = {
|
|
28
|
+
"if" => :AT_IF,
|
|
29
|
+
"elseif" => :AT_ELSEIF,
|
|
30
|
+
"else" => :AT_ELSE,
|
|
31
|
+
"endif" => :AT_ENDIF,
|
|
32
|
+
"for" => :AT_FOR,
|
|
33
|
+
"end" => :AT_END
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
OPERATORS = [
|
|
37
|
+
["==", :EQ], ["!=", :NEQ], ["<=", :LTE], [">=", :GTE],
|
|
38
|
+
["<", :LT], [">", :GT], ["=", :ASSIGN],
|
|
39
|
+
["+", :PLUS], ["-", :MINUS], ["*", :STAR], ["/", :SLASH], ["%", :PERCENT],
|
|
40
|
+
["(", :LPAREN], [")", :RPAREN], ["[", :LBRACKET], ["]", :RBRACKET],
|
|
41
|
+
["{", :LBRACE], ["}", :RBRACE], [",", :COMMA], [":", :COLON], [".", :DOT]
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
def initialize(source, line: 1)
|
|
45
|
+
@src = source
|
|
46
|
+
@pos = 0
|
|
47
|
+
@len = source.length
|
|
48
|
+
@line = line
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 直前のトークンがこれらのいずれかである場合、続く改行は「行継続」とみなし
|
|
52
|
+
# NEWLINEトークンを生成しない。これにより
|
|
53
|
+
# comments =
|
|
54
|
+
# db.select(...)
|
|
55
|
+
# のように "=" や演算子・開き括弧・カンマの直後で改行して値を次行に続える
|
|
56
|
+
# 書き方(仕様書28の例で実際に使われている)を、文終端の改行と区別できる。
|
|
57
|
+
CONTINUATION_TOKENS = %i[
|
|
58
|
+
ASSIGN PLUS MINUS STAR SLASH PERCENT
|
|
59
|
+
EQ NEQ LT LTE GT GTE AND OR NOT
|
|
60
|
+
COMMA COLON DOT LPAREN LBRACKET LBRACE
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
63
|
+
def tokenize
|
|
64
|
+
tokens = []
|
|
65
|
+
loop do
|
|
66
|
+
tok = next_token
|
|
67
|
+
if tok.type == :NEWLINE && CONTINUATION_TOKENS.include?(tokens.last&.type)
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
tokens << tok
|
|
71
|
+
break if tok.type == :EOF
|
|
72
|
+
end
|
|
73
|
+
tokens
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def peek_char(offset = 0)
|
|
79
|
+
idx = @pos + offset
|
|
80
|
+
idx < @len ? @src[idx] : nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def advance_char
|
|
84
|
+
ch = @src[@pos]
|
|
85
|
+
@pos += 1
|
|
86
|
+
@line += 1 if ch == "\n"
|
|
87
|
+
ch
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def eof?
|
|
91
|
+
@pos >= @len
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def next_token
|
|
95
|
+
skip_insignificant
|
|
96
|
+
start_line = @line
|
|
97
|
+
return Token.new(:EOF, nil, start_line) if eof?
|
|
98
|
+
|
|
99
|
+
ch = peek_char
|
|
100
|
+
return lex_newline(start_line) if ch == "\n"
|
|
101
|
+
return lex_number(start_line) if digit?(ch)
|
|
102
|
+
return lex_string(start_line) if ch == '"'
|
|
103
|
+
return lex_at_keyword(start_line) if ch == "@"
|
|
104
|
+
return lex_ident(start_line) if ident_start?(ch)
|
|
105
|
+
|
|
106
|
+
lex_operator(start_line)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def digit?(ch)
|
|
110
|
+
!ch.nil? && ch >= "0" && ch <= "9"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ident_start?(ch)
|
|
114
|
+
!ch.nil? && (ch =~ /[A-Za-z_]/)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ident_char?(ch)
|
|
118
|
+
!ch.nil? && (ch =~ /[A-Za-z0-9_]/)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# 改行以外の空白・コメント(# から行末まで)を読み飛ばす。
|
|
122
|
+
# 改行はステートメント区切りとして意味を持つのでここでは消費しない。
|
|
123
|
+
def skip_insignificant
|
|
124
|
+
loop do
|
|
125
|
+
ch = peek_char
|
|
126
|
+
if ch == " " || ch == "\t" || ch == "\r"
|
|
127
|
+
advance_char
|
|
128
|
+
elsif ch == "#"
|
|
129
|
+
advance_char while peek_char && peek_char != "\n"
|
|
130
|
+
else
|
|
131
|
+
break
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# 連続する改行(間の空白/コメント含む)をまとめて1つのNEWLINEトークンにする
|
|
137
|
+
def lex_newline(line)
|
|
138
|
+
loop do
|
|
139
|
+
advance_char if peek_char == "\n"
|
|
140
|
+
skip_insignificant
|
|
141
|
+
break unless peek_char == "\n"
|
|
142
|
+
end
|
|
143
|
+
Token.new(:NEWLINE, nil, line)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def lex_number(line)
|
|
147
|
+
start = @pos
|
|
148
|
+
advance_char while digit?(peek_char)
|
|
149
|
+
if peek_char == "." && digit?(peek_char(1))
|
|
150
|
+
advance_char
|
|
151
|
+
advance_char while digit?(peek_char)
|
|
152
|
+
return Token.new(:FLOAT, @src[start...@pos].to_f, line)
|
|
153
|
+
end
|
|
154
|
+
Token.new(:INT, @src[start...@pos].to_i, line)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def lex_ident(line)
|
|
158
|
+
start = @pos
|
|
159
|
+
advance_char while ident_char?(peek_char)
|
|
160
|
+
word = @src[start...@pos]
|
|
161
|
+
type = KEYWORDS[word]
|
|
162
|
+
type ? Token.new(type, word, line) : Token.new(:IDENT, word, line)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def lex_at_keyword(line)
|
|
166
|
+
advance_char # '@'
|
|
167
|
+
start = @pos
|
|
168
|
+
advance_char while ident_char?(peek_char)
|
|
169
|
+
word = @src[start...@pos]
|
|
170
|
+
type = AT_KEYWORDS[word]
|
|
171
|
+
raise Gemite::SyntaxError.new("不明なディレクティブ '@#{word}'", line: line) unless type
|
|
172
|
+
|
|
173
|
+
Token.new(type, "@#{word}", line)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# 文字列リテラル。#{ ... } による式補間を [:str, text] / [:expr, source] の
|
|
177
|
+
# 配列として保持する(実際の式へのパースは Parser 側で行う)。
|
|
178
|
+
def lex_string(line)
|
|
179
|
+
advance_char # 開き "
|
|
180
|
+
parts = []
|
|
181
|
+
buf = +""
|
|
182
|
+
loop do
|
|
183
|
+
ch = peek_char
|
|
184
|
+
raise Gemite::SyntaxError.new("文字列リテラルが閉じられていません", line: line) if ch.nil?
|
|
185
|
+
|
|
186
|
+
case ch
|
|
187
|
+
when '"'
|
|
188
|
+
advance_char
|
|
189
|
+
break
|
|
190
|
+
when "\\"
|
|
191
|
+
advance_char
|
|
192
|
+
buf << unescape(peek_char)
|
|
193
|
+
advance_char
|
|
194
|
+
when "#"
|
|
195
|
+
if peek_char(1) == "{"
|
|
196
|
+
parts << [:str, buf] unless buf.empty?
|
|
197
|
+
buf = +""
|
|
198
|
+
advance_char # '#'
|
|
199
|
+
advance_char # '{'
|
|
200
|
+
parts << [:expr, scan_interpolation_source(line)]
|
|
201
|
+
else
|
|
202
|
+
buf << ch
|
|
203
|
+
advance_char
|
|
204
|
+
end
|
|
205
|
+
else
|
|
206
|
+
buf << ch
|
|
207
|
+
advance_char
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
parts << [:str, buf] unless buf.empty?
|
|
211
|
+
parts << [:str, ""] if parts.empty?
|
|
212
|
+
Token.new(:STRING, parts, line)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def unescape(ch)
|
|
216
|
+
case ch
|
|
217
|
+
when "n" then "\n"
|
|
218
|
+
when "t" then "\t"
|
|
219
|
+
when "r" then "\r"
|
|
220
|
+
when '"' then '"'
|
|
221
|
+
when "\\" then "\\"
|
|
222
|
+
when "#" then "#"
|
|
223
|
+
when nil then ""
|
|
224
|
+
else ch
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# #{ の直後から対応する } までの生ソースを取り出す。
|
|
229
|
+
# ネストした {}(ハッシュリテラル等)や、内部の文字列リテラルにも対応する。
|
|
230
|
+
def scan_interpolation_source(line)
|
|
231
|
+
depth = 1
|
|
232
|
+
in_str = false
|
|
233
|
+
buf = +""
|
|
234
|
+
loop do
|
|
235
|
+
ch = peek_char
|
|
236
|
+
raise Gemite::SyntaxError.new("補間式 #{'#'}{...} が閉じられていません", line: line) if ch.nil?
|
|
237
|
+
|
|
238
|
+
if in_str
|
|
239
|
+
buf << ch
|
|
240
|
+
if ch == "\\"
|
|
241
|
+
advance_char
|
|
242
|
+
buf << peek_char.to_s
|
|
243
|
+
advance_char
|
|
244
|
+
elsif ch == '"'
|
|
245
|
+
in_str = false
|
|
246
|
+
advance_char
|
|
247
|
+
else
|
|
248
|
+
advance_char
|
|
249
|
+
end
|
|
250
|
+
next
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
case ch
|
|
254
|
+
when '"'
|
|
255
|
+
in_str = true
|
|
256
|
+
buf << ch
|
|
257
|
+
advance_char
|
|
258
|
+
when "{"
|
|
259
|
+
depth += 1
|
|
260
|
+
buf << ch
|
|
261
|
+
advance_char
|
|
262
|
+
when "}"
|
|
263
|
+
depth -= 1
|
|
264
|
+
if depth.zero?
|
|
265
|
+
advance_char
|
|
266
|
+
break
|
|
267
|
+
else
|
|
268
|
+
buf << ch
|
|
269
|
+
advance_char
|
|
270
|
+
end
|
|
271
|
+
else
|
|
272
|
+
buf << ch
|
|
273
|
+
advance_char
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
buf
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def lex_operator(line)
|
|
280
|
+
OPERATORS.each do |(text, type)|
|
|
281
|
+
if @src[@pos, text.length] == text
|
|
282
|
+
text.length.times { advance_char }
|
|
283
|
+
return Token.new(type, text, line)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
raise Gemite::SyntaxError.new("不明な文字 '#{peek_char}'", line: line)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|