idlc 0.1.2 → 0.1.5

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.
@@ -9,7 +9,7 @@ module Idl
9
9
 
10
10
  class AstNode
11
11
  def find_src_registers(symtab)
12
- # if is_a?(Executable)
12
+ # if executable?
13
13
  # value_result = value_try do
14
14
  # execute(symtab)
15
15
  # end
@@ -17,7 +17,7 @@ module Idl
17
17
  # execute_unknown(symtab)
18
18
  # end
19
19
  # end
20
- add_symbol(symtab) if is_a?(Declaration)
20
+ add_symbol(symtab) if declaration?
21
21
 
22
22
  srcs = []
23
23
  @children.each do |child|
@@ -27,7 +27,7 @@ module Idl
27
27
  end
28
28
 
29
29
  def find_dst_registers(symtab)
30
- # if is_a?(Executable)
30
+ # if executable?
31
31
  # value_result = value_try do
32
32
  # execute(symtab)
33
33
  # end
@@ -35,7 +35,7 @@ module Idl
35
35
  # execute_unknown(symtab)
36
36
  # end
37
37
  # end
38
- add_symbol(symtab) if is_a?(Declaration)
38
+ add_symbol(symtab) if declaration?
39
39
 
40
40
  srcs = []
41
41
  @children.each do |child|
@@ -80,16 +80,20 @@ module Idl
80
80
  class AryElementAccessAst
81
81
  def find_src_registers(symtab)
82
82
  value_result = value_try do
83
- if var.text_value == "X"
84
- return [index.value(symtab)]
83
+ var_type = var.type(symtab) rescue nil
84
+ if var_type&.kind == :array && var_type.sub_type.is_a?(RegFileElementType) && var_type.qualifiers.include?(:global)
85
+ rf_name = var_type.sub_type.name
86
+ return [[rf_name, index.value(symtab)]]
85
87
  else
86
88
  return []
87
89
  end
88
90
  end
89
91
  value_else(value_result) do
90
- if var.text_value == "X"
92
+ var_type = var.type(symtab) rescue nil
93
+ if var_type&.kind == :array && var_type.sub_type.is_a?(RegFileElementType) && var_type.qualifiers.include?(:global)
94
+ rf_name = var_type.sub_type.name
91
95
  if index.type(symtab).const?
92
- return [index.gen_cpp(symtab, 0)]
96
+ return [[rf_name, index.gen_cpp(symtab, 0)]]
93
97
  else
94
98
  raise ComplexRegDetermination
95
99
  end
@@ -102,22 +106,58 @@ module Idl
102
106
 
103
107
  class AryElementAssignmentAst
104
108
  def find_dst_registers(symtab)
105
- value_result = value_try do
106
- if lhs.text_value == "X"
107
- return [idx.value(symtab)]
109
+ # Identify the base variable and the register index based on assignment shape.
110
+ # F[rd] = v → lhs is IdAst(F), reg_idx = idx
111
+ # F[rd][b] = v → lhs is AryElementAccessAst(F[rd]), reg_idx = lhs.index
112
+ lhs_base, reg_idx =
113
+ if lhs.is_a?(Idl::IdAst)
114
+ [lhs, idx]
115
+ elsif lhs.is_a?(Idl::AryElementAccessAst) && lhs.var.is_a?(Idl::IdAst)
116
+ [lhs.var, lhs.index]
108
117
  else
109
118
  return []
110
119
  end
120
+
121
+ # Only proceed if the base variable is a global array of RegFileElementType.
122
+ var_type = lhs_base.type(symtab) rescue nil
123
+ return [] unless var_type&.kind == :array &&
124
+ var_type.sub_type.is_a?(RegFileElementType) &&
125
+ var_type.qualifiers.include?(:global)
126
+
127
+ rf_name = var_type.sub_type.name
128
+
129
+ value_result = value_try do
130
+ return [[rf_name, reg_idx.value(symtab)]]
111
131
  end
112
132
  value_else(value_result) do
113
- if lhs.text_value == "X"
114
- if idx.type(symtab).const?
115
- return [idx.gen_cpp(symtab, 0)]
116
- else
117
- raise ComplexRegDetermination
118
- end
133
+ if reg_idx.type(symtab).const?
134
+ return [[rf_name, reg_idx.gen_cpp(symtab, 0)]]
119
135
  else
120
- return []
136
+ raise ComplexRegDetermination
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ class AryRangeAssignmentAst
143
+ def find_dst_registers(symtab)
144
+ return [] unless variable.is_a?(Idl::AryElementAccessAst)
145
+
146
+ var_type = variable.var.type(symtab) rescue nil
147
+ return [] unless var_type&.kind == :array &&
148
+ var_type.sub_type.is_a?(RegFileElementType) &&
149
+ var_type.qualifiers.include?(:global)
150
+
151
+ rf_name = var_type.sub_type.name
152
+
153
+ value_result = value_try do
154
+ return [[rf_name, variable.index.value(symtab)]]
155
+ end
156
+ value_else(value_result) do
157
+ if variable.index.type(symtab).const?
158
+ return [[rf_name, variable.index.gen_cpp(symtab, 0)]]
159
+ else
160
+ raise ComplexRegDetermination
121
161
  end
122
162
  end
123
163
  end
@@ -97,23 +97,27 @@ end
97
97
  module Idl
98
98
  # set up a default
99
99
  class AstNode
100
+ def always_terminates? = false
101
+
100
102
  # forced_type, when not nil, is the type that the pruned result must be
101
103
  # if is used when pruning expressions to ensure that the prune doesn't change
102
104
  # bit width just because a value is known and would fit in something smaller
103
105
  def prune(symtab, forced_type: nil)
104
106
  new_children = children.map { |child| child.prune(symtab, forced_type:) }
105
107
 
106
- new_node = dup
107
- new_node.instance_variable_set(:@children, new_children)
108
-
109
- if is_a?(Executable)
108
+ if executable?
110
109
  value_try do
111
110
  execute(symtab)
112
111
  end
113
112
  # value_else: execute raised ValueError; symtab state is already correct
114
113
  end
115
- add_symbol(symtab) if is_a?(Declaration)
114
+ add_symbol(symtab) if declaration?
116
115
 
116
+ # avoid allocation when nothing changed
117
+ return self if !frozen? && new_children.each_with_index.all? { |c, i| c.equal?(children[i]) }
118
+
119
+ new_node = dup
120
+ new_node.instance_variable_set(:@children, new_children)
117
121
  new_node
118
122
  end
119
123
 
@@ -155,7 +159,9 @@ module Idl
155
159
  # array var itself is unknown; nothing more to do
156
160
  end
157
161
  when :bits
158
- var = symtab.get(lhs.text_value)
162
+ root = lhs
163
+ root = root.var while root.is_a?(AryElementAccessAst) || root.is_a?(AryRangeAccessAst)
164
+ var = symtab.get(root.name)
159
165
  var.value = nil unless var.nil?
160
166
  end
161
167
  end
@@ -163,7 +169,9 @@ module Idl
163
169
  class AryRangeAssignmentAst < AstNode
164
170
  def nullify_assignments(symtab)
165
171
  return if variable.type(symtab).global?
166
- var = symtab.get(variable.name)
172
+ root = variable
173
+ root = root.var while root.is_a?(AryElementAccessAst) || root.is_a?(AryRangeAccessAst)
174
+ var = symtab.get(root.name)
167
175
  var.value = nil unless var.nil?
168
176
  end
169
177
  end
@@ -317,6 +325,14 @@ module Idl
317
325
  end
318
326
 
319
327
  pruned_body = nil
328
+ prune_stmts = -> {
329
+ [].tap do |out|
330
+ statements.each do |s|
331
+ out << s.prune(symtab)
332
+ break if out.last.always_terminates?
333
+ end
334
+ end
335
+ }
320
336
 
321
337
  value_result = value_try do
322
338
  # go through the statements, and stop if we find one that returns or raises an exception
@@ -343,10 +359,10 @@ module Idl
343
359
  end
344
360
  end
345
361
 
346
- pruned_body = FunctionBodyAst.new(input, interval, statements.map { |s| s.prune(symtab) })
362
+ pruned_body = FunctionBodyAst.new(input, interval, prune_stmts.())
347
363
  end
348
364
  value_else(value_result) do
349
- pruned_body = FunctionBodyAst.new(input, interval, statements.map { |s| s.prune(symtab) })
365
+ pruned_body = FunctionBodyAst.new(input, interval, prune_stmts.())
350
366
  end
351
367
  ensure
352
368
  symtab.pop
@@ -356,13 +372,17 @@ module Idl
356
372
  end
357
373
  end
358
374
  class StatementAst < AstNode
375
+ def always_terminates?
376
+ action.is_a?(FunctionCallExpressionAst) && action.name == "raise"
377
+ end
378
+
359
379
  def prune(symtab, forced_type: nil)
360
380
  pruned_action = action.prune(symtab)
361
381
 
362
382
  new_stmt = StatementAst.new(input, interval, pruned_action)
363
383
  # pruned_action.freeze_tree(symtab) unless pruned_action.frozen?
364
384
 
365
- pruned_action.add_symbol(symtab) if pruned_action.is_a?(Declaration)
385
+ pruned_action.add_symbol(symtab) if pruned_action.declaration?
366
386
  # action#prune already handles symtab update (execute)
367
387
 
368
388
  new_stmt
@@ -469,6 +489,10 @@ module Idl
469
489
  end
470
490
 
471
491
  class IfBodyAst < AstNode
492
+ def always_terminates?
493
+ !stmts.empty? && stmts.last.always_terminates?
494
+ end
495
+
472
496
  def prune(symtab, restore: true, forced_type: nil)
473
497
  pruned_stmts = []
474
498
  symtab.push(nil)
@@ -476,7 +500,7 @@ module Idl
476
500
  stmts.each do |s|
477
501
  pruned_stmts << s.prune(symtab)
478
502
 
479
- break if pruned_stmts.last.is_a?(StatementAst) && pruned_stmts.last.action.is_a?(FunctionCallExpressionAst) && pruned_stmts.last.action.name == "raise"
503
+ break if pruned_stmts.last.always_terminates?
480
504
  end
481
505
  if restore
482
506
  symtab.restore_values(snapshot)
@@ -545,16 +569,23 @@ module Idl
545
569
  end
546
570
  end
547
571
  # we get here, then we don't know the value of anything. just return this if with everything pruned
572
+ # After pruning, some elseif conditions may resolve to a literal (e.g., `false && <runtime_csr_read>`
573
+ # fails value() due to the CSR read but prune() short-circuits the && to false). Filter those out.
574
+ pruned_elsifs = unknown_elsifs.filter_map do |eif|
575
+ pruned = eif.prune(symtab)
576
+ next nil if pruned.cond.is_a?(FalseExpressionAst)
577
+ pruned
578
+ end
548
579
  result = IfAst.new(
549
580
  input, interval,
550
581
  if_cond.prune(symtab),
551
582
  if_body.prune(symtab),
552
- unknown_elsifs.map { |eif| eif.prune(symtab) },
583
+ pruned_elsifs,
553
584
  final_else_body.prune(symtab)
554
585
  )
555
586
  # Nullify any variable assigned in any branch, since we don't know which ran
556
587
  if_body.nullify_assignments(symtab)
557
- unknown_elsifs.each { |eif| eif.body.nullify_assignments(symtab) }
588
+ pruned_elsifs.each { |eif| eif.body.nullify_assignments(symtab) }
558
589
  final_else_body.nullify_assignments(symtab)
559
590
  result
560
591
  end
@@ -581,9 +612,9 @@ module Idl
581
612
  value_result = value_try do
582
613
  if condition.value(symtab)
583
614
  pruned_action = action.prune(symtab)
584
- pruned_action.add_symbol(symtab) if pruned_action.is_a?(Declaration)
615
+ pruned_action.add_symbol(symtab) if pruned_action.declaration?
585
616
  value_result = value_try do
586
- pruned_action.execute(symtab) if pruned_action.is_a?(Executable)
617
+ pruned_action.execute(symtab) if pruned_action.executable?
587
618
  end
588
619
 
589
620
  return StatementAst.new(input, interval, pruned_action)
@@ -594,9 +625,9 @@ module Idl
594
625
  value_else(value_result) do
595
626
  # condition not known
596
627
  pruned_action = action.prune(symtab)
597
- pruned_action.add_symbol(symtab) if pruned_action.is_a?(Declaration)
628
+ pruned_action.add_symbol(symtab) if pruned_action.declaration?
598
629
  value_result = value_try do
599
- pruned_action.execute(symtab) if pruned_action.is_a?(Executable)
630
+ pruned_action.execute(symtab) if pruned_action.executable?
600
631
  end
601
632
  # Condition is unknown, so the assignment may not have run; nullify to prevent leakage
602
633
  pruned_action.nullify_assignments(symtab)
@@ -835,6 +866,8 @@ module Idl
835
866
  end
836
867
 
837
868
  class ReturnStatementAst < AstNode
869
+ def always_terminates? = true
870
+
838
871
  def prune(symtab, forced_type: nil)
839
872
  ReturnStatementAst.new(input, interval, return_expression.prune(symtab))
840
873
  end
@@ -79,8 +79,8 @@ module Idl
79
79
  # else
80
80
  # 0
81
81
  # end
82
- action.add_symbol(symtab) if action.is_a?(Declaration)
83
- if action.is_a?(Executable)
82
+ action.add_symbol(symtab) if action.declaration?
83
+ if action.executable?
84
84
  value_try do
85
85
  action.execute(symtab)
86
86
  end
@@ -162,8 +162,8 @@ module Idl
162
162
  mask |= condition.reachable_exceptions(symtab, cache)
163
163
  if condition.value(symtab)
164
164
  mask |= action.reachable_exceptions(symtab, cache)
165
- action.add_symbol(symtab) if action.is_a?(Declaration)
166
- if action.is_a?(Executable)
165
+ action.add_symbol(symtab) if action.declaration?
166
+ if action.executable?
167
167
  value_result = value_try do
168
168
  action.execute(symtab)
169
169
  end
@@ -175,8 +175,8 @@ module Idl
175
175
  # condition not known
176
176
  mask |= condition.reachable_exceptions(symtab, cache)
177
177
  mask |= action.reachable_exceptions(symtab, cache)
178
- action.add_symbol(symtab) if action.is_a?(Declaration)
179
- if action.is_a?(Executable)
178
+ action.add_symbol(symtab) if action.declaration?
179
+ if action.executable?
180
180
  value_result = value_try do
181
181
  action.execute(symtab)
182
182
  end
@@ -82,9 +82,9 @@ module Idl
82
82
  def reachable_functions(symtab, cache = T.let({}, ReachableFunctionCacheType))
83
83
  fns = action.reachable_functions(symtab, cache)
84
84
 
85
- action.add_symbol(symtab) if action.is_a?(Declaration)
85
+ action.add_symbol(symtab) if action.declaration?
86
86
  value_try do
87
- action.execute(symtab) if action.is_a?(Executable)
87
+ action.execute(symtab) if action.executable?
88
88
  rescue SystemStackError
89
89
  type_error "Detected unbounded recursion during compile-time constant evaluation at #{input_file}:#{input_line}.. This recursion cannot be represented or validated."
90
90
  end
@@ -208,10 +208,12 @@ module Idl
208
208
  builtin_funcs: T.nilable(BuiltinFunctionCallbacks),
209
209
  csrs: T::Array[Csr],
210
210
  params: T::Array[RuntimeParam],
211
- name: String
211
+ name: String,
212
+ register_files: T::Array[T.untyped],
213
+ register_file_max_widths: T::Hash[String, Integer]
212
214
  ).void
213
215
  }
214
- def initialize(mxlen: nil, possible_xlens_cb: nil, builtin_global_vars: [], builtin_enums: [], builtin_funcs: nil, csrs: [], params: [], name: "")
216
+ def initialize(mxlen: nil, possible_xlens_cb: nil, builtin_global_vars: [], builtin_enums: [], builtin_funcs: nil, csrs: [], params: [], name: "", register_files: [], register_file_max_widths: {})
215
217
  @mutex = Thread::Mutex.new
216
218
  @mxlen = mxlen
217
219
  @possible_xlens_cb = possible_xlens_cb
@@ -221,11 +223,6 @@ module Idl
221
223
 
222
224
  # builtin types
223
225
  @scopes = [{
224
- "X" => Var.new(
225
- "X",
226
- Type.new(:array, sub_type: XregType.new(@mxlen.nil? ? 64 : @mxlen), width: 32, qualifiers: [:global])
227
- ),
228
- "XReg" => XregType.new(@mxlen.nil? ? 64 : @mxlen),
229
226
  "Boolean" => Type.new(:boolean),
230
227
  "true" => Var.new(
231
228
  "true",
@@ -237,8 +234,29 @@ module Idl
237
234
  Type.new(:boolean),
238
235
  false
239
236
  )
240
-
241
237
  }]
238
+ # @params must be set before the register_files loop so that param schema
239
+ # max lookups (used to compute int_width for dynamic-width register files)
240
+ # can access @params without triggering a cfg_arch.symtab circular dependency.
241
+ @params = params
242
+
243
+ # Register file globals (X, F, V, etc.) from YAML-derived register file objects.
244
+ # Each RF provides: .name (String), .register_length (IDL body String), .registers.count (Integer).
245
+ register_files.each do |rf|
246
+ int_width = if register_file_max_widths.key?(rf.name)
247
+ register_file_max_widths[rf.name]
248
+ else
249
+ # Fallback for callers without pre-computed widths (CLI, tests with literal-width RFs).
250
+ # eval_register_length_idl handles literals ("return 64;") and simple MXLEN references.
251
+ w = eval_register_length_idl(rf.register_length)
252
+ w.is_a?(Integer) ? w : raise("Cannot determine max register width for '#{rf.name}'. " \
253
+ "Pass register_file_max_widths: to SymbolTable.new.")
254
+ end
255
+ elem_type = RegFileElementType.new(rf.name, int_width, max_width: int_width)
256
+ array_type = Type.new(:array, sub_type: elem_type, width: rf.registers.count, qualifiers: [:global])
257
+ @scopes[0][rf.name] = Var.new(rf.name, array_type)
258
+ @scopes[0]["#{rf.name}Reg"] = elem_type
259
+ end
242
260
  builtin_global_vars.each do |v|
243
261
  add!(v.name, v)
244
262
  end
@@ -248,12 +266,35 @@ module Idl
248
266
  @builtin_funcs = builtin_funcs
249
267
  @csrs = csrs
250
268
  @csr_hash = @csrs.map { |csr| [csr.name.freeze, csr].freeze }.to_h.freeze
251
- @params = params
252
269
 
253
270
  # set up the global clone that be used as a mutable table
254
271
  @global_clone_pool = T.let([], T::Array[SymbolTable])
255
272
  end
256
273
 
274
+ # Compile and evaluate the IDL function body string that defines register width.
275
+ # Returns an Integer if statically known, or the expression string if dynamic.
276
+ # @param idl_body [String] e.g. "return 64;" or "return MXLEN;"
277
+ #
278
+ # NOTE: The body-stripping logic here (strip 'return', strip ';', match literal/MXLEN)
279
+ # is intentionally duplicated in Udb::RegisterFile#register_length_expr and
280
+ # Udb::RegisterFile#eval_register_length (udb gem). Changes here must be mirrored there.
281
+ # idlc cannot depend on udb (udb depends on idlc), so a shared utility is not possible
282
+ # without an additional gem layer.
283
+ def eval_register_length_idl(idl_body)
284
+ # Strip 'return' and ';'
285
+ expr = idl_body.strip.sub(/\Areturn\s+/, "").sub(/;\z/, "").strip
286
+ case expr
287
+ when /\A\d+\z/
288
+ expr.to_i
289
+ when /\AMXLEN\z/
290
+ @mxlen || 64
291
+ else
292
+ # Dynamic parameter — return the parameter name as a String
293
+ expr
294
+ end
295
+ end
296
+ private :eval_register_length_idl
297
+
257
298
  # @return [String] inspection string
258
299
  sig { returns(String) }
259
300
  def inspect
@@ -18,18 +18,12 @@ module Treetop
18
18
  # @param starting_line [Integer] Starting line in the file
19
19
  # @param starting_offset [Integer] Starting byte offset in the file
20
20
  # @param line_file_offsets [Array<Integer>, nil] Per-IDL-line file byte offsets
21
- sig { params(filename: T.nilable(String), starting_line: Integer, starting_offset: Integer, line_file_offsets: T.nilable(T::Array[Integer])).void }
21
+ sig { params(filename: T.nilable(String), starting_line: Integer, starting_offset: Integer, line_file_offsets: T.nilable(T::Array[Integer])).void.checked(:never) }
22
22
  def set_input_file(filename, starting_line = 0, starting_offset = 0, line_file_offsets = nil)
23
23
  @input_file = filename
24
24
  @starting_line = starting_line
25
25
  @starting_offset = starting_offset
26
26
  @line_file_offsets = line_file_offsets
27
- elements&.each do |child|
28
- # Adjust the starting offset for each child based on its position in the input
29
- child_offset = starting_offset + child.interval.first
30
- child.set_input_file(filename, starting_line, child_offset, line_file_offsets)
31
- end
32
- raise "?" if @starting_line.nil?
33
27
  end
34
28
 
35
29
  sig { returns(T::Boolean) }
@@ -43,7 +37,7 @@ module Treetop
43
37
  # @param [Integer] starting_line The starting line number in the input file.
44
38
  # @param [Integer] starting_offset The starting byte offset in the input file.
45
39
  # @param [Array<Integer>, nil] line_file_offsets Per-IDL-line file byte offsets
46
- sig { params(filename: T.nilable(String), starting_line: Integer, starting_offset: Integer, line_file_offsets: T.nilable(T::Array[Integer])).void }
40
+ sig { params(filename: T.nilable(String), starting_line: Integer, starting_offset: Integer, line_file_offsets: T.nilable(T::Array[Integer])).void.checked(:never) }
47
41
  def set_input_file_unless_already_set(filename, starting_line = 0, starting_offset = 0, line_file_offsets = nil)
48
42
  if @input_file.nil?
49
43
  set_input_file(filename, starting_line, starting_offset, line_file_offsets)
data/lib/idlc/type.rb CHANGED
@@ -963,19 +963,24 @@ module Idl
963
963
  def body = @func_def_ast.body
964
964
  end
965
965
 
966
- # XReg is really a Bits<> type, so we override it just to get
967
- # prettier prints
968
- class XregType < Type
969
- def initialize(xlen)
970
- super(:bits, width: xlen, max_width: 64)
971
- end
966
+ # General-purpose register file element type.
967
+ # Represents the type of one element in a named register file (e.g. F, V, X).
968
+ class RegFileElementType < Type
969
+ attr_reader :name
972
970
 
973
- def to_s
974
- "XReg"
971
+ def initialize(name, width, max_width: nil)
972
+ super(:bits, width:, max_width: max_width || width)
973
+ @name = name
975
974
  end
976
975
 
977
- def to_cxx
978
- "XReg"
976
+ def to_s = "#{name}Reg"
977
+ def to_cxx = "#{name}Reg"
978
+ end
979
+
980
+ # XReg is really a Bits<> type -- keep as a named alias for backwards compatibility
981
+ class XregType < RegFileElementType
982
+ def initialize(xlen)
983
+ super("X", xlen, max_width: 64)
979
984
  end
980
985
  end
981
986
 
data/lib/idlc/version.rb CHANGED
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Idl
7
7
  class Compiler
8
- def self.version = "0.1.2"
8
+ def self.version = "0.1.5"
9
9
  end
10
10
  end
data/lib/idlc.rb CHANGED
@@ -55,6 +55,14 @@ module Idl
55
55
 
56
56
  attr_reader :parser
57
57
 
58
+ # Class-level parse cache: absolute file path (String) → IsaSyntaxNode.
59
+ # Shared across all Compiler instances so each file is parsed only once per
60
+ # process. Safe to share because IsaSyntaxNode#to_ast is non-destructive
61
+ # and returns a fresh, independent IsaAst on every call.
62
+ # Mutex guards writes; under MRI, reads without the lock are safe.
63
+ @@parse_cache = {}
64
+ @@parse_cache_mutex = Mutex.new
65
+
58
66
  def initialize
59
67
  @parser = ::IdlParser.new
60
68
  end
@@ -71,40 +79,56 @@ module Idl
71
79
  end
72
80
 
73
81
  def compile_file(path, source_mapper = nil)
74
- @parser.set_input_file(path.to_s)
82
+ path_key = path.realpath.to_s
75
83
 
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
84
+ m = T.let(@@parse_cache[path_key], T.nilable(IsaSyntaxNode))
96
85
 
97
86
  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
87
+ @@parse_cache_mutex.synchronize do
88
+ # Re-check inside the lock in case another thread just populated the entry.
89
+ unless @@parse_cache.key?(path_key)
90
+ @parser.set_input_file(path_key)
91
+
92
+ content = path.read
93
+ source_mapper[path_key] = content unless source_mapper.nil?
94
+
95
+ old_format = @pb.format unless @pb.nil?
96
+ @pb.format = "Parsing #{File.basename(path)} [:bar]" unless @pb.nil?
97
+ pid = unless @pb.nil?
98
+ fork {
99
+ loop do
100
+ sleep 1
101
+ @pb.advance unless @pb.nil?
102
+ end
103
+ }
104
+ end
105
+ m = @parser.parse(content)
106
+ unless @pb.nil?
107
+ Process.kill("TERM", T.must(pid))
108
+ Process.wait(T.must(pid))
109
+ @pb.format = old_format
110
+ end
111
+
112
+ if m.nil?
113
+ raise SyntaxError, <<~MSG
114
+ While parsing #{@parser.input_file}:#{@parser.failure_line}:#{@parser.failure_column}
115
+
116
+ #{@parser.failure_reason}
117
+ MSG
118
+ end
119
+
120
+ raise "unexpected type #{m.class.name}" unless m.is_a?(IsaSyntaxNode)
121
+
122
+ @@parse_cache[path_key] = m
123
+ end
124
+ m = @@parse_cache[path_key]
125
+ end
126
+ else
127
+ # Cache hit: still populate source_mapper if provided (test-only path).
128
+ source_mapper[path_key] = path.read unless source_mapper.nil?
103
129
  end
104
130
 
105
- raise "unexpected type #{m.class.name}" unless m.is_a?(IsaSyntaxNode)
106
-
107
- ast = m.to_ast
131
+ ast = T.must(m).to_ast
108
132
 
109
133
  ast.children.each do |child|
110
134
  next unless child.is_a?(IncludeStatementAst)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idlc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Hower
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-03-29 00:00:00.000000000 Z
10
+ date: 2026-05-19 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -372,7 +371,6 @@ metadata:
372
371
  homepage_uri: https://github.com/riscv/riscv-unified-db
373
372
  mailing_list_uri: https://lists.riscv.org/g/tech-unifieddb
374
373
  bug_tracker_uri: https://github.com/riscv/riscv-unified-db/issues
375
- post_install_message:
376
374
  rdoc_options: []
377
375
  require_paths:
378
376
  - lib
@@ -387,8 +385,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
387
385
  - !ruby/object:Gem::Version
388
386
  version: '0'
389
387
  requirements: []
390
- rubygems_version: 3.4.19
391
- signing_key:
388
+ rubygems_version: 3.6.9
392
389
  specification_version: 4
393
390
  summary: ISA Description Language Compiler
394
391
  test_files: []