idlc 0.1.1 → 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?
115
+
116
+ # avoid allocation when nothing changed
117
+ return self if !frozen? && new_children.each_with_index.all? { |c, i| c.equal?(children[i]) }
116
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
@@ -265,6 +273,9 @@ module Idl
265
273
  symtab.restore_values(snapshot)
266
274
  symtab.pop
267
275
  end
276
+ # Nullify any outer-scope variable assigned in the loop body, since we
277
+ # don't know how many iterations ran (or if any ran at all)
278
+ stmts.each { |stmt| stmt.nullify_assignments(symtab) }
268
279
  new_loop
269
280
  end
270
281
  end
@@ -314,6 +325,14 @@ module Idl
314
325
  end
315
326
 
316
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
+ }
317
336
 
318
337
  value_result = value_try do
319
338
  # go through the statements, and stop if we find one that returns or raises an exception
@@ -340,10 +359,10 @@ module Idl
340
359
  end
341
360
  end
342
361
 
343
- pruned_body = FunctionBodyAst.new(input, interval, statements.map { |s| s.prune(symtab) })
362
+ pruned_body = FunctionBodyAst.new(input, interval, prune_stmts.())
344
363
  end
345
364
  value_else(value_result) do
346
- pruned_body = FunctionBodyAst.new(input, interval, statements.map { |s| s.prune(symtab) })
365
+ pruned_body = FunctionBodyAst.new(input, interval, prune_stmts.())
347
366
  end
348
367
  ensure
349
368
  symtab.pop
@@ -353,13 +372,17 @@ module Idl
353
372
  end
354
373
  end
355
374
  class StatementAst < AstNode
375
+ def always_terminates?
376
+ action.is_a?(FunctionCallExpressionAst) && action.name == "raise"
377
+ end
378
+
356
379
  def prune(symtab, forced_type: nil)
357
380
  pruned_action = action.prune(symtab)
358
381
 
359
382
  new_stmt = StatementAst.new(input, interval, pruned_action)
360
- pruned_action.freeze_tree(symtab) unless pruned_action.frozen?
383
+ # pruned_action.freeze_tree(symtab) unless pruned_action.frozen?
361
384
 
362
- pruned_action.add_symbol(symtab) if pruned_action.is_a?(Declaration)
385
+ pruned_action.add_symbol(symtab) if pruned_action.declaration?
363
386
  # action#prune already handles symtab update (execute)
364
387
 
365
388
  new_stmt
@@ -466,6 +489,10 @@ module Idl
466
489
  end
467
490
 
468
491
  class IfBodyAst < AstNode
492
+ def always_terminates?
493
+ !stmts.empty? && stmts.last.always_terminates?
494
+ end
495
+
469
496
  def prune(symtab, restore: true, forced_type: nil)
470
497
  pruned_stmts = []
471
498
  symtab.push(nil)
@@ -473,7 +500,7 @@ module Idl
473
500
  stmts.each do |s|
474
501
  pruned_stmts << s.prune(symtab)
475
502
 
476
- 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?
477
504
  end
478
505
  if restore
479
506
  symtab.restore_values(snapshot)
@@ -542,16 +569,23 @@ module Idl
542
569
  end
543
570
  end
544
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
545
579
  result = IfAst.new(
546
580
  input, interval,
547
581
  if_cond.prune(symtab),
548
582
  if_body.prune(symtab),
549
- unknown_elsifs.map { |eif| eif.prune(symtab) },
583
+ pruned_elsifs,
550
584
  final_else_body.prune(symtab)
551
585
  )
552
586
  # Nullify any variable assigned in any branch, since we don't know which ran
553
587
  if_body.nullify_assignments(symtab)
554
- unknown_elsifs.each { |eif| eif.body.nullify_assignments(symtab) }
588
+ pruned_elsifs.each { |eif| eif.body.nullify_assignments(symtab) }
555
589
  final_else_body.nullify_assignments(symtab)
556
590
  result
557
591
  end
@@ -578,9 +612,9 @@ module Idl
578
612
  value_result = value_try do
579
613
  if condition.value(symtab)
580
614
  pruned_action = action.prune(symtab)
581
- pruned_action.add_symbol(symtab) if pruned_action.is_a?(Declaration)
615
+ pruned_action.add_symbol(symtab) if pruned_action.declaration?
582
616
  value_result = value_try do
583
- pruned_action.execute(symtab) if pruned_action.is_a?(Executable)
617
+ pruned_action.execute(symtab) if pruned_action.executable?
584
618
  end
585
619
 
586
620
  return StatementAst.new(input, interval, pruned_action)
@@ -591,9 +625,9 @@ module Idl
591
625
  value_else(value_result) do
592
626
  # condition not known
593
627
  pruned_action = action.prune(symtab)
594
- pruned_action.add_symbol(symtab) if pruned_action.is_a?(Declaration)
628
+ pruned_action.add_symbol(symtab) if pruned_action.declaration?
595
629
  value_result = value_try do
596
- pruned_action.execute(symtab) if pruned_action.is_a?(Executable)
630
+ pruned_action.execute(symtab) if pruned_action.executable?
597
631
  end
598
632
  # Condition is unknown, so the assignment may not have run; nullify to prevent leakage
599
633
  pruned_action.nullify_assignments(symtab)
@@ -832,6 +866,8 @@ module Idl
832
866
  end
833
867
 
834
868
  class ReturnStatementAst < AstNode
869
+ def always_terminates? = true
870
+
835
871
  def prune(symtab, forced_type: nil)
836
872
  ReturnStatementAst.new(input, interval, return_expression.prune(symtab))
837
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
@@ -65,7 +65,7 @@ module Idl
65
65
  if @kind == :array
66
66
  T.must(@sub_type).runtime?
67
67
  else
68
- @kind == :bits && @width == :unknown
68
+ @kind == :bits && !@width.is_a?(Integer)
69
69
  end
70
70
  end
71
71
 
@@ -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.1"
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.1
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-17 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: []