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.
@@ -0,0 +1,433 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "token"
4
+ require_relative "ast"
5
+ require_relative "errors"
6
+ require_relative "lexer"
7
+
8
+ module Gemite
9
+ # <?gemite ... ?> の中身や、式の断片(補間・@ifの条件式など)をASTへ変換する。
10
+ # 再帰下降パーサー。
11
+ class Parser
12
+ include AST
13
+
14
+ # ソース文字列全体を「文の並び」としてパースする(<?gemite ?>ブロックの中身用)
15
+ def self.parse_statements_from_source(source, line: 1)
16
+ tokens = Lexer.new(source, line: line).tokenize
17
+ new(tokens).parse_statements_until(nil)
18
+ end
19
+
20
+ # ソース文字列を単一の式としてパースする({expr}補間や@if条件式用)
21
+ def self.parse_expression_from_source(source, line: 1)
22
+ tokens = Lexer.new(source, line: line).tokenize
23
+ parser = new(tokens)
24
+ parser.send(:skip_newlines)
25
+ expr = parser.send(:parse_expression)
26
+ parser.send(:skip_newlines)
27
+ unless parser.send(:check, :EOF)
28
+ tok = parser.send(:current)
29
+ raise Gemite::SyntaxError.new("式の後に余分なトークンがあります (#{tok.type})", line: tok.line)
30
+ end
31
+ expr
32
+ end
33
+
34
+ # "var in iterable" 形式(@forの節)をパースする
35
+ def self.parse_for_clause_from_source(source, line: 1)
36
+ tokens = Lexer.new(source, line: line).tokenize
37
+ new(tokens).parse_for_clause
38
+ end
39
+
40
+ def initialize(tokens)
41
+ @tokens = tokens
42
+ @pos = 0
43
+ end
44
+
45
+ def parse_for_clause
46
+ name_tok = expect(:IDENT)
47
+ expect(:IN)
48
+ iterable = parse_expression
49
+ [name_tok.value, iterable]
50
+ end
51
+
52
+ # stop_types: nilならEOFまで。配列ならそのトークン型に出会うまで(消費はしない)。
53
+ def parse_statements_until(stop_types)
54
+ stmts = []
55
+ skip_newlines
56
+ loop do
57
+ break if check(:EOF)
58
+ break if stop_types && stop_types.include?(current.type)
59
+
60
+ stmts << parse_statement
61
+ skip_newlines
62
+ end
63
+ stmts
64
+ end
65
+
66
+ private
67
+
68
+ def current
69
+ @tokens[@pos]
70
+ end
71
+
72
+ def peek(offset = 1)
73
+ idx = @pos + offset
74
+ idx < @tokens.length ? @tokens[idx] : @tokens.last
75
+ end
76
+
77
+ def advance
78
+ tok = current
79
+ @pos += 1 unless tok.type == :EOF
80
+ tok
81
+ end
82
+
83
+ def check(type)
84
+ current.type == type
85
+ end
86
+
87
+ def accept(type)
88
+ check(type) ? advance : nil
89
+ end
90
+
91
+ def expect(type)
92
+ unless check(type)
93
+ raise Gemite::SyntaxError.new(
94
+ "#{type} を期待しましたが #{current.type}(#{current.value.inspect}) でした",
95
+ line: current.line
96
+ )
97
+ end
98
+ advance
99
+ end
100
+
101
+ def skip_newlines
102
+ advance while check(:NEWLINE)
103
+ end
104
+
105
+ # ============ 文 ============
106
+
107
+ def parse_statement
108
+ case current.type
109
+ when :DEF then parse_function_def
110
+ when :RETURN then parse_return
111
+ when :AT_IF then parse_if_stmt
112
+ when :AT_FOR then parse_for_stmt
113
+ when :BEGIN then parse_begin_catch
114
+ when :IDENT
115
+ peek.type == :ASSIGN ? parse_assignment : ExprStmt.new(parse_expression)
116
+ else
117
+ ExprStmt.new(parse_expression)
118
+ end
119
+ end
120
+
121
+ def parse_assignment
122
+ name_tok = expect(:IDENT)
123
+ expect(:ASSIGN)
124
+ skip_newlines
125
+ value = parse_expression
126
+ Assignment.new(name_tok.value, value)
127
+ end
128
+
129
+ def parse_function_def
130
+ expect(:DEF)
131
+ name_tok = expect(:IDENT)
132
+ expect(:LPAREN)
133
+ params = []
134
+ skip_newlines
135
+ unless check(:RPAREN)
136
+ loop do
137
+ skip_newlines
138
+ params << expect(:IDENT).value
139
+ skip_newlines
140
+ break unless accept(:COMMA)
141
+ end
142
+ end
143
+ skip_newlines
144
+ expect(:RPAREN)
145
+ body = parse_statements_until(%i[END])
146
+ expect(:END)
147
+ FunctionDef.new(name_tok.value, params, body)
148
+ end
149
+
150
+ RETURN_STOPPERS = %i[NEWLINE EOF END AT_ENDIF AT_ELSEIF AT_ELSE AT_END CATCH].freeze
151
+
152
+ def parse_return
153
+ expect(:RETURN)
154
+ if RETURN_STOPPERS.include?(current.type)
155
+ ReturnStmt.new(NilLiteral.new(nil))
156
+ else
157
+ ReturnStmt.new(parse_expression)
158
+ end
159
+ end
160
+
161
+ def parse_if_stmt
162
+ branches = []
163
+ expect(:AT_IF)
164
+ cond = parse_expression
165
+ body = parse_statements_until(%i[AT_ELSEIF AT_ELSE AT_ENDIF])
166
+ branches << [cond, body]
167
+
168
+ while check(:AT_ELSEIF)
169
+ advance
170
+ c = parse_expression
171
+ b = parse_statements_until(%i[AT_ELSEIF AT_ELSE AT_ENDIF])
172
+ branches << [c, b]
173
+ end
174
+
175
+ else_body = nil
176
+ if accept(:AT_ELSE)
177
+ else_body = parse_statements_until(%i[AT_ENDIF])
178
+ end
179
+ expect(:AT_ENDIF)
180
+ IfStmt.new(branches, else_body)
181
+ end
182
+
183
+ def parse_for_stmt
184
+ expect(:AT_FOR)
185
+ name_tok = expect(:IDENT)
186
+ expect(:IN)
187
+ iterable = parse_expression
188
+ body = parse_statements_until(%i[AT_END])
189
+ expect(:AT_END)
190
+ ForStmt.new(name_tok.value, iterable, body)
191
+ end
192
+
193
+ def parse_begin_catch
194
+ expect(:BEGIN)
195
+ begin_body = parse_statements_until(%i[CATCH])
196
+ expect(:CATCH)
197
+ err_tok = expect(:IDENT)
198
+ catch_body = parse_statements_until(%i[END])
199
+ expect(:END)
200
+ BeginCatchStmt.new(begin_body, err_tok.value, catch_body)
201
+ end
202
+
203
+ # ============ 式(優先順位の低い順) ============
204
+
205
+ def parse_expression
206
+ parse_or
207
+ end
208
+
209
+ def parse_or
210
+ left = parse_and
211
+ while check(:OR)
212
+ advance
213
+ left = LogicalOp.new(:or, left, parse_and)
214
+ end
215
+ left
216
+ end
217
+
218
+ def parse_and
219
+ left = parse_equality
220
+ while check(:AND)
221
+ advance
222
+ left = LogicalOp.new(:and, left, parse_equality)
223
+ end
224
+ left
225
+ end
226
+
227
+ EQUALITY_OPS = { EQ: :==, NEQ: :!= }.freeze
228
+
229
+ def parse_equality
230
+ left = parse_comparison
231
+ while EQUALITY_OPS.key?(current.type)
232
+ op = EQUALITY_OPS[advance.type]
233
+ left = BinaryOp.new(op, left, parse_comparison)
234
+ end
235
+ left
236
+ end
237
+
238
+ COMPARISON_OPS = { LT: :<, LTE: :<=, GT: :>, GTE: :>= }.freeze
239
+
240
+ def parse_comparison
241
+ left = parse_additive
242
+ while COMPARISON_OPS.key?(current.type)
243
+ op = COMPARISON_OPS[advance.type]
244
+ left = BinaryOp.new(op, left, parse_additive)
245
+ end
246
+ left
247
+ end
248
+
249
+ ADDITIVE_OPS = { PLUS: :+, MINUS: :- }.freeze
250
+
251
+ def parse_additive
252
+ left = parse_multiplicative
253
+ while ADDITIVE_OPS.key?(current.type)
254
+ op = ADDITIVE_OPS[advance.type]
255
+ left = BinaryOp.new(op, left, parse_multiplicative)
256
+ end
257
+ left
258
+ end
259
+
260
+ MULT_OPS = { STAR: :*, SLASH: :/, PERCENT: :% }.freeze
261
+
262
+ def parse_multiplicative
263
+ left = parse_unary
264
+ while MULT_OPS.key?(current.type)
265
+ op = MULT_OPS[advance.type]
266
+ left = BinaryOp.new(op, left, parse_unary)
267
+ end
268
+ left
269
+ end
270
+
271
+ def parse_unary
272
+ if check(:MINUS)
273
+ advance
274
+ return UnaryOp.new(:-@, parse_unary)
275
+ end
276
+ if check(:NOT)
277
+ advance
278
+ return UnaryOp.new(:not, parse_unary)
279
+ end
280
+ parse_postfix
281
+ end
282
+
283
+ def parse_postfix
284
+ expr = parse_primary
285
+ loop do
286
+ case current.type
287
+ when :DOT
288
+ advance
289
+ name_tok = expect(:IDENT)
290
+ if check(:LPAREN)
291
+ advance
292
+ args = parse_call_args
293
+ expect(:RPAREN)
294
+ expr = MethodCallExpr.new(expr, name_tok.value, args)
295
+ else
296
+ expr = MemberAccess.new(expr, name_tok.value)
297
+ end
298
+ when :LBRACKET
299
+ advance
300
+ skip_newlines
301
+ idx = parse_expression
302
+ skip_newlines
303
+ expect(:RBRACKET)
304
+ expr = IndexAccess.new(expr, idx)
305
+ else
306
+ break
307
+ end
308
+ end
309
+ expr
310
+ end
311
+
312
+ def parse_primary
313
+ tok = current
314
+ case tok.type
315
+ when :INT
316
+ advance
317
+ IntLiteral.new(tok.value)
318
+ when :FLOAT
319
+ advance
320
+ FloatLiteral.new(tok.value)
321
+ when :STRING
322
+ advance
323
+ StringLiteral.new(build_string_parts(tok.value, tok.line))
324
+ when :TRUE
325
+ advance
326
+ BoolLiteral.new(true)
327
+ when :FALSE
328
+ advance
329
+ BoolLiteral.new(false)
330
+ when :NIL
331
+ advance
332
+ NilLiteral.new(nil)
333
+ when :IDENT
334
+ advance
335
+ if check(:LPAREN)
336
+ advance
337
+ args = parse_call_args
338
+ expect(:RPAREN)
339
+ CallExpr.new(tok.value, args)
340
+ else
341
+ Identifier.new(tok.value)
342
+ end
343
+ when :LPAREN
344
+ advance
345
+ skip_newlines
346
+ expr = parse_expression
347
+ skip_newlines
348
+ expect(:RPAREN)
349
+ expr
350
+ when :LBRACKET
351
+ parse_array_literal
352
+ when :LBRACE
353
+ parse_hash_literal
354
+ else
355
+ raise Gemite::SyntaxError.new("式が必要です ('#{tok.type}' は使用できません)", line: tok.line)
356
+ end
357
+ end
358
+
359
+ def parse_array_literal
360
+ expect(:LBRACKET)
361
+ elements = []
362
+ skip_newlines
363
+ unless check(:RBRACKET)
364
+ loop do
365
+ skip_newlines
366
+ elements << parse_expression
367
+ skip_newlines
368
+ break unless accept(:COMMA)
369
+ end
370
+ end
371
+ skip_newlines
372
+ expect(:RBRACKET)
373
+ ArrayLiteral.new(elements)
374
+ end
375
+
376
+ def parse_hash_literal
377
+ expect(:LBRACE)
378
+ pairs = []
379
+ skip_newlines
380
+ unless check(:RBRACE)
381
+ loop do
382
+ skip_newlines
383
+ key_tok = expect(:IDENT)
384
+ expect(:COLON)
385
+ skip_newlines
386
+ value = parse_expression
387
+ pairs << [key_tok.value.to_sym, value]
388
+ skip_newlines
389
+ break unless accept(:COMMA)
390
+ end
391
+ end
392
+ skip_newlines
393
+ expect(:RBRACE)
394
+ HashLiteral.new(pairs)
395
+ end
396
+
397
+ # 呼び出し引数。位置引数と "key: value" 形式(末尾に暗黙のハッシュとしてまとめる)の
398
+ # 両方に対応する。 db.select("users", where: {age: 15}) のような呼び出しのため。
399
+ def parse_call_args
400
+ positional = []
401
+ keyword_pairs = []
402
+ skip_newlines
403
+ unless check(:RPAREN)
404
+ loop do
405
+ skip_newlines
406
+ if check(:IDENT) && peek.type == :COLON
407
+ key_tok = advance
408
+ advance # COLON
409
+ skip_newlines
410
+ keyword_pairs << [key_tok.value.to_sym, parse_expression]
411
+ else
412
+ positional << parse_expression
413
+ end
414
+ skip_newlines
415
+ break unless accept(:COMMA)
416
+ end
417
+ end
418
+ skip_newlines
419
+ positional << HashLiteral.new(keyword_pairs) unless keyword_pairs.empty?
420
+ positional
421
+ end
422
+
423
+ def build_string_parts(raw_parts, line)
424
+ raw_parts.map do |(kind, val)|
425
+ if kind == :str
426
+ [:str, val]
427
+ else
428
+ [:expr, Parser.parse_expression_from_source(val, line: line)]
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemite
4
+ # 生のHTTPリクエストをパースした結果を保持する。
5
+ class Request
6
+ attr_reader :method, :path, :query_params, :headers, :body, :form_params
7
+
8
+ def initialize(method:, path:, query_params:, headers:, body:)
9
+ @method = method
10
+ @path = path
11
+ @query_params = query_params
12
+ @headers = headers
13
+ @body = body
14
+ @form_params = parse_form_body
15
+ end
16
+
17
+ # application/x-www-form-urlencoded のボディをパースする。
18
+ # multipart/form-data(ファイルアップロード)はv0.1では対象外。
19
+ def parse_form_body
20
+ return {} if body.nil? || body.empty?
21
+
22
+ content_type = headers["content-type"] || ""
23
+ return {} unless content_type.include?("application/x-www-form-urlencoded") || content_type.empty?
24
+
25
+ parse_www_form(body)
26
+ end
27
+
28
+ def self.parse_www_form(str)
29
+ result = {}
30
+ str.split("&").each do |pair|
31
+ next if pair.empty?
32
+
33
+ k, v = pair.split("=", 2)
34
+ result[uri_decode(k)] = uri_decode(v || "")
35
+ end
36
+ result
37
+ end
38
+
39
+ def self.uri_decode(str)
40
+ str.to_s.tr("+", " ").gsub(/%([0-9A-Fa-f]{2})/) { [Regexp.last_match(1)].pack("H2").force_encoding("BINARY") }
41
+ .force_encoding("BINARY").force_encoding("UTF-8")
42
+ end
43
+
44
+ def parse_www_form(str)
45
+ self.class.parse_www_form(str)
46
+ end
47
+ private :parse_www_form
48
+ end
49
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemite
4
+ # リクエストパスから .gmt ファイル、または public/ 配下の静的ファイルを解決する。
5
+ #
6
+ # 仕様4: ファイルはURLへ直接マッピングされる(index.gmt -> /, login.gmt -> /login 等)。
7
+ # ディレクトリ構成は自由なので、サーバ起動時のカレントディレクトリを
8
+ # プロジェクトルートとして扱い、リクエストパスをそのまま相対パスとして解決する。
9
+ class Router
10
+ EXTENSION = ".gmt"
11
+ STATIC_DIR = "public"
12
+
13
+ MIME_TYPES = {
14
+ ".html" => "text/html; charset=utf-8",
15
+ ".htm" => "text/html; charset=utf-8",
16
+ ".css" => "text/css; charset=utf-8",
17
+ ".js" => "text/javascript; charset=utf-8",
18
+ ".mjs" => "text/javascript; charset=utf-8",
19
+ ".json" => "application/json; charset=utf-8",
20
+ ".png" => "image/png",
21
+ ".jpg" => "image/jpeg",
22
+ ".jpeg" => "image/jpeg",
23
+ ".gif" => "image/gif",
24
+ ".svg" => "image/svg+xml",
25
+ ".ico" => "image/x-icon",
26
+ ".webp" => "image/webp",
27
+ ".woff" => "font/woff",
28
+ ".woff2" => "font/woff2",
29
+ ".txt" => "text/plain; charset=utf-8",
30
+ ".xml" => "application/xml; charset=utf-8",
31
+ ".pdf" => "application/pdf"
32
+ }.freeze
33
+
34
+ def initialize(root)
35
+ @root = File.expand_path(root)
36
+ end
37
+
38
+ Result = Struct.new(:type, :path) # type: :gemite | :static | :not_found
39
+
40
+ def resolve(request_path)
41
+ clean = normalize(request_path)
42
+ return Result.new(:not_found, nil) unless clean
43
+
44
+ gemite_path = find_gemite_file(clean)
45
+ return Result.new(:gemite, gemite_path) if gemite_path
46
+
47
+ static_path = find_static_file(clean)
48
+ return Result.new(:static, static_path) if static_path
49
+
50
+ Result.new(:not_found, nil)
51
+ end
52
+
53
+ def mime_type_for(path)
54
+ MIME_TYPES[File.extname(path).downcase] || "application/octet-stream"
55
+ end
56
+
57
+ private
58
+
59
+ # ".." 等によるルート外アクセスを防ぐための正規化。
60
+ def normalize(request_path)
61
+ path = request_path.split("?").first.to_s
62
+ path = "/" if path.empty?
63
+ segments = path.split("/").reject { |s| s.empty? || s == "." }
64
+ return nil if segments.include?("..")
65
+
66
+ "/" + segments.join("/")
67
+ end
68
+
69
+ def find_gemite_file(clean_path)
70
+ rel = clean_path == "/" ? "index" : clean_path.sub(%r{\A/}, "")
71
+ direct = safe_join("#{rel}#{EXTENSION}")
72
+ return direct if direct && File.file?(direct)
73
+
74
+ index = safe_join(File.join(rel, "index#{EXTENSION}"))
75
+ return index if index && File.file?(index)
76
+
77
+ nil
78
+ end
79
+
80
+ def find_static_file(clean_path)
81
+ rel = clean_path.sub(%r{\A/}, "")
82
+ return nil if rel.empty?
83
+
84
+ candidate = safe_join(File.join(STATIC_DIR, rel))
85
+ return candidate if candidate && File.file?(candidate)
86
+
87
+ nil
88
+ end
89
+
90
+ # @rootの外に出ないことを保証した上でパスを結合する。範囲外なら nil を返す。
91
+ def safe_join(rel)
92
+ full = File.expand_path(File.join(@root, rel))
93
+ return nil unless full == @root || full.start_with?(@root + File::SEPARATOR)
94
+
95
+ full
96
+ end
97
+ end
98
+ end