Class: Idl::Compiler

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/idlc.rb,
lib/idlc/version.rb

Overview

the Idl compiler

Constant Summary collapse

@@parse_cache =

Class-level parse cache: absolute file path (String) → IsaSyntaxNode. Shared across all Compiler instances so each file is parsed only once per process. Safe to share because IsaSyntaxNode#to_ast is non-destructive and returns a fresh, independent IsaAst on every call. Mutex guards writes; under MRI, reads without the lock are safe.

{}
@@parse_cache_mutex =
Mutex.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCompiler

Returns a new instance of Compiler.



66
67
68
# File 'lib/idlc.rb', line 66

def initialize
  @parser = ::IdlParser.new
end

Instance Attribute Details

#parserObject (readonly)

Returns the value of attribute parser.



56
57
58
# File 'lib/idlc.rb', line 56

def parser
  @parser
end

Class Method Details

.versionObject



8
# File 'lib/idlc/version.rb', line 8

def self.version = "0.1.5"

Instance Method Details

#compile_constraint(body, symtab, pass_error: false, input_file: "[CONSTRAINT]", input_line: 0, starting_offset: 0, line_file_offsets: nil) ⇒ Object



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/idlc.rb', line 413

def compile_constraint(body, symtab, pass_error: false,
                       input_file: "[CONSTRAINT]", input_line: 0,
                       starting_offset: 0, line_file_offsets: nil)
  m = @parser.parse(body, root: :constraint_body)
  if m.nil?
    raise SyntaxError, <<~MSG
      While parsing #{body}:#{@parser.failure_line}:#{@parser.failure_column}

      #{@parser.failure_reason}
    MSG
  end

  # fix up left recursion
  ast = m.to_ast
  ast.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
  ast.freeze_tree(symtab)

  ast
end

#compile_expression(expression, symtab, pass_error: false) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/idlc.rb', line 359

def compile_expression(expression, symtab, pass_error: false)
  m = @parser.parse(expression, root: :expression)
  if m.nil?
    raise SyntaxError, <<~MSG
      While parsing #{expression}:#{@parser.failure_line}:#{@parser.failure_column}

      #{@parser.failure_reason}
    MSG
  end

  ast = m.to_ast
  ast.set_input_file("[EXPRESSION]", 0)
  value_result = ast.value_try do
    ast.freeze_tree(symtab)
  end
  if value_result == :unknown_value
    raise AstNode::TypeError, "Bad literal value" if pass_error

    warn "Compiling #{expression}"
    warn "Bad literal value"
    exit 1
  end
  begin
    ast.type_check(symtab, strict: false)
  rescue AstNode::TypeError => e
    raise e if pass_error

    warn "Compiling #{expression}"
    warn e.what
    warn T.must(e.backtrace).join("\n")
    exit 1
  rescue AstNode::InternalError => e
    raise e if pass_error

    warn "Compiling #{expression}"
    warn e.what
    warn T.must(e.backtrace).join("\n")
    exit 1
  end

  ast
end

#compile_file(path, source_mapper = nil) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/idlc.rb', line 81

def compile_file(path, source_mapper = nil)
  path_key = path.realpath.to_s

  m = T.let(@@parse_cache[path_key], T.nilable(IsaSyntaxNode))

  if m.nil?
    @@parse_cache_mutex.synchronize do
      # Re-check inside the lock in case another thread just populated the entry.
      unless @@parse_cache.key?(path_key)
        @parser.set_input_file(path_key)

        content = path.read
        source_mapper[path_key] = content unless source_mapper.nil?

        old_format = @pb.format unless @pb.nil?
        @pb.format = "Parsing #{File.basename(path)} [:bar]" unless @pb.nil?
        pid = unless @pb.nil?
                fork {
                  loop do
                    sleep 1
                    @pb.advance unless @pb.nil?
                  end
                }
              end
        m = @parser.parse(content)
        unless @pb.nil?
          Process.kill("TERM", T.must(pid))
          Process.wait(T.must(pid))
          @pb.format = old_format
        end

        if m.nil?
          raise SyntaxError, <<~MSG
            While parsing #{@parser.input_file}:#{@parser.failure_line}:#{@parser.failure_column}

            #{@parser.failure_reason}
          MSG
        end

        raise "unexpected type #{m.class.name}" unless m.is_a?(IsaSyntaxNode)

        @@parse_cache[path_key] = m
      end
      m = @@parse_cache[path_key]
    end
  else
    # Cache hit: still populate source_mapper if provided (test-only path).
    source_mapper[path_key] = path.read unless source_mapper.nil?
  end

  ast = T.must(m).to_ast

  ast.children.each do |child|
    next unless child.is_a?(IncludeStatementAst)

    if child.filename.empty?
      raise SyntaxError, <<~MSG
        While parsing #{path}:#{child.lineno}:

        Empty include statement
      MSG
    end

    include_path =
      if child.filename[0] == "/"
        Pathname.new(child.filename)
      else
        (path.dirname / child.filename)
      end

    unless include_path.exist?
      raise SyntaxError, <<~MSG
        While parsing #{path}:#{child.lineno}:

        Path #{include_path} does not exist
      MSG
    end
    unless include_path.readable?
      raise SyntaxError, <<~MSG
        While parsing #{path}:#{child.lineno}:

        Path #{include_path} cannot be read
      MSG
    end

    include_ast = compile_file(include_path)
    include_ast.set_input_file_unless_already_set(include_path)
    ast.replace_include!(child, include_ast)
  end

  # we may have already set an input file from an include, so only set it if it's not already set
  ast.set_input_file_unless_already_set(path.to_s)

  ast
end

#compile_for_loop(loop, symtab, pass_error: false) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/idlc.rb', line 178

def compile_for_loop(loop, symtab, pass_error: false)
  m = @parser.parse(loop, root: :for_loop)
  if m.nil?
    raise SyntaxError, <<~MSG
      While parsing #{loop}:#{@parser.failure_line}:#{@parser.failure_column}

      #{@parser.failure_reason}
    MSG
  end

  ast = m.to_ast
  ast.set_input_file("[LOOP]", 0)
  value_result = ast.value_try do
    ast.freeze_tree(symtab)
  end
  if value_result == :unknown_value
    raise AstNode::TypeError, "Bad literal value" if pass_error

    warn "Compiling #{loop}"
    warn "Bad literal value"
    exit 1
  end
  begin
    ast.type_check(symtab, strict: false)
  rescue AstNode::TypeError => e
    raise e if pass_error

    warn "Compiling #{loop}"
    warn e.what
    warn T.must(e.backtrace).join("\n")
    exit 1
  rescue AstNode::InternalError => e
    raise e if pass_error

    warn "Compiling #{loop}"
    warn e.what
    warn T.must(e.backtrace).join("\n")
    exit 1
  end

  ast
end

#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) ⇒ Ast

compile a function body, and return the abstract syntax tree

Parameters:

  • body (String)

    Function body source code

  • return_type (Type) (defaults to: nil)

    Expected return type, if known

  • symtab (SymbolTable) (defaults to: nil)

    Symbol table to use for type checking

  • name (String) (defaults to: nil)

    Function name, used for error messages

  • input_file (Pathname) (defaults to: nil)

    Path to the input file this source comes from

  • input_line (Integer) (defaults to: 0)

    Starting line in the input file that this source comes from

  • no_rescue (Boolean) (defaults to: false)

    Whether or not to automatically catch any errors

Returns:

  • (Ast)

    The root of the abstract syntax tree



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/idlc.rb', line 231

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)
  @parser.set_input_file(input_file, input_line, starting_offset, line_file_offsets)

  m = @parser.parse(body, root: :function_body)
  if m.nil?
    unless input_file.nil? || input_line.nil?
      raise SyntaxError, <<~MSG
        While parsing #{name} at #{input_file}:#{input_line + @parser.failure_line}

        #{@parser.failure_reason}
      MSG
    else
      raise SyntaxError, <<~MSG
        While parsing #{name}

        #{@parser.failure_reason}
      MSG
    end
  end

  # fix up left recursion
  ast = m.to_ast
  ast.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
  ast.freeze_tree(symtab)

  # type check
  unless type_check == false
    cloned_symtab = symtab.deep_clone

    cloned_symtab.push(ast)
    cloned_symtab.add("__expected_return_type", return_type) unless return_type.nil?

    extra_syms.each { |k, v|
      cloned_symtab.add(k, v)
    }

    begin
      ast.statements.each do |s|
        s.type_check(cloned_symtab, strict: false)
      end
    rescue AstNode::TypeError => e
      raise e if no_rescue

      warn "In function #{name}:"
      warn e.what
      exit 1
    rescue AstNode::InternalError => e
      raise if no_rescue

      warn "In function #{name}:"
      warn e.what
      warn T.must(e.backtrace).join("\n")
      exit 1
    ensure
      cloned_symtab.pop
    end

  end

  ast
end

#compile_inst_operation(inst, symtab:, input_file: nil, input_line: 0, starting_offset: 0, line_file_offsets: nil) ⇒ Ast

compile an instruction operation, and return the abstract syntax tree

Parameters:

  • inst (Instruction)

    Instruction object

  • symtab (SymbolTable)

    Symbol table

  • input_file (Pathname) (defaults to: nil)

    Path to the input file this source comes from

  • input_line (Integer) (defaults to: 0)

    Starting line in the input file that this source comes from

Returns:

  • (Ast)

    The root of the abstract syntax tree



320
321
322
323
# File 'lib/idlc.rb', line 320

def compile_inst_operation(inst, symtab:, input_file: nil, input_line: 0, starting_offset: 0, line_file_offsets: nil)
  operation = inst.data["operation()"]
  compile_inst_scope(operation, symtab:, input_file:, input_line:, starting_offset:, line_file_offsets:)
end

#compile_inst_scope(idl, symtab:, input_file:, input_line: 0, starting_offset: 0, line_file_offsets: nil) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/idlc.rb', line 293

def compile_inst_scope(idl, symtab:, input_file:, input_line: 0, starting_offset: 0, line_file_offsets: nil)
  @parser.set_input_file(input_file, input_line, starting_offset, line_file_offsets)

  m = @parser.parse(idl, root: :instruction_operation)
  if m.nil?
    raise SyntaxError, <<~MSG
      While parsing #{input_file}:#{input_line + @parser.failure_line}

      #{@parser.failure_reason}
    MSG
  end

  # fix up left recursion
  ast = m.to_ast
  ast.set_input_file(input_file, input_line, starting_offset, line_file_offsets)
  ast.freeze_tree(symtab)

  ast
end

#pb=(pb) ⇒ Object

set a progressbar



71
72
73
# File 'lib/idlc.rb', line 71

def pb=(pb)
  @pb = pb
end

#type_check(ast, symtab, what) ⇒ Object

Type check an abstract syntax tree

Parameters:

  • ast (AstNode)

    An abstract syntax tree

  • symtab (SymbolTable)

    The compilation context

  • what (String)

    A description of what you are type checking (for error messages)

Raises:

  • AstNode::TypeError if a type error is found



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/idlc.rb', line 331

def type_check(ast, symtab, what)
  # type check
  raise "Tree should be frozen" unless ast.frozen?

  begin
    value_result = AstNode.value_try do
      ast.type_check(symtab, strict: false)
    end
    AstNode.value_else(value_result) do
      warn "While type checking #{what}, got a value error on:"
      warn ast.text_value
      warn AstNode.value_error_reason
      warn symtab.callstack
      unless AstNode.value_error_ast.nil?
        warn "At #{AstNode.value_error_ast.input_file}:#{AstNode.value_error_ast.lineno}"
      end
      exit 1
    end
  rescue AstNode::InternalError => e
    warn "While type checking #{what}:"
    warn e.what
    warn T.must(e.backtrace).join("\n")
    exit 1
  end

  ast
end

#unset_pbObject

unset a progressbar



76
77
78
79
# File 'lib/idlc.rb', line 76

def unset_pb
  @pb.finish unless @pb.nil?
  @pb = nil
end