gisel 0.2.0__py3-none-any.whl

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.
gisel/dsl.py ADDED
@@ -0,0 +1,1469 @@
1
+ #!/usr/bin/env python3
2
+ """Spike: a free-form (C/Python-style) DSL for the GlobalISel legalizer core IR.
3
+
4
+ The point is not to add a second IR. The DSL is *sugar over the existing
5
+ schema-0.4 normal form*: it parses statement/infix syntax and (a) compiles it to the same
6
+ schema-0.4 normal form `llvm-tblgen -gen-legalizer-core` emits, and (b) emits
7
+ a self-contained core-IR `.td` file that the existing backends consume, so both
8
+ paths can be diffed field-by-field after stripping the per-rule bookkeeping
9
+ keys (origin / rule_index / ruleset / schema / def).
10
+
11
+ Subset supported here (enough to re-derive real X86/AArch64 rules):
12
+ rule <opcode>+ :
13
+ <stmt>
14
+ ...
15
+ stmt := legal|lower|custom|unsupported|libcall <pred>
16
+ | widen|narrow|more|fewer|bitcast ( <expr> , <expr> ) [ when <pred> ]
17
+ | scalarize ( <expr> ) [ when <pred> ]
18
+ | clamp_min ( INT , TYPE , <expr> )
19
+ | clamp_max ( INT , TYPE , <expr> ) # lanes: int or select(...)
20
+ pred := or of ( and of ( not | primary ) )
21
+ primary := true | ( pred ) | feature ( STR ) | in ( expr , [TYPE,...] | TYPE )
22
+ | expr ( ==|!=|<|<=|>|>= ) expr
23
+ expr := INT | TYPE | KIND | call ( ... )
24
+
25
+ Static checks: J1 sort, J2b comparison sorts, J3a action table, J3b slot
26
+ bounds (arity imported from GenericOpcodes.td via build_arity_manifest).
27
+ | select ( <expr> , <expr> , <expr> ) # int select
28
+ | select_type ( <expr> , TYPE , TYPE ) # type select (in ty(...))
29
+ """
30
+
31
+ import json
32
+ import os
33
+ import re
34
+ import subprocess
35
+ import sys
36
+ import tempfile
37
+
38
+ # ---- normal-form node builders (schema 0.4) -------------------------------
39
+
40
+ def _node(kind, **extra):
41
+ d = {"kind": kind}
42
+ d.update(extra)
43
+ return d
44
+
45
+ def slot(i): return _node("slot", index=i)
46
+ def int_(v): return _node("int", value=v)
47
+ def type_(v): return _node("type", value=v)
48
+ def kind_(name): return _node(name)
49
+ def leaf(name, args): return _node("leaf", name=name, args=args)
50
+ def call1(k, a): return _node(k, args=[a])
51
+ def call2(k, a, b): return _node(k, args=[a, b])
52
+ def op1(k): return _node(k) # zero-arg constructor, e.g. pow2
53
+
54
+ KIND_TAGS = {"vector": "kindVector", "scalable_vector": "kindScalableVector",
55
+ "scalar": "kindScalar", "pointer": "kindPointer",
56
+ "pointer_vector": "kindPointerVector"}
57
+ ORDERING_TAGS = {"ordering_unordered": "orderingUnordered",
58
+ "ordering_monotonic": "orderingMonotonic",
59
+ "ordering_acquire": "orderingAcquire",
60
+ "ordering_release": "orderingRelease",
61
+ "ordering_acquire_release": "orderingAcquireRelease",
62
+ "ordering_sequentially_consistent": "orderingSequentiallyConsistent"}
63
+ ZERO_ARG = dict(KIND_TAGS, **ORDERING_TAGS)
64
+ ZERO_ARG["pow2"] = "pow2"
65
+
66
+ UNARY_CALL = {"elem_of": "elemOf", "scalar_of": "scalarOf", "sort_of": "sortOf",
67
+ "num_elems": "numElems", "bits_of": "bitsOf", "size_of": "sizeOf",
68
+ "size_in_bytes_of": "sizeInBytesOf", "is_byte_sized": "isByteSized",
69
+ "addr_space": "addrSpace", "align_of": "alignOf",
70
+ "mem_slot": "memSlot", "mem_ty": "memTy", "mem_ordering": "memOrdering",
71
+ "mem_failure_ordering": "memFailureOrdering",
72
+ "is_bfloat16": "isBFloat16", "is_float16": "isFloat16",
73
+ "is_pow2": "isPow2", "replace": "replace", "pow2_ceil": "pow2Ceil",
74
+ "multiple_of": "multipleOf", "same_as": "sameAs",
75
+ "min_vector_size": "minVectorSize", "int_of_size": "intOfSize",
76
+ "ty": "ty", "pow2_min": "pow2Min"}
77
+ BINARY_CALL = {"vec_of": "vecOf", "scalar_or_vec_of": "scalarOrVecOf",
78
+ "elt_type_to": "eltTypeTo", "elt_size_to": "eltSizeTo",
79
+ "div_lanes": "divLanes", "mul": "mul", "urem": "urem",
80
+ "udiv": "udiv", "clamp": "clamp"}
81
+
82
+ # A canonical type literal: scalar (s8/i32/f16/bf16), pointer (p0:64/p0:*),
83
+ # fixed vector (v16s8, v2p0:*) or scalable vector (nxv16i8).
84
+ TYPE_TOKEN_RE = re.compile(
85
+ r"(?:(?:nx)?v\d+)?(?:s\d+|i\d+|f\d+|bf16|p\d+:\d+|p\d+:\*)")
86
+
87
+ # ---- tokenizer ------------------------------------------------------------
88
+
89
+ class Tok:
90
+ __slots__ = ("t", "v", "line")
91
+ def __init__(self, t, v, line):
92
+ self.t, self.v, self.line = t, v, line
93
+
94
+ def tokenize(text, line_map=None):
95
+ toks = []
96
+ lines = text.splitlines()
97
+ # indentation -> INDENT/DEDENT, Python-style
98
+ stack = [0]
99
+ for ln, raw in enumerate(lines, 1):
100
+ # line_map[k] is the .gisel line a snippet line came from
101
+ sl = line_map[ln - 1] if line_map and ln - 1 < len(line_map) else ln
102
+ if not raw.strip():
103
+ continue
104
+ indent = len(raw) - len(raw.lstrip(" "))
105
+ if indent > stack[-1]:
106
+ stack.append(indent); toks.append(Tok("INDENT", None, sl))
107
+ while indent < stack[-1]:
108
+ stack.pop(); toks.append(Tok("DEDENT", None, sl))
109
+ i = 0
110
+ line = raw.strip()
111
+ n = len(line)
112
+ while i < n:
113
+ c = line[i]
114
+ if c.isspace():
115
+ i += 1; continue
116
+ if line.startswith(("==", "!=", "<=", ">="), i):
117
+ toks.append(Tok("OP", line[i:i+2], sl)); i += 2; continue
118
+ if c in "<>":
119
+ toks.append(Tok("OP", c, sl)); i += 1; continue
120
+ if c in "(),[]:":
121
+ toks.append(Tok(c, c, sl)); i += 1; continue
122
+ if c == '"':
123
+ j = i + 1
124
+ while j < n and line[j] != '"':
125
+ j += 1
126
+ toks.append(Tok("STR", line[i+1:j], sl)); i = j + 1; continue
127
+ if c.isdigit():
128
+ j = i
129
+ while j < n and line[j].isdigit():
130
+ j += 1
131
+ toks.append(Tok("INT", int(line[i:j]), sl)); i = j; continue
132
+ if c.isalpha():
133
+ m = TYPE_TOKEN_RE.match(line, i)
134
+ if m and (m.end() == n or not (line[m.end()].isalnum()
135
+ or line[m.end()] == "_")):
136
+ toks.append(Tok("TYPE", m.group(), sl)); i = m.end(); continue
137
+ if c.isalpha() or c == "_":
138
+ j = i
139
+ while j < n and (line[j].isalnum() or line[j] == "_"):
140
+ j += 1
141
+ toks.append(Tok("ID", line[i:j], sl)); i = j; continue
142
+ raise ValueError(f"line {sl}: bad char {c!r}")
143
+ toks.append(Tok("NEWLINE", None, sl))
144
+ while len(stack) > 1:
145
+ stack.pop(); toks.append(Tok("DEDENT", None, None))
146
+ toks.append(Tok("EOF", None, None))
147
+ return toks
148
+
149
+ # ---- parser ---------------------------------------------------------------
150
+
151
+ class Parser:
152
+ def __init__(self, text, line_map=None):
153
+ self.toks = tokenize(text, line_map)
154
+ self.p = 0
155
+
156
+ def peek(self, t=None):
157
+ tok = self.toks[self.p]
158
+ if t is None or tok.t == t:
159
+ return tok
160
+ return None
161
+
162
+ def next(self):
163
+ tok = self.toks[self.p]
164
+ self.p += 1
165
+ return tok
166
+
167
+ def expect(self, t):
168
+ tok = self.next()
169
+ if tok.t != t:
170
+ raise ValueError(f"line {tok.line}: expected {t}, got {tok.t} ({tok.v!r})")
171
+ return tok
172
+
173
+ def parse(self):
174
+ out = []
175
+ while self.peek() and self.peek().t == "ID" and self.peek().v == "rule":
176
+ out.append(self.parse_ruleset())
177
+ return out
178
+
179
+ def parse_ruleset(self):
180
+ self.expect("ID") # 'rule'
181
+ opcodes = []
182
+ while self.peek() and self.peek().t == "ID":
183
+ opcodes.append(self.next().v)
184
+ self.expect(":")
185
+ self.expect("NEWLINE")
186
+ self.expect("INDENT")
187
+ stmts = []
188
+ while self.peek() and self.peek().t != "DEDENT" and self.peek().t != "EOF":
189
+ stmts.append(self.parse_stmt())
190
+ self.expect("NEWLINE")
191
+ self.expect("DEDENT")
192
+ return {"opcodes": opcodes, "stmts": stmts}
193
+
194
+ def parse_stmt(self):
195
+ kw = self.next()
196
+ assert kw.t == "ID", kw
197
+ k = kw.v
198
+ line = kw.line
199
+ if k in ("legal", "lower", "custom", "unsupported", "libcall"):
200
+ pred = self.parse_pred()
201
+ return {"op": k, "pred": pred, "_line": line}
202
+ if k in ("widen", "narrow", "more", "fewer", "bitcast"):
203
+ self.expect("(")
204
+ on = self.parse_expr()
205
+ self.expect(",")
206
+ to = self.parse_expr()
207
+ self.expect(")")
208
+ pred = None
209
+ if self.peek() and self.peek().t == "ID" and self.peek().v == "when":
210
+ self.next(); pred = self.parse_pred()
211
+ return {"op": k, "on": on, "to": to, "pred": pred, "_line": line}
212
+ if k == "scalarize":
213
+ self.expect("(")
214
+ on = self.parse_expr()
215
+ self.expect(")")
216
+ pred = None
217
+ if self.peek() and self.peek().t == "ID" and self.peek().v == "when":
218
+ self.next(); pred = self.parse_pred()
219
+ return {"op": k, "on": on, "to": None, "pred": pred, "_line": line}
220
+ if k in ("clamp_min", "clamp_max"):
221
+ self.expect("(")
222
+ idx = self.next().v; self.expect(",")
223
+ elt = self.next().v; self.expect(",")
224
+ lanes = self.parse_expr()
225
+ self.expect(")")
226
+ return {"op": k, "idx": idx, "elt": elt, "lanes": lanes,
227
+ "_line": line}
228
+ raise ValueError(f"line {kw.line}: unknown statement {k!r}")
229
+
230
+ # predicate grammar: or -> and -> not -> primary
231
+ def parse_pred(self):
232
+ # or -> and -> not -> primary; n-ary (flat), matching CoreAny/CoreAll
233
+ args = [self.parse_and()]
234
+ while self.peek() and self.peek().t == "ID" and self.peek().v == "or":
235
+ self.next(); args.append(self.parse_and())
236
+ return args[0] if len(args) == 1 else _node("any", args=args)
237
+
238
+ def parse_and(self):
239
+ args = [self.parse_not()]
240
+ while self.peek() and self.peek().t == "ID" and self.peek().v == "and":
241
+ self.next(); args.append(self.parse_not())
242
+ return args[0] if len(args) == 1 else _node("all", args=args)
243
+
244
+ def parse_not(self):
245
+ if self.peek() and self.peek().t == "ID" and self.peek().v == "not":
246
+ self.next()
247
+ return call1("not", self.parse_not())
248
+ return self.parse_primary()
249
+
250
+ def parse_primary(self):
251
+ if self.peek() and self.peek().t == "ID" and self.peek().v == "true":
252
+ self.next(); return leaf("true", [])
253
+ if self.peek("("):
254
+ self.next(); p = self.parse_pred(); self.expect(")"); return p
255
+ if self.peek() and self.peek().t == "ID" and self.peek().v == "feature":
256
+ self.next(); self.expect("("); st = self.next().v; self.expect(")")
257
+ return leaf("hasFeature", [st])
258
+ # explicit n-ary combinators (all_of/any_of) for arity-1 and readability
259
+ if self.peek() and self.peek().t == "ID" and self.peek().v in (
260
+ "in", "in_pairs", "in_triples", "in_mem_desc"):
261
+ return self.parse_membership()
262
+ if self.peek() and self.peek().t == "ID" and self.peek().v in (
263
+ "all_of", "any_of"):
264
+ return self.parse_combinator()
265
+ # Bool-returning observers can stand alone as a predicate primary
266
+ if self.peek() and self.peek().t == "ID" and self.peek().v in (
267
+ "is_bfloat16", "is_float16", "is_byte_sized", "is_pow2"):
268
+ return self.parse_expr()
269
+ # comparison
270
+ left = self.parse_expr()
271
+ tok = self.next()
272
+ if tok.t != "OP":
273
+ raise ValueError(f"line {tok.line}: expected comparison, got {tok.v!r}")
274
+ right = self.parse_expr()
275
+ return call2({"==": "eq", "!=": "ne", "<": "lt", "<=": "le",
276
+ ">": "gt", ">=": "ge"}[tok.v], left, right)
277
+
278
+ def parse_combinator(self):
279
+ name = self.next().v
280
+ self.expect("(")
281
+ args = []
282
+ while True:
283
+ args.append(self.parse_pred())
284
+ if not self.peek(","):
285
+ break
286
+ self.next()
287
+ self.expect(")")
288
+ return _node("all" if name == "all_of" else "any", args=args)
289
+
290
+ def parse_membership(self):
291
+ name = self.next().v
292
+ self.expect("(")
293
+ a = self.parse_expr()
294
+ self.expect(",")
295
+ if name == "in":
296
+ rhs = self.parse_set_or_type()
297
+ self.expect(")")
298
+ return call2("in", a, rhs)
299
+ b = self.parse_expr()
300
+ self.expect(",")
301
+ if name == "in_pairs":
302
+ st = self.parse_set()
303
+ self.expect(")")
304
+ return _node("inPairs", args=[a, b, st])
305
+ c = self.parse_expr()
306
+ self.expect(",")
307
+ st = self.parse_set()
308
+ self.expect(")")
309
+ if name == "in_triples":
310
+ return _node("inTriples", args=[a, b, c, st])
311
+ return _node("inMemDesc", args=[a, b, c, st])
312
+
313
+ def parse_set_or_type(self):
314
+ if self.peek("["):
315
+ return self.parse_set()
316
+ return type_(self.parse_type_value())
317
+
318
+ def parse_set(self):
319
+ self.expect("[")
320
+ if self.peek("["):
321
+ rows = []
322
+ while True:
323
+ self.expect("[")
324
+ row = []
325
+ while True:
326
+ row.append(self.parse_type_value())
327
+ if not self.peek(","):
328
+ break
329
+ self.next()
330
+ self.expect("]")
331
+ rows.append(row)
332
+ if not self.peek(","):
333
+ break
334
+ self.next()
335
+ self.expect("]")
336
+ return _node("set", of="Type", arity=len(rows[0]), elems=rows)
337
+ if self.peek().t == "ID" and self.peek().v == "mem_desc":
338
+ rows = []
339
+ while True:
340
+ self.next() # mem_desc
341
+ self.expect("(")
342
+ t0 = self.parse_type_value(); self.expect(",")
343
+ t1 = self.parse_type_value(); self.expect(",")
344
+ t2 = self.parse_type_value(); self.expect(",")
345
+ al = self.next().v; self.expect(")")
346
+ rows.append([t0, t1, t2, al])
347
+ if not self.peek(","):
348
+ break
349
+ self.next()
350
+ self.expect("]")
351
+ return _node("set", of="Type", arity=2, elems=rows)
352
+ elems = []
353
+ while True:
354
+ elems.append(self.parse_type_value())
355
+ if not self.peek(","):
356
+ break
357
+ self.next()
358
+ self.expect("]")
359
+ if len(elems) == 1:
360
+ return type_(elems[0])
361
+ return _node("set", of="Type", elems=elems)
362
+
363
+ def parse_type_value(self):
364
+ if self.peek().t == "TYPE":
365
+ return self.next().v
366
+ if self.peek().t == "ID" and self.peek().v == "select_type":
367
+ return self.parse_select_type()
368
+ if self.peek().t == "ID" and self.peek().v[:1].isupper():
369
+ # A named record from the target's scaffolding (a LegalizeNamedType,
370
+ # LegalizeSelectType or LegalizeCoreTypeSet). The .gisel source
371
+ # spells these by name; the generator keeps the name in the .td.
372
+ return self.next().v
373
+ raise ValueError(f"line {self.peek().line}: expected a type, got "
374
+ f"{self.peek().t} {self.peek().v!r}")
375
+
376
+ def parse_select_type(self):
377
+ self.next() # select_type
378
+ self.expect("(")
379
+ cond = self.parse_pred()
380
+ self.expect(",")
381
+ then = self.parse_type_value()
382
+ self.expect(",")
383
+ els = self.parse_type_value()
384
+ self.expect(")")
385
+ return {"select": {"cond": cond, "then": then, "else": els}}
386
+
387
+ def parse_expr(self):
388
+ tok = self.peek()
389
+ if tok.t == "INT":
390
+ self.next(); return int_(tok.v)
391
+ if tok.t == "TYPE":
392
+ self.next(); return type_(tok.v)
393
+ if tok.t == "ID" and tok.v[:1].isupper():
394
+ self.next(); return type_(tok.v)
395
+ if tok.t == "ID":
396
+ v = tok.v
397
+ if v in ZERO_ARG:
398
+ self.next(); return kind_(ZERO_ARG[v])
399
+ return self.parse_call()
400
+ raise ValueError(f"line {tok.line}: expected expr, got {tok.t} {tok.v!r}")
401
+
402
+ def parse_call(self):
403
+ name = self.next().v
404
+ self.expect("(")
405
+ if name == "feature":
406
+ st = self.next().v
407
+ self.expect(")")
408
+ return leaf("hasFeature", [st])
409
+ if name == "select":
410
+ cond = self.parse_pred(); self.expect(",")
411
+ a = self.parse_expr(); self.expect(",")
412
+ b = self.parse_expr(); self.expect(")")
413
+ return _node("select", args=[cond, a, b])
414
+ if name == "select_type":
415
+ cond = self.parse_pred(); self.expect(",")
416
+ t = self.parse_type_value(); self.expect(",")
417
+ e = self.parse_type_value(); self.expect(")")
418
+ return type_({"select": {"cond": cond, "then": t, "else": e}})
419
+ if name == "slot":
420
+ i = self.next().v
421
+ self.expect(")")
422
+ return slot(i)
423
+ args = []
424
+ if not self.peek(")"):
425
+ while True:
426
+ args.append(self.parse_expr())
427
+ if not self.peek(","):
428
+ break
429
+ self.next()
430
+ self.expect(")")
431
+ if name in UNARY_CALL:
432
+ if len(args) != 1:
433
+ raise ValueError(f"{name}: expects 1 argument, got {len(args)}")
434
+ return call1(UNARY_CALL[name], args[0])
435
+ if name in BINARY_CALL:
436
+ if len(args) != 2:
437
+ raise ValueError(f"{name}: expects 2 arguments, got {len(args)}")
438
+ return call2(BINARY_CALL[name], args[0], args[1])
439
+ raise ValueError(f"line {self.toks[self.p-2].line}: unknown constructor "
440
+ f"{name!r}")
441
+
442
+ # ---- compiler: AST -> normal form ----------------------------------------
443
+
444
+ def compile_rulesets(ast):
445
+ """Return list of rule dicts in schema-0.4 normal form (semantic fields)."""
446
+ out = []
447
+ for rs in ast:
448
+ for st in rs["stmts"]:
449
+ rule = compile_stmt(st)
450
+ if "_line" in st:
451
+ rule["_line"] = st["_line"]
452
+ out.append(rule)
453
+ return out
454
+
455
+ def type_idx_of(on):
456
+ """The type slot a rule mutates, from its observation (coreTypeIdxOf)."""
457
+ if on["kind"] == "slot":
458
+ return on["index"]
459
+ if on["kind"] == "elemOf":
460
+ return type_idx_of(on["args"][0])
461
+ if on["kind"] == "int":
462
+ return on["value"]
463
+ return 0
464
+
465
+
466
+ def compile_stmt(st):
467
+ op = st["op"]
468
+ pred = st.get("pred")
469
+ if op in ("legal", "lower", "custom", "unsupported", "libcall"):
470
+ action = {"legal": "Legal", "lower": "Lower", "custom": "Custom",
471
+ "unsupported": "Unsupported", "libcall": "Libcall"}[op]
472
+ return {"action": action, "type_idx": 0, "op": op,
473
+ "predicate": pred if pred is not None else leaf("true", [])}
474
+ if op in ("widen", "narrow", "more", "fewer", "bitcast"):
475
+ action = {"widen": "WidenScalar", "narrow": "NarrowScalar",
476
+ "more": "MoreElements", "fewer": "FewerElements",
477
+ "bitcast": "Bitcast"}[op]
478
+ return {"action": action, "type_idx": type_idx_of(st["on"]), "op": op,
479
+ "on": st["on"], "to": st["to"],
480
+ "predicate": pred if pred is not None else leaf("true", [])}
481
+ if op == "scalarize":
482
+ return {"action": "FewerElements", "type_idx": type_idx_of(st["on"]),
483
+ "op": "scalarize", "on": st["on"],
484
+ "predicate": pred if pred is not None else leaf("true", [])}
485
+ if op in ("clamp_min", "clamp_max"):
486
+ idx, elt, lanes = st["idx"], st["elt"], st["lanes"]
487
+ if op == "clamp_min":
488
+ to = call1("replace", call2("vecOf", lanes, call1("elemOf", slot(idx))))
489
+ cmp = call2("lt", call1("numElems", slot(idx)), lanes)
490
+ action = "MoreElements"; opname = "more"
491
+ else:
492
+ to = call1("replace", call2("scalarOrVecOf", lanes, call1("elemOf", slot(idx))))
493
+ cmp = call2("gt", call1("numElems", slot(idx)), lanes)
494
+ action = "FewerElements"; opname = "fewer"
495
+ fixed = [
496
+ call2("eq", call1("sortOf", slot(idx)), kind_("kindVector")),
497
+ call1("not", call2("eq", call1("sortOf", slot(idx)), kind_("kindScalableVector"))),
498
+ call2("eq", call1("elemOf", slot(idx)), call1("ty", type_(elt))),
499
+ cmp,
500
+ ]
501
+ guard = _node("all", args=fixed)
502
+ return {"action": action, "type_idx": int(idx), "op": opname,
503
+ "on": slot(idx), "to": to, "predicate": guard}
504
+ raise ValueError(f"unknown stmt {op!r}")
505
+
506
+ # ---- compare against real -gen-legalizer-core output ----------------------
507
+
508
+ STRIP_KEYS = {"schema", "ruleset", "rule_index", "origin", "def", "_line"}
509
+
510
+ def strip(rule):
511
+ return {k: v for k, v in rule.items() if k not in STRIP_KEYS}
512
+
513
+ def _llt_leaf(name):
514
+ """A non-vector canonical type name -> the LLT expression a
515
+ LegalizeNamedType carries. This is the direct inverse of
516
+ normalizeLlt() in LegalizerInfoEmitter.cpp: any expression that
517
+ normalizes back to `name` is faithful."""
518
+ if re.fullmatch(r"s\d+", name):
519
+ return f"LLT::scalar({name[1:]})"
520
+ if re.fullmatch(r"i\d+", name):
521
+ return f"LLT::integer({name[1:]})"
522
+ kinds = {"f16": "float16", "f32": "float32", "f64": "float64",
523
+ "f128": "float128", "bf16": "bfloat16"}
524
+ if name in kinds:
525
+ return f"LLT::{kinds[name]}()"
526
+ m = re.fullmatch(r"p(\d+):(\d+)", name)
527
+ if m:
528
+ return f"LLT::pointer({m.group(1)}, {m.group(2)})"
529
+ m = re.fullmatch(r"p(\d+):\*", name)
530
+ if m:
531
+ # A target-config-dependent pointer width. normalizeLlt maps any
532
+ # non-digit size back to `pN:*`, so this round-trips even though the
533
+ # original expression was target-specific.
534
+ return f"LLT::pointer({m.group(1)}, 0 ? 64 : 32)"
535
+ raise ValueError(f"unsupported type name {name!r}")
536
+
537
+
538
+ def _llt(name):
539
+ """Canonical type name -> LLT string, vectors included."""
540
+ m = re.fullmatch(r"((?:nx)?v)(\d+)(.+)", name)
541
+ if m:
542
+ head = ("LLT::scalable_vector" if m.group(1) == "nxv"
543
+ else "LLT::fixed_vector")
544
+ return f"{head}({m.group(2)}, {_llt_leaf(m.group(3))})"
545
+ return _llt_leaf(name)
546
+
547
+
548
+ class TdEmitter:
549
+ """Emit a self-contained core-IR .td file from schema-0.4 normal form.
550
+
551
+ Covers the whole vocabulary the X86/AArch64 corpora exercise, including the
552
+ tuple/memdesc membership sets and the legacy `nary` predicate-dag that a
553
+ LegalizeSelectType cond can still carry (predicateJson's escape path).
554
+ """
555
+
556
+ _OPS = {"legal": "CoreLegal", "lower": "CoreLower", "custom": "CoreCustom",
557
+ "unsupported": "CoreUnsupported", "libcall": "CoreLibcall"}
558
+ _ALWAYS = {"legal": "CoreAlwaysLegal", "lower": "CoreAlwaysLower",
559
+ "custom": "CoreAlwaysCustom",
560
+ "unsupported": "CoreAlwaysUnsupported",
561
+ "libcall": "CoreAlwaysLibcall"}
562
+ _LATTICE = {"widen": "CoreWiden", "narrow": "CoreNarrow", "more": "CoreMore",
563
+ "fewer": "CoreFewer", "bitcast": "CoreBitcast"}
564
+ _KIND = {"kindVector": "KindVector", "kindScalableVector": "KindScalableVector",
565
+ "kindScalar": "KindScalar", "kindPointer": "KindPointer",
566
+ "kindPointerVector": "KindPointerVector"}
567
+ _ORDERING = {"orderingUnordered": "OrderingUnordered",
568
+ "orderingMonotonic": "OrderingMonotonic",
569
+ "orderingAcquire": "OrderingAcquire",
570
+ "orderingRelease": "OrderingRelease",
571
+ "orderingAcquireRelease": "OrderingAcquireRelease",
572
+ "orderingSequentiallyConsistent": "OrderingSequentiallyConsistent"}
573
+ _CMP = {"eq": "CoreEq", "ne": "CoreNe", "lt": "CoreLt", "le": "CoreLe",
574
+ "gt": "CoreGt", "ge": "CoreGe"}
575
+ _UNARY = {"elemOf": "ElemOf", "scalarOf": "ScalarOf", "sortOf": "SortOf",
576
+ "numElems": "NumElems", "bitsOf": "BitsOf", "sizeOf": "SizeOf",
577
+ "sizeInBytesOf": "SizeInBytesOf", "isByteSized": "IsByteSized",
578
+ "addrSpace": "AddrSpace", "alignOf": "AlignOf",
579
+ "memSlot": "MemSlot", "memTy": "MemTy", "memOrdering": "MemOrdering",
580
+ "memFailureOrdering": "MemFailureOrdering",
581
+ "isBFloat16": "IsBFloat16", "isFloat16": "IsFloat16",
582
+ "isPow2": "CoreIsPow2", "replace": "Replace",
583
+ "pow2Ceil": "Pow2Ceil", "multipleOf": "MultipleOf",
584
+ "sameAs": "SameAs", "minVectorSize": "MinVectorSize",
585
+ "intOfSize": "IntOfSize"}
586
+ _BINARY = {"eltTypeTo": "EltTypeTo", "eltSizeTo": "EltSizeTo",
587
+ "divLanes": "DivLanes", "mul": "CoreMul", "urem": "CoreURem",
588
+ "udiv": "CoreUDiv"}
589
+
590
+ def __init__(self, rulesets):
591
+ # rulesets: [{"def": str, "opcodes": [str], "rules": [rule, ...]}, ...]
592
+ self.rulesets = rulesets
593
+ self.features = {} # cond string -> record name
594
+ self.pred_dags = {} # reconstructed dag text -> record name
595
+ self.types = {} # canonical type name -> record name
596
+ self.selects = {} # (cond, then, else) -> record name (children first)
597
+
598
+ def _feat(self, s):
599
+ if s not in self.features:
600
+ self.features[s] = f"F_{len(self.features)}"
601
+ return self.features[s]
602
+
603
+ def _type(self, name):
604
+ if name not in self.types:
605
+ self.types[name] = f"T_{len(self.types)}"
606
+ return self.types[name]
607
+
608
+ def _type_value(self, v):
609
+ """A type value (str or nested select dict) -> a record name."""
610
+ if isinstance(v, str):
611
+ return self._type(v)
612
+ sel = v["select"]
613
+ cond = self._select_cond(sel["cond"])
614
+ then = self._type_value(sel["then"])
615
+ els = self._type_value(sel["else"])
616
+ key = (cond, then, els)
617
+ if key not in self.selects:
618
+ self.selects[key] = f"ST_{len(self.selects)}"
619
+ return self.selects[key]
620
+
621
+ def _select_cond(self, cond):
622
+ """A select-type guard -> a LegalizeBoolPredicate record name."""
623
+ k = cond["kind"]
624
+ if k == "leaf" and cond["name"] == "hasFeature":
625
+ return self._feat(cond["args"][0])
626
+ if k == "nary":
627
+ return self._pred_dag(cond)
628
+ raise ValueError(f"unsupported select cond {cond!r}")
629
+
630
+ def _pred_dag(self, node):
631
+ text = self._pred_dag_text(node)
632
+ if text not in self.pred_dags:
633
+ self.pred_dags[text] = f"PD_{len(self.pred_dags)}"
634
+ return self.pred_dags[text]
635
+
636
+ def _pred_dag_text(self, node):
637
+ k = node["kind"]
638
+ if k == "leaf" and node["name"] == "hasFeature":
639
+ return self._feat(node["args"][0])
640
+ if k == "nary":
641
+ return (f"({node['op']} "
642
+ + ", ".join(self._pred_dag_text(a) for a in node["args"])
643
+ + ")")
644
+ if k == "not":
645
+ # legacy singular form used by the nary escape path
646
+ return f"(not {self._pred_dag_text(node['arg'])})"
647
+ raise ValueError(f"unsupported predicate-dag node {node!r}")
648
+
649
+ def _set(self, n):
650
+ arity = n.get("arity", 1)
651
+ elems = n["elems"]
652
+ if not elems:
653
+ raise ValueError("empty set literal")
654
+ if arity == 1:
655
+ return "[" + ", ".join(self._type_value(e) for e in elems) + "]"
656
+ # memdesc: {Type0, Type1, MemTy, Align}; arity counts the type slots (2).
657
+ if arity == 2 and len(elems[0]) == 4:
658
+ return ("[" + ", ".join(
659
+ f"LegalizeMemDesc<{self._type_value(e[0])}, {self._type_value(e[1])}, "
660
+ f"{self._type_value(e[2])}, {e[3]}>" for e in elems) + "]")
661
+ # pair (arity 2) or triple (arity 3): a list of lists
662
+ return ("[" + ", ".join(
663
+ "[" + ", ".join(self._type_value(x) for x in e) + "]" for e in elems) + "]")
664
+
665
+ def dag(self, n):
666
+ k = n["kind"]
667
+ if k == "int":
668
+ return str(n["value"])
669
+ if k == "slot":
670
+ return f"(Slot {n['index']})"
671
+ if k == "type":
672
+ return self._type_value(n["value"])
673
+ if k in self._KIND:
674
+ return self._KIND[k]
675
+ if k in self._ORDERING:
676
+ return self._ORDERING[k]
677
+ if k == "leaf":
678
+ if n["name"] == "true":
679
+ return "(CoreTrue)"
680
+ if n["name"] == "hasFeature":
681
+ return self._feat(n["args"][0])
682
+ raise ValueError(n)
683
+ if k == "set":
684
+ return self._set(n)
685
+ if k == "ty":
686
+ return f"(Ty {self.dag(n['args'][0])})"
687
+ if k in self._UNARY:
688
+ return f"({self._UNARY[k]} {self.dag(n['args'][0])})"
689
+ if k in self._BINARY:
690
+ return (f"({self._BINARY[k]} {self.dag(n['args'][0])}, "
691
+ f"{self.dag(n['args'][1])})")
692
+ if k == "vecOf":
693
+ return f"(VecOf {self.dag(n['args'][0])}, {self.dag(n['args'][1])})"
694
+ if k == "scalarOrVecOf":
695
+ return f"(ScalarOrVecOf {self.dag(n['args'][0])}, {self.dag(n['args'][1])})"
696
+ if k == "pow2":
697
+ return "(Pow2)"
698
+ if k == "pow2Min":
699
+ return f"(Pow2Min {self.dag(n['args'][0])})"
700
+ if k == "clamp":
701
+ return f"(Clamp {self.dag(n['args'][0])}, {self.dag(n['args'][1])})"
702
+ if k in self._CMP:
703
+ return f"({self._CMP[k]} {self.dag(n['args'][0])}, {self.dag(n['args'][1])})"
704
+ if k == "not":
705
+ return f"(CoreNot {self.dag(n['args'][0])})"
706
+ if k in ("all", "any"):
707
+ c = "CoreAll" if k == "all" else "CoreAny"
708
+ return f"({c} " + ", ".join(self.dag(a) for a in n["args"]) + ")"
709
+ if k == "in":
710
+ return f"(CoreIn {self.dag(n['args'][0])}, {self.dag(n['args'][1])})"
711
+ if k == "inPairs":
712
+ a = n["args"]
713
+ return f"(CoreInPairs {self.dag(a[0])}, {self.dag(a[1])}, {self.dag(a[2])})"
714
+ if k == "inTriples":
715
+ a = n["args"]
716
+ return (f"(CoreInTriples {self.dag(a[0])}, {self.dag(a[1])}, "
717
+ f"{self.dag(a[2])}, {self.dag(a[3])})")
718
+ if k == "inMemDesc":
719
+ a = n["args"]
720
+ return (f"(CoreInMemDesc {self.dag(a[0])}, {self.dag(a[1])}, "
721
+ f"{self.dag(a[2])}, {self.dag(a[3])})")
722
+ if k == "select":
723
+ return (f"(CoreSelect {self.dag(n['args'][0])}, "
724
+ f"{self.dag(n['args'][1])}, {self.dag(n['args'][2])})")
725
+ if k == "nary":
726
+ return self._pred_dag(n)
727
+ raise ValueError(f"unknown node kind {k!r}")
728
+
729
+ def rule(self, r):
730
+ op, pred = r["op"], r["predicate"]
731
+ is_true = (pred.get("kind") == "leaf" and pred.get("name") == "true")
732
+ if op in self._OPS:
733
+ # An unconditional terminal is the `CoreAlways*` def: the schema
734
+ # serializes an unset guard as the true leaf, while
735
+ # `CoreLibcall<(CoreTrue)>` would serialize as {"kind":"true"}.
736
+ return self._ALWAYS[op] if is_true else f"{self._OPS[op]}<{self.dag(pred)}>"
737
+ if op in self._LATTICE:
738
+ if op == "widen" and r["to"].get("kind") == "int":
739
+ # dag params cannot take a bare int; the schema gives widen one
740
+ # dedicated class for the int target (CoreWidenPow2OrMinSize).
741
+ return (f"CoreWidenPow2OrMinSize<{self.dag(r['on'])}, "
742
+ f"{r['to']['value']}>")
743
+ c = self._LATTICE[op]
744
+ on, to = self.dag(r["on"]), self.dag(r["to"])
745
+ if is_true:
746
+ return f"{c}<{on}, {to}>"
747
+ return f"{c}If<{on}, {to}, {self.dag(pred)}>"
748
+ if op == "scalarize":
749
+ on = self.dag(r["on"])
750
+ if is_true:
751
+ return f"CoreScalarize<{on}>"
752
+ return f"CoreScalarizeIf<{on}, {self.dag(pred)}>"
753
+ raise ValueError(r)
754
+
755
+ def emit(self):
756
+ rendered = [(rs, [self.rule(r) for r in rs["rules"]])
757
+ for rs in self.rulesets]
758
+ lines = ['include "llvm/Target/LegalizerInfo.td"',
759
+ 'include "llvm/Target/Target.td"', ""]
760
+ for s, name in self.features.items():
761
+ lines.append(f'def {name} : LegalizeFeature<"{s}">;')
762
+ for text, name in self.pred_dags.items():
763
+ lines.append(f'def {name} : LegalizePredicateDag<{text}>;')
764
+ for tname, name in self.types.items():
765
+ lines.append(f'def {name} : LegalizeNamedType<"{_llt(tname)}">;')
766
+ for (cond, then, els), name in self.selects.items():
767
+ lines.append(f'def {name} : LegalizeSelectType<[{cond}], [{then}], {els}>;')
768
+ lines.append("")
769
+ for rs, texts in rendered:
770
+ op = ", ".join(f'"{o}"' for o in rs["opcodes"])
771
+ lines.append(f'def {rs["def"]} : LegalizeRuleSet<[{op}]> {{')
772
+ lines.append(" let Rules = [")
773
+ for t in texts:
774
+ lines.append(f" {t},")
775
+ lines.append(" ];")
776
+ lines.append("}")
777
+ lines.append("")
778
+ return "\n".join(lines) + "\n"
779
+
780
+
781
+ # ---- static checks (J1 sort / J2b compare / J3a action / J3b slot bound) --
782
+
783
+ COMPARE_SORTS = {"Int", "Kind", "Ordering", "Type"}
784
+ KIND_ORDERING = {"orderingUnordered", "orderingMonotonic", "orderingAcquire",
785
+ "orderingRelease", "orderingAcquireRelease",
786
+ "orderingSequentiallyConsistent"}
787
+ # admissible target sorts per lattice operator (action table, J3a)
788
+ TARGET_SORTS = {"widen": {"Type", "Int"}, "narrow": {"Type", "Int"},
789
+ "more": {"Int", "Type"}, "fewer": {"Int", "Type"},
790
+ "bitcast": {"Type"}}
791
+
792
+ def _tc(node, arity, errs):
793
+ """Type-check one normal-form node, return its sort (J1 / J2b).
794
+
795
+ The sorts are the ones the evaluator distinguishes at run time
796
+ (gisel/eval.py value()/bool_value()); MemDesc is the only one that never
797
+ reaches a comparison.
798
+ """
799
+ k = node["kind"]
800
+ args = node.get("args", [])
801
+
802
+ def tc(n):
803
+ return _tc(n, arity, errs)
804
+
805
+ def need(i, sort):
806
+ got = tc(args[i])
807
+ if got != sort:
808
+ errs.append(f"{k}: arg {i} sort mismatch (got {got}, want {sort})")
809
+ return got
810
+
811
+ def need_any(i, sorts):
812
+ got = tc(args[i])
813
+ if got not in sorts:
814
+ errs.append(f"{k}: arg {i} sort mismatch (got {got}, want {sorts})")
815
+ return got
816
+
817
+ if k == "int":
818
+ return "Int"
819
+ if k == "true":
820
+ return "Bool"
821
+ if k == "slot":
822
+ if node["index"] >= arity:
823
+ errs.append(f"(Slot {node['index']}) out of range: opcode has {arity} slot(s)")
824
+ return "Type"
825
+ if k == "type":
826
+ return "Type"
827
+ if k == "set":
828
+ return "Set"
829
+ if k == "leaf":
830
+ return "Bool"
831
+ if k in ("kindVector", "kindScalableVector", "kindScalar", "kindPointer",
832
+ "kindPointerVector"):
833
+ return "Kind"
834
+ if k in KIND_ORDERING:
835
+ return "Ordering"
836
+ if k == "memSlot":
837
+ need(0, "Int")
838
+ return "MemDesc"
839
+ if k == "sameAs":
840
+ need(0, "Int")
841
+ return "Type"
842
+ if k in ("elemOf", "scalarOf", "replace", "ty"):
843
+ need(0, "Type")
844
+ return "Type"
845
+ if k == "sortOf":
846
+ need(0, "Type")
847
+ return "Kind"
848
+ if k in ("numElems", "bitsOf"):
849
+ need(0, "Type")
850
+ return "Int"
851
+ if k == "sizeOf":
852
+ need_any(0, {"Type", "MemDesc"})
853
+ return "Int"
854
+ if k == "sizeInBytesOf":
855
+ need(0, "Type")
856
+ return "Int"
857
+ if k == "alignOf":
858
+ need(0, "MemDesc")
859
+ return "Int"
860
+ if k == "addrSpace":
861
+ need(0, "Type")
862
+ return "Int"
863
+ if k == "memTy":
864
+ need(0, "MemDesc")
865
+ return "Type"
866
+ if k in ("memOrdering", "memFailureOrdering"):
867
+ need(0, "MemDesc")
868
+ return "Ordering"
869
+ if k in ("mul", "urem", "udiv"):
870
+ need(0, "Int"); need(1, "Int")
871
+ return "Int"
872
+ if k == "pow2Ceil":
873
+ need(0, "Int")
874
+ return "Int"
875
+ if k == "intOfSize":
876
+ need(0, "Int")
877
+ return "Type"
878
+ if k == "minVectorSize":
879
+ need(0, "Int")
880
+ return "Int"
881
+ if k in ("vecOf", "scalarOrVecOf"):
882
+ need(0, "Int"); need(1, "Type")
883
+ return "Type"
884
+ if k == "eltTypeTo":
885
+ need(0, "Type"); need(1, "Type")
886
+ return "Type"
887
+ if k in ("eltSizeTo", "divLanes"):
888
+ need(0, "Type"); need(1, "Int")
889
+ return "Type"
890
+ if k == "pow2":
891
+ return "Int"
892
+ if k == "pow2Min":
893
+ need(0, "Int")
894
+ return "Int"
895
+ if k == "select":
896
+ need(0, "Bool")
897
+ a, b = tc(args[1]), tc(args[2])
898
+ if a != b:
899
+ errs.append(f"select: branch sorts differ ({a} vs {b})")
900
+ elif a not in ("Int", "Type"):
901
+ errs.append(f"select: unsupported branch sort {a}")
902
+ return a
903
+ if k == "not":
904
+ sub = node.get("arg", args[0] if args else None)
905
+ if sub is None:
906
+ errs.append("not: missing operand")
907
+ elif tc(sub) != "Bool":
908
+ errs.append("not: operand must be Bool")
909
+ return "Bool"
910
+ if k in ("all", "any", "nary"):
911
+ if k == "nary" and node.get("op") not in ("all_of", "any_of"):
912
+ errs.append(f"nary: unknown op {node.get('op')!r}")
913
+ for i in range(len(args)):
914
+ need(i, "Bool")
915
+ return "Bool"
916
+ if k in ("eq", "ne", "lt", "le", "gt", "ge"):
917
+ a = tc(args[0]); b = tc(args[1])
918
+ if a != b:
919
+ errs.append(f"{k}: operand sorts differ ({a} vs {b})")
920
+ elif a not in COMPARE_SORTS:
921
+ errs.append(f"{k}: comparison over unsupported sort {a}")
922
+ return "Bool"
923
+ if k == "in":
924
+ if tc(args[0]) != "Type":
925
+ errs.append("in: left operand must be Type")
926
+ return "Bool"
927
+ if k == "inPairs":
928
+ need(0, "Type"); need(1, "Type"); need(2, "Set")
929
+ return "Bool"
930
+ if k == "inTriples":
931
+ for i in range(3):
932
+ need(i, "Type")
933
+ need(3, "Set")
934
+ return "Bool"
935
+ if k == "inMemDesc":
936
+ need(0, "Type"); need(1, "Type"); need(2, "MemDesc"); need(3, "Set")
937
+ return "Bool"
938
+ if k in ("isPow2", "isBFloat16", "isFloat16", "isByteSized"):
939
+ # A type predicate: isBFloat16 takes a type, isPow2 a byte count.
940
+ need_any(0, {"Type", "Int"})
941
+ return "Bool"
942
+ raise ValueError(f"typecheck: unknown node kind {k!r}")
943
+
944
+ def check_rule(rule, arity, errs):
945
+ op = rule["op"]
946
+ if op in ("legal", "lower", "custom", "unsupported", "libcall"):
947
+ if _tc(rule["predicate"], arity, errs) != "Bool":
948
+ errs.append(f"{op}: guard must be Bool")
949
+ elif op in TARGET_SORTS:
950
+ _tc(rule["on"], arity, errs)
951
+ ts = _tc(rule["to"], arity, errs)
952
+ if ts not in TARGET_SORTS[op]:
953
+ errs.append(f"{op}: target sort {ts} not in action table")
954
+ _tc(rule["predicate"], arity, errs)
955
+ elif op == "scalarize":
956
+ _tc(rule["on"], arity, errs)
957
+ else:
958
+ errs.append(f"unknown op {op!r}")
959
+
960
+ _TYPE_INDEX_RE = re.compile(r"\b(?:ptype|type)(\d+)\b")
961
+ _DECL_RE = re.compile(r"\b(class|def)\s+([A-Za-z_]\w*)\s*(?::\s*([^{;]*))?([;{])")
962
+
963
+
964
+ def _strip_td_comments(text):
965
+ """Drop // comments, keeping string literals intact."""
966
+ out = []
967
+ i, n = 0, len(text)
968
+ in_str = False
969
+ while i < n:
970
+ c = text[i]
971
+ if in_str:
972
+ out.append(c)
973
+ if c == "\\" and i + 1 < n:
974
+ out.append(text[i + 1])
975
+ i += 2
976
+ continue
977
+ if c == '"':
978
+ in_str = False
979
+ i += 1
980
+ continue
981
+ if c == '"':
982
+ in_str = True
983
+ out.append(c)
984
+ i += 1
985
+ continue
986
+ if text.startswith("//", i):
987
+ j = text.find("\n", i)
988
+ i = n if j < 0 else j
989
+ continue
990
+ out.append(c)
991
+ i += 1
992
+ return "".join(out)
993
+
994
+
995
+ def _parse_td_defs(text):
996
+ """name -> (bases, highest type operand index or None, variadic)."""
997
+ text = _strip_td_comments(text)
998
+ recs = {}
999
+ pos = 0
1000
+ while True:
1001
+ m = _DECL_RE.search(text, pos)
1002
+ if not m:
1003
+ break
1004
+ kind, name = m.group(1), m.group(2)
1005
+ bases, term = m.group(3) or "", m.group(4)
1006
+ body = ""
1007
+ end = m.end()
1008
+ if term == "{":
1009
+ depth, j = 0, m.end() - 1
1010
+ while j < len(text):
1011
+ if text[j] == "{":
1012
+ depth += 1
1013
+ elif text[j] == "}":
1014
+ depth -= 1
1015
+ if depth == 0:
1016
+ break
1017
+ j += 1
1018
+ body = text[m.end():j]
1019
+ end = j + 1
1020
+ prev = recs.get(name, ("class", (), None, False))
1021
+ idx = [int(x) for x in _TYPE_INDEX_RE.findall(body)]
1022
+ own = max(idx) if idx else prev[2]
1023
+ recs[name] = (kind, tuple(b.strip() for b in bases.split(",") if b.strip()),
1024
+ own, "variable_ops" in body or prev[3])
1025
+ pos = end
1026
+ return recs
1027
+
1028
+
1029
+ def _max_type_index(recs, name, seen=None):
1030
+ """Highest type-operand index for `name`, or None when unknowable.
1031
+
1032
+ A base class defined outside this file (GenericInstruction, ...) carries
1033
+ no type operands; a variadic instruction has no fixed arity.
1034
+ """
1035
+ seen = set() if seen is None else seen
1036
+ if name in seen:
1037
+ return None
1038
+ seen.add(name)
1039
+ rec = recs.get(name)
1040
+ if rec is None:
1041
+ return -1 # external base: no type operands
1042
+ _kind, bases, own, variadic = rec
1043
+ if variadic:
1044
+ return None
1045
+ best = own
1046
+ for b in bases:
1047
+ got = _max_type_index(recs, b, seen)
1048
+ if got is None:
1049
+ return None
1050
+ if best is None or got > best:
1051
+ best = got
1052
+ return -1 if best is None else best
1053
+
1054
+
1055
+ def build_arity_manifest(generic_td_path):
1056
+ """Per-opcode type-slot count from GenericOpcodes.td.
1057
+
1058
+ Handles both `def G_X : Class { ... }` and `def G_X : Class;`, walking the
1059
+ class chain (`class VectorReduction { type0, type1 }`). `None` means the
1060
+ count is not fixed (variadic intrinsics).
1061
+
1062
+ The previous regex silently attached the *next* def's body to `def X :
1063
+ Class;`; e.g. G_VECREDUCE_FADD inherited G_MEMCPY's type2 and was recorded
1064
+ as arity 3 instead of 2.
1065
+ """
1066
+ with open(generic_td_path) as f:
1067
+ recs = _parse_td_defs(f.read())
1068
+ out = {}
1069
+ for name, (kind, _, _, _) in recs.items():
1070
+ if kind != "def" or not name.startswith("G_"):
1071
+ continue
1072
+ idx = _max_type_index(recs, name)
1073
+ out[name] = None if idx is None else idx + 1
1074
+ return out
1075
+
1076
+
1077
+ def load_arity_manifest():
1078
+ """The bundled opcode table: lint works without an LLVM source tree."""
1079
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
1080
+ "data", "generic_opcodes.json")
1081
+ with open(path) as f:
1082
+ return json.load(f)["opcodes"]
1083
+
1084
+
1085
+ def check_rules(compiled, opcodes, manifest, errs):
1086
+ arities = [manifest.get(o) for o in opcodes]
1087
+ known = [a for a in arities if a is not None]
1088
+ arity = min(known) if known else None # multi-opcode takes the minimum (J3b)
1089
+ for r in compiled:
1090
+ check_rule(r, arity if arity is not None else 10**9, errs)
1091
+ return arity
1092
+
1093
+
1094
+ TBLGEN = "/home/ubuntu/llvm/builds/explore/bin/llvm-tblgen"
1095
+ INC = ["-I/home/ubuntu/llvm/trees/explore/llvm/include",
1096
+ "-I/home/ubuntu/llvm/builds/explore/include",
1097
+ "-I/home/ubuntu/llvm/trees/explore/llvm/lib/Target"]
1098
+
1099
+
1100
+ def roundtrip_corpus(name, core_json_path, target_dir):
1101
+ """baseline core.json -> reverse-emit .td -> tblgen -> normal form diff."""
1102
+ baseline = json.load(open(core_json_path))
1103
+ rulesets = [{"def": rs["def"], "opcodes": rs["opcodes"], "rules": rs["rules"]}
1104
+ for rs in baseline["rulesets"]]
1105
+ td = TdEmitter(rulesets).emit()
1106
+ d = tempfile.mkdtemp(prefix="dsl-rt-")
1107
+ path = os.path.join(d, "ReverseLegalizerInfo.td")
1108
+ with open(path, "w") as f:
1109
+ f.write(td)
1110
+ out = os.path.join(d, "out.core.json")
1111
+ inc = INC + [f"-I/home/ubuntu/llvm/trees/explore/llvm/lib/Target/{target_dir}"]
1112
+ r = subprocess.run([TBLGEN, "-gen-legalizer-core", *inc, path, "-o", out],
1113
+ capture_output=True, text=True)
1114
+ if r.returncode != 0:
1115
+ print(f"({name}) tblgen failed:\n{r.stderr}")
1116
+ print("emitted .td head:\n" + "\n".join(td.splitlines()[:40]))
1117
+ return 10**9
1118
+ gen = json.load(open(out))
1119
+ brs = {rs["def"]: rs for rs in baseline["rulesets"]}
1120
+ grs = {rs["def"]: rs for rs in gen["rulesets"]}
1121
+ total = diff = 0
1122
+ for defn, br in brs.items():
1123
+ if defn not in grs:
1124
+ print(f" {name}: missing ruleset {defn}")
1125
+ diff += 1
1126
+ continue
1127
+ gr = grs[defn]
1128
+ if br["opcodes"] != gr["opcodes"]:
1129
+ print(f" {name}:{defn} opcodes differ {br['opcodes']} vs {gr['opcodes']}")
1130
+ diff += 1
1131
+ brr, grr = br["rules"], gr["rules"]
1132
+ total += len(brr)
1133
+ if len(brr) != len(grr):
1134
+ print(f" {name}:{defn} rule count {len(brr)} vs {len(grr)}")
1135
+ diff += 1
1136
+ continue
1137
+ for i, (e, g) in enumerate(zip(brr, grr)):
1138
+ if strip(e) != strip(g):
1139
+ if diff < 3:
1140
+ print(f" {name}:{defn}#{i} DIFF")
1141
+ print(" oracle:", json.dumps(strip(e), sort_keys=True)[:400])
1142
+ print(" gen :", json.dumps(strip(g), sort_keys=True)[:400])
1143
+ diff += 1
1144
+ print(f"({name}) round-trip: {total} rules, {diff} diffs "
1145
+ f"{'ALL MATCH' if diff == 0 else 'SOME DIFF'}")
1146
+ return diff
1147
+
1148
+
1149
+
1150
+ # ---- free-form DSL source renderer (normal form -> DSL text) --------------
1151
+
1152
+ KIND_FROM = {"kindVector": "vector", "kindScalableVector": "scalable_vector",
1153
+ "kindScalar": "scalar", "kindPointer": "pointer",
1154
+ "kindPointerVector": "pointer_vector"}
1155
+ ORDERING_FROM = {"orderingUnordered": "ordering_unordered",
1156
+ "orderingMonotonic": "ordering_monotonic",
1157
+ "orderingAcquire": "ordering_acquire",
1158
+ "orderingRelease": "ordering_release",
1159
+ "orderingAcquireRelease": "ordering_acquire_release",
1160
+ "orderingSequentiallyConsistent": "ordering_sequentially_consistent"}
1161
+ UNARY_FROM = {"elemOf": "elem_of", "scalarOf": "scalar_of", "sortOf": "sort_of",
1162
+ "numElems": "num_elems", "bitsOf": "bits_of", "sizeOf": "size_of",
1163
+ "sizeInBytesOf": "size_in_bytes_of", "isByteSized": "is_byte_sized",
1164
+ "addrSpace": "addr_space", "alignOf": "align_of",
1165
+ "memSlot": "mem_slot", "memTy": "mem_ty", "memOrdering": "mem_ordering",
1166
+ "memFailureOrdering": "mem_failure_ordering",
1167
+ "isBFloat16": "is_bfloat16", "isFloat16": "is_float16",
1168
+ "isPow2": "is_pow2", "replace": "replace", "pow2Ceil": "pow2_ceil",
1169
+ "multipleOf": "multiple_of", "sameAs": "same_as",
1170
+ "minVectorSize": "min_vector_size", "intOfSize": "int_of_size",
1171
+ "ty": "ty", "pow2Min": "pow2_min"}
1172
+ BINARY_FROM = {"vecOf": "vec_of", "scalarOrVecOf": "scalar_or_vec_of",
1173
+ "eltTypeTo": "elt_type_to", "eltSizeTo": "elt_size_to",
1174
+ "divLanes": "div_lanes", "mul": "mul", "urem": "urem",
1175
+ "udiv": "udiv", "clamp": "clamp"}
1176
+ CMP_OPS = {"eq": "==", "ne": "!=", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
1177
+
1178
+
1179
+ def render_type_value(v):
1180
+ if isinstance(v, str):
1181
+ return v
1182
+ sel = v["select"]
1183
+ return (f"select_type({render_pred(sel['cond'])}, "
1184
+ f"{render_type_value(sel['then'])}, "
1185
+ f"{render_type_value(sel['else'])})")
1186
+
1187
+
1188
+ def render_set(n):
1189
+ arity = n.get("arity", 1)
1190
+ elems = n["elems"]
1191
+ if arity == 1:
1192
+ return "[" + ", ".join(render_type_value(e) for e in elems) + "]"
1193
+ if arity == 2 and len(elems[0]) == 4:
1194
+ return ("[" + ", ".join(
1195
+ f"mem_desc({render_type_value(e[0])}, {render_type_value(e[1])}, "
1196
+ f"{render_type_value(e[2])}, {e[3]})" for e in elems) + "]")
1197
+ return ("[" + ", ".join(
1198
+ "[" + ", ".join(render_type_value(x) for x in e) + "]"
1199
+ for e in elems) + "]")
1200
+
1201
+
1202
+ def render_expr(n):
1203
+ k = n["kind"]
1204
+ if k == "int":
1205
+ return str(n["value"])
1206
+ if k == "slot":
1207
+ return f"slot({n['index']})"
1208
+ if k == "type":
1209
+ return render_type_value(n["value"])
1210
+ if k in KIND_FROM:
1211
+ return KIND_FROM[k]
1212
+ if k in ORDERING_FROM:
1213
+ return ORDERING_FROM[k]
1214
+ if k == "pow2":
1215
+ return "pow2"
1216
+ if k in UNARY_FROM:
1217
+ return f"{UNARY_FROM[k]}({render_expr(n['args'][0])})"
1218
+ if k in BINARY_FROM:
1219
+ return (f"{BINARY_FROM[k]}({render_expr(n['args'][0])}, "
1220
+ f"{render_expr(n['args'][1])})")
1221
+ if k == "select":
1222
+ return (f"select({render_pred(n['args'][0])}, "
1223
+ f"{render_expr(n['args'][1])}, {render_expr(n['args'][2])})")
1224
+ raise ValueError(f"cannot render expr kind {k!r}")
1225
+
1226
+
1227
+ def render_pred(n):
1228
+ k = n["kind"]
1229
+ if k == "leaf":
1230
+ if n["name"] == "true":
1231
+ return "true"
1232
+ if n["name"] == "hasFeature":
1233
+ return f'feature("{n["args"][0]}")'
1234
+ raise ValueError(n)
1235
+ if k == "all":
1236
+ if len(n["args"]) == 1:
1237
+ return "all_of(" + render_pred(n["args"][0]) + ")"
1238
+ return "(" + " and ".join(render_pred(a) for a in n["args"]) + ")"
1239
+ if k == "any":
1240
+ if len(n["args"]) == 1:
1241
+ return "any_of(" + render_pred(n["args"][0]) + ")"
1242
+ return "(" + " or ".join(render_pred(a) for a in n["args"]) + ")"
1243
+ if k == "not":
1244
+ return "not " + render_pred(n["args"][0])
1245
+ if k in CMP_OPS:
1246
+ return (f"{render_expr(n['args'][0])} {CMP_OPS[k]} "
1247
+ f"{render_expr(n['args'][1])}")
1248
+ if k == "in":
1249
+ return (f"in({render_expr(n['args'][0])}, "
1250
+ f"{render_set_or_type(n['args'][1])})")
1251
+ if k == "inPairs":
1252
+ a = n["args"]
1253
+ return (f"in_pairs({render_expr(a[0])}, {render_expr(a[1])}, "
1254
+ f"{render_set(a[2])})")
1255
+ if k == "inTriples":
1256
+ a = n["args"]
1257
+ return (f"in_triples({render_expr(a[0])}, {render_expr(a[1])}, "
1258
+ f"{render_expr(a[2])}, {render_set(a[3])})")
1259
+ if k == "inMemDesc":
1260
+ a = n["args"]
1261
+ return (f"in_mem_desc({render_expr(a[0])}, {render_expr(a[1])}, "
1262
+ f"{render_expr(a[2])}, {render_set(a[3])})")
1263
+ if k == "nary":
1264
+ # legacy all_of/any_of predicate-dag -> core and/or (same meaning)
1265
+ op = n["op"]
1266
+ parts = [render_pred_legacy(a) for a in n["args"]]
1267
+ join = " and " if op == "all_of" else " or "
1268
+ return "(" + join.join(parts) + ")"
1269
+ if k in UNARY_FROM:
1270
+ return render_expr(n)
1271
+ raise ValueError(f"cannot render pred kind {k!r}")
1272
+
1273
+
1274
+ def render_pred_legacy(n):
1275
+ k = n["kind"]
1276
+ if k == "leaf":
1277
+ return render_pred(n)
1278
+ if k == "not":
1279
+ return "not " + render_pred_legacy(n["arg"])
1280
+ if k == "nary":
1281
+ op = n["op"]
1282
+ join = " and " if op == "all_of" else " or "
1283
+ return "(" + join.join(render_pred_legacy(a) for a in n["args"]) + ")"
1284
+ raise ValueError(n)
1285
+
1286
+
1287
+ def render_set_or_type(n):
1288
+ if n["kind"] == "type":
1289
+ return render_type_value(n["value"])
1290
+ return render_set(n)
1291
+
1292
+
1293
+ def render_rule(r):
1294
+ op = r["op"]
1295
+ pred = r["predicate"]
1296
+ is_true = pred.get("kind") == "leaf" and pred.get("name") == "true"
1297
+ if op in ("legal", "lower", "custom", "unsupported", "libcall"):
1298
+ return f"{op} {render_pred(pred)}"
1299
+ if op in ("widen", "narrow", "more", "fewer", "bitcast"):
1300
+ text = f"{op}({render_expr(r['on'])}, {render_expr(r['to'])})"
1301
+ if not is_true:
1302
+ text += f" when {render_pred(pred)}"
1303
+ return text
1304
+ if op == "scalarize":
1305
+ text = f"scalarize({render_expr(r['on'])})"
1306
+ if not is_true:
1307
+ text += f" when {render_pred(pred)}"
1308
+ return text
1309
+ raise ValueError(r)
1310
+
1311
+
1312
+ def render_ruleset(rs):
1313
+ lines = [f"rule {' '.join(rs['opcodes'])}:"]
1314
+ for r in rs["rules"]:
1315
+ lines.append(f" {render_rule(r)}")
1316
+ return "\n".join(lines)
1317
+
1318
+
1319
+ def normalize_nary(n):
1320
+ """Legacy nary/not(arg) predicate-dag -> core all/any/not(args) form."""
1321
+ if isinstance(n, dict):
1322
+ k = n.get("kind")
1323
+ if k == "nary":
1324
+ return _node("all" if n["op"] == "all_of" else "any",
1325
+ args=[normalize_nary(a) for a in n["args"]])
1326
+ if k == "not" and "arg" in n and "args" not in n:
1327
+ return _node("not", args=[normalize_nary(n["arg"])])
1328
+ return {kk: normalize_nary(vv) for kk, vv in n.items()}
1329
+ if isinstance(n, list):
1330
+ return [normalize_nary(x) for x in n]
1331
+ return n
1332
+
1333
+
1334
+ def forward_roundtrip(name, core_json_path):
1335
+ """baseline core.json -> free-form DSL source -> parse -> normal form diff."""
1336
+ baseline = json.load(open(core_json_path))
1337
+ total = diff = 0
1338
+ src_parts = []
1339
+ for rs in baseline["rulesets"]:
1340
+ src_parts.append(render_ruleset(rs))
1341
+ src = "\n\n".join(src_parts) + "\n"
1342
+ ast = Parser(src).parse()
1343
+ compiled = [normalize_nary(r) for r in compile_rulesets(ast)]
1344
+ expect = [normalize_nary(r) for rs in baseline["rulesets"]
1345
+ for r in rs["rules"]]
1346
+ total = len(expect)
1347
+ if len(compiled) != len(expect):
1348
+ print(f"({name}) forward: rule count {len(compiled)} vs {len(expect)}")
1349
+ diff += abs(len(compiled) - len(expect))
1350
+ for i, (e, g) in enumerate(zip(expect, compiled)):
1351
+ if strip(e) != strip(g):
1352
+ if diff < 3:
1353
+ print(f"({name}) forward #{i} DIFF")
1354
+ print(" oracle:", json.dumps(strip(e), sort_keys=True)[:400])
1355
+ print(" gen :", json.dumps(strip(g), sort_keys=True)[:400])
1356
+ diff += 1
1357
+ print(f"({name}) forward round-trip: {total} rules, {diff} diffs "
1358
+ f"{'ALL MATCH' if diff == 0 else 'SOME DIFF'}")
1359
+ return diff
1360
+
1361
+
1362
+
1363
+ def main():
1364
+ src = r'''
1365
+ rule G_ADD G_SUB:
1366
+ legal in(slot(0), [s8, s16, s32])
1367
+ legal feature("Subtarget.is64Bit()") and in(slot(0), s64)
1368
+ legal feature("Subtarget.hasSSE2()") and in(slot(0), [v16s8, v8s16, v4s32, v2s64])
1369
+ legal feature("Subtarget.hasAVX2()") and in(slot(0), [v32s8, v16s16, v8s32, v4s64])
1370
+ legal feature("Subtarget.hasAVX512()") and in(slot(0), [v16s32, v8s64])
1371
+ legal feature("Subtarget.hasAVX512() && Subtarget.hasBWI()") and in(slot(0), [v64s8, v32s16])
1372
+ clamp_min(0, s8, 16)
1373
+ clamp_min(0, s16, 8)
1374
+ clamp_min(0, s32, 4)
1375
+ clamp_min(0, s64, 2)
1376
+ clamp_max(0, s8, select(feature("Subtarget.hasAVX512() && Subtarget.hasBWI()"), 64, select(feature("Subtarget.hasAVX2()"), 32, 16)))
1377
+ clamp_max(0, s16, select(feature("Subtarget.hasAVX512() && Subtarget.hasBWI()"), 32, select(feature("Subtarget.hasAVX2()"), 16, 8)))
1378
+ clamp_max(0, s32, select(feature("Subtarget.hasAVX512()"), 16, select(feature("Subtarget.hasAVX2()"), 8, 4)))
1379
+ clamp_max(0, s64, select(feature("Subtarget.hasAVX512()"), 8, select(feature("Subtarget.hasAVX2()"), 4, 2)))
1380
+ widen(slot(0), pow2_min(32))
1381
+ widen(slot(0), ty(s8))
1382
+ narrow(slot(0), ty(select_type(feature("Subtarget.is64Bit()"), s64, s32)))
1383
+ scalarize(slot(0))
1384
+ '''
1385
+ ast = Parser(src).parse()
1386
+ compiled = compile_rulesets(ast)
1387
+
1388
+ x86 = json.load(open("/home/ubuntu/legalizer-baseline/x86.core.json"))
1389
+ oracle = next(r for r in x86["rulesets"] if r["def"] == "X86_ADD")["rules"]
1390
+ assert len(compiled) == len(oracle), (len(compiled), len(oracle))
1391
+
1392
+ # (a) direct compile -> normal form, vs baseline
1393
+ ok1 = all(strip(e) == g for e, g in zip(oracle, compiled))
1394
+ print(f"(a) direct compile: {len(compiled)} rules "
1395
+ f"{'ALL MATCH' if ok1 else 'SOME DIFF'}")
1396
+
1397
+ # (b) free-form -> .td -> llvm-tblgen -gen-legalizer-core -> normal form
1398
+ td = TdEmitter([{"def": "X86_ADD", "opcodes": ["G_ADD", "G_SUB"],
1399
+ "rules": compiled}]).emit()
1400
+ d = tempfile.mkdtemp(prefix="dsl-td-")
1401
+ path = os.path.join(d, "X86LegalizerInfo.td")
1402
+ with open(path, "w") as f:
1403
+ f.write(td)
1404
+ out = os.path.join(d, "out.core.json")
1405
+ tblgen = "/home/ubuntu/llvm/builds/explore/bin/llvm-tblgen"
1406
+ inc = ["-I/home/ubuntu/llvm/trees/explore/llvm/lib/Target/X86",
1407
+ "-I/home/ubuntu/llvm/builds/explore/include",
1408
+ "-I/home/ubuntu/llvm/trees/explore/llvm/include",
1409
+ "-I/home/ubuntu/llvm/trees/explore/llvm/lib/Target"]
1410
+ r = subprocess.run([tblgen, "-gen-legalizer-core", *inc, path, "-o", out],
1411
+ capture_output=True, text=True)
1412
+ if r.returncode != 0:
1413
+ print("tblgen failed:\n" + r.stderr)
1414
+ print("emitted .td:\n" + td)
1415
+ return 1
1416
+ gen = json.load(open(out))
1417
+ gen_rules = next(x for x in gen["rulesets"] if x["def"] == "X86_ADD")["rules"]
1418
+ assert len(gen_rules) == len(oracle), (len(gen_rules), len(oracle))
1419
+ ok2 = all(strip(e) == strip(g) for e, g in zip(oracle, gen_rules))
1420
+ print(f"(b) .td round-trip: {len(gen_rules)} rules "
1421
+ f"{'ALL MATCH' if ok2 else 'SOME DIFF'}")
1422
+ if not ok2:
1423
+ for i, (e, g) in enumerate(zip(oracle, gen_rules)):
1424
+ if strip(e) != strip(g):
1425
+ print(f" #{i} oracle:", json.dumps(strip(e), sort_keys=True))
1426
+ print(f" #{i} gen :", json.dumps(strip(g), sort_keys=True))
1427
+ break
1428
+ # (c) static checks: positive (X86_ADD) + negative (sort / slot-bound)
1429
+ manifest = build_arity_manifest(
1430
+ "/home/ubuntu/llvm/trees/explore/llvm/include/llvm/Target/GenericOpcodes.td")
1431
+ errs = []
1432
+ arity = check_rules(compiled, ["G_ADD", "G_SUB"], manifest, errs)
1433
+ print(f"(c) static checks: X86_ADD arity={arity} errors={len(errs)}")
1434
+ ok3 = (len(errs) == 0 and arity == 1)
1435
+
1436
+ neg = [
1437
+ ("sort mismatch", "rule G_ADD:\n legal slot(0) == pow2"),
1438
+ ("slot out of range", "rule G_ADD:\n legal in(slot(1), s8)"),
1439
+ ]
1440
+ ok4 = True
1441
+ for label, src in neg:
1442
+ ncomp = compile_rulesets(Parser(src).parse())
1443
+ nerrs = []
1444
+ check_rules(ncomp, ["G_ADD"], manifest, nerrs)
1445
+ got_err = len(nerrs) > 0
1446
+ ok4 &= got_err
1447
+ print(f" negative [{label}]: {'rejected' if got_err else 'MISSED'} -> {nerrs[:1]}")
1448
+
1449
+ print("static checks: " + ("ALL PASS" if (ok3 and ok4) else "FAIL"))
1450
+
1451
+ # (d) full-corpus reverse round-trip
1452
+ d1 = roundtrip_corpus("X86", "/home/ubuntu/legalizer-baseline/x86.core.json", "X86")
1453
+ d2 = roundtrip_corpus("AArch64",
1454
+ "/home/ubuntu/legalizer-baseline/aarch64.core.json",
1455
+ "AArch64")
1456
+ ok5 = (d1 == 0 and d2 == 0)
1457
+ print("full corpus: " + ("ALL PASS" if ok5 else "FAIL"))
1458
+
1459
+ # (e) forward: baseline core.json -> free-form DSL -> parse -> normal form
1460
+ e1 = forward_roundtrip("X86", "/home/ubuntu/legalizer-baseline/x86.core.json")
1461
+ e2 = forward_roundtrip("AArch64",
1462
+ "/home/ubuntu/legalizer-baseline/aarch64.core.json")
1463
+ ok6 = (e1 == 0 and e2 == 0)
1464
+ print("forward DSL: " + ("ALL PASS" if ok6 else "FAIL"))
1465
+ return 0 if (ok1 and ok2 and ok3 and ok4 and ok5 and ok6) else 1
1466
+
1467
+
1468
+ if __name__ == "__main__":
1469
+ sys.exit(main())