kumi 0.0.9 → 0.0.10

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +28 -44
  3. data/README.md +187 -120
  4. data/docs/AST.md +1 -1
  5. data/docs/FUNCTIONS.md +52 -8
  6. data/docs/compiler_design_principles.md +86 -0
  7. data/docs/features/README.md +15 -2
  8. data/docs/features/hierarchical-broadcasting.md +349 -0
  9. data/docs/features/javascript-transpiler.md +148 -0
  10. data/docs/features/performance.md +1 -3
  11. data/docs/schema_metadata.md +7 -7
  12. data/examples/game_of_life.rb +2 -4
  13. data/lib/kumi/analyzer.rb +0 -2
  14. data/lib/kumi/compiler.rb +6 -275
  15. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +600 -42
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +4 -2
  17. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +27 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +6 -2
  19. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +90 -46
  20. data/lib/kumi/core/cascade_executor_builder.rb +132 -0
  21. data/lib/kumi/core/compiler/expression_compiler.rb +146 -0
  22. data/lib/kumi/core/compiler/function_invoker.rb +55 -0
  23. data/lib/kumi/core/compiler/path_traversal_compiler.rb +158 -0
  24. data/lib/kumi/core/compiler/reference_compiler.rb +46 -0
  25. data/lib/kumi/core/compiler_base.rb +137 -0
  26. data/lib/kumi/core/explain.rb +2 -2
  27. data/lib/kumi/core/function_registry/collection_functions.rb +86 -3
  28. data/lib/kumi/core/function_registry/function_builder.rb +5 -3
  29. data/lib/kumi/core/function_registry/logical_functions.rb +171 -1
  30. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  31. data/lib/kumi/core/function_registry.rb +32 -10
  32. data/lib/kumi/core/nested_structure_utils.rb +78 -0
  33. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +2 -2
  34. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  35. data/lib/kumi/core/schema_instance.rb +4 -0
  36. data/lib/kumi/core/vectorized_function_builder.rb +88 -0
  37. data/lib/kumi/errors.rb +2 -0
  38. data/lib/kumi/js/compiler.rb +878 -0
  39. data/lib/kumi/js/function_registry.rb +333 -0
  40. data/lib/kumi/js.rb +23 -0
  41. data/lib/kumi/registry.rb +61 -1
  42. data/lib/kumi/schema.rb +1 -1
  43. data/lib/kumi/support/s_expression_printer.rb +16 -15
  44. data/lib/kumi/syntax/array_expression.rb +6 -6
  45. data/lib/kumi/syntax/call_expression.rb +4 -4
  46. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  47. data/lib/kumi/syntax/case_expression.rb +4 -4
  48. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  49. data/lib/kumi/syntax/hash_expression.rb +4 -4
  50. data/lib/kumi/syntax/input_declaration.rb +6 -5
  51. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  52. data/lib/kumi/syntax/input_reference.rb +5 -5
  53. data/lib/kumi/syntax/literal.rb +4 -4
  54. data/lib/kumi/syntax/node.rb +34 -34
  55. data/lib/kumi/syntax/root.rb +6 -6
  56. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  57. data/lib/kumi/syntax/value_declaration.rb +4 -4
  58. data/lib/kumi/version.rb +1 -1
  59. data/lib/kumi.rb +1 -1
  60. data/scripts/analyze_broadcast_methods.rb +68 -0
  61. data/scripts/analyze_cascade_methods.rb +74 -0
  62. data/scripts/check_broadcasting_coverage.rb +51 -0
  63. data/scripts/find_dead_code.rb +114 -0
  64. metadata +20 -4
  65. data/docs/features/array-broadcasting.md +0 -170
  66. data/lib/kumi/cli.rb +0 -449
  67. data/lib/kumi/core/vectorization_metadata.rb +0 -110
@@ -0,0 +1,148 @@
1
+ # JavaScript Transpiler
2
+
3
+ Transpiles compiled schemas to standalone JavaScript code.
4
+
5
+ ## Usage
6
+
7
+ ### Export Schema
8
+
9
+ ```ruby
10
+ class TaxCalculator
11
+ extend Kumi::Schema
12
+
13
+ schema do
14
+ input do
15
+ float :income
16
+ string :filing_status
17
+ end
18
+
19
+ trait :single, input.filing_status == "single"
20
+
21
+ value :std_deduction do
22
+ on single, 14_600
23
+ base 29_200
24
+ end
25
+
26
+ value :taxable_income, fn(:max, [input.income - std_deduction, 0])
27
+ value :tax_owed, taxable_income * 0.22
28
+ end
29
+ end
30
+
31
+ Kumi::Js.export_to_file(TaxCalculator, "tax-calculator.js")
32
+ ```
33
+
34
+ ### Use in JavaScript
35
+
36
+ ```javascript
37
+ const { schema } = require('./tax-calculator.js');
38
+
39
+ const taxpayer = {
40
+ income: 75000,
41
+ filing_status: "single"
42
+ };
43
+
44
+ const calculator = schema.from(taxpayer);
45
+ console.log(calculator.fetch('tax_owed'));
46
+
47
+ const results = calculator.slice('taxable_income', 'tax_owed');
48
+ ```
49
+
50
+ ## Export Methods
51
+
52
+ ### Command Line
53
+
54
+ ```bash
55
+ bundle exec kumi --export-js output.js SchemaClass
56
+ ```
57
+
58
+ ### Programmatic
59
+
60
+ ```ruby
61
+ Kumi::Js.export_to_file(MySchema, "schema.js")
62
+
63
+ js_code = Kumi::Js.compile(MySchema)
64
+ File.write("output.js", js_code)
65
+ ```
66
+
67
+ ## JavaScript API
68
+
69
+ ### schema.from(input)
70
+
71
+ Creates runner instance.
72
+
73
+ ```javascript
74
+ const runner = schema.from({ income: 50000, status: "single" });
75
+ ```
76
+
77
+ ### runner.fetch(key)
78
+
79
+ Returns computed value. Results are cached.
80
+
81
+ ```javascript
82
+ const tax = runner.fetch('tax_owed');
83
+ ```
84
+
85
+ ### runner.slice(...keys)
86
+
87
+ Returns multiple values.
88
+
89
+ ```javascript
90
+ const results = runner.slice('taxable_income', 'tax_owed');
91
+ // Returns: { taxable_income: 35400, tax_owed: 7788 }
92
+ ```
93
+
94
+ ### runner.functionsUsed
95
+
96
+ Array of functions used by the schema.
97
+
98
+ ```javascript
99
+ console.log(runner.functionsUsed); // ["max", "subtract", "multiply"]
100
+ ```
101
+
102
+ ## Function Optimization
103
+
104
+ The transpiler only includes functions actually used by the schema.
105
+
106
+ Example schema using 4 functions generates ~3 KB instead of ~8 KB with all 67 functions.
107
+
108
+ ## Browser Compatibility
109
+
110
+ - ES6+ (Chrome 60+, Firefox 55+, Safari 10+)
111
+ - Modern bundlers (Webpack, Rollup, Vite)
112
+ - Node.js 12+
113
+
114
+ ## Limitations
115
+
116
+ - No `explain()` method (Ruby only)
117
+ - Custom Ruby functions need JavaScript equivalents
118
+
119
+ ## Module Formats
120
+
121
+ Generated JavaScript supports:
122
+ - CommonJS (`require()`)
123
+ - ES Modules (`import`)
124
+ - Global variables (browser)
125
+
126
+ ## Minification
127
+
128
+ Use production minifiers like Terser or UglifyJS for smaller bundles.
129
+
130
+ ## Dual Mode Validation
131
+
132
+ Set `KUMI_DUAL_MODE=true` to automatically execute both Ruby and JavaScript versions and validate they produce identical results:
133
+
134
+ ```bash
135
+ KUMI_DUAL_MODE=true ruby my_script.rb
136
+ ```
137
+
138
+ Every calculation is validated in real-time. Mismatches throw detailed error reports with both results for debugging.
139
+
140
+ ## Error Handling
141
+
142
+ ```javascript
143
+ try {
144
+ const runner = schema.from({ invalid: "data" });
145
+ } catch (error) {
146
+ console.error(error.message);
147
+ }
148
+ ```
@@ -1,8 +1,6 @@
1
1
  # Performance
2
2
 
3
- TODO: Add benchmark data
4
-
5
- Processes large schemas with optimized algorithms for analysis, compilation, and execution.
3
+ Analysis, compilation, and execution performance for large schemas.
6
4
 
7
5
  ## Execution Model
8
6
 
@@ -1,6 +1,6 @@
1
1
  # Schema Metadata
2
2
 
3
- Kumi's SchemaMetadata interface provides structured access to analyzed schema information for building external tools like form generators, documentation systems, and analysis utilities.
3
+ Kumi's SchemaMetadata interface accesses analyzed schema information for building external tools like form generators, documentation systems, and analysis utilities.
4
4
 
5
5
  ## Primary Interface
6
6
 
@@ -10,7 +10,7 @@ SchemaMetadata is the main interface for extracting metadata from Kumi schemas:
10
10
  metadata = MySchema.schema_metadata
11
11
  ```
12
12
 
13
- See the comprehensive API documentation in the SchemaMetadata class for detailed method documentation, examples, and usage patterns.
13
+ See the API documentation in the SchemaMetadata class for method documentation, examples, and usage patterns.
14
14
 
15
15
  ## Processed Metadata (Tool-Friendly)
16
16
 
@@ -83,24 +83,24 @@ metadata.values
83
83
  # }
84
84
  ```
85
85
 
86
- ### Clean Public Interface Examples
86
+ ### Public Interface Examples
87
87
  ```ruby
88
- # Processed dependency information (clean hashes)
88
+ # Processed dependency information
89
89
  metadata.dependencies
90
90
  # => { :tax_amount => [{ to: :income, conditional: false }, { to: :tax_rate, conditional: false }] }
91
91
 
92
- # Processed declaration metadata (clean hashes)
92
+ # Processed declaration metadata
93
93
  metadata.declarations
94
94
  # => { :adult => { type: :trait, expression: ">=(input.age, 18)" }, :tax_amount => { type: :value, expression: "multiply(input.income, tax_rate)" } }
95
95
 
96
- # Type inference results (clean data)
96
+ # Type inference results
97
97
  metadata.inferred_types
98
98
  # => { :adult => :boolean, :tax_amount => :float, :item_totals => { array: :float } }
99
99
  ```
100
100
 
101
101
  ### Raw Analyzer State (Advanced Usage)
102
102
  ```ruby
103
- # Complete raw state hash with internal objects (AST nodes, Edge objects)
103
+ # Raw state hash with internal objects (AST nodes, Edge objects)
104
104
  metadata.analyzer_state
105
105
  # => { declarations: {AST nodes...}, dependencies: {Edge objects...}, ... }
106
106
  ```
@@ -1,19 +1,17 @@
1
- $LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
2
1
  require "kumi"
3
2
 
4
- NEIGHBOR_DELTAS = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
5
3
  begin
6
4
  # in a block so we dont define this globally
7
5
  def neighbor_cells_sum_method(cells, row, col, height, width)
8
6
  # Calculate neighbor indices with wraparound
9
- NEIGHBOR_DELTAS.map do |dr, dc|
7
+ [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]].map do |dr, dc|
10
8
  neighbor_row = (row + dr) % height
11
9
  neighbor_col = (col + dc) % width
12
10
  neighbor_index = (neighbor_row * width) + neighbor_col
13
11
  cells[neighbor_index]
14
12
  end.sum
15
13
  end
16
- Kumi::Core::FunctionRegistry.register_with_metadata(:neighbor_cells_sum, method(:neighbor_cells_sum_method),
14
+ Kumi::Registry.register_with_metadata(:neighbor_cells_sum, method(:neighbor_cells_sum_method),
17
15
  return_type: :integer, arity: 5,
18
16
  param_types: %i[array integer integer integer integer],
19
17
  description: "Get neighbor cells for Conway's Game of Life")
data/lib/kumi/analyzer.rb CHANGED
@@ -4,8 +4,6 @@ module Kumi
4
4
  module Analyzer
5
5
  Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
6
6
 
7
- module_function
8
-
9
7
  DEFAULT_PASSES = [
10
8
  Core::Analyzer::Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
11
9
  Core::Analyzer::Passes::InputCollector, # 2. Collects field metadata from input declarations.
data/lib/kumi/compiler.rb CHANGED
@@ -2,152 +2,18 @@
2
2
 
3
3
  module Kumi
4
4
  # Compiles an analyzed schema into executable lambdas
5
- class Compiler
6
- # ExprCompilers holds per-node compile implementations
7
- module ExprCompilers
8
- def compile_literal(expr)
9
- v = expr.value
10
- ->(_ctx) { v }
11
- end
12
-
13
- def compile_field_node(expr)
14
- compile_field(expr)
15
- end
16
-
17
- def compile_element_field_reference(expr)
18
- path = expr.path
19
-
20
- lambda do |ctx|
21
- # Start with the top-level collection from the context.
22
- collection = ctx[path.first]
23
-
24
- # Recursively map over the nested collections.
25
- # The `dig_and_map` helper will handle any level of nesting.
26
- dig_and_map(collection, path[1..])
27
- end
28
- end
29
-
30
- def compile_binding_node(expr)
31
- name = expr.name
32
- # Handle forward references in cycles by deferring binding lookup to runtime
33
- lambda do |ctx|
34
- fn = @bindings[name].last
35
- fn.call(ctx)
36
- end
37
- end
38
-
39
- def compile_list(expr)
40
- fns = expr.elements.map { |e| compile_expr(e) }
41
- ->(ctx) { fns.map { |fn| fn.call(ctx) } }
42
- end
43
-
44
- def compile_call(expr)
45
- fn_name = expr.fn_name
46
- arg_fns = expr.args.map { |a| compile_expr(a) }
47
-
48
- # Check if this is a vectorized operation
49
- if vectorized_operation?(expr)
50
- ->(ctx) { invoke_vectorized_function(fn_name, arg_fns, ctx, expr.loc) }
51
- else
52
- ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
53
- end
54
- end
55
-
56
- def compile_cascade(expr)
57
- # Check if current declaration is vectorized
58
- broadcast_meta = @analysis.state[:broadcasts]
59
- is_vectorized = @current_declaration && broadcast_meta&.dig(:vectorized_operations, @current_declaration)
60
-
61
- # For vectorized cascades, we need to transform conditions that use all?
62
- pairs = if is_vectorized
63
- expr.cases.map do |c|
64
- condition_fn = transform_vectorized_condition(c.condition)
65
- result_fn = compile_expr(c.result)
66
- [condition_fn, result_fn]
67
- end
68
- else
69
- expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
70
- end
71
-
72
- if is_vectorized
73
- lambda do |ctx|
74
- # This cascade can be vectorized - check if we actually need to at runtime
75
- # Evaluate all conditions and results to check for arrays
76
- cond_results = pairs.map { |cond, _res| cond.call(ctx) }
77
- res_results = pairs.map { |_cond, res| res.call(ctx) }
78
-
79
- # Check if any conditions or results are arrays (vectorized)
80
- has_vectorized_data = (cond_results + res_results).any?(Array)
81
-
82
- if has_vectorized_data
83
- # Apply element-wise cascade evaluation
84
- array_length = cond_results.find { |v| v.is_a?(Array) }&.length ||
85
- res_results.find { |v| v.is_a?(Array) }&.length || 1
86
-
87
- (0...array_length).map do |i|
88
- pairs.each_with_index do |(_cond, _res), pair_idx|
89
- cond_val = cond_results[pair_idx].is_a?(Array) ? cond_results[pair_idx][i] : cond_results[pair_idx]
90
-
91
- if cond_val
92
- res_val = res_results[pair_idx].is_a?(Array) ? res_results[pair_idx][i] : res_results[pair_idx]
93
- break res_val
94
- end
95
- end || nil
96
- end
97
- else
98
- # All data is scalar - use regular cascade evaluation
99
- pairs.each_with_index do |(_cond, _res), pair_idx|
100
- return res_results[pair_idx] if cond_results[pair_idx]
101
- end
102
- nil
103
- end
104
- end
105
- else
106
- lambda do |ctx|
107
- pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
108
- nil
109
- end
110
- end
111
- end
112
-
113
- def transform_vectorized_condition(condition_expr)
114
- # If this is fn(:all?, [trait_ref]), extract the trait_ref for vectorized cascades
115
- if condition_expr.is_a?(Kumi::Syntax::CallExpression) &&
116
- condition_expr.fn_name == :all? &&
117
- condition_expr.args.length == 1
118
-
119
- arg = condition_expr.args.first
120
- if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
121
- trait_ref = arg.elements.first
122
- return compile_expr(trait_ref)
123
- end
124
- end
125
-
126
- # Otherwise compile normally
127
- compile_expr(condition_expr)
128
- end
129
- end
130
-
131
- include ExprCompilers
132
-
133
- # Map node classes to compiler methods
134
- DISPATCH = {
135
- Kumi::Syntax::Literal => :compile_literal,
136
- Kumi::Syntax::InputReference => :compile_field_node,
137
- Kumi::Syntax::InputElementReference => :compile_element_field_reference,
138
- Kumi::Syntax::DeclarationReference => :compile_binding_node,
139
- Kumi::Syntax::ArrayExpression => :compile_list,
140
- Kumi::Syntax::CallExpression => :compile_call,
141
- Kumi::Syntax::CascadeExpression => :compile_cascade
142
- }.freeze
5
+ class Compiler < Core::CompilerBase
6
+ include Kumi::Core::Compiler::ReferenceCompiler
7
+ include Kumi::Core::Compiler::PathTraversalCompiler
8
+ include Kumi::Core::Compiler::ExpressionCompiler
9
+ include Kumi::Core::Compiler::FunctionInvoker
143
10
 
144
11
  def self.compile(schema, analyzer:)
145
12
  new(schema, analyzer).compile
146
13
  end
147
14
 
148
15
  def initialize(schema, analyzer)
149
- @schema = schema
150
- @analysis = analyzer
16
+ super
151
17
  @bindings = {}
152
18
  end
153
19
 
@@ -160,140 +26,5 @@ module Kumi
160
26
 
161
27
  Core::CompiledSchema.new(@bindings.freeze)
162
28
  end
163
-
164
- private
165
-
166
- def build_index
167
- @index = {}
168
- @schema.attributes.each { |a| @index[a.name] = a }
169
- @schema.traits.each { |t| @index[t.name] = t }
170
- end
171
-
172
- def dig_and_map(collection, path_segments)
173
- return collection unless collection.is_a?(Array)
174
-
175
- current_segment = path_segments.first
176
- remaining_segments = path_segments[1..]
177
-
178
- collection.map do |element|
179
- value = element[current_segment]
180
-
181
- # If there are more segments, recurse. Otherwise, return the value.
182
- if remaining_segments.empty?
183
- value
184
- else
185
- dig_and_map(value, remaining_segments)
186
- end
187
- end
188
- end
189
-
190
- def compile_declaration(decl)
191
- @current_declaration = decl.name
192
- kind = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? :trait : :attr
193
- fn = compile_expr(decl.expression)
194
- @bindings[decl.name] = [kind, fn]
195
- @current_declaration = nil
196
- end
197
-
198
- # Dispatch to the appropriate compile_* method
199
- def compile_expr(expr)
200
- method = DISPATCH.fetch(expr.class)
201
- send(method, expr)
202
- end
203
-
204
- def compile_field(node)
205
- name = node.name
206
- loc = node.loc
207
- lambda do |ctx|
208
- return ctx[name] if ctx.respond_to?(:key?) && ctx.key?(name)
209
-
210
- raise Errors::RuntimeError,
211
- "Key '#{name}' not found at #{loc}. Available: #{ctx.respond_to?(:keys) ? ctx.keys.join(', ') : 'N/A'}"
212
- end
213
- end
214
-
215
- def vectorized_operation?(expr)
216
- # Check if this operation uses vectorized inputs
217
- broadcast_meta = @analysis.state[:broadcasts]
218
- return false unless broadcast_meta
219
-
220
- # Reduction functions are NOT vectorized operations - they consume arrays
221
- return false if Kumi::Registry.reducer?(expr.fn_name)
222
-
223
- expr.args.any? do |arg|
224
- case arg
225
- when Kumi::Syntax::InputElementReference
226
- broadcast_meta[:array_fields]&.key?(arg.path.first)
227
- when Kumi::Syntax::DeclarationReference
228
- broadcast_meta[:vectorized_operations]&.key?(arg.name)
229
- else
230
- false
231
- end
232
- end
233
- end
234
-
235
- def invoke_vectorized_function(name, arg_fns, ctx, loc)
236
- # Evaluate arguments
237
- values = arg_fns.map { |fn| fn.call(ctx) }
238
-
239
- # Check if any argument is vectorized (array)
240
- has_vectorized_args = values.any?(Array)
241
-
242
- if has_vectorized_args
243
- # Apply function with broadcasting to all vectorized arguments
244
- vectorized_function_call(name, values)
245
- else
246
- # All arguments are scalars - regular function call
247
- fn = Kumi::Registry.fetch(name)
248
- fn.call(*values)
249
- end
250
- rescue StandardError => e
251
- enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
252
- runtime_error = Errors::RuntimeError.new(enhanced_message)
253
- runtime_error.set_backtrace(e.backtrace)
254
- runtime_error.define_singleton_method(:cause) { e }
255
- raise runtime_error
256
- end
257
-
258
- def vectorized_function_call(fn_name, values)
259
- # Get the function from registry
260
- fn = Kumi::Registry.fetch(fn_name)
261
-
262
- # Find array dimensions for broadcasting
263
- array_values = values.select { |v| v.is_a?(Array) }
264
- return fn.call(*values) if array_values.empty?
265
-
266
- # All arrays should have the same length (validation could be added)
267
- array_length = array_values.first.size
268
-
269
- # Broadcast and apply function element-wise
270
- (0...array_length).map do |i|
271
- element_args = values.map do |v|
272
- v.is_a?(Array) ? v[i] : v # Broadcast scalars
273
- end
274
- fn.call(*element_args)
275
- end
276
- end
277
-
278
- def invoke_function(name, arg_fns, ctx, loc)
279
- fn = Kumi::Registry.fetch(name)
280
- values = arg_fns.map { |fn| fn.call(ctx) }
281
- fn.call(*values)
282
- rescue StandardError => e
283
- # Preserve original error class and backtrace while adding context
284
- enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
285
-
286
- if e.is_a?(Kumi::Core::Errors::Error)
287
- # Re-raise Kumi errors with enhanced message but preserve type
288
- e.define_singleton_method(:message) { enhanced_message }
289
- raise e
290
- else
291
- # For non-Kumi errors, wrap in RuntimeError but preserve original error info
292
- runtime_error = Errors::RuntimeError.new(enhanced_message)
293
- runtime_error.set_backtrace(e.backtrace)
294
- runtime_error.define_singleton_method(:cause) { e }
295
- raise runtime_error
296
- end
297
- end
298
29
  end
299
30
  end