Files
StarPilot/tinygrad_repo/test/mockgpu/amd/pcode.py
T
firestar5683 22a8575199 Vendor dependencies and resources for standalone builds
Keep official openpilot ancestry and dependency licenses. Store resources directly in Git, remove LFS setup requirements, and leave the downloadable big model outside the source tree.
2026-10-03 21:01:18 +00:00

1451 lines
74 KiB
Python

# Tokenizer-based expression parser for AMD pcode
import ast, itertools, operator, re
from typing import Any, Callable
_BINOPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.LShift: operator.lshift, ast.RShift: operator.rshift,
ast.BitAnd: operator.and_, ast.BitOr: operator.or_, ast.BitXor: operator.xor}
def _const_int(expr: str) -> int:
"""Evaluate a compile-time integer expression (integer literals and basic arithmetic only)."""
def ev(node: ast.AST) -> int:
if isinstance(node, ast.Expression): return ev(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, int): return node.value
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
return (-1 if isinstance(node.op, ast.USub) else 1) * ev(node.operand)
if isinstance(node, ast.BinOp) and type(node.op) in _BINOPS: return _BINOPS[type(node.op)](ev(node.left), ev(node.right))
raise ValueError(f"not a constant integer expression: {expr!r}")
return ev(ast.parse(expr.strip(), mode='eval'))
from tinygrad.dtype import dtypes
from tinygrad.uop.ops import Ops, UOp
from tinygrad.codegen.decomp.dtype import f2f
# Type alias for vars dict: stores UOps and tuples for lambda definitions
VarVal = UOp | tuple[str, list[str], str]
def _const(dt, v): return UOp.const(v, dt)
def _single_value(v: UOp): return v.vmin if v.vmin == v.vmax else None
def _u32(v): return _const(dtypes.uint32, v)
def _u64(v): return _const(dtypes.uint64, v)
def _to_u32(v): return v if v.dtype == dtypes.uint32 else v.bitcast(dtypes.uint32) if v.dtype.itemsize == 4 else v.cast(dtypes.uint32)
def _to_bool(v): return v if v.dtype == dtypes.bool else v.ne(_const(v.dtype, 0))
def _cast_to(v, dt):
if v.dtype == dt: return v
if dt == dtypes.half: return v.cast(dtypes.uint16).bitcast(dtypes.half)
return v.cast(dt) if dt.itemsize != v.dtype.itemsize else v.bitcast(dt)
# Float bit extraction - returns (bits, exp_mask, mant_mask, quiet_bit, exp_shift) based on float type
def _float_info(v: UOp) -> tuple[UOp, UOp, UOp, UOp, int]:
if v.dtype in (dtypes.float64, dtypes.uint64):
bits = v.bitcast(dtypes.uint64) if v.dtype == dtypes.float64 else v.cast(dtypes.uint64)
return bits, _u64(0x7FF0000000000000), _u64(0x000FFFFFFFFFFFFF), _u64(0x0008000000000000), 52
if v.dtype in (dtypes.half, dtypes.uint16):
bits = (v.bitcast(dtypes.uint16) if v.dtype == dtypes.half else (v & _u32(0xFFFF)).cast(dtypes.uint16)).cast(dtypes.uint32)
return bits, _u32(0x7C00), _u32(0x03FF), _u32(0x0200), 10
bits = v.bitcast(dtypes.uint32) if v.dtype == dtypes.float32 else v.cast(dtypes.uint32)
return bits, _u32(0x7F800000), _u32(0x007FFFFF), _u32(0x00400000), 23
def _isnan(v: UOp) -> UOp:
bits, exp_m, mant_m, _, _ = _float_info(v.cast(dtypes.float32) if v.dtype == dtypes.half else v)
return (bits & exp_m).eq(exp_m) & (bits & mant_m).ne(_const(bits.dtype, 0))
def _bitreverse(v: UOp, bits: int) -> UOp:
dt, masks = (dtypes.uint64, [(0x5555555555555555,1),(0x3333333333333333,2),(0x0F0F0F0F0F0F0F0F,4),(0x00FF00FF00FF00FF,8),(0x0000FFFF0000FFFF,16)]) \
if bits == 64 else (dtypes.uint32, [(0x55555555,1),(0x33333333,2),(0x0F0F0F0F,4),(0x00FF00FF,8)])
v = v.cast(dt) if v.dtype != dt else v
for m, s in masks: v = ((v >> _const(dt, s)) & _const(dt, m)) | ((v & _const(dt, m)) << _const(dt, s))
return (v >> _const(dt, 32 if bits == 64 else 16)) | (v << _const(dt, 32 if bits == 64 else 16))
def _extract_bits(val: UOp, hi: int, lo: int) -> UOp:
dt = dtypes.uint64 if val.dtype in (dtypes.uint64, dtypes.int64) else dtypes.uint32
width = hi - lo + 1
# Cast to dt first to ensure shift operands have matching types
val_cast = val.cast(dt) if val.dtype != dt else val
result = ((val_cast >> _const(dt, lo)) if lo > 0 else val_cast) & _const(dt, (1 << width) - 1)
# Downcast to match extracted bit width so brace-concat { hi, lo } computes correct output dtype
target_dt = _BITS_DT.get(width) or (dtypes.uint32 if width <= 32 else dtypes.uint64 if width <= 64 else dt)
if result.dtype != target_dt: result = result.cast(target_dt)
return result
def _expr_bits(v: UOp) -> int:
if v.dtype == dtypes.bool: return 1
if v.op in (Ops.AND, Ops.XOR):
widths: list[int] = []
for src in v.src:
if isinstance(sv:=_single_value(src), int) and sv > 0 and (sv & (sv + 1)) == 0:
widths.append(sv.bit_length())
if widths: return max(widths)
return v.dtype.bitsize
def _countbits(v: UOp) -> UOp:
dt = dtypes.uint64 if _expr_bits(v) > 32 or v.dtype in (dtypes.uint64, dtypes.int64) else dtypes.uint32
vv, out = v.cast(dt), _u32(0)
for i in range(_expr_bits(v)): out = out + ((vv >> _const(dt, i)) & _const(dt, 1)).cast(dtypes.uint32)
return out
def _set_bit(old, pos, val):
mask = _u32(1) << pos
return (old & (mask ^ _u32(0xFFFFFFFF))) | ((val.cast(dtypes.uint32) & _u32(1)) << pos)
def _val_to_bits(val):
if val.dtype == dtypes.half: return val.bitcast(dtypes.uint16).cast(dtypes.uint32)
if val.dtype == dtypes.float32: return val.bitcast(dtypes.uint32)
if val.dtype == dtypes.float64: return val.bitcast(dtypes.uint64)
return val if val.dtype == dtypes.uint32 else val.cast(dtypes.uint32)
def _floor(x):
t = UOp(Ops.TRUNC, src=(x,))
return ((x < _const(x.dtype, 0)) & x.ne(t)).where(t - _const(x.dtype, 1), t)
def _f16_extract(v): return (v & _u32(0xFFFF)).cast(dtypes.uint16).bitcast(dtypes.half) if v.dtype == dtypes.uint32 else v
# ═════ FP8 (E4M3) and BF8 (E5M2) conversion helpers ═════
# f32→fp8/bf8 uses f2f decomposition directly. fp8/bf8→f32 wraps f2f with subnormal handling
# (f2f flushes denormals to zero, but AMD V_CVT_F32_FP8/BF8 preserves subnormals).
def _fp8_to_f32(v: UOp) -> UOp:
b = (v.cast(dtypes.uint32) & _u32(0xFF)).cast(dtypes.uint8)
# E4M3 subnormal: exp==0, mant!=0 -> (-1)^sign * 2^(1-7) * (mant/8) = (-1)^sign * mant * 2^(-9)
bu = b.cast(dtypes.uint32)
sign, exp, mant = (bu >> _u32(7)) << _u32(31), (bu >> _u32(3)) & _u32(0xF), bu & _u32(0x7)
is_sub = exp.eq(_u32(0)) & mant.ne(_u32(0))
sub_f32 = (mant.cast(dtypes.float32) * _const(dtypes.float32, 1.0/512.0)).bitcast(dtypes.uint32) | sign
normal = f2f(b, dtypes.fp8e4m3, dtypes.float32)
return is_sub.where(sub_f32.bitcast(dtypes.float32), normal)
def _bf8_to_f32(v: UOp) -> UOp:
b = (v.cast(dtypes.uint32) & _u32(0xFF)).cast(dtypes.uint8)
# E5M2 subnormal: exp==0, mant!=0 -> (-1)^sign * 2^(1-15) * (mant/4) = (-1)^sign * mant * 2^(-16)
bu = b.cast(dtypes.uint32)
sign, exp, mant = (bu >> _u32(7)) << _u32(31), (bu >> _u32(2)) & _u32(0x1F), bu & _u32(0x3)
is_sub = exp.eq(_u32(0)) & mant.ne(_u32(0))
sub_f32 = (mant.cast(dtypes.float32) * _const(dtypes.float32, 1.0/65536.0)).bitcast(dtypes.uint32) | sign
normal = f2f(b, dtypes.fp8e5m2, dtypes.float32)
return is_sub.where(sub_f32.bitcast(dtypes.float32), normal)
def _f32_to_fp8(v: UOp) -> UOp:
return f2f((v.bitcast(dtypes.float32) if v.dtype != dtypes.float32 else v).bitcast(dtypes.uint32), dtypes.float32, dtypes.fp8e4m3, sat=False)
def _f32_to_bf8(v: UOp) -> UOp:
return f2f((v.bitcast(dtypes.float32) if v.dtype != dtypes.float32 else v).bitcast(dtypes.uint32), dtypes.float32, dtypes.fp8e5m2, sat=False)
def _f32_to_bf16(v: UOp) -> UOp:
"""Convert f32 to bf16 with round-to-nearest-even. BF16 is the upper 16 bits of F32 with rounding."""
bits = (v.bitcast(dtypes.float32) if v.dtype != dtypes.float32 else v).bitcast(dtypes.uint32)
# Round-to-nearest-even: add rounding bias. If the bit just below the truncation point is 1 and the rest are 0, round to even.
round_bit = (bits >> _u32(16)) & _u32(1) # bit 16 (LSB of kept part)
rounding = _u32(0x7FFF) + round_bit # 0x7FFF + bit16: rounds to even
rounded = bits + rounding
return (rounded >> _u32(16)).cast(dtypes.uint16)
def _f32_to_bf16_sr(v: UOp, stoch: UOp) -> UOp:
"""Convert f32 to bf16 with stochastic rounding."""
bits = (v.bitcast(dtypes.float32) if v.dtype != dtypes.float32 else v).bitcast(dtypes.uint32)
# Stochastic rounding: add lower 16 bits of stochastic value to lower 16 bits of f32
rounded = bits + (stoch & _u32(0xFFFF))
return (rounded >> _u32(16)).cast(dtypes.uint16)
def _check_nan(v: UOp, quiet: bool) -> UOp:
if v.op == Ops.CAST and v.dtype == dtypes.float64: v = v.src[0]
bits, exp_m, mant_m, qb, _ = _float_info(v)
is_nan_exp, has_mant, is_q = (bits & exp_m).eq(exp_m), (bits & mant_m).ne(_const(bits.dtype, 0)), (bits & qb).ne(_const(bits.dtype, 0))
return (is_nan_exp & is_q) if quiet else (is_nan_exp & has_mant & is_q.logical_not())
def _minmax_reduce(is_max: bool, dt, *args: UOp) -> UOp:
def cast(v: UOp) -> UOp: return v.bitcast(dt) if dt == dtypes.float32 and v.dtype == dtypes.uint32 else v.cast(dt)
def minmax(a: UOp, b: UOp) -> UOp:
if dt in (dtypes.uint8, dtypes.uint16, dtypes.uint32, dtypes.uint64): return (a > b).where(a, b) if is_max else (a < b).where(a, b)
return a.maximum(b) if is_max else a.minimum(b)
result = cast(args[0])
for a in args[1:]:
b = cast(a)
if dt == dtypes.float32: result = _isnan(result).where(b, _isnan(b).where(result, minmax(result, b)))
else: result = minmax(result, b)
return result
def _find_two_pi_mul(x):
if x.op != Ops.MUL or len(x.src) != 2: return None
for i, s in enumerate(x.src):
if (sv:=_single_value(s)) is not None and abs(sv - 6.283185307179586) < 1e-5: return (x.src[1-i], 6.283185307179586)
if s.op == Ops.MUL and len(s.src) == 2:
vals = [sv for ss in s.src if (sv:=_single_value(ss)) is not None]
if len(vals) == 2 and abs(vals[0] * vals[1] - 6.283185307179586) < 1e-5: return (x.src[1-i], vals[0] * vals[1])
return None
def _trig_reduce(x, phase=0.0):
match = _find_two_pi_mul(x)
if match is not None:
turns, two_pi = match
if phase: turns = turns + _const(turns.dtype, phase)
n = _floor(turns + _const(turns.dtype, 0.5))
return UOp(Ops.SIN, src=((turns - n) * _const(turns.dtype, two_pi),))
if phase: x = x + _const(x.dtype, phase * 6.283185307179586)
n = _floor(x * _const(x.dtype, 0.15915494309189535) + _const(x.dtype, 0.5))
return UOp(Ops.SIN, src=(x - n * _const(x.dtype, 6.283185307179586),))
def _signext(val: UOp) -> UOp:
for bits, mask, ext in [(4, 0xF, 0xFFFFFFF0), (8, 0xFF, 0xFFFFFF00), (16, 0xFFFF, 0xFFFF0000)]:
if (val.op == Ops.AND and len(val.src) == 2 and _single_value(val.src[1]) == mask) or val.dtype.itemsize == bits // 8:
v32 = val.cast(dtypes.uint32) if val.dtype != dtypes.uint32 else val
sb = (v32 >> _u32(bits - 1)) & _u32(1)
return sb.ne(_u32(0)).where(v32 | _u32(ext), v32).cast(dtypes.int)
return val.cast(dtypes.int64) if val.dtype in (dtypes.int, dtypes.int32) else val
def _signext_4bit(val: UOp) -> UOp:
"""Sign extend a 4-bit value to 32-bit signed integer."""
v32 = val.cast(dtypes.uint32) if val.dtype != dtypes.uint32 else val
sb = (v32 >> _u32(3)) & _u32(1) # sign bit at position 3
return sb.ne(_u32(0)).where(v32 | _u32(0xFFFFFFF0), v32).bitcast(dtypes.int)
def _abs(val: UOp) -> UOp:
if val.dtype not in (dtypes.float32, dtypes.float64, dtypes.half): return val
_, _, _, _, shift = _float_info(val)
sign_mask = {10: 0x7FFF, 23: 0x7FFFFFFF, 52: 0x7FFFFFFFFFFFFFFF}[shift]
bt, ft = {10: (dtypes.uint16, dtypes.half), 23: (dtypes.uint32, dtypes.float32), 52: (dtypes.uint64, dtypes.float64)}[shift]
return (val.bitcast(bt) & _const(bt, sign_mask)).bitcast(ft)
def _f_to_u(f, dt):
clamped = (f < _const(f.dtype, 0.0)).where(_const(f.dtype, 0.0), f)
truncated = UOp(Ops.TRUNC, src=(clamped,))
res = (truncated >= _const(f.dtype, 2**(dt.itemsize*8))).where(_const(dt, dt.max), truncated.cast(dt))
return _isnan(f).where(_const(dt, 0), res) # float->uint conversion of NaN is 0 on hardware
def _f_to_i32(a: UOp) -> UOp:
"""v_cvt_i32_f32/f64: truncate toward zero, saturate to [INT_MIN, INT_MAX], NaN -> 0.
(x86 cvttss2si returns 0x80000000 for all of these, which matches hardware only for negative overflow.)"""
res = (a >= _const(a.dtype, 2147483648.0)).where(_const(dtypes.int, 0x7FFFFFFF), UOp(Ops.TRUNC, src=(a,)).cast(dtypes.int))
return _isnan(a).where(_const(dtypes.int, 0), res)
def _ftz_f32(v: UOp) -> UOp:
"""Flush f32 denormals to signed zero (RDNA default float mode flushes denormal f32 inputs on select-style ops)."""
bits = v.bitcast(dtypes.uint32) if v.dtype == dtypes.float32 else v
return ((bits & _u32(0x7FFFFFFF)) < _u32(0x00800000)).where((bits & _u32(0x80000000)).bitcast(dtypes.float32),
v if v.dtype == dtypes.float32 else v.bitcast(dtypes.float32))
def _cvt_quiet(val: UOp) -> UOp:
bits, _, _, qb, _ = _float_info(val)
bt, ft = (dtypes.uint64, dtypes.float64) if val.dtype == dtypes.float64 else \
(dtypes.uint16, dtypes.half) if val.dtype == dtypes.half else (dtypes.uint32, dtypes.float32)
return (val.bitcast(bt) | qb).bitcast(ft)
def _is_denorm(val: UOp) -> UOp:
bits, exp_m, mant_m, _, _ = _float_info(val)
return (bits & exp_m).eq(_const(bits.dtype, 0)) & (bits & mant_m).ne(_const(bits.dtype, 0))
_EXP_BITS = {10: 0x1F, 23: 0xFF, 52: 0x7FF}
def _get_exp(bits: UOp, shift: int) -> UOp: return ((bits >> _const(bits.dtype, shift)) & _const(bits.dtype, _EXP_BITS[shift])).cast(dtypes.int)
def _exponent(val: UOp) -> UOp:
bits, _, _, _, shift = _float_info(val)
return _get_exp(bits, shift)
def _div_would_be_denorm(a: UOp, b: UOp) -> UOp:
bits_n, _, _, _, shift = _float_info(a)
bits_d, _, _, _, _ = _float_info(b)
min_exp = {10: -14, 23: -126, 52: -1022}[shift]
return (_get_exp(bits_n, shift) - _get_exp(bits_d, shift)) < _const(dtypes.int, min_exp)
def _sign(val: UOp) -> UOp:
bits, _, _, _, shift = _float_info(val)
sign_shift = {10: 15, 23: 31, 52: 63}[shift]
return ((bits >> _const(bits.dtype, sign_shift)) & _const(bits.dtype, 1)).cast(dtypes.uint32)
def _signext_from_bit(val: UOp, w: UOp) -> UOp:
is_64bit = val.dtype in (dtypes.uint64, dtypes.int64)
dt = dtypes.uint64 if is_64bit else dtypes.uint32
mask_all = _const(dt, 0xFFFFFFFFFFFFFFFF if is_64bit else 0xFFFFFFFF)
one = _const(dt, 1)
val_u = val.cast(dt) if val.dtype != dt else val
w_val = w.cast(dt) if w.dtype != dt else w
sign_bit = (val_u >> (w_val - one)) & one
ext_mask = ((one << w_val) - one) ^ mask_all
return sign_bit.ne(_const(dt, 0)).where(val_u | ext_mask, val_u)
def _ldexp(val: UOp, exp: UOp) -> UOp:
if val.dtype == dtypes.uint32: val = val.bitcast(dtypes.float32)
elif val.dtype == dtypes.uint64: val = val.bitcast(dtypes.float64)
if exp.dtype in (dtypes.uint32, dtypes.uint64): exp = exp.cast(dtypes.int if exp.dtype == dtypes.uint32 else dtypes.int64)
bits = val.bitcast(dtypes.uint32) if val.dtype == dtypes.float32 else val.bitcast(dtypes.uint64)
abs_max = _const(bits.dtype, 0x7F800000 if val.dtype == dtypes.float32 else 0x7FF0000000000000)
sign_mask = _const(bits.dtype, 0x80000000 if val.dtype == dtypes.float32 else 0x8000000000000000)
# hardware flushes denormal inputs to signed zero
magn_mask = _const(bits.dtype, 0x7FFFFFFF if val.dtype == dtypes.float32 else 0x7FFFFFFFFFFFFFFF)
is_denorm = ((bits & abs_max).eq(_const(bits.dtype, 0))) & ((bits & magn_mask).ne(_const(bits.dtype, 0)))
val = is_denorm.where((bits & sign_mask).bitcast(val.dtype), val)
# hardware propagates 0/+-inf/NaN unchanged (avoids 0*inf = NaN on the host)
res = val * UOp(Ops.EXP2, src=(exp.cast(val.dtype),))
is_special = (bits & abs_max).eq(_const(bits.dtype, 0)) | ((bits & abs_max) >= abs_max)
return is_special.where(val, res)
def _frexp_mant(val: UOp) -> UOp:
val = val.bitcast(dtypes.float32) if val.dtype == dtypes.uint32 else val.bitcast(dtypes.float64) if val.dtype == dtypes.uint64 else val
if val.dtype == dtypes.float32:
bits = val.bitcast(dtypes.uint32)
# denormal/zero inputs (exponent field == 0) return signed zero on hardware
return ((bits & _u32(0x7F800000)).ne(_u32(0))).where(((bits & _u32(0x807FFFFF)) | _u32(0x3F000000)).bitcast(dtypes.float32),
(bits & _u32(0x80000000)).bitcast(dtypes.float32))
bits = val.bitcast(dtypes.uint64)
return ((bits & _const(dtypes.uint64, 0x7FF0000000000000)).ne(_const(dtypes.uint64, 0))).where(
((bits & _const(dtypes.uint64, 0x800FFFFFFFFFFFFF)) | _const(dtypes.uint64, 0x3fe0000000000000)).bitcast(dtypes.float64),
(bits & _const(dtypes.uint64, 0x8000000000000000)).bitcast(dtypes.float64))
def _msb(val: UOp, bits: int) -> UOp:
"""Index of the highest set bit, or -1 if val == 0."""
dt = dtypes.uint64 if bits > 32 else dtypes.uint32
val = val.cast(dt) if val.dtype != dt else val
result = _const(dtypes.int, -1)
for i in range(bits - 1, -1, -1):
cond = ((val >> _const(dt, i)) & _const(dt, 1)).ne(_const(dt, 0)) & result.eq(_const(dtypes.int, -1))
result = cond.where(_const(dtypes.int, i), result)
return result
def _frexp_exp(val: UOp) -> UOp:
val = val.bitcast(dtypes.float32) if val.dtype == dtypes.uint32 else val.bitcast(dtypes.float64) if val.dtype == dtypes.uint64 else val
if val.dtype == dtypes.float32:
e = (val.bitcast(dtypes.uint32) >> _u32(23)) & _u32(0xFF)
return e.ne(_u32(0)).where(e.cast(dtypes.int) - _const(dtypes.int, 126), _const(dtypes.int, 0)) # f32 denormals -> 0 (hardware verified)
bits = val.bitcast(dtypes.uint64)
e = (bits >> _const(dtypes.uint64, 52)) & _const(dtypes.uint64, 0x7FF)
mant = bits & _const(dtypes.uint64, 0xFFFFFFFFFFFFF)
# f64 denormals: normalized exponent = highest set mantissa bit - 1073, zero -> 0 (hardware verified)
denorm = mant.ne(_const(dtypes.uint64, 0)).where(_msb(mant, 52) - _const(dtypes.int, 1073), _const(dtypes.int, 0))
return e.ne(_const(dtypes.uint64, 0)).where(e.cast(dtypes.int) - _const(dtypes.int, 1022), denorm)
TWO_OVER_PI = int(
"0145f306dc9c882a53f84eafa3ea69bb81b6c52b3278872083fca2c757bd778ac36e48dc74849ba5c00c925dd413a32439fc3bd"
"63962534e7dd1046bea5d768909d338e04d68befc827323ac7306a673e93908bf177bf250763ff12fffbc0b301fde5e2316b414"
"da3eda6cfd9e4f96136e9e8c7ecd3cbfd45aea4f758fd7cbe2f67a0e73ef14a525d4d7f6bf623f1aba10ac06608df8f6", 16)
# TWO_OVER_PI as 19 u64 words for trig_preop_result (word[0] = bits 0-63, word[18] = bits 1152-1200)
_PREOP_WORDS = tuple((TWO_OVER_PI >> (64 * i)) & 0xFFFFFFFFFFFFFFFF for i in range(19))
def _trig_preop(val: UOp) -> UOp:
# Extract 53 bits from position (1148 - shift) in the 1201-bit 2/PI constant
# Using word-based selection: 19 conditions instead of 1149
shift = val.cast(dtypes.uint32)
bit_pos = _u32(1148) - shift # starting bit position from LSB
word_idx = bit_pos >> _u32(6) # // 64
bit_off = bit_pos & _u32(63) # % 64
# Select lo_word and hi_word using shared conditions
lo_word, hi_word = _u64(_PREOP_WORDS[18]), _u64(0)
for i in range(17, -1, -1):
cond = word_idx.eq(_u32(i))
lo_word = cond.where(_u64(_PREOP_WORDS[i]), lo_word)
hi_word = cond.where(_u64(_PREOP_WORDS[i + 1]), hi_word)
# Combine and extract 53 bits: ((lo >> bit_off) | (hi << (64 - bit_off))) & mask
bit_off_64 = bit_off.cast(dtypes.uint64)
result = ((lo_word >> bit_off_64) | (hi_word << (_u64(64) - bit_off_64))) & _u64(0x1fffffffffffff)
return result.cast(dtypes.float64)
def _ff1(val: UOp, bits: int) -> UOp:
dt = dtypes.uint64 if bits == 64 else dtypes.uint32
val = val.cast(dt) if val.dtype != dt else val
result = _const(dtypes.int, -1)
for i in range(bits):
cond = ((val >> _const(dt, i)) & _const(dt, 1)).ne(_const(dt, 0)) & result.eq(_const(dtypes.int, -1))
result = cond.where(_const(dtypes.int, i), result)
return result
def _sad_u8(a: UOp, b: UOp, acc: UOp, masked: bool = False) -> UOp:
"""Sum of absolute differences of 4 unsigned bytes + accumulator. If masked, skips bytes where a == 0."""
a, b, acc = a.cast(dtypes.uint32), b.cast(dtypes.uint32), acc.cast(dtypes.uint32)
result = acc
for i in range(4):
a_byte = (a >> _u32(i * 8)) & _u32(0xFF)
b_byte = (b >> _u32(i * 8)) & _u32(0xFF)
diff = (a_byte > b_byte).where(a_byte - b_byte, b_byte - a_byte)
result = result + (a_byte.ne(_u32(0)).where(diff, _u32(0)) if masked else diff)
return result
_FUNCS: dict[str, Callable[..., UOp]] = {
'sqrt': lambda a: UOp(Ops.SQRT, src=(a,)), 'trunc': lambda a: UOp(Ops.TRUNC, src=(a,)),
'log2': lambda a: UOp(Ops.LOG2, src=(a,)), 'sin': lambda a: _trig_reduce(a),
'cos': lambda a: _trig_reduce(a, 0.25), 'floor': _floor, 'fract': lambda a: a - _floor(a),
'signext': _signext, 'abs': _abs,
'isEven': lambda a: (UOp(Ops.TRUNC, src=(a,)).cast(dtypes.int) & _const(dtypes.int, 1)).eq(_const(dtypes.int, 0)),
'max': lambda a, b: UOp(Ops.MAX, src=(a, b)),
'min': lambda a, b: UOp(Ops.MAX, src=(a.neg(), b.neg())).neg(),
'pow': lambda a, b: UOp(Ops.EXP2, src=(b.bitcast(dtypes.float32),)),
'fma': lambda a, b, c: a * b + c,
'i32_to_f32': lambda a: a.cast(dtypes.int).cast(dtypes.float32),
'u32_to_f32': lambda a: a.cast(dtypes.uint32).cast(dtypes.float32),
'f32_to_i32': lambda a: _f_to_i32(a.bitcast(dtypes.float32)),
'f32_to_u32': lambda a: _f_to_u(a.bitcast(dtypes.float32), dtypes.uint32),
'f64_to_i32': lambda a: _f_to_i32(a.bitcast(dtypes.float64)),
'f64_to_u32': lambda a: _f_to_u(a.bitcast(dtypes.float64), dtypes.uint32),
'f16_to_f32': lambda a: _f16_extract(a).cast(dtypes.float32),
'f32_to_f16': lambda a: a.cast(dtypes.half),
'f32_to_f64': lambda a: a.bitcast(dtypes.float32).cast(dtypes.float64),
'f64_to_f32': lambda a: a.bitcast(dtypes.float64).cast(dtypes.float32),
'i32_to_f64': lambda a: a.cast(dtypes.int).cast(dtypes.float64),
'u32_to_f64': lambda a: a.cast(dtypes.uint32).cast(dtypes.float64),
'f16_to_i16': lambda a: UOp(Ops.TRUNC, src=(_f16_extract(a),)).cast(dtypes.int16),
'f16_to_u16': lambda a: UOp(Ops.TRUNC, src=(_f16_extract(a),)).cast(dtypes.uint16),
'i16_to_f16': lambda a: a.cast(dtypes.int16).cast(dtypes.half),
'u16_to_f16': lambda a: a.cast(dtypes.uint16).cast(dtypes.half),
'bf16_to_f32': lambda a: (((a.cast(dtypes.uint32) if a.dtype != dtypes.uint32 else a) & _u32(0xFFFF)) << _u32(16)).bitcast(dtypes.float32),
'isNAN': _isnan, 'isSignalNAN': lambda a: _check_nan(a, False),
'isQuietNAN': lambda a: _check_nan(a, True), 'cvtToQuietNAN': _cvt_quiet,
'isDENORM': _is_denorm, 'exponent': _exponent, 'divWouldBeDenorm': _div_would_be_denorm, 'sign': _sign,
'signext_from_bit': _signext_from_bit, 'ldexp': _ldexp, 'frexp_mant': _frexp_mant, 'mantissa': _frexp_mant,
'frexp_exp': _frexp_exp, 'trig_preop_result': _trig_preop,
's_ff1_i32_b32': lambda a: _ff1(a, 32), 's_ff1_i32_b64': lambda a: _ff1(a, 64),
# Normalization conversions: map [-1,1] or [0,1] to integer range
# Use floor(x + 0.5) for round-to-nearest
# SNORM: round(value * 32767), range is [-32767, 32767] (hardware behavior)
'f16_to_snorm': lambda a: _floor(
_f16_extract(a).cast(dtypes.float32) * _const(dtypes.float32, 32767) + _const(dtypes.float32, 0.5)).cast(dtypes.int).cast(dtypes.int16),
'f16_to_unorm': lambda a: _floor(
_f16_extract(a).cast(dtypes.float32) * _const(dtypes.float32, 65535) + _const(dtypes.float32, 0.5)).cast(dtypes.uint16),
'f32_to_snorm': lambda a: _floor(
a.bitcast(dtypes.float32) * _const(dtypes.float32, 32767) + _const(dtypes.float32, 0.5)).cast(dtypes.int).cast(dtypes.int16),
'f32_to_unorm': lambda a: _floor(
a.bitcast(dtypes.float32) * _const(dtypes.float32, 65535) + _const(dtypes.float32, 0.5)).cast(dtypes.uint16),
'f32_to_u8': lambda a: _f_to_u(a.bitcast(dtypes.float32), dtypes.uint8),
# Integer truncation conversions
'i32_to_i16': lambda a: a.cast(dtypes.int).cast(dtypes.int16),
'u32_to_u16': lambda a: a.cast(dtypes.uint32).cast(dtypes.uint16),
'u16_to_u32': lambda a: (a.cast(dtypes.uint32) & _u32(0xFFFF)),
'u8_to_u32': lambda a: (a.cast(dtypes.uint32) & _u32(0xFF)),
'u4_to_u32': lambda a: (a.cast(dtypes.uint32) & _u32(0xF)),
# Signed extraction with sign extension for dot products
'i16_to_i32': lambda a: _signext(a.cast(dtypes.uint32) & _u32(0xFFFF)),
'i8_to_i32': lambda a: _signext(a.cast(dtypes.uint32) & _u32(0xFF)),
'i4_to_i32': lambda a: _signext_4bit(a.cast(dtypes.uint32) & _u32(0xF)),
# Float to int16 conversions
'v_cvt_i16_f32': lambda a: UOp(Ops.TRUNC, src=(a.bitcast(dtypes.float32),)).cast(dtypes.int16),
'v_cvt_u16_f32': lambda a: _f_to_u(a.bitcast(dtypes.float32), dtypes.uint16),
# SAD (Sum of Absolute Differences) - sum |a_i - b_i| for 4 bytes + accumulator
'v_sad_u8': lambda a, b, c: _sad_u8(a, b, c),
'v_msad_u8': lambda a, b, c: _sad_u8(a, b, c, masked=True),
# System NOPs - these are scheduling hints, no effect on emulation
'MIN': lambda a, b: (a < b).where(a, b),
's_nop': lambda a: _u32(0),
'countbits': _countbits,
# Address calculation for memory operations
'CalcDsAddr': lambda a, o, *r: a.cast(dtypes.uint32) + o.cast(dtypes.uint32),
'CalcGlobalAddr': lambda v, s, *r: v.cast(dtypes.uint64) + s.cast(dtypes.uint64),
'CalcScratchAddr': lambda v, s, *r: v.cast(dtypes.uint64) + s.cast(dtypes.uint64),
# FP8/BF8/BF16 conversion functions
'fp8_to_f32': _fp8_to_f32, 'bf8_to_f32': _bf8_to_f32, 'f32_to_fp8': _f32_to_fp8, 'f32_to_bf8': _f32_to_bf8,
'f32_to_bf16': _f32_to_bf16, 'f32_to_bf16_SR': _f32_to_bf16_sr, 'f32_to_bf16_sr': _f32_to_bf16_sr,
}
# min/max family: min/max + 3-input (x3), IEEE num variants (f16/f32 only), and long names minimum/maximum (f16/f32 only)
for is_max, name, full in [(False, 'min', 'minimum'), (True, 'max', 'maximum')]:
for dt, sfx, pre in [(dtypes.float32, 'f32', None), (dtypes.int, 'i32', None), (dtypes.uint32, 'u32', None),
(dtypes.int16, 'i16', None), (dtypes.uint16, 'u16', None), (dtypes.half, 'f16', _f16_extract)]:
def mm(*a, im=is_max, d=dt, p=pre): return _minmax_reduce(im, d, *(a if p is None else [p(x) for x in a]))
extra = (f'v_{name}_num_{sfx}', f'v_{name}3_num_{sfx}', f'v_{full}_{sfx}', f'v_{full}3_{sfx}') if dt in (dtypes.float32, dtypes.half) else ()
for fn in (f'v_{name}_{sfx}', f'v_{name}3_{sfx}', *extra): _FUNCS[fn] = mm
# ═══════════════════════════════════════════════════════════════════════════════
# TOKENIZER/PARSER
# ═══════════════════════════════════════════════════════════════════════════════
DTYPES = {'u32': dtypes.uint32, 'i32': dtypes.int, 'f32': dtypes.float32, 'b32': dtypes.uint32, 'u64': dtypes.uint64, 'i64': dtypes.int64,
'f64': dtypes.float64, 'b64': dtypes.uint64, 'u16': dtypes.uint16, 'i16': dtypes.short, 'f16': dtypes.half, 'b16': dtypes.uint16,
'u8': dtypes.uint8, 'i8': dtypes.int8, 'b8': dtypes.uint8, 'u4': dtypes.uint8, 'i4': dtypes.int8, 'u1': dtypes.uint32,
'fp8': dtypes.uint8, 'bf8': dtypes.uint8, 'b3': dtypes.uint8, 'b2': dtypes.uint8}
_BITS_DT = {8: dtypes.uint8, 16: dtypes.uint16, 32: dtypes.uint32, 64: dtypes.uint64}
_NUM_SUFFIXES = ('ULL', 'LL', 'UL', 'U', 'L', 'F', 'f')
def _strip_suffix(num: str) -> tuple[str, str]:
for sfx in _NUM_SUFFIXES:
if num.endswith(sfx): return sfx, num[:-len(sfx)]
return '', num
_SINGLE_CHAR = {'(': 'LPAREN', ')': 'RPAREN', '[': 'LBRACKET', ']': 'RBRACKET', '{': 'LBRACE', '}': 'RBRACE',
':': 'COLON', ',': 'COMMA', '?': 'QUESTION', '.': 'DOT', '=': 'EQUALS', "'": 'QUOTE'}
class Token:
__slots__ = ('type', 'val')
def __init__(self, kind: str, val: str): self.type, self.val = kind, val
def __repr__(self): return f'{self.type}:{self.val}'
def tokenize(s: str) -> list[Token]:
tokens, i, n = [], 0, len(s)
while i < n:
c = s[i]
if c.isspace():
i += 1
continue
if i + 1 < n and s[i:i+2] in ('+=', '-=', '|=', '&=', '^='):
tokens.append(Token('ASSIGN_OP', s[i:i+2]))
i += 2
continue
if i + 1 < n and s[i:i+2] in ('||', '&&', '>=', '<=', '==', '!=', '<>', '>>', '<<', '**', '+:', '-:'):
tokens.append(Token('OP', s[i:i+2]))
i += 2
continue
if c in '|^&><+-*/~!%':
tokens.append(Token('OP', c))
i += 1
continue
if (t := _SINGLE_CHAR.get(c)):
tokens.append(Token(t, c))
i += 1
continue
if c == ';':
i += 1
continue
if c.isdigit() or (c == '-' and i + 1 < n and s[i+1].isdigit()):
start = i
if c == '-': i += 1
if i + 1 < n and s[i] == '0' and s[i+1] in 'xX':
i += 2
while i < n and s[i] in '0123456789abcdefABCDEF': i += 1
else:
while i < n and s[i].isdigit(): i += 1
if i < n and s[i] == '.' and i + 1 < n and s[i+1].isdigit():
i += 1
while i < n and s[i].isdigit(): i += 1
for sfx in ('ULL', 'LL', 'UL', 'U', 'L', 'F', 'f'):
if s[i:i+len(sfx)] == sfx:
i += len(sfx)
break
tokens.append(Token('NUM', s[start:i]))
continue
if c.isalpha() or c == '_':
start = i
while i < n and (s[i].isalnum() or s[i] == '_'): i += 1
tokens.append(Token('IDENT', s[start:i]))
continue
raise RuntimeError(f"unexpected char '{c}' at pos {i} in: {s}")
tokens.append(Token('EOF', ''))
return tokens
class Parser:
def __init__(self, tokens: list[Token], env: dict, funcs: dict | None = None):
self.tokens, self.vars, self.funcs, self.pos = tokens, env, funcs if funcs is not None else _FUNCS, 0
def peek(self, offset=0) -> Token: return self.tokens[min(self.pos + offset, len(self.tokens) - 1)]
def at(self, *types) -> bool: return self.peek().type in types
def _advance(self) -> Token:
tok = self.tokens[self.pos]
self.pos += 1
return tok
def eat(self, kind: str) -> Token:
if self.peek().type != kind: raise RuntimeError(f"expected {kind}, got {self.peek()}")
return self._advance()
def try_eat(self, kind: str) -> Token | None: return self._advance() if self.peek().type == kind else None
def try_eat_val(self, val: str, kind: str) -> Token | None:
return self._advance() if self.peek().type == kind and self.peek().val == val else None
def eat_val(self, val: str, kind: str) -> Token:
if self.peek().type != kind or self.peek().val != val: raise RuntimeError(f"expected {kind}:{val}, got {self.peek()}")
return self._advance()
def parse(self) -> UOp:
cond = self.binop(0)
if self.try_eat('QUESTION'):
then_val = self.parse()
self.eat('COLON')
return _to_bool(cond).where(then_val, self.parse())
return cond
def _apply_binop(self, left, right, op):
if op in ('||', '&&', '|', '^', '&'): left, right = self._coerce_bitwise(left, right)
elif op in ('>=', '<=', '>', '<', '==', '!=', '<>', '>>', '<<'): left, right = self._coerce_cmp(left, right)
elif left.dtype != right.dtype: right = right.cast(left.dtype)
match op:
case '||' | '|': return left | right
case '&&' | '&': return left & right
case '^': return left ^ right
case '==': return left.eq(right)
case '!=': return left.ne(right)
case '>=' | '<=' | '>' | '<' | '<>':
ops = {'>=':(lambda a,b:a>=b),'<=':(lambda a,b:a<=b),'>':(lambda a,b:a>b),'<':(lambda a,b:a<b),'<>':(lambda a,b:a.ne(b))}
return self._cmp_nan(left, right, ops[op])
case '>>' | '<<':
if not dtypes.is_int(left.dtype): left = left.cast(dtypes.uint32)
if not dtypes.is_int(right.dtype): right = right.cast(dtypes.uint32)
return (left >> right) if op == '>>' else (left << right)
case '+' | '-':
if op == '-' and (lv:=_single_value(left)) is not None and (rv:=_single_value(right)) is not None: return _const(left.dtype, lv - rv)
return (left + right) if op == '+' else (left - right)
case '*' | '/':
# Integer promotion: promote 16-bit integers to 32-bit before multiply to avoid overflow
# (e.g. SOPP branch offset: SIMM16.i16 * 16'4 can exceed int16 range)
if op == '*' and left.dtype.itemsize == 2 and left.dtype in (dtypes.int16, dtypes.short, dtypes.uint16, dtypes.ushort):
pdt = dtypes.int if left.dtype in (dtypes.int16, dtypes.short) else dtypes.uint
left, right = left.cast(pdt), right.cast(pdt)
if op == '*': return left * right
return (left // right) if dtypes.is_int(left.dtype) else (left / right)
case '**': return UOp(Ops.EXP2, src=(right.cast(left.dtype),)) if _single_value(left) == 2.0 else left
_PREC = [('||',), ('&&',), ('|',), ('^',), ('&',), ('==', '!=', '<>'), ('>=', '<=', '>', '<'), ('>>', '<<'), ('+', '-'), ('*', '/'), ('**',)]
def binop(self, prec: int) -> UOp:
if prec >= len(self._PREC): return self.unary()
left = self.binop(prec + 1)
ops = self._PREC[prec]
while self.at('OP') and self.peek().val in ops:
op = self.eat('OP').val
left = self._apply_binop(left, self.binop(prec + 1), op)
return left
def unary(self) -> UOp:
if self.try_eat_val('~', 'OP'):
inner = self.unary()
return inner ^ _const(inner.dtype, (1 << _expr_bits(inner)) - 1)
if self.try_eat_val('!', 'OP'):
inner = self.unary()
return inner.eq(_const(inner.dtype, 0))
if self.try_eat_val('-', 'OP'):
inner = self.unary()
if (v:=_single_value(inner)) is not None:
return _const(dtypes.int if inner.dtype == dtypes.uint32 else inner.dtype, -v)
return inner.neg()
if self.try_eat_val('+', 'OP'): return self.unary()
return self.postfix()
def postfix(self) -> UOp:
base = self.primary()
while True:
if self.try_eat('DOT'):
field = self.eat('IDENT').val
base = self._handle_dot(base, field)
elif self.at('LBRACKET'):
base = self._handle_bracket(base)
elif self.at('LBRACE'):
base = self._handle_brace_index(base)
else:
break
return base
def primary(self) -> UOp:
if self.try_eat('LPAREN'):
e = self.parse()
self.eat('RPAREN')
return e
if self.try_eat('LBRACE'):
hi = self.parse()
self.eat('COMMA')
lo = self.parse()
self.eat('RBRACE')
lo_bits, hi_bits = _expr_bits(lo), _expr_bits(hi)
total_bits = lo_bits + hi_bits
dt = _BITS_DT.get(total_bits, dtypes.uint32 if total_bits <= 32 else dtypes.uint64)
return (hi.cast(dt) << _const(dt, lo_bits)) | lo.cast(dt)
if self.at('NUM'):
num = self.eat('NUM').val
if self.try_eat('QUOTE'):
return self._sized_literal(int(num.rstrip('ULlf')))
return self._parse_number(num)
if self.at('IDENT'):
name = self.eat('IDENT').val
if name == 'MEM':
self.eat('LBRACKET')
addr = self.parse()
self.eat('RBRACKET')
self.eat('DOT')
dt_name = self.eat('IDENT').val
return self._handle_mem_load(addr, DTYPES.get(dt_name, dtypes.uint32))
if name in self.funcs and self.try_eat('LBRACKET'):
index = self.parse()
self.eat('RBRACKET')
return self.funcs[name](index)
if name == 'VGPR' and self.at('LBRACKET'):
self.eat('LBRACKET')
lane = self.parse()
self.eat('RBRACKET')
self.eat('LBRACKET')
reg = self.parse()
self.eat('RBRACKET')
vgpr = self.vars.get('_vgpr')
if vgpr is None: return _u32(0)
ws = self.vars.get('_wave_size', 32)
return vgpr.index(_to_u32(reg) * _u32(ws) + _to_u32(lane)).load()
if self.try_eat('LPAREN'):
args = self._parse_args()
self.eat('RPAREN')
return self._call_func(name, args)
if name == 'PI': return _const(dtypes.float32, 3.141592653589793)
if name == 'INF': return _const(dtypes.float64, float('inf'))
if name == 'NAN': return _const(dtypes.uint32, 0x7FC00000).bitcast(dtypes.float32)
if name == 'UNDERFLOW_F32': return _const(dtypes.uint32, 1).bitcast(dtypes.float32)
if name == 'OVERFLOW_F32': return _const(dtypes.uint32, 0x7F7FFFFF).bitcast(dtypes.float32)
if name == 'UNDERFLOW_F64': return _const(dtypes.uint64, 1).bitcast(dtypes.float64)
if name == 'OVERFLOW_F64': return _const(dtypes.uint64, 0x7FEFFFFFFFFFFFFF).bitcast(dtypes.float64)
if name.lower() == 'wave32': return _const(dtypes.bool, self.vars.get('_wave_size', 32) <= 32)
if name.lower() == 'wave64': return _const(dtypes.bool, self.vars.get('_wave_size', 32) > 32)
if name == 'WAVE_MODE' and self.try_eat('DOT') and self.try_eat_val('IEEE', 'IDENT'): return _u32(1)
if self.try_eat('LBRACE'):
idx = self.eat('NUM').val
self.eat('RBRACE')
# Handle VGPR{lane}[reg] - 2D array access after loop unrolling
if name == 'VGPR' and self.at('LBRACKET'):
self.eat('LBRACKET')
reg = self.parse()
self.eat('RBRACKET')
vgpr = self.vars.get('_vgpr')
if vgpr is None: return _u32(0)
ws = self.vars.get('_wave_size', 32)
return vgpr.index(_to_u32(reg) * _u32(ws) + _u32(int(idx))).load()
elem = self.vars.get(f'{name}@{idx}', self.vars.get(f'{name}{idx}'))
if elem is None:
# Extract bit idx from base variable (like var[idx])
base = self.vars.get(name)
assert isinstance(base, UOp), f"unknown variable: {name}{idx}"
dt = dtypes.uint64 if base.dtype in (dtypes.uint64, dtypes.int64) else dtypes.uint32
elem = (base.cast(dt) >> _const(dt, int(idx))) & _const(dt, 1)
if self.try_eat('DOT'):
dt_name = self.eat('IDENT').val
return _cast_to(elem, DTYPES.get(dt_name, dtypes.uint32))
if self.at('LBRACKET'):
return self._handle_bracket(elem, name + idx)
return elem
if self.at('LBRACKET') and name not in self.vars:
self.eat('LBRACKET')
first = self.parse()
return self._handle_bracket_rest(first, _u32(0), name)
if name in self.vars:
v = self.vars[name]
assert isinstance(v, UOp), f"expected UOp for {name}, got {type(v)}"
return v
raise RuntimeError(f"unknown variable: {name}")
raise RuntimeError(f"unexpected token in primary: {self.peek()}")
def _handle_dot(self, base, field: str) -> UOp:
assert isinstance(base, UOp), f"expected UOp for dot access, got {type(base)}"
if field == 'u64' and self.at('LBRACKET') and self.peek(1).type == 'IDENT' and self.peek(1).val == 'laneId':
self.eat('LBRACKET')
self.eat_val('laneId', 'IDENT')
self.eat('RBRACKET')
lane = self.vars['laneId']
shift = lane.cast(base.dtype) if base.dtype != dtypes.uint32 else _to_u32(lane)
result = (base >> shift) & _const(base.dtype, 1)
if self.try_eat('DOT'):
dt_name = self.eat('IDENT').val
return result.cast(DTYPES.get(dt_name, dtypes.uint32))
return result
dt = DTYPES.get(field)
if dt is None: return base
if dt == base.dtype: return base
if dt.itemsize == 2 and base.dtype.itemsize == 4:
if dt == dtypes.uint16: return (base & _const(base.dtype, 0xFFFF)).cast(dtypes.uint16)
return (base & _const(base.dtype, 0xFFFF)).cast(dtypes.uint16).bitcast(dt)
if field == 'i4': return _signext_4bit(base)
return _cast_to(base, dt)
def _handle_bracket(self, base, var_name: str | None = None) -> UOp:
self.eat('LBRACKET')
return self._handle_bracket_rest(self.parse(), base, var_name)
def _handle_bracket_rest(self, first: UOp, base: UOp, var_name: str | None = None) -> UOp:
if self.at('OP') and self.peek().val in ('+:', '-:'):
self.eat('OP')
width = self.parse()
self.eat('RBRACKET')
if isinstance(w:=_single_value(width), int):
return (base >> _to_u32(first)) & _const(base.dtype, (1 << w) - 1)
return base
if self.try_eat('COLON'):
second = self.parse()
self.eat('RBRACKET')
if isinstance(a:=_single_value(first), int) and isinstance(b:=_single_value(second), int):
if a < b: return _bitreverse(base, b - a + 1)
hi, lo = a, b
if lo >= base.dtype.itemsize * 8:
vn = var_name or self._find_var_name(base)
if vn and f'{vn}{lo // 32}' in self.vars:
base = self.vars[f'{vn}{lo // 32}']
lo, hi = lo % 32, (hi % 32) + (lo % 32)
return _extract_bits(base, hi, lo)
# Dynamic bit slice: (base >> lo) & ((1 << (hi - lo + 1)) - 1)
dt = dtypes.uint64 if base.dtype in (dtypes.uint64, dtypes.int64) else dtypes.uint32
hi_u, lo_u = first.cast(dt), second.cast(dt)
width = hi_u - lo_u + _const(dt, 1)
mask = (_const(dt, 1) << width) - _const(dt, 1)
return (base.cast(dt) >> lo_u) & mask
self.eat('RBRACKET')
dt_suffix = None
if self.try_eat('DOT'):
dt_suffix = DTYPES.get(self.eat('IDENT').val, dtypes.uint32)
if var_name is None:
var_name = self._find_var_name(base)
if isinstance(idx:=_single_value(first), int):
# Check for array element (var@idx)
if var_name and f'{var_name}@{idx}' in self.vars:
v = self.vars[f'{var_name}@{idx}']
return _cast_to(v, dt_suffix) if dt_suffix else v
# Bit extraction
dt = dtypes.uint64 if base.dtype in (dtypes.uint64, dtypes.int64) else dtypes.uint32
base_cast = base.cast(dt) if base.dtype != dt else base
result = ((base_cast >> _const(dt, idx)) & _const(dt, 1))
return _cast_to(result, dt_suffix) if dt_suffix else result.cast(dtypes.bool)
if var_name:
idx_u32 = _to_u32(first)
elems = [(i, self.vars[f'{var_name}@{i}']) for i in range(256) if f'{var_name}@{i}' in self.vars]
if elems:
result = elems[-1][1]
for ei, ev in reversed(elems[:-1]):
if ev.dtype != result.dtype and ev.dtype.itemsize == result.dtype.itemsize: result = result.cast(ev.dtype)
elif ev.dtype != result.dtype: ev = ev.cast(result.dtype)
result = idx_u32.eq(_u32(ei)).where(ev, result)
return result
dt = dtypes.uint64 if base.dtype in (dtypes.uint64, dtypes.int64) else dtypes.uint32
base_cast = base.cast(dt) if base.dtype != dt else base
result = (base_cast >> first.cast(dt)) & _const(dt, 1)
return _cast_to(result, dt_suffix) if dt_suffix else result.cast(dtypes.bool)
def _handle_brace_index(self, base) -> UOp:
self.eat('LBRACE')
idx = self.eat('NUM').val
self.eat('RBRACE')
var_name = self._find_var_name(base)
if var_name:
elem = self.vars.get(f'{var_name}@{idx}', _u32(0)) # use @ to avoid collision with temps like A4
if self.try_eat('DOT'):
dt_name = self.eat('IDENT').val
return _cast_to(elem, DTYPES.get(dt_name, dtypes.uint32))
if self.at('LBRACKET'):
return self._handle_bracket(elem)
return elem
return _u32(0)
def _find_var_name(self, base: UOp) -> str | None:
if base.op == Ops.PARAM and base.arg.name is not None: return base.arg.name
for name, v in self.vars.items():
if isinstance(v, UOp) and v is base: return name
return None
def _sized_literal(self, bits: int) -> UOp:
if self.at('IDENT') and self.peek().val in ('U', 'I', 'F', 'B', 'BF'):
type_char = self.eat('IDENT').val
self.eat('LPAREN')
inner = self.parse()
self.eat('RPAREN')
dt = {('U',32): dtypes.uint32, ('U',64): dtypes.uint64, ('I',32): dtypes.int, ('I',64): dtypes.int64,
('F',16): dtypes.half, ('F',32): dtypes.float32, ('F',64): dtypes.float64,
('BF',16): dtypes.bfloat16,
('B',32): dtypes.uint32, ('B',64): dtypes.uint64}.get((type_char, bits), dtypes.uint64 if bits > 32 else dtypes.uint32)
if type_char == 'F' and inner.dtype in (dtypes.uint32, dtypes.uint64, dtypes.ulong, dtypes.int, dtypes.int64):
if inner.dtype.itemsize != dt.itemsize: inner = inner.cast(dtypes.uint32 if dt.itemsize == 4 else dtypes.uint64)
return inner.bitcast(dt)
return inner.cast(dt)
if self.at('IDENT'):
ident = self.peek().val
fmt = ident[0].lower()
if fmt in ('h', 'b', 'd'):
self.eat('IDENT')
if len(ident) > 1: num = ident[1:]
elif self.at('NUM'): num = self.eat('NUM').val
elif self.at('IDENT'): num = self.eat('IDENT').val
else: raise RuntimeError(f"expected number after {bits}'{fmt}")
if fmt == 'h': val = int(num, 16)
elif fmt == 'b': val = int(num, 2)
else: val = int(num)
return _const(_BITS_DT.get(bits, dtypes.uint32), val)
if self.at('NUM') and self.peek().val.startswith('0x'):
num = self.eat('NUM').val
return _const(_BITS_DT.get(bits, dtypes.uint32), int(num, 16))
if self.at('NUM') or (self.at('OP') and self.peek().val == '-'):
neg = self.try_eat_val('-', 'OP') is not None
suffix, num = _strip_suffix(self.eat('NUM').val)
if num.startswith('0x'):
val = int(num, 16)
if neg: val = -val
elif '.' in num:
fval = float(num)
if neg: fval = -fval
return _const({16: dtypes.half, 32: dtypes.float32, 64: dtypes.float64}.get(bits, dtypes.float32), fval)
else:
val = int(num)
if neg: val = -val
dt = {1: dtypes.uint32, 8: dtypes.uint8, 16: dtypes.int16 if 'U' not in suffix else dtypes.uint16,
32: dtypes.int if 'U' not in suffix else dtypes.uint32, 64: dtypes.int64 if 'U' not in suffix else dtypes.uint64}.get(bits, dtypes.uint32)
return _const(dt, val)
raise RuntimeError(f"unexpected token after {bits}': {self.peek()}")
def _parse_number(self, num: str) -> UOp:
if num.startswith('0x') or num.startswith('0X'):
is_u64 = num.upper().endswith('ULL') or num.upper().endswith('LL') or num.upper().endswith('UL')
return _const(dtypes.uint64 if is_u64 else dtypes.uint32, int(num.rstrip('ULul'), 16))
suffix, num_str = _strip_suffix(num)
if '.' in num_str or suffix in ('F', 'f'):
return _const(dtypes.float32 if suffix in ('F', 'f') else dtypes.float64, float(num_str))
val = int(num_str)
if 'ULL' in suffix or 'LL' in suffix or 'L' in suffix: return _const(dtypes.uint64, val)
if 'U' in suffix: return _const(dtypes.uint32, val)
return _const(dtypes.int if val < 0 else dtypes.uint32, val)
def _parse_args(self) -> list[UOp]:
if self.at('RPAREN'): return []
args = [self.parse()]
while self.try_eat('COMMA'):
args.append(self.parse())
return args
def _call_func(self, name: str, args: list[UOp]) -> UOp:
if name in self.vars and isinstance(self.vars[name], tuple) and self.vars[name][0] == 'lambda':
_, params, body = self.vars[name]
lv = {**self.vars, **dict(zip(params, args))}
if ';' in body or '\n' in body or 'return' in body.lower():
lines = [l.strip() for l in body.replace(';', '\n').split('\n') if l.strip() and not l.strip().startswith('//')]
_, _, result = parse_block(lines, 0, lv, self.funcs)
assert result is not None, f"lambda {name} must return a value"
return result
return parse_expr(body, lv, self.funcs)
if name in self.funcs:
return self.funcs[name](*args)
raise RuntimeError(f"unknown function: {name}")
def _handle_mem_load(self, addr: UOp, dt) -> UOp:
mem = self.vars.get('_vmem') if '_vmem' in self.vars else self.vars.get('_lds')
assert mem is not None, "memory load requires _vmem or _lds"
adt = dtypes.uint64 if addr.dtype == dtypes.uint64 else dtypes.uint32
active = self.vars.get('_active')
def mindex(idx:UOp): return mem.index(idx.valid(active) if active is not None else idx)
byte_mem = mem.dtype == dtypes.uint8
if byte_mem:
idx = addr
if dt in (dtypes.uint64, dtypes.int64, dtypes.float64):
val = _u32(0).cast(dtypes.uint64)
for i in range(8): val = val | (mindex(idx + _const(adt, i)).load().cast(dtypes.uint64) << _u64(i * 8))
elif dt in (dtypes.uint8, dtypes.int8):
val = mindex(idx).load().cast(dt)
elif dt in (dtypes.uint16, dtypes.int16, dtypes.short):
lo = mindex(idx).load().cast(dtypes.uint32)
hi = mindex(idx + _const(adt, 1)).load().cast(dtypes.uint32)
val = (lo | (hi << _u32(8))).cast(dt)
else:
val = _u32(0)
for i in range(4): val = val | (mindex(idx + _const(adt, i)).load().cast(dtypes.uint32) << _u32(i * 8))
else:
idx = addr >> _const(addr.dtype, 2)
val = mindex(idx)
if dt in (dtypes.uint64, dtypes.int64, dtypes.float64):
idx2 = (addr + _const(adt, 4)) >> _const(adt, 2)
val = val.cast(dtypes.uint64) | (mindex(idx2).cast(dtypes.uint64) << _u64(32))
elif dt in (dtypes.uint8, dtypes.int8): val = (val >> ((addr & _const(adt, 3)).cast(dtypes.uint32) * _u32(8))) & _u32(0xFF)
else:
# Handle unaligned 16/32-bit loads: combine two consecutive dwords and shift.
# The next dword is only read when the value straddles into it, so a load at the end of a buffer stays in bounds.
# Use int64 for the WHERE to avoid 32-bit int overflow in C pointer arithmetic (addr can be >8GB).
byte_off = (addr & _const(adt, 3)).cast(dtypes.uint32)
idx_native = (addr >> _const(adt, 2)).cast(dtypes.int64)
idx_hi_native = ((addr + _const(adt, 4)) >> _const(adt, 2)).cast(dtypes.int64)
hi = mindex((byte_off > _u32(4 - dt.itemsize)).where(idx_hi_native, idx_native))
combined = val.cast(dtypes.uint64) | (hi.cast(dtypes.uint64) << UOp.const(32, dtypes.uint64))
val = (combined >> (byte_off.cast(dtypes.uint64) * UOp.const(8, dtypes.uint64))).cast(dtypes.uint32)
return _cast_to(val, dt)
def _coerce_cmp(self, l: UOp, r: UOp) -> tuple[UOp, UOp]:
if l.dtype != r.dtype:
if r.dtype == dtypes.int and isinstance(rv:=_single_value(r), int) and rv < 0: l = l.cast(dtypes.int)
else: r = r.cast(l.dtype)
return l, r
def _coerce_bitwise(self, l: UOp, r: UOp) -> tuple[UOp, UOp]:
if l.dtype != r.dtype:
if l.dtype.itemsize == r.dtype.itemsize:
t = dtypes.uint32 if l.dtype.itemsize == 4 else dtypes.uint64 if l.dtype.itemsize == 8 else l.dtype
l, r = l.bitcast(t), r.bitcast(t)
else: r = r.cast(l.dtype)
return l, r
def _cmp_nan(self, l: UOp, r: UOp, fn) -> UOp:
result = fn(l, r)
if l.dtype in (dtypes.float32, dtypes.float64, dtypes.half):
return result & _isnan(l).logical_not() & _isnan(r).logical_not()
return result
_break_var_ids = itertools.count() # unique names for per-loop break-tracking variables
def _match_bracket(toks: list[Token], start: int) -> tuple[int, list[Token]]:
"""Match brackets from start, return (end_idx, inner_tokens)."""
j, depth = start + 1, 1
while j < len(toks) and depth > 0:
if toks[j].type == 'LBRACKET': depth += 1
elif toks[j].type == 'RBRACKET': depth -= 1
j += 1
return j, [t for t in toks[start+1:j-1] if t.type != 'EOF']
def _tok_str(toks: list[Token]) -> str: return ' '.join(t.val for t in toks if t.type != 'EOF')
def parse_tokens(toks: list[Token], env: dict[str, VarVal], funcs: dict | None = None) -> UOp:
return Parser(toks, env, funcs).parse()
# Unified block parser for pcode
def _subst_loop_var(line: str, loop_var: str, val: int) -> str:
"""Substitute loop variable with its value."""
toks = tokenize(line)
return ' '.join(str(val) if t.type == 'IDENT' and t.val == loop_var else t.val for t in toks if t.type != 'EOF')
def _set_bits(old: UOp, val: UOp, width: int, offset: int) -> UOp:
"""Set bits [offset:offset+width) in old to val, masking and shifting appropriately."""
if old.dtype in (dtypes.half, dtypes.float32): old = _val_to_bits(old)
is64 = old.dtype in (dtypes.uint64, dtypes.int64) or offset + width > 32
if is64:
old = old.cast(dtypes.uint64) if old.dtype != dtypes.uint64 else old
mask = _u64(((1 << width) - 1) << offset)
v = (val.cast(dtypes.uint64) if val.dtype != dtypes.uint64 else val) & _u64((1 << width) - 1)
return (old & (mask ^ _u64(0xFFFFFFFFFFFFFFFF))) | (v << _u64(offset))
mask = _u32(((1 << width) - 1) << offset)
v = (val.cast(dtypes.uint32) if val.dtype != dtypes.uint32 else val) & _u32((1 << width) - 1)
return (old & (mask ^ _u32(0xFFFFFFFF))) | (v << _u32(offset))
def _find_paren_end(s: str, start: int = 0, open_ch: str = '(', close_ch: str = ')') -> int:
"""Find index of matching close paren, starting after the open paren at start."""
depth = 0
for j, ch in enumerate(s[start:], start):
if ch == open_ch: depth += 1
elif ch == close_ch:
depth -= 1
if depth == 0: return j
return len(s)
def parse_block(lines: list[str], start: int, env: dict[str, VarVal], funcs: dict | None = None,
assigns: list | None = None) -> tuple[int, dict[str, VarVal], UOp | None]:
"""Parse a block of pcode. Returns (next_line, block_assigns, return_value).
If assigns list is provided, side effects (MEM/VGPR writes) are appended to it."""
if funcs is None: funcs = _FUNCS
block_assigns: dict[str, VarVal] = {}
i = start
while i < len(lines):
line = lines[i]
toks = tokenize(line)
if toks[0].type != 'IDENT' and toks[0].type != 'LBRACE':
i += 1
continue
first = toks[0].val.lower() if toks[0].type == 'IDENT' else '{'
# Block terminators
if first in ('elsif', 'else', 'endif', 'endfor'): break
# return expr (lambda bodies)
if first == 'return':
rest = line[line.lower().find('return') + 6:].strip()
return i + 1, block_assigns, parse_expr(rest, env, funcs)
# for loop
if first == 'for':
# C-style loops use an exclusive bound; for/in loops use an inclusive bound.
if m := re.fullmatch(r'for\s*\(\s*(\w+)\s*=\s*(\d+);\s*\1\s*<\s*(\d+);\s*\1\s*(\+\+|\+=\s*\d+)\s*\)', line):
loop_var, start_val, end_val = m[1], int(m[2]), int(m[3]) - 1
step = 1 if m[4] == '++' else int(m[4][2:])
else:
p = Parser(toks, env, funcs)
p.eat_val('for', 'IDENT')
loop_var = p.eat('IDENT').val
p.eat_val('in', 'IDENT')
def parse_bound():
if p.at('NUM') and p.peek(1).type == 'QUOTE':
p.eat('NUM')
p.eat('QUOTE')
if p.at('NUM'): return int(p.eat('NUM').val.rstrip('UuLl'))
return int(p.parse())
start_val = parse_bound()
p.eat('COLON')
end_val, step = parse_bound(), 1
# Collect body
i += 1
body_lines: list[str] = []
depth = 1
while i < len(lines) and depth > 0:
btoks = tokenize(lines[i])
if btoks[0].type == 'IDENT':
if btoks[0].val.lower() == 'for': depth += 1
elif btoks[0].val.lower() == 'endfor': depth -= 1
if depth > 0: body_lines.append(lines[i])
i += 1
# Execute loop with break support
has_break = any('break' in bl.lower() for bl in body_lines)
found_var = f'_found_{next(_break_var_ids)}' if has_break else None
if found_var: env[found_var] = block_assigns[found_var] = _const(dtypes.bool, False)
for loop_i in range(start_val, end_val + 1, step):
subst_lines = [_subst_loop_var(bl, loop_var, loop_i) for bl in body_lines if not (has_break and bl.strip().lower() == 'break')]
_, iter_assigns, _ = parse_block(subst_lines, 0, {**env, **block_assigns}, funcs, assigns)
if has_break:
assert found_var is not None
found = block_assigns.get(found_var, env.get(found_var))
assert isinstance(found, UOp)
not_found = found.eq(_const(dtypes.bool, False))
for var, val in iter_assigns.items():
if var != found_var and isinstance(val, UOp):
old = block_assigns.get(var, env.get(var, _u32(0)))
if isinstance(old, UOp):
block_assigns[var] = env[var] = not_found.where(
val, old.cast(val.dtype) if val.dtype != old.dtype and val.dtype.itemsize == old.dtype.itemsize else old)
for j, bl in enumerate(body_lines):
bl_l = bl.strip().lower()
if bl_l.startswith('if ') and bl_l.endswith(' then'):
if any(body_lines[k].strip().lower() == 'break' for k in range(j+1, len(body_lines))):
cond_str = _subst_loop_var(bl.strip()[3:-5].strip(), loop_var, loop_i)
cond = _to_bool(parse_expr(cond_str, env, funcs))
block_assigns[found_var] = env[found_var] = not_found.where(cond, found)
break
else:
block_assigns.update(iter_assigns)
env.update(iter_assigns)
continue
# declare
if first == 'declare':
# Initialize scalar declarations (skip arrays and env already passed as srcs)
if '[' not in line and len(toks) >= 2 and toks[1].type == 'IDENT':
env.setdefault(toks[1].val, _u32(0))
i += 1
continue
# lambda definition
if first != '{' and '=' in line and 'lambda' in line and any(t.type == 'IDENT' and t.val == 'lambda' for t in toks):
name = toks[0].val
body_start = line[line.find('(', line.find('lambda')):]
params_end = _find_paren_end(body_start) + 1
params = [p.strip() for p in body_start[1:params_end-1].split(',') if p.strip()]
rest = body_start[params_end:].strip()
if rest.startswith('('):
body_end = _find_paren_end(rest)
if body_end < len(rest): # found matching paren on same line
body = rest[1:body_end].strip()
i += 1
else: # multiline body
body_lines_lst, depth = [rest[1:]], 1
i += 1
while i < len(lines) and depth > 0:
for j, ch in enumerate(lines[i]):
if ch == '(': depth += 1
elif ch == ')':
depth -= 1
if depth == 0:
body_lines_lst.append(lines[i][:j])
break
else: body_lines_lst.append(lines[i])
i += 1
body = '\n'.join(body_lines_lst).strip()
env[name] = ('lambda', params, body)
continue
# MEM assignment: MEM[addr].type (+|-)?= value
if first == 'mem' and toks[1].type == 'LBRACKET':
j, addr_toks = _match_bracket(toks, 1)
addr = parse_tokens(addr_toks, env, funcs)
if j < len(toks) and toks[j].type == 'DOT': j += 1
dt_name = toks[j].val if j < len(toks) and toks[j].type == 'IDENT' else 'u32'
dt, j = DTYPES.get(dt_name, dtypes.uint32), j + 1
compound_op = None
if j < len(toks) and toks[j].type == 'ASSIGN_OP':
compound_op = toks[j].val
j += 1
elif j < len(toks) and toks[j].type == 'EQUALS': j += 1
rhs = parse_tokens(toks[j:], env, funcs)
if compound_op:
old = Parser([Token('EOF', '')], env, funcs)._handle_mem_load(addr, dt)
if compound_op == '+=': rhs = old + rhs
elif compound_op == '-=': rhs = old - rhs
elif compound_op == '|=': rhs = old | rhs
elif compound_op == '&=': rhs = old & rhs
elif compound_op == '^=': rhs = old ^ rhs
if assigns is not None: assigns.append((f'MEM[{_tok_str(addr_toks)}].{dt_name}', (addr, rhs)))
i += 1
continue
# VGPR assignment: VGPR[lane][reg] = value or VGPR[lane][reg][hi:lo].type = { ... }
if first == 'vgpr' and toks[1].type == 'LBRACKET':
j, lane_toks = _match_bracket(toks, 1)
if j < len(toks) and toks[j].type == 'LBRACKET':
j, reg_toks = _match_bracket(toks, j)
# Check for bit-slice: VGPR[lane][reg][hi:lo].type = value (read-modify-write)
if j < len(toks) and toks[j].type == 'LBRACKET':
j, slice_toks = _match_bracket(toks, j)
slice_str = _tok_str(slice_toks)
hi_str, lo_str = slice_str.split(':')
hi_val, lo_val = _const_int(hi_str), _const_int(lo_str)
if j < len(toks) and toks[j].type == 'DOT': j += 2 # skip .type suffix
if j < len(toks) and toks[j].type == 'EQUALS': j += 1
ln = parse_tokens(lane_toks, env, funcs)
rg, val = parse_tokens(reg_toks, env, funcs), parse_tokens(toks[j:], env, funcs)
ws = env.get('_wave_size', 32)
vgpr_idx = _to_u32(rg) * _u32(ws) + _to_u32(ln)
if assigns is not None:
assigns.append((f'VGPR[{_tok_str(lane_toks)}][{_tok_str(reg_toks)}][{hi_val}:{lo_val}]', (vgpr_idx, val, _u32(hi_val), _u32(lo_val))))
i += 1
continue
if j < len(toks) and toks[j].type == 'DOT': j += 2 # skip .type suffix
if j < len(toks) and toks[j].type == 'EQUALS': j += 1
ln = parse_tokens(lane_toks, env, funcs)
rg, val = parse_tokens(reg_toks, env, funcs), parse_tokens(toks[j:], env, funcs)
if assigns is not None:
ws = env.get('_wave_size', 32)
assigns.append((f'VGPR[{_tok_str(lane_toks)}][{_tok_str(reg_toks)}]', (_to_u32(rg) * _u32(ws) + _to_u32(ln), val)))
i += 1
continue
# Compound destination: {hi.type, lo.type} = value
if first == '{':
j = 1
if j+2 < len(toks) and toks[j].type == 'IDENT' and toks[j+1].type == 'DOT':
hi_var, hi_type = toks[j].val, toks[j+2].val
j += 3
if j < len(toks) and toks[j].type == 'COMMA': j += 1
if j+2 < len(toks) and toks[j].type == 'IDENT' and toks[j+1].type == 'DOT':
lo_var, lo_type = toks[j].val, toks[j+2].val
j += 3
if j < len(toks) and toks[j].type == 'RBRACE': j += 1
if j < len(toks) and toks[j].type == 'EQUALS': j += 1
val = parse_tokens(toks[j:], env, funcs)
lo_dt, hi_dt = DTYPES.get(lo_type, dtypes.uint64), DTYPES.get(hi_type, dtypes.uint32)
lo_bits = 64 if lo_dt in (dtypes.uint64, dtypes.int64) else 32
lo_u = val.cast(lo_dt) if val.dtype.itemsize * 8 <= lo_bits else (val & _const(val.dtype, (1 << lo_bits) - 1)).cast(lo_dt)
hi_u = (val >> _const(val.dtype, lo_bits)).cast(hi_dt)
block_assigns[lo_var] = env[lo_var] = lo_u
block_assigns[hi_var] = env[hi_var] = hi_u
if assigns is not None: assigns.extend([(f'{lo_var}.{lo_type}', lo_u), (f'{hi_var}.{hi_type}', hi_u)])
i += 1
continue
# Bit slice/index: var[hi:lo] = value, var.type[hi:lo] = value, or var[expr] = value
if len(toks) >= 5 and toks[0].type == 'IDENT' and \
(toks[1].type == 'LBRACKET' or (toks[1].type == 'DOT' and toks[3].type == 'LBRACKET')):
bracket_start = 2 if toks[1].type == 'LBRACKET' else 4
j = bracket_start
colon_pos = None
while j < len(toks) and toks[j].type != 'RBRACKET':
if toks[j].type == 'COLON': colon_pos = j
j += 1
var = toks[0].val
if colon_pos is not None: # bit slice: var[hi:lo]
hi_str = ' '.join(t.val for t in toks[bracket_start:colon_pos] if t.type != 'EOF')
lo_str = ' '.join(t.val for t in toks[colon_pos+1:j] if t.type != 'EOF')
try:
hi_val, lo_val = _const_int(hi_str), _const_int(lo_str)
hi, lo = max(hi_val, lo_val), min(hi_val, lo_val)
j += 1
if j < len(toks) and toks[j].type == 'DOT': j += 2
if j < len(toks) and toks[j].type == 'EQUALS': j += 1
val = parse_tokens(toks[j:], env, funcs)
dt_suffix = toks[2].val if toks[1].type == 'DOT' else None
if assigns is not None: assigns.append((f'{var}[{hi}:{lo}]' + (f'.{dt_suffix}' if dt_suffix else ''), val))
if var not in env: env[var] = _const(dtypes.uint64 if hi >= 32 else dtypes.uint32, 0)
old = block_assigns.get(var, env.get(var))
assert isinstance(old, UOp)
block_assigns[var] = env[var] = _set_bits(old, _val_to_bits(val), hi - lo + 1, lo)
i += 1
continue
except (ValueError, SyntaxError): pass # non-constant slice bounds - fall through to other statement forms
elif toks[1].type == 'LBRACKET': # bit index: var[expr] (only for var[...], not var.type[...])
existing = block_assigns.get(var, env.get(var))
if existing is not None and isinstance(existing, UOp) and \
not any(f'{var}{k}' in env or f'{var}{k}' in block_assigns for k in range(8)):
bit_toks = toks[2:j]
j += 1
while j < len(toks) and toks[j].type != 'EQUALS': j += 1
if j < len(toks):
block_assigns[var] = env[var] = _set_bit(
existing, _to_u32(parse_tokens(bit_toks, env, funcs)), parse_tokens(toks[j+1:], env, funcs))
i += 1
continue
# Array element: var[idx] = value (static index) or var[expr] = value (dynamic)
if len(toks) >= 4 and toks[0].type == 'IDENT' and toks[1].type == 'LBRACKET':
var = toks[0].val
j, idx_toks = _match_bracket(toks, 1)
if j < len(toks) and toks[j].type == 'EQUALS':
idx_expr = parse_tokens(idx_toks, env, funcs)
# Static index: var[NUM] = value
if isinstance(idx := _single_value(idx_expr), int):
val = parse_tokens(toks[j+1:], env, funcs)
existing = block_assigns.get(var, env.get(var))
if existing is not None and isinstance(existing, UOp):
block_assigns[var] = env[var] = _set_bit(existing, _u32(idx), val)
else:
block_assigns[f'{var}@{idx}'] = env[f'{var}@{idx}'] = val
i += 1
continue
# Dynamic index: var[expr] = value where var has @-elements
elems = [(k.split('@')[1], v) for k, v in {**env, **block_assigns}.items() if k.startswith(f'{var}@') and isinstance(v, UOp)]
if elems:
val = parse_tokens(toks[j+1:], env, funcs)
for elem_idx_str, old_elem in elems:
elem_idx = int(elem_idx_str)
cond = _to_u32(idx_expr).eq(_u32(elem_idx))
new_val = cond.where(val.cast(old_elem.dtype) if val.dtype != old_elem.dtype else val, old_elem)
block_assigns[f'{var}@{elem_idx}'] = env[f'{var}@{elem_idx}'] = new_val
i += 1
continue
# Compound assignment: var += or var -=
assign_op = next((j for j, t in enumerate(toks) if t.type == 'ASSIGN_OP'), None)
if assign_op is not None:
var = toks[0].val
old = block_assigns.get(var, env.get(var, _u32(0)))
rhs = parse_tokens(toks[assign_op+1:], env, funcs)
if rhs.dtype != old.dtype: rhs = rhs.cast(old.dtype)
if toks[assign_op].val == '+=': block_assigns[var] = env[var] = old + rhs
elif toks[assign_op].val == '-=': block_assigns[var] = env[var] = old - rhs
elif toks[assign_op].val == '|=': block_assigns[var] = env[var] = old | rhs
elif toks[assign_op].val == '&=': block_assigns[var] = env[var] = old & rhs
elif toks[assign_op].val == '^=': block_assigns[var] = env[var] = old ^ rhs
i += 1
continue
# Typed element: var.type[idx] = value
if len(toks) >= 7 and toks[0].type == 'IDENT' and toks[1].type == 'DOT' and \
toks[2].type == 'IDENT' and toks[3].type == 'LBRACKET' and toks[4].type == 'NUM':
var, dt_name, idx = toks[0].val, toks[2].val, int(toks[4].val)
dt = DTYPES.get(dt_name, dtypes.uint32)
j = 6
while j < len(toks) and toks[j].type != 'EQUALS': j += 1
if j < len(toks):
val, old = parse_tokens(toks[j+1:], env, funcs), block_assigns.get(var, env.get(var, _u32(0)))
bw = dt.itemsize * 8
block_assigns[var] = env[var] = _set_bits(old, val, bw, idx * bw)
if assigns is not None: assigns.append((f'{var}.{dt_name}[{idx}]', val))
i += 1
continue
# Dynamic bit: var.type[expr_with_brackets] = value
if len(toks) >= 5 and toks[0].type == 'IDENT' and toks[1].type == 'DOT' and \
toks[2].type == 'IDENT' and toks[3].type == 'LBRACKET':
j, depth, has_inner = 4, 1, False
while j < len(toks) and depth > 0:
if toks[j].type == 'LBRACKET':
depth += 1
has_inner = True
elif toks[j].type == 'RBRACKET': depth -= 1
j += 1
if has_inner:
var = toks[0].val
bit_pos = _to_u32(parse_tokens(toks[4:j-1], env, funcs))
while j < len(toks) and toks[j].type != 'EQUALS': j += 1
if j < len(toks):
val = parse_tokens(toks[j+1:], env, funcs)
old = block_assigns.get(var, env.get(var, _u32(0)))
block_assigns[var] = env[var] = _set_bit(old, bit_pos, val)
i += 1
continue
# If/elsif/else - skip branches with statically false conditions (WAVE32/WAVE64)
if first == 'if':
def parse_cond(s, kw):
ll = s.lower()
return _to_bool(parse_expr(s[ll.find(kw) + len(kw):ll.rfind('then')].strip(), env, funcs))
def is_const(c, v): return c.op == Ops.CONST and c.val is v
cond = parse_cond(line, 'if')
conditions: list[tuple[UOp, UOp | dict[str, VarVal] | None]] = [(cond, None)] if not is_const(cond, False) else []
branch_assigns: list[tuple[UOp, list]] = [] # (cond, assigns_list) for side-effect merging
else_branch: tuple[UOp | None, dict[str, VarVal]] = (None, {})
else_side_effects: list = []
env_snap = dict(env)
static_true = is_const(cond, True) # track if any condition is statically true
i += 1
if_side: list = [] if assigns is not None and not is_const(cond, False) else []
i, branch, ret = parse_block(lines, i, env, funcs, if_side if assigns is not None and not is_const(cond, False) else None)
if conditions: conditions[0] = (cond, ret if ret is not None else branch)
if assigns is not None and not is_const(cond, False): branch_assigns.append((cond, if_side))
env.clear()
env.update(env_snap)
while i < len(lines):
ltoks = tokenize(lines[i])
if ltoks[0].type != 'IDENT': break
lf = ltoks[0].val.lower()
if lf == 'elsif':
c = parse_cond(lines[i], 'elsif')
take = not static_true and not is_const(c, False)
i += 1
br_side: list = [] if assigns is not None and take else []
i, branch, ret = parse_block(lines, i, env, funcs, br_side if assigns is not None and take else None)
if take:
conditions.append((c, ret if ret is not None else branch))
if is_const(c, True): static_true = True
if assigns is not None: branch_assigns.append((c, br_side))
env.clear()
env.update(env_snap)
elif lf == 'else':
i += 1
el_side: list = [] if assigns is not None and not static_true else []
i, branch, ret = parse_block(lines, i, env, funcs, el_side if assigns is not None and not static_true else None)
if not static_true:
else_branch = (ret, branch)
if assigns is not None: else_side_effects = el_side
env.clear()
env.update(env_snap)
elif lf == 'endif':
i += 1
break
else: break
# Check if any branch returned a value (lambda-style)
if any(isinstance(br, UOp) for _, br in conditions):
result = else_branch[0]
for c, rv in reversed(conditions):
if isinstance(rv, UOp) and isinstance(result, UOp):
if rv.dtype != result.dtype: result = result.cast(rv.dtype)
result = c.where(rv, result)
return i, block_assigns, result
# If statically true, use that branch directly; otherwise merge with WHERE
if static_true:
ba = next((b for c, b in conditions if is_const(c, True) and isinstance(b, dict)), {})
block_assigns.update(ba)
env.update(ba)
# For static true, forward side effects unconditionally
if assigns is not None:
for bc, bse in branch_assigns:
if is_const(bc, True): assigns.extend(bse)
else:
else_assigns = else_branch[1]
all_vars = set().union(*[ba.keys() for _, ba in conditions if isinstance(ba, dict)], else_assigns.keys())
for var in all_vars:
res: Any = else_assigns.get(var, block_assigns.get(var, env.get(var, _u32(0))))
for cond, ba in reversed(conditions): # type: ignore[assignment]
if isinstance(ba, dict) and var in ba:
tv = ba[var]
if isinstance(tv, UOp) and isinstance(res, UOp):
res = cond.where(tv, res.cast(tv.dtype) if tv.dtype != res.dtype else res)
block_assigns[var] = env[var] = res
# Merge side effects from branches with conditions
if assigns is not None:
def _cond_side_effect(cnd, dest, val):
if isinstance(val, tuple) and len(val) == 4: # VGPR bit-slice: (idx, rhs, hi, lo) -> add condition
return (dest, (val[0], val[1], val[2], val[3], cnd))
if isinstance(val, tuple) and len(val) == 2: # VGPR/MEM write: (addr, rhs) -> condition rhs
return (dest, (val[0], cnd.where(val[1], val[1])))
return (dest, val)
# Build combined condition: each branch fires when its cond is true AND no earlier cond was true
remaining = UOp.const(True)
for bc, bse in branch_assigns:
effective = remaining & bc if remaining.op != Ops.CONST else bc
for dest, val in bse: assigns.append(_cond_side_effect(effective, dest, val))
remaining = remaining & bc.logical_not() if remaining.op != Ops.CONST else bc.logical_not()
for dest, val in else_side_effects: assigns.append(_cond_side_effect(remaining, dest, val))
continue
# Regular assignment: var = value
for j, t in enumerate(toks):
if t.type == 'EQUALS':
if any(toks[k].type == 'OP' and toks[k].val in ('<', '>', '!', '=') for k in range(j)): break
base_var = toks[0].val
block_assigns[base_var] = env[base_var] = parse_tokens(toks[j+1:], env, funcs)
i += 1
break
else: i += 1
return i, block_assigns, None
def parse_expr(expr: str, env: dict[str, VarVal], funcs: dict | None = None) -> UOp:
return parse_tokens(tokenize(expr.strip().rstrip(';')), env, funcs)
def parse_pcode(pcode: str, srcs: dict[str, UOp | int] | None = None, funcs: dict | None = None) -> tuple[dict, list]:
env: dict = srcs.copy() if srcs else {}
assigns: list[tuple[str, UOp]] = []
lines: list[str] = []
blocks: list[str] = []
for raw in pcode.splitlines():
line = raw.split('//')[0].strip().rstrip(';')
if not line: continue
# Both block syntaxes share the same parser; braces supply the implicit end markers.
if line.startswith('}') and blocks:
end = blocks.pop()
line = line[1:].strip()
if not line.startswith(('elsif', 'else')): lines.append(end)
if m := re.match(r'(if|elsif|else|for)\b.*\{$', line):
blocks.append('endfor' if m[1] == 'for' else 'endif')
line = line[:-1].rstrip()
if m[1] in ('if', 'elsif'): line += ' then'
if not line: continue
line = re.sub(r'=\s*(\w+):(\w+)$', r'= {\1, \2}', line)
if lines and re.search(r'(&&|\|\||[&|+\-*/^])\s*$', lines[-1]): lines[-1] += ' ' + line
else: lines.append(line)
assert not blocks, "unclosed pcode block"
_, final, _ = parse_block(lines, 0, env, {**_FUNCS, **funcs} if funcs else None, assigns=assigns)
sliced = set(d.split('[')[0] for d, _ in assigns if '[' in d)
for var, val in final.items():
if var in ['D0', 'S0', 'SCC', 'VCC', 'EXEC', 'PC', 'RETURN_DATA', 'VDATA'] and isinstance(val, UOp):
if var in sliced and not any(re.match(rf'{var}\.\w+\s*=', l) for l in lines): continue
for l in lines:
if (m := re.match(rf'{var}\.(\w+(?:\[\w+\])?)', l)):
assigns.append((f'{var}.{m.group(1)}', val))
break
else: assigns.append((var, val))
return env, assigns