idlc 0.1.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.
@@ -0,0 +1,10 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # frozen_string_literal: true
5
+
6
+ module Idl
7
+ class Compiler
8
+ def self.version = "0.1.1"
9
+ end
10
+ end
data/lib/idlc.rb ADDED
@@ -0,0 +1,409 @@
1
+ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2
+ # SPDX-License-Identifier: BSD-3-Clause-Clear
3
+
4
+ # typed: true
5
+ # frozen_string_literal: true
6
+
7
+ require "pathname"
8
+ require "sorbet-runtime"
9
+ require "treetop"
10
+
11
+ require_relative "idlc/syntax_node"
12
+
13
+ class IdlParser < Treetop::Runtime::CompiledParser
14
+ attr_reader :input_file
15
+
16
+ def set_input_file(filename, starting_line = 0, starting_offset = 0, line_file_offsets = nil)
17
+ @input_file = filename
18
+ @starting_line = starting_line
19
+ @starting_offset = starting_offset
20
+ @line_file_offsets = line_file_offsets
21
+ end
22
+
23
+ # alias instantiate_node so we can call it from the override
24
+ alias idlc_instantiate_node instantiate_node
25
+
26
+ # override instatiate_node so we can set the input file
27
+ def instantiate_node(node_type, *args)
28
+ node = T.unsafe(self).idlc_instantiate_node(node_type, *args)
29
+ node.set_input_file(input_file, @starting_line.nil? ? 0 : @starting_line, @starting_offset.nil? ? 0 : @starting_offset, @line_file_offsets)
30
+ node
31
+ end
32
+ end
33
+
34
+ # rebuild the idl parser if the grammar has changed
35
+ tt_path = Pathname.new(__dir__) / "idlc" / "idl.treetop"
36
+ rb_path = Pathname.new(__dir__) / "idlc" / "idl_parser.rb"
37
+ needs_grammar_recompile = !rb_path.exist? || (rb_path.mtime < tt_path.mtime)
38
+ if needs_grammar_recompile
39
+ tt_compiler = Treetop::Compiler::GrammarCompiler.new
40
+ tt_compiler.compile(tt_path, rb_path)
41
+
42
+ # make sure there is a single newline at the end
43
+ compiler_src = File.read rb_path
44
+ File.write rb_path, "#{compiler_src.strip}\n"
45
+ end
46
+
47
+ require_relative "idlc/ast"
48
+ require_relative "idlc/symbol_table"
49
+ require_relative "idlc/idl_parser"
50
+
51
+ module Idl
52
+ # the Idl compiler
53
+ class Compiler
54
+ extend T::Sig
55
+
56
+ attr_reader :parser
57
+
58
+ def initialize
59
+ @parser = ::IdlParser.new
60
+ end
61
+
62
+ # set a progressbar
63
+ def pb=(pb)
64
+ @pb = pb
65
+ end
66
+
67
+ # unset a progressbar
68
+ def unset_pb
69
+ @pb.finish unless @pb.nil?
70
+ @pb = nil
71
+ end
72
+
73
+ def compile_file(path, source_mapper = nil)
74
+ @parser.set_input_file(path.to_s)
75
+
76
+ unless source_mapper.nil?
77
+ source_mapper[path.to_s] = path.read
78
+ end
79
+
80
+ old_format = @pb.format unless @pb.nil?
81
+ @pb.format = "Parsing #{File.basename(path)} [:bar]" unless @pb.nil?
82
+ pid = unless @pb.nil?
83
+ fork {
84
+ loop do
85
+ sleep 1
86
+ @pb.advance unless @pb.nil?
87
+ end
88
+ }
89
+ end
90
+ m = @parser.parse path.read
91
+ unless @pb.nil?
92
+ Process.kill("TERM", T.must(pid))
93
+ Process.wait(T.must(pid))
94
+ @pb.format = old_format
95
+ end
96
+
97
+ if m.nil?
98
+ raise SyntaxError, <<~MSG
99
+ While parsing #{@parser.input_file}:#{@parser.failure_line}:#{@parser.failure_column}
100
+
101
+ #{@parser.failure_reason}
102
+ MSG
103
+ end
104
+
105
+ raise "unexpected type #{m.class.name}" unless m.is_a?(IsaSyntaxNode)
106
+
107
+ ast = m.to_ast
108
+
109
+ ast.children.each do |child|
110
+ next unless child.is_a?(IncludeStatementAst)
111
+
112
+ if child.filename.empty?
113
+ raise SyntaxError, <<~MSG
114
+ While parsing #{path}:#{child.lineno}:
115
+
116
+ Empty include statement
117
+ MSG
118
+ end
119
+
120
+ include_path =
121
+ if child.filename[0] == "/"
122
+ Pathname.new(child.filename)
123
+ else
124
+ (path.dirname / child.filename)
125
+ end
126
+
127
+ unless include_path.exist?
128
+ raise SyntaxError, <<~MSG
129
+ While parsing #{path}:#{child.lineno}:
130
+
131
+ Path #{include_path} does not exist
132
+ MSG
133
+ end
134
+ unless include_path.readable?
135
+ raise SyntaxError, <<~MSG
136
+ While parsing #{path}:#{child.lineno}:
137
+
138
+ Path #{include_path} cannot be read
139
+ MSG
140
+ end
141
+
142
+ include_ast = compile_file(include_path)
143
+ include_ast.set_input_file_unless_already_set(include_path)
144
+ ast.replace_include!(child, include_ast)
145
+ end
146
+
147
+ # we may have already set an input file from an include, so only set it if it's not already set
148
+ ast.set_input_file_unless_already_set(path.to_s)
149
+
150
+ ast
151
+ end
152
+
153
+ sig { params(loop: String, symtab: SymbolTable, pass_error: T::Boolean).returns(ForLoopAst) }
154
+ def compile_for_loop(loop, symtab, pass_error: false)
155
+ m = @parser.parse(loop, root: :for_loop)
156
+ if m.nil?
157
+ raise SyntaxError, <<~MSG
158
+ While parsing #{loop}:#{@parser.failure_line}:#{@parser.failure_column}
159
+
160
+ #{@parser.failure_reason}
161
+ MSG
162
+ end
163
+
164
+ ast = m.to_ast
165
+ ast.set_input_file("[LOOP]", 0)
166
+ value_result = ast.value_try do
167
+ ast.freeze_tree(symtab)
168
+ end
169
+ if value_result == :unknown_value
170
+ raise AstNode::TypeError, "Bad literal value" if pass_error
171
+
172
+ warn "Compiling #{loop}"
173
+ warn "Bad literal value"
174
+ exit 1
175
+ end
176
+ begin
177
+ ast.type_check(symtab, strict: false)
178
+ rescue AstNode::TypeError => e
179
+ raise e if pass_error
180
+
181
+ warn "Compiling #{loop}"
182
+ warn e.what
183
+ warn T.must(e.backtrace).join("\n")
184
+ exit 1
185
+ rescue AstNode::InternalError => e
186
+ raise e if pass_error
187
+
188
+ warn "Compiling #{loop}"
189
+ warn e.what
190
+ warn T.must(e.backtrace).join("\n")
191
+ exit 1
192
+ end
193
+
194
+ ast
195
+ end
196
+
197
+ # compile a function body, and return the abstract syntax tree
198
+ #
199
+ # @param body [String] Function body source code
200
+ # @param return_type [Type] Expected return type, if known
201
+ # @param symtab [SymbolTable] Symbol table to use for type checking
202
+ # @param name [String] Function name, used for error messages
203
+ # @param input_file [Pathname] Path to the input file this source comes from
204
+ # @param input_line [Integer] Starting line in the input file that this source comes from
205
+ # @param no_rescue [Boolean] Whether or not to automatically catch any errors
206
+ # @return [Ast] The root of the abstract syntax tree
207
+ def compile_func_body(body, return_type: nil, symtab: nil, name: nil, input_file: nil, input_line: 0, starting_offset: 0, line_file_offsets: nil, no_rescue: false, extra_syms: {}, type_check: true)
208
+ @parser.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
209
+
210
+ m = @parser.parse(body, root: :function_body)
211
+ if m.nil?
212
+ unless input_file.nil? || input_line.nil?
213
+ raise SyntaxError, <<~MSG
214
+ While parsing #{name} at #{input_file}:#{input_line + @parser.failure_line}
215
+
216
+ #{@parser.failure_reason}
217
+ MSG
218
+ else
219
+ raise SyntaxError, <<~MSG
220
+ While parsing #{name}
221
+
222
+ #{@parser.failure_reason}
223
+ MSG
224
+ end
225
+ end
226
+
227
+ # fix up left recursion
228
+ ast = m.to_ast
229
+ ast.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
230
+ ast.freeze_tree(symtab)
231
+
232
+ # type check
233
+ unless type_check == false
234
+ cloned_symtab = symtab.deep_clone
235
+
236
+ cloned_symtab.push(ast)
237
+ cloned_symtab.add("__expected_return_type", return_type) unless return_type.nil?
238
+
239
+ extra_syms.each { |k, v|
240
+ cloned_symtab.add(k, v)
241
+ }
242
+
243
+ begin
244
+ ast.statements.each do |s|
245
+ s.type_check(cloned_symtab, strict: false)
246
+ end
247
+ rescue AstNode::TypeError => e
248
+ raise e if no_rescue
249
+
250
+ warn "In function #{name}:"
251
+ warn e.what
252
+ exit 1
253
+ rescue AstNode::InternalError => e
254
+ raise if no_rescue
255
+
256
+ warn "In function #{name}:"
257
+ warn e.what
258
+ warn T.must(e.backtrace).join("\n")
259
+ exit 1
260
+ ensure
261
+ cloned_symtab.pop
262
+ end
263
+
264
+ end
265
+
266
+ ast
267
+ end
268
+
269
+ def compile_inst_scope(idl, symtab:, input_file:, input_line: 0, starting_offset: 0, line_file_offsets: nil)
270
+ @parser.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
271
+
272
+ m = @parser.parse(idl, root: :instruction_operation)
273
+ if m.nil?
274
+ raise SyntaxError, <<~MSG
275
+ While parsing #{input_file}:#{input_line + @parser.failure_line}
276
+
277
+ #{@parser.failure_reason}
278
+ MSG
279
+ end
280
+
281
+ # fix up left recursion
282
+ ast = m.to_ast
283
+ ast.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
284
+ ast.freeze_tree(symtab)
285
+
286
+ ast
287
+ end
288
+
289
+ # compile an instruction operation, and return the abstract syntax tree
290
+ #
291
+ # @param inst [Instruction] Instruction object
292
+ # @param symtab [SymbolTable] Symbol table
293
+ # @param input_file [Pathname] Path to the input file this source comes from
294
+ # @param input_line [Integer] Starting line in the input file that this source comes from
295
+ # @return [Ast] The root of the abstract syntax tree
296
+ def compile_inst_operation(inst, symtab:, input_file: nil, input_line: 0, starting_offset: 0, line_file_offsets: nil)
297
+ operation = inst.data["operation()"]
298
+ compile_inst_scope(operation, symtab:, input_file:, input_line:, starting_offset:, line_file_offsets:)
299
+ end
300
+
301
+ # Type check an abstract syntax tree
302
+ #
303
+ # @param ast [AstNode] An abstract syntax tree
304
+ # @param symtab [SymbolTable] The compilation context
305
+ # @param what [String] A description of what you are type checking (for error messages)
306
+ # @raise AstNode::TypeError if a type error is found
307
+ def type_check(ast, symtab, what)
308
+ # type check
309
+ raise "Tree should be frozen" unless ast.frozen?
310
+
311
+ begin
312
+ value_result = AstNode.value_try do
313
+ ast.type_check(symtab, strict: false)
314
+ end
315
+ AstNode.value_else(value_result) do
316
+ warn "While type checking #{what}, got a value error on:"
317
+ warn ast.text_value
318
+ warn AstNode.value_error_reason
319
+ warn symtab.callstack
320
+ unless AstNode.value_error_ast.nil?
321
+ warn "At #{AstNode.value_error_ast.input_file}:#{AstNode.value_error_ast.lineno}"
322
+ end
323
+ exit 1
324
+ end
325
+ rescue AstNode::InternalError => e
326
+ warn "While type checking #{what}:"
327
+ warn e.what
328
+ warn T.must(e.backtrace).join("\n")
329
+ exit 1
330
+ end
331
+
332
+ ast
333
+ end
334
+
335
+ def compile_expression(expression, symtab, pass_error: false)
336
+ m = @parser.parse(expression, root: :expression)
337
+ if m.nil?
338
+ raise SyntaxError, <<~MSG
339
+ While parsing #{expression}:#{@parser.failure_line}:#{@parser.failure_column}
340
+
341
+ #{@parser.failure_reason}
342
+ MSG
343
+ end
344
+
345
+ ast = m.to_ast
346
+ ast.set_input_file("[EXPRESSION]", 0)
347
+ value_result = ast.value_try do
348
+ ast.freeze_tree(symtab)
349
+ end
350
+ if value_result == :unknown_value
351
+ raise AstNode::TypeError, "Bad literal value" if pass_error
352
+
353
+ warn "Compiling #{expression}"
354
+ warn "Bad literal value"
355
+ exit 1
356
+ end
357
+ begin
358
+ ast.type_check(symtab, strict: false)
359
+ rescue AstNode::TypeError => e
360
+ raise e if pass_error
361
+
362
+ warn "Compiling #{expression}"
363
+ warn e.what
364
+ warn T.must(e.backtrace).join("\n")
365
+ exit 1
366
+ rescue AstNode::InternalError => e
367
+ raise e if pass_error
368
+
369
+ warn "Compiling #{expression}"
370
+ warn e.what
371
+ warn T.must(e.backtrace).join("\n")
372
+ exit 1
373
+ end
374
+
375
+ ast
376
+ end
377
+
378
+ sig {
379
+ params(
380
+ body: String,
381
+ symtab: SymbolTable,
382
+ pass_error: T::Boolean,
383
+ input_file: T.nilable(T.any(String, Pathname)),
384
+ input_line: Integer,
385
+ starting_offset: Integer,
386
+ line_file_offsets: T.nilable(T::Array[Integer])
387
+ ).returns(ConstraintBodyAst)
388
+ }
389
+ def compile_constraint(body, symtab, pass_error: false,
390
+ input_file: "[CONSTRAINT]", input_line: 0,
391
+ starting_offset: 0, line_file_offsets: nil)
392
+ m = @parser.parse(body, root: :constraint_body)
393
+ if m.nil?
394
+ raise SyntaxError, <<~MSG
395
+ While parsing #{body}:#{@parser.failure_line}:#{@parser.failure_column}
396
+
397
+ #{@parser.failure_reason}
398
+ MSG
399
+ end
400
+
401
+ # fix up left recursion
402
+ ast = m.to_ast
403
+ ast.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
404
+ ast.freeze_tree(symtab)
405
+
406
+ ast
407
+ end
408
+ end
409
+ end