Module: Kumi::Pack::Builder

Defined in:
lib/kumi/pack/builder.rb

Constant Summary collapse

VERSION =
"0.1"

Class Method Summary collapse

Class Method Details

.accessor_name_for(name) ⇒ Object



194
195
196
# File 'lib/kumi/pack/builder.rb', line 194

def accessor_name_for(name)
  name.gsub(/[^a-zA-Z0-9_]/, "_").downcase
end

.assemble_pack(module_id, ir, planning, bindings, inputs, _targets, include_ir) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/kumi/pack/builder.rb', line 62

def assemble_pack(module_id, ir, planning, bindings, inputs, _targets, include_ir)
  plan_obj = planning["plan"] || planning
  plan_declarations = plan_obj["declarations"] || {}

  pack = {
    "pack_version" => VERSION,
    "module_id" => module_id,
    "declarations" => extract_ops_by_decl_with_planning(ir, plan_declarations),
    "inputs" => extract_inputs(inputs),
    "bindings" => format_bindings_for_pack(bindings),
    "capabilities" => { "layout" => "nested_array" }
  }
  pack["ir_debug"] = ir if include_ir
  pack["hashes"] = compute_hashes(pack)
  pack
end

.build(schema:, out_dir:, targets: %w[ruby],, include_ir: false) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/kumi/pack/builder.rb', line 14

def build(schema:, out_dir:, targets: %w[ruby], include_ir: false)
  ir, planning, bindings, inputs, module_id = generate_artifacts(schema)
  FileUtils.mkdir_p(out_dir)
  write_json("#{out_dir}/irv2.json", ir)
  write_json("#{out_dir}/planning.json", planning)
  write_json("#{out_dir}/bindings.json", bindings)
  write_json("#{out_dir}/inputs.json", inputs)

  pack = assemble_pack(module_id, ir, planning, bindings, inputs, targets, include_ir)
  write_json("#{out_dir}/pack.json", pack)
  canonical_json(pack)
end

.build_for_golden(schema_path, golden_dir, targets: %w[ruby])) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/kumi/pack/builder.rb', line 33

def build_for_golden(schema_path, golden_dir, targets: %w[ruby])
  ir, planning, bindings, inputs, module_id = generate_artifacts(schema_path)

  targets.each do |target|
    pack = assemble_pack(module_id, ir, planning, bindings, inputs, [target], false)

    filename = targets.size == 1 ? "pack.json" : "pack_#{target}.json"
    pack_file = File.join(golden_dir, filename)
    File.write(pack_file, canonical_json(pack))
  end
end

.canonical_json(obj) ⇒ Object

Stable JSON for hashing



210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/kumi/pack/builder.rb', line 210

def canonical_json(obj)
  case obj
  when Hash
    "{#{obj.keys.map(&:to_s).sort.map { |k| "\"#{k}\":#{canonical_json(obj[k])}" }.join(',')}}"
  when Array
    "[#{obj.map { |v| canonical_json(v) }.join(',')}]"
  when String   then JSON.generate(obj)
  when Numeric  then obj.to_s
  when true, false then obj.to_s
  when NilClass then "null"
  else JSON.generate(obj)
  end
end

.compute_hashes(pack) ⇒ Object

Compute canonical section hashes (sorted-key JSON)



203
204
205
206
207
# File 'lib/kumi/pack/builder.rb', line 203

def compute_hashes(pack)
  keys = %w[declarations inputs bindings]
  keys << "ir_debug" if pack.key?("ir_debug")
  keys.to_h { |k| [k, sha256(pack[k])] }
end

.extract_inputs(input_table) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/kumi/pack/builder.rb', line 127

def extract_inputs(input_table)
  # input_table is already in the new format with navigation_steps
  input_table.map do |entry|
    path_fqn = entry.path_fqn
    navigation_steps = stringify_keys(entry.navigation_steps)

    {
      "name" => path_fqn,
      "path_fqn" => path_fqn,
      "axes" => entry.axes.map(&:to_s),
      "dtype" => entry.dtype.to_s,
      "accessor_name" => accessor_name_for(path_fqn),
      "navigation_steps" => navigation_steps
    }
  end
end

.extract_ops_by_decl(ir) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/kumi/pack/builder.rb', line 108

def extract_ops_by_decl(ir)
  declarations = ir["declarations"] || {}
  declarations.keys.sort.map do |name|
    d = declarations[name]
    ops = (d["operations"] || []).map do |op|
      {
        "id" => op["id"],
        "op" => op["op"] || op["kind"],
        "args" => op["args"] || [],
        "attrs" => op["attrs"] || {}
      }
    end
    result = { "name" => name, "operations" => ops }
    result["result_op_id"] = d["result"] if d.key?("result")
    result["axes"] = d["axes"] if d.key?("axes")
    result
  end
end

.extract_ops_by_decl_with_planning(ir, plan_declarations) ⇒ Object



79
80
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
# File 'lib/kumi/pack/builder.rb', line 79

def extract_ops_by_decl_with_planning(ir, plan_declarations)
  declarations = ir["declarations"] || {}
  declarations.keys.map do |name|
    d = declarations[name]
    ops = (d["operations"] || []).map do |op|
      {
        "id" => op["id"],
        "op" => op["op"] || op["kind"],
        "args" => op["args"] || [],
        "attrs" => op["attrs"] || {}
      }
    end

    # Start with IRV2 operations
    result = { "name" => name, "operations" => ops }
    result["result_op_id"] = d["result"] if d.key?("result")
    result["axes"] = d["axes"] if d.key?("axes")

    # Merge in planning metadata in sorted key order
    plan_data = plan_declarations[name] || {}
    plan_data.keys.sort.each do |key|
      # Don't override IRV2 fields, but add planning fields
      result[key] = plan_data[key] unless result.key?(key)
    end

    result
  end
end

.format_bindings_for_pack(bindings) ⇒ Object



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
# File 'lib/kumi/pack/builder.rb', line 144

def format_bindings_for_pack(bindings)
  return {} unless bindings.is_a?(Hash)

  formatted = {}
  target = bindings["target"]
  kernels_array = bindings["kernels"].map do |kernel_data|
    kernel_id = kernel_data["kernel_id"]
    impl = kernel_data["impl"]
    attrs = kernel_data["attrs"] || {}

    # Extract the function name from kernel_id (e.g., "core.add:ruby:v1" -> "core.add")
    fn_name = kernel_id.split(":").first
    # Format the implementation as proper Ruby lambda
    ruby_impl = format_ruby_lambda(impl)

    {
      "kernel_id" => fn_name,
      "impl" => ruby_impl,
      "attrs" => attrs
    }
  end
  formatted[target] = { "kernels" => kernels_array }

  formatted
end

.format_ruby_lambda(impl) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/kumi/pack/builder.rb', line 170

def format_ruby_lambda(impl)
  # Convert "(a, b)\n  a + b" to "->(a, b) { a + b }"
  lines = impl.strip.split("\n")
  if lines.length >= 2
    params = lines[0].strip.gsub(/^\(|\)$/, "") # Remove outer parentheses
    body = lines[1..-1].map(&:strip).join("; ")
    "->(#{params}) { #{body} }"
  else
    # Fallback for single line
    "->(#{impl.strip})"
  end
end

.generate_artifacts(schema_path) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/kumi/pack/builder.rb', line 45

def generate_artifacts(schema_path)
  schema, = Kumi::Frontends.load(path: schema_path)
  res = Kumi::Analyzer.analyze!(schema, side_tables: true)

  irv2 = stringify_keys(res.state[:irv2]) or raise "No IRV2"
  module_id = irv2["module"] || irv2["module_id"] || "kumi_module"

  require_relative "../codegen/planning"
  plan_bundle = Kumi::Codegen::Planning.from_ir(res.state[:irv2])
  planning = Kumi::Codegen::Planning.to_json(plan_bundle)

  bindings = stringify_keys(res.state[:binding_manifest] || {})
  inputs = stringify_keys(res.state[:input_table] || {})

  [irv2, planning, bindings, inputs, module_id]
end


27
28
29
30
31
# File 'lib/kumi/pack/builder.rb', line 27

def print(schema:, targets: %w[ruby], include_ir: false)
  ir, planning, bindings, inputs, module_id = generate_artifacts(schema)
  pack = assemble_pack(module_id, ir, planning, bindings, inputs, targets, include_ir)
  canonical_json(pack)
end

.sha256(obj) ⇒ Object



224
225
226
# File 'lib/kumi/pack/builder.rb', line 224

def sha256(obj)
  Digest::SHA256.hexdigest(canonical_json(obj))
end

.stringify_keys(obj) ⇒ Object



183
184
185
186
187
188
189
190
191
192
# File 'lib/kumi/pack/builder.rb', line 183

def stringify_keys(obj)
  case obj
  when Hash
    obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
  when Array
    obj.map { |v| stringify_keys(v) }
  else
    obj
  end
end

.write_json(file_path, data) ⇒ Object



198
199
200
# File 'lib/kumi/pack/builder.rb', line 198

def write_json(file_path, data)
  File.write(file_path, JSON.pretty_generate(data))
end