use crate::FuncPtr;
use crate::ir::{
ProtoAssignDynamicStatement, ProtoAssignStatement, ProtoExpression, ProtoForBound,
ProtoForRange, ProtoForStatement, ProtoStatement, VarOffset, native_bytes,
};
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Arc, OnceLock};
use veryl_analyzer::ir::Op;
use veryl_analyzer::value::Value;
pub type AotCell = Arc<OnceLock<EmittedModule>>;
use std::cell::Cell;
thread_local! {
static EVENT_MODE: Cell<bool> = const { Cell::new(false) };
}
fn event_mode() -> bool {
EVENT_MODE.with(|c| c.get())
}
fn set_event_mode(on: bool) {
EVENT_MODE.with(|c| c.set(on));
}
fn emit_log_push(offset_expr: &str, payload_expr: &str, wc: usize) -> String {
use crate::ir::write_log::{
WRITE_LOG_ENTRY_OFFSET_MASK_XZ, WRITE_LOG_ENTRY_OFFSET_OFFSET,
WRITE_LOG_ENTRY_OFFSET_PAYLOAD, WRITE_LOG_ENTRY_OFFSET_WIDTH_CLASS, WRITE_LOG_ENTRY_SIZE,
WRITE_LOG_NARROW_OFFSET_COUNT, WRITE_LOG_NARROW_OFFSET_ENTRIES_PTR,
};
format!(
"{{ unsigned char* _lb = (unsigned char*)write_log; \
unsigned int _lc = *(unsigned int*)(_lb + {cnt}); \
unsigned char* _ls = (*(unsigned char**)(_lb + {eptr})) + (unsigned long)_lc * {esz}ul; \
*(unsigned int*)(_ls + {o_off}) = (unsigned int)({off}); \
*(unsigned short*)(_ls + {o_mask}) = 0; \
*(unsigned short*)(_ls + {o_wc}) = (unsigned short){wc}u; \
*(unsigned long long*)(_ls + {o_pay}) = (unsigned long long)({pay}); \
*(unsigned int*)(_lb + {cnt}) = _lc + 1u; }}",
cnt = WRITE_LOG_NARROW_OFFSET_COUNT,
eptr = WRITE_LOG_NARROW_OFFSET_ENTRIES_PTR,
esz = WRITE_LOG_ENTRY_SIZE,
o_off = WRITE_LOG_ENTRY_OFFSET_OFFSET,
o_mask = WRITE_LOG_ENTRY_OFFSET_MASK_XZ,
o_wc = WRITE_LOG_ENTRY_OFFSET_WIDTH_CLASS,
o_pay = WRITE_LOG_ENTRY_OFFSET_PAYLOAD,
off = offset_expr,
pay = payload_expr,
wc = wc,
)
}
pub fn diag_enabled() -> bool {
std::env::var("VERYL_AOT_C_DIAG").as_deref() == Ok("1")
}
fn ev_diag(msg: &str) {
use std::sync::atomic::{AtomicUsize, Ordering};
static N: AtomicUsize = AtomicUsize::new(0);
if (std::env::var("VERYL_AOT_C_EVENT_DIAG").as_deref() == Ok("1") || diag_enabled())
&& N.fetch_add(1, Ordering::Relaxed) < 24
{
eprintln!("[aot_event_ff] {msg}");
}
}
pub fn comb_fallback_reason(stmts: &[ProtoStatement]) -> String {
for s in stmts {
if emit_stmt(s).is_none() {
return diag_find_fail(s);
}
}
"no single stmt isolated".to_string()
}
fn diag_find_fail(stmt: &ProtoStatement) -> String {
match stmt {
ProtoStatement::CompiledBlock(cb) => {
for s in &cb.original_stmts {
let mut adj = s.clone();
adj.adjust_offsets(cb.ff_delta_bytes, cb.comb_delta_bytes);
if emit_stmt(&adj).is_none() {
return format!("CB/{}", diag_find_fail(&adj));
}
}
"CB(?)".to_string()
}
ProtoStatement::If(x) => {
if let Some(c) = &x.cond
&& emit_expr(c).is_none()
{
return "If-cond-expr".to_string();
}
for s in x.true_side.iter().chain(x.false_side.iter()) {
if emit_stmt(s).is_none() {
return format!("If/{}", diag_find_fail(s));
}
}
"If(?)".to_string()
}
ProtoStatement::SequentialBlock(body) => {
for s in body {
if emit_stmt(s).is_none() {
return format!("Seq/{}", diag_find_fail(s));
}
}
"Seq(?)".to_string()
}
ProtoStatement::Assign(a) => format!(
"Assign(ff={},dw={},sel={:?},dynsel={},rhssel={:?},exprOK={})",
a.dst.is_ff(),
a.dst_width,
a.select,
a.dynamic_select.is_some(),
a.rhs_select,
emit_expr(&a.expr).is_some(),
),
ProtoStatement::AssignDynamic(a) => format!(
"AssignDyn(ff={},dw={},sel={:?},dynsel={},idxOK={},exprOK={})",
a.dst_base.is_ff(),
a.dst_width,
a.select,
a.dynamic_select.is_some(),
emit_expr(&a.dst_index_expr).is_some(),
emit_expr(&a.expr).is_some(),
),
ProtoStatement::SystemFunctionCall(_) => "SysFn".to_string(),
ProtoStatement::For(_) => "For".to_string(),
ProtoStatement::Break => "Break".to_string(),
_ => "leaf".to_string(),
}
}
fn apply_rhs_select(rhs: String, rhs_select: Option<(usize, usize)>) -> Option<String> {
match rhs_select {
None => Some(rhs),
Some((hi, lo)) => {
let nbits = hi.checked_sub(lo)?.checked_add(1)?;
if nbits >= 64 {
return None;
}
let mask = (1u64 << nbits) - 1;
Some(format!(
"((({rhs}) >> {lo}) & 0x{m:x}ULL)",
rhs = rhs,
lo = lo,
m = mask
))
}
}
}
fn width_mask(width: usize) -> u64 {
if width >= 64 {
u64::MAX
} else {
(1u64 << width) - 1
}
}
fn emit_event_ff_assign(a: &ProtoAssignStatement) -> Option<String> {
if a.dst_width == 0 || a.dst_width > 64 {
ev_diag(&format!("static FF: width={} (wide/zero)", a.dst_width));
return None; }
let nb = native_bytes(a.dst_width);
let cty = native_c_type(nb)?;
let dst_raw = match a.dst {
VarOffset::Ff(o) => o,
VarOffset::Comb(_) => return None,
};
let cur_off = a.dst_ff_current_offset;
if cur_off < 0 || dst_raw < 0 {
return None;
}
let packed = dst_raw == cur_off;
let log_off = format!("{:#x}", cur_off);
let dst_off = format!("{:#x}", dst_raw);
let dwmask = width_mask(a.dst_width);
let rhs = apply_rhs_select(emit_expr_root(&a.expr)?, a.rhs_select)?;
if let Some(dyn_sel) = &a.dynamic_select {
let ew = dyn_sel.elem_width;
let ne = dyn_sel.num_elements;
if ew == 0 || ew >= 64 || ne == 0 || ne.checked_mul(ew)? > 64 {
ev_diag(&format!(
"static FF: dynamic_select ew={ew} ne={ne} unsupported"
));
return None;
}
let vmask = (1u64 << ew) - 1;
let max_idx = ne - 1;
let idx = emit_expr(&dyn_sel.index_expr)?;
let body = format!(
"uint64_t _di_raw = (uint64_t)({idx}); \
uint64_t _di = _di_raw < {max} ? _di_raw : {max}; \
uint64_t _sh = _di * {ew}ull; \
uint64_t _m = ((((uint64_t)*((const {ct}*)(ff_values + {dst})) & ~(0x{vm:x}ULL << _sh)) | \
(((uint64_t)({rhs}) & 0x{vm:x}ULL) << _sh)) & 0x{dw:x}ULL);",
idx = idx,
max = max_idx,
ew = ew,
ct = cty,
dst = dst_off,
vm = vmask,
rhs = rhs,
dw = dwmask,
);
let store = if packed {
String::new()
} else {
format!(
"*(({ct}*)(ff_values + {dst})) = ({ct})_m;",
ct = cty,
dst = dst_off
)
};
let push = emit_log_push(&log_off, "_m", nb);
return Some(format!("{{ {body} {store} {push} }}"));
}
if let Some((hi, lo)) = a.select {
let nbits = hi.checked_sub(lo)?.checked_add(1)?;
if nbits >= 64 {
return None;
}
let vmask = (1u64 << nbits) - 1;
let pmask = vmask << lo;
let merged = format!(
"((((uint64_t)*((const {ct}*)(ff_values + {dst})) & ~0x{pm:x}ULL) | \
((((uint64_t)({rhs})) & 0x{vm:x}ULL) << {lo})) & 0x{dw:x}ULL)",
ct = cty,
dst = dst_off,
pm = pmask,
rhs = rhs,
vm = vmask,
lo = lo,
dw = dwmask,
);
let push = emit_log_push(&log_off, "_m", nb);
if packed {
Some(format!("{{ uint64_t _m = {merged}; {push} }}"))
} else {
Some(format!(
"{{ uint64_t _m = {merged}; *(({ct}*)(ff_values + {dst})) = ({ct})_m; {push} }}",
ct = cty,
dst = dst_off,
))
}
} else {
let payload = format!(
"(((uint64_t)({rhs})) & 0x{dw:x}ULL)",
rhs = rhs,
dw = dwmask
);
let push = emit_log_push(&log_off, "_v", nb);
if packed {
Some(format!("{{ uint64_t _v = {payload}; {push} }}"))
} else {
Some(format!(
"{{ uint64_t _v = {payload}; *(({ct}*)(ff_values + {dst})) = ({ct})_v; {push} }}",
ct = cty,
dst = dst_off,
))
}
}
}
fn emit_event_ff_assign_dynamic(a: &ProtoAssignDynamicStatement) -> Option<String> {
if a.select.is_some() || a.dynamic_select.is_some() {
ev_diag(&format!(
"dyn FF: select={:?} dynsel={}",
a.select,
a.dynamic_select.is_some()
));
return None;
}
if a.dst_width == 0 || a.dst_width > 64 {
ev_diag(&format!("dyn FF: width={}", a.dst_width));
return None;
}
if a.dst_num_elements == 0 {
return None;
}
let nb = native_bytes(a.dst_width);
let cty = native_c_type(nb)?;
let dst_base_raw = match a.dst_base {
VarOffset::Ff(o) => o,
VarOffset::Comb(_) => return None,
};
let cur_base = a.dst_ff_current_base_offset;
if cur_base < 0 || dst_base_raw < 0 {
return None;
}
let rhs = apply_rhs_select(emit_expr_root(&a.expr)?, a.rhs_select)?;
let idx = emit_expr(&a.dst_index_expr)?;
let max_idx = a.dst_num_elements.saturating_sub(1);
let dwmask = width_mask(a.dst_width);
let payload = format!(
"(((uint64_t)({rhs})) & 0x{dw:x}ULL)",
rhs = rhs,
dw = dwmask
);
let push = emit_log_push("_woff", "_wval", nb);
Some(format!(
"({{ uint64_t _idx_raw = (uint64_t)({idx}); \
uint64_t _idx = _idx_raw < {max} ? _idx_raw : {max}; \
uint64_t _wval = {pay}; \
*(({ct}*)(ff_values + {wbase:#x} + (intptr_t){stride} * (intptr_t)_idx)) = ({ct})_wval; \
unsigned int _woff = (unsigned int)((intptr_t){cbase:#x} + (intptr_t){stride} * (intptr_t)_idx); \
{push} }});",
idx = idx,
max = max_idx,
pay = payload,
ct = cty,
wbase = dst_base_raw,
stride = a.dst_stride,
cbase = cur_base,
push = push,
))
}
pub struct EmittedModule {
pub func: FuncPtr,
_lib: libloading::Library,
}
fn compile_or_spawn(src: String, async_mode: bool) -> AotCell {
let cell = Arc::new(OnceLock::new());
if async_mode {
let c = Arc::clone(&cell);
std::thread::spawn(move || {
if let Ok(m) = compile_source(&src) {
let _ = c.set(m);
}
});
} else if let Ok(m) = compile_source(&src) {
let _ = cell.set(m);
}
cell
}
pub fn prepare_comb(stmts: &[ProtoStatement], async_mode: bool) -> Option<AotCell> {
let src = emit_function(stmts)?; Some(compile_or_spawn(src, async_mode))
}
pub fn prepare_event(stmts: &[ProtoStatement], async_mode: bool) -> Option<AotCell> {
let src = emit_event_function(stmts)?;
Some(compile_or_spawn(src, async_mode))
}
fn emit_event_function(stmts: &[ProtoStatement]) -> Option<String> {
let diag = std::env::var("VERYL_AOT_C_EVENT_DIAG").as_deref() == Ok("1");
set_event_mode(true);
let body_res = (|| {
let mut cb = String::new();
for (i, stmt) in stmts.iter().enumerate() {
let s = match emit_stmt(stmt) {
Some(s) => s,
None => {
if diag {
let label: &str = match stmt {
ProtoStatement::Assign(a) => {
if a.dst.is_ff() {
let raw = match a.dst {
VarOffset::Ff(o) => o,
VarOffset::Comb(o) => o,
};
eprintln!(
"[aot_event_diag] bail stmt#{i} Assign(FF) dst_raw={} cur_off={} packed={} width={} select={:?} dynsel={}",
raw,
a.dst_ff_current_offset,
raw == a.dst_ff_current_offset,
a.dst_width,
a.select,
a.dynamic_select.is_some(),
);
}
"Assign"
}
ProtoStatement::AssignDynamic(a) => {
eprintln!(
"[aot_event_diag] bail stmt#{i} AssignDynamic dst_ff={} width={} select={:?} dynsel={}",
a.dst_base.is_ff(),
a.dst_width,
a.select,
a.dynamic_select.is_some(),
);
"AssignDynamic"
}
ProtoStatement::If(_) => "If",
ProtoStatement::SequentialBlock(_) => "SeqBlock",
ProtoStatement::CompiledBlock(_) => "CompiledBlock",
ProtoStatement::For(_) => "For",
ProtoStatement::SystemFunctionCall(_) => "SysFn",
ProtoStatement::Break => "Break",
_ => "Other",
};
let leaf = diag_find_fail(stmt);
eprintln!(
"[aot_event_diag] first bail at stmt#{i} kind={label} leaf={leaf} (total={})",
stmts.len()
);
}
return None;
}
};
cb.push_str(" ");
cb.push_str(&s);
cb.push('\n');
}
if diag {
eprintln!(
"[aot_event_diag] ALL {} top-level event stmts emitted OK",
stmts.len()
);
}
Some(cb)
})();
set_event_mode(false);
let body = body_res?;
let mut src = String::from(
"// AOT-C event; do not edit.\n\
#include <stdint.h>\n\
typedef __uint128_t veryl_u128_ua __attribute__((__aligned__(1)));\n\
\n\
__attribute__((visibility(\"default\")))\n\
void veryl_aot_eval(uint8_t *__restrict__ ff_values, uint8_t *__restrict__ comb_values, uint64_t *__restrict__ write_log) {\n",
);
src.push_str(&body);
src.push_str("}\n");
Some(src)
}
pub fn compile_source(src: &str) -> Result<EmittedModule, String> {
let cache_dir = aot_c_cache_dir().map_err(|e| format!("cache dir: {e}"))?;
std::fs::create_dir_all(&cache_dir).map_err(|e| format!("create_dir_all: {e}"))?;
let cc_name = std::env::var("VERYL_AOT_CC").unwrap_or_else(|_| "cc".to_string());
let mut flags: Vec<String> = [
"-O3",
"-fPIC",
"-shared",
"-fvisibility=hidden",
"-fno-strict-aliasing",
"-Wno-unused-but-set-variable",
"-Wno-overflow",
]
.iter()
.map(|s| s.to_string())
.collect();
if let Ok(extra) = std::env::var("VERYL_AOT_CFLAGS") {
flags.extend(extra.split_whitespace().map(str::to_string));
}
let flags_joined = flags.join(" ");
let hash = fnv1a_64_hex_parts(&[
env!("CARGO_PKG_VERSION"),
&cc_name,
&flags_joined,
std::env::consts::ARCH,
std::env::consts::OS,
src,
]);
let so_path = cache_dir.join(format!("veryl_aot_{hash}.so"));
if !so_path.exists() {
let c_path = cache_dir.join(format!("veryl_aot_{hash}.c"));
std::fs::write(&c_path, src).map_err(|e| format!("write {}: {}", c_path.display(), e))?;
let mut cmd = Command::new(&cc_name);
cmd.args(&flags).arg("-o").arg(&so_path).arg(&c_path);
let out = cmd
.output()
.map_err(|e| format!("spawn cc: {e} (set VERYL_AOT_CC to override)"))?;
if !out.status.success() {
let _ = std::fs::remove_file(&so_path);
return Err(format!(
"cc {} failed: {}\n{}",
c_path.display(),
out.status,
String::from_utf8_lossy(&out.stderr),
));
}
}
let lib = unsafe { libloading::Library::new(&so_path) }
.map_err(|e| format!("dlopen {}: {}", so_path.display(), e))?;
let func: FuncPtr = unsafe {
*lib.get::<FuncPtr>(b"veryl_aot_eval\0")
.map_err(|e| format!("dlsym veryl_aot_eval: {e}"))?
};
Ok(EmittedModule { func, _lib: lib })
}
fn aot_c_cache_dir() -> Result<PathBuf, String> {
if let Ok(p) = std::env::var("VERYL_AOT_CACHE_DIR") {
return Ok(PathBuf::from(p));
}
let base = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
.ok_or("neither XDG_CACHE_HOME nor HOME set")?;
Ok(base.join("veryl").join("aot_c"))
}
fn fnv1a_64_hex_parts(parts: &[&str]) -> String {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut h: u64 = FNV_OFFSET;
for part in parts {
for &b in part.as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h ^= 0xff;
h = h.wrapping_mul(FNV_PRIME);
}
format!("{h:016x}")
}
pub fn emit_function(stmts: &[ProtoStatement]) -> Option<String> {
let chunk_size: usize = std::env::var("VERYL_AOT_C_CHUNK_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(900);
let mut body = String::new();
body.push_str(
"// AOT-C generated; do not edit.\n\
#include <stdint.h>\n\
typedef __uint128_t veryl_u128_ua __attribute__((__aligned__(1)));\n\
\n",
);
let chunks: Vec<&[ProtoStatement]> = if chunk_size == 0 || stmts.len() <= chunk_size {
vec![stmts]
} else {
stmts.chunks(chunk_size).collect()
};
let mut chunk_bodies: Vec<String> = Vec::with_capacity(chunks.len());
for chunk in &chunks {
let mut cb = String::new();
for stmt in *chunk {
let s = emit_stmt(stmt)?;
cb.push_str(" ");
cb.push_str(&s);
cb.push('\n');
}
chunk_bodies.push(cb);
}
if chunks.len() == 1 {
body.push_str(
"__attribute__((visibility(\"default\")))\n\
void veryl_aot_eval(uint8_t *__restrict__ ff_values, uint8_t *__restrict__ comb_values, uint64_t *__restrict__ write_log) {\n\
\x20 (void)write_log;\n",
);
body.push_str(&chunk_bodies[0]);
body.push_str("}\n");
} else {
for (i, cb) in chunk_bodies.iter().enumerate() {
body.push_str(&format!(
"static __attribute__((noinline)) \
void veryl_aot_chunk_{i}(uint8_t *__restrict__ ff_values, uint8_t *__restrict__ comb_values, uint64_t *__restrict__ write_log) {{\n\
\x20 (void)write_log;\n",
));
body.push_str(cb);
body.push_str("}\n\n");
}
body.push_str(
"__attribute__((visibility(\"default\")))\n\
void veryl_aot_eval(uint8_t *__restrict__ ff_values, uint8_t *__restrict__ comb_values, uint64_t *__restrict__ write_log) {\n",
);
for i in 0..chunks.len() {
body.push_str(&format!(
" veryl_aot_chunk_{i}(ff_values, comb_values, write_log);\n",
));
}
body.push_str("}\n");
}
Some(body)
}
pub fn emit_stmt(stmt: &ProtoStatement) -> Option<String> {
match stmt {
ProtoStatement::Assign(a) => {
if a.dst.is_ff() {
return emit_event_ff_assign(a);
}
if a.dynamic_select.is_some() {
return None;
}
let nb = native_bytes(a.dst_width);
let cty = native_c_type(nb)?;
let rhs_unselected = emit_expr_root(&a.expr)?;
let rhs_str = match a.rhs_select {
None => rhs_unselected,
Some((rhs_hi, rhs_lo)) => {
let nbits = rhs_hi.checked_sub(rhs_lo)?.checked_add(1)?;
if nbits >= 64 {
return None;
}
let mask = (1u64 << nbits) - 1;
format!(
"((({src}) >> {lo}) & 0x{m:x}ULL)",
src = rhs_unselected,
lo = rhs_lo,
m = mask,
)
}
};
let VarOffset::Comb(store_off) = a.dst else {
return None;
};
let buf = "comb_values";
if let Some((hi, lo)) = a.select {
let nbits = hi.checked_sub(lo)?.checked_add(1)?;
if nbits >= 64 || lo >= 64 || lo + nbits > 64 {
return None;
}
let value_mask = (1u64 << nbits) - 1;
let pos_mask = value_mask << lo;
Some(format!(
"{{ uint64_t _v = ((uint64_t)({rhs})) & 0x{vmask:x}ULL; \
{ct} _o = *(({ct}*)({b} + {o:#x})); \
*(({ct}*)({b} + {o:#x})) = ({ct})((_o & ({ct})(~(uint64_t)0x{pmask:x}ULL)) | ({ct})(_v << {lo})); }}",
rhs = rhs_str,
vmask = value_mask,
ct = cty,
b = buf,
o = store_off,
pmask = pos_mask,
lo = lo,
))
} else {
let native_bits = nb * 8;
if a.dst_width > 64 && a.dst_width < native_bits {
let mask: u128 = (1u128 << a.dst_width) - 1;
Some(format!(
"*(({ct}*)({b} + {o:#x})) = ({ct})(((__uint128_t)({rhs})) \
& (((__uint128_t)0x{hi:x}ULL << 64) | (__uint128_t)0x{lo:x}ULL));",
ct = cty,
b = buf,
o = store_off,
rhs = rhs_str,
hi = (mask >> 64) as u64,
lo = mask as u64,
))
} else if a.dst_width < native_bits && a.dst_width > 0 {
let mask = (1u64 << a.dst_width) - 1;
Some(format!(
"*(({ct}*)({b} + {o:#x})) = ({ct})(((uint64_t)({rhs})) & 0x{m:x}ULL);",
ct = cty,
b = buf,
o = store_off,
rhs = rhs_str,
m = mask,
))
} else {
Some(format!(
"*(({ct}*)({b} + {o:#x})) = ({ct})({rhs});",
ct = cty,
b = buf,
o = store_off,
rhs = rhs_str,
))
}
}
}
ProtoStatement::If(if_stmt) => {
let true_body = emit_block(&if_stmt.true_side)?;
let false_body = emit_block(&if_stmt.false_side)?;
match &if_stmt.cond {
None => Some(format!("{{ {} }}", false_body)),
Some(cond) => {
let c = emit_expr(cond)?;
Some(format!(
"if ({c}) {{ {t} }} else {{ {f} }}",
c = c,
t = true_body,
f = false_body,
))
}
}
}
ProtoStatement::SequentialBlock(body) => {
let inner = emit_block(body)?;
Some(format!("{{ {} }}", inner))
}
ProtoStatement::AssignDynamic(a) => {
if event_mode() && a.dst_base.is_ff() {
return emit_event_ff_assign_dynamic(a);
}
if a.dynamic_select.is_some() {
return None;
}
if a.dst_base.is_ff() {
return None; }
if a.dst_num_elements == 0 || a.dst_width == 0 || a.dst_width > 64 {
return None;
}
let nb = native_bytes(a.dst_width);
let cty = native_c_type(nb)?;
let base_off = match a.dst_base {
VarOffset::Comb(o) => o,
VarOffset::Ff(_) => unreachable!(),
};
let rhs = apply_rhs_select(emit_expr_root(&a.expr)?, a.rhs_select)?;
let idx_str = emit_expr(&a.dst_index_expr)?;
let max_idx = a.dst_num_elements.saturating_sub(1);
let addr = format!(
"(comb_values + {off:#x} + (intptr_t){stride} * (intptr_t)_idx)",
off = base_off,
stride = a.dst_stride,
);
let store = if let Some((hi, lo)) = a.select {
let nbits = hi.checked_sub(lo)?.checked_add(1)?;
if nbits >= 64 {
return None;
}
let vmask = (1u64 << nbits) - 1;
let pmask = vmask << lo;
format!(
"{ct}* _p = ({ct}*){addr}; {ct} _o = *_p; \
*_p = ({ct})((_o & ({ct})(~(uint64_t)0x{pm:x}ULL)) | \
({ct})((((uint64_t)({rhs})) & 0x{vm:x}ULL) << {lo}));",
ct = cty,
addr = addr,
pm = pmask,
rhs = rhs,
vm = vmask,
lo = lo,
)
} else {
let dwmask = width_mask(a.dst_width);
format!(
"*(({ct}*){addr}) = ({ct})(((uint64_t)({rhs})) & 0x{m:x}ULL);",
ct = cty,
addr = addr,
rhs = rhs,
m = dwmask,
)
};
Some(format!(
"({{ uint64_t _idx_raw = (uint64_t)({idx}); \
uint64_t _idx = _idx_raw < {max} ? _idx_raw : {max}; \
{store} }});",
idx = idx_str,
max = max_idx,
store = store,
))
}
ProtoStatement::CompiledBlock(cb) => {
let mut s = String::from("{ ");
for stmt in &cb.original_stmts {
let mut adjusted = stmt.clone();
adjusted.adjust_offsets(cb.ff_delta_bytes, cb.comb_delta_bytes);
let inner = emit_stmt(&adjusted)?;
s.push_str(&inner);
s.push(' ');
}
s.push('}');
Some(s)
}
ProtoStatement::For(for_stmt) => emit_for(for_stmt),
ProtoStatement::Break => Some("break;".to_string()),
ProtoStatement::SystemFunctionCall(call) => {
let _ = call;
None
}
ProtoStatement::TbMethodCall { .. } => {
None
}
}
}
fn emit_for(for_stmt: &ProtoForStatement) -> Option<String> {
let (start, end_excl, step) = match &for_stmt.range {
ProtoForRange::Forward {
start,
end,
inclusive,
step,
} => {
let s = match start {
ProtoForBound::Const(v) => *v,
ProtoForBound::Dynamic(_) => return None,
};
let e = match end {
ProtoForBound::Const(v) => *v,
ProtoForBound::Dynamic(_) => return None,
};
let e_excl = if *inclusive { e.checked_add(1)? } else { e };
(s, e_excl, *step)
}
ProtoForRange::Reverse { .. } | ProtoForRange::Stepped { .. } => return None,
};
if step == 0 {
return None; }
if for_stmt.var_width == 0 || for_stmt.var_width > 64 {
return None;
}
let nb = native_bytes(for_stmt.var_width);
let cty = native_c_type(nb)?;
let (buf, off) = match for_stmt.var_offset {
VarOffset::Ff(o) => ("ff_values", o),
VarOffset::Comb(o) => ("comb_values", o),
};
let mut body = String::new();
for s in &for_stmt.body {
body.push_str(&emit_stmt(s)?);
body.push(' ');
}
Some(format!(
"for (uint64_t _it = {start}ULL; _it < {end}ULL; _it += {step}ULL) {{ \
*(({ct}*)({b} + {off:#x})) = ({ct})_it; \
{body} \
}}",
start = start,
end = end_excl,
step = step,
ct = cty,
b = buf,
off = off,
body = body,
))
}
fn emit_block(stmts: &[ProtoStatement]) -> Option<String> {
let mut s = String::new();
for st in stmts {
s.push_str(&emit_stmt(st)?);
s.push(' ');
}
Some(s)
}
pub fn emit_expr(expr: &ProtoExpression) -> Option<String> {
emit_expr_inner(expr, true)
}
pub fn emit_expr_root(expr: &ProtoExpression) -> Option<String> {
emit_expr_inner(expr, false)
}
fn emit_expr_inner(expr: &ProtoExpression, needs_clean: bool) -> Option<String> {
match expr {
ProtoExpression::Value {
value,
width,
expr_context,
} => {
let mut effective_width = *width;
if effective_width == 0
&& let Value::U64(v) = value
&& v.width == 0
&& v.payload != 0
&& expr_context.width > 0
{
effective_width = expr_context.width.min(128);
}
emit_value(value, effective_width)
}
ProtoExpression::Variable {
var_offset,
select,
dynamic_select,
width,
var_full_width,
..
} => {
if let Some(dyn_sel) = dynamic_select {
if *var_full_width == 0 || *var_full_width > 64 {
return None;
}
if dyn_sel.elem_width == 0 || dyn_sel.elem_width >= 64 {
return None;
}
if dyn_sel.num_elements == 0 {
return None;
}
let load = emit_var_load(var_offset, *var_full_width)?;
let idx_str = emit_expr(&dyn_sel.index_expr)?;
let max_idx = dyn_sel.num_elements.saturating_sub(1);
let mask = (1u64 << dyn_sel.elem_width) - 1;
return Some(format!(
"({{ uint64_t _idx_raw = (uint64_t)({idx}); \
uint64_t _idx = _idx_raw < {max} ? _idx_raw : {max}; \
((({load}) >> (_idx * {ew})) & 0x{mask:x}ULL); }})",
idx = idx_str,
max = max_idx,
load = load,
ew = dyn_sel.elem_width,
mask = mask,
));
}
let load_width = if let Some((hi, _)) = select {
(*hi + 1).max(*width)
} else {
*width
};
let load = emit_var_load(var_offset, load_width)?;
if let Some((hi, lo)) = select {
let nbits = hi.checked_sub(*lo)?.checked_add(1)?;
if nbits >= 64 {
return None; }
let mask = (1u64 << nbits) - 1;
Some(format!(
"((({load}) >> {lo}) & 0x{mask:x}ULL)",
load = load,
lo = lo,
mask = mask,
))
} else {
Some(load)
}
}
ProtoExpression::Unary {
op,
x,
expr_context,
..
} => {
let xs = emit_expr(x)?;
let xw = x.width();
let xv = if expr_context.signed && xw > 0 && xw < 64 {
let shift = 64 - xw;
format!(
"(((int64_t)((uint64_t)({}) << {})) >> {})",
xs, shift, shift
)
} else {
format!("((int64_t)((uint64_t)({})))", xs)
};
match op {
Op::LogicNot => Some(format!("(!({}))", xs)),
Op::BitNot => Some(format!("(~({}))", xv)),
Op::Sub => Some(format!("(-({}))", xv)),
_ => None, }
}
ProtoExpression::Binary {
x,
op,
y,
expr_context,
..
} => {
let is_signed_cmp = expr_context.signed
&& matches!(op, Op::Less | Op::Greater | Op::LessEq | Op::GreaterEq);
let is_signed_divrem = expr_context.signed && matches!(op, Op::Div | Op::Rem);
let operand_needs_clean = if is_signed_cmp || is_signed_divrem {
false
} else {
match op {
Op::Add | Op::Sub | Op::Mul => false,
Op::BitAnd
| Op::BitOr
| Op::BitXor
| Op::BitNand
| Op::BitNor
| Op::BitXnor => needs_clean,
_ => true,
}
};
let xs = emit_expr_inner(x, operand_needs_clean)?;
let ys = emit_expr_inner(y, operand_needs_clean)?;
if is_signed_cmp || is_signed_divrem {
let x_w = x.width();
let y_w = y.width();
if x_w == 0 || y_w == 0 || x_w > 64 || y_w > 64 {
return None;
}
let c_op = match op {
Op::Less => "<",
Op::Greater => ">",
Op::LessEq => "<=",
Op::GreaterEq => ">=",
Op::Div => "/",
Op::Rem => "%",
_ => unreachable!(),
};
let sext = |s: &str, w: usize| -> String {
if w == 64 {
format!("((int64_t)((uint64_t)({})))", s)
} else {
let shift = 64 - w;
format!("(((int64_t)((uint64_t)({}) << {})) >> {})", s, shift, shift,)
}
};
let inner = format!("(({}) {} ({}))", sext(&xs, x_w), c_op, sext(&ys, y_w),);
if is_signed_divrem {
return Some(format!(
"({{ int64_t _y = {y}; int64_t _x = {x}; \
(_y == 0) ? (int64_t)0 : \
((_y == -1 && _x == INT64_MIN) ? \
{fallback} : (_x {op} _y)); }})",
x = sext(&xs, x_w),
y = sext(&ys, y_w),
op = c_op,
fallback = if matches!(op, Op::Rem) { "0" } else { "_x" },
));
}
return Some(inner);
}
let direct = match op {
Op::Add => Some("+"),
Op::Sub => Some("-"),
Op::Mul => Some("*"),
Op::Div => Some("/"),
Op::Rem => Some("%"),
Op::Eq => Some("=="),
Op::Ne => Some("!="),
Op::EqWildcard => Some("=="),
Op::NeWildcard => Some("!="),
Op::Less => Some("<"),
Op::Greater => Some(">"),
Op::LessEq => Some("<="),
Op::GreaterEq => Some(">="),
Op::LogicAnd => Some("&&"),
Op::LogicOr => Some("||"),
Op::BitAnd => Some("&"),
Op::BitOr => Some("|"),
Op::BitXor => Some("^"),
Op::LogicShiftL | Op::ArithShiftL => Some("<<"),
Op::LogicShiftR => Some(">>"),
_ => None,
};
if let Some(c_op) = direct {
let wide_truncates = match op {
Op::Add | Op::Sub | Op::Mul => x.width() <= 64 && y.width() <= 64,
Op::LogicShiftL | Op::ArithShiftL => x.width() <= 64,
_ => false,
};
if expr_context.width > 64 && (wide_truncates || expr_context.signed) {
return None;
}
let overflow_cond: Option<String> =
if expr_context.signed || expr_context.width == 0 || expr_context.width >= 64 {
None
} else {
let sh = expr_context.width - 1;
let w = expr_context.width;
match op {
Op::Add => Some(format!("((({xs}) | ({ys})) >> {sh})")),
Op::Sub => Some(format!(
"(((({xs}) | ({ys})) >> {sh}) != 0 || ({xs}) < ({ys}))"
)),
Op::LogicShiftL | Op::ArithShiftL => Some(format!(
"(({ys}) >= {w} || ((({xs}) >> ({w} - ({ys}))) != 0))"
)),
_ => None,
}
};
let wmask = |s: String| -> String {
if needs_clean
&& expr_context.width < 64
&& matches!(
op,
Op::Add | Op::Sub | Op::Mul | Op::LogicShiftL | Op::ArithShiftL
)
{
let mask = (1u64 << expr_context.width) - 1;
match &overflow_cond {
Some(cond) => format!(
"({{ uint64_t _t = ({s}); \
if (__builtin_expect(({cond}) != 0, 0)) {{ _t &= 0x{mask:x}ULL; \
__asm__ volatile(\"\" : \"+r\"(_t)); }} _t; }})"
),
None => format!("(({s}) & 0x{mask:x}ULL)"),
}
} else {
s
}
};
if expr_context.signed && expr_context.width > 0 && expr_context.width <= 64 {
let x_w = x.width();
let y_w = y.width();
let target = expr_context.width;
let sext = |s: &str, w: usize| -> String {
if w == 0 || w >= target {
s.to_string()
} else {
let shift = 64 - w;
format!("(((int64_t)((uint64_t)({}) << {})) >> {})", s, shift, shift,)
}
};
let is_shift = matches!(
op,
Op::LogicShiftL | Op::LogicShiftR | Op::ArithShiftL | Op::ArithShiftR
);
let xe = sext(&xs, x_w);
let ye = if is_shift { ys } else { sext(&ys, y_w) };
if matches!(op, Op::LogicShiftR) {
let tmask = if target >= 64 {
u64::MAX
} else {
(1u64 << target) - 1
};
return Some(format!(
"(((uint64_t)(({}) & 0x{:x}ULL)) >> ({}))",
xe, tmask, ye,
));
}
return Some(wmask(format!("(({}) {} ({}))", xe, c_op, ye)));
}
return Some(wmask(format!("(({}) {} ({}))", xs, c_op, ys)));
}
match op {
Op::ArithShiftR => {
let x_w = x.width();
if x_w == 0 || x_w > 64 {
return None; }
if !expr_context.signed {
Some(format!("((uint64_t)({xs}) >> ({ys}))", xs = xs, ys = ys,))
} else if x_w == 64 {
Some(format!(
"((uint64_t)((int64_t)((uint64_t)({xs})) >> ({ys})))",
xs = xs,
ys = ys,
))
} else {
let shift = 64 - x_w;
Some(format!(
"((uint64_t)((((int64_t)((uint64_t)({xs}) << {sh})) >> {sh}) >> ({ys})))",
xs = xs,
ys = ys,
sh = shift,
))
}
}
Op::BitXnor => Some(format!("(~(({}) ^ ({})))", xs, ys)),
Op::BitNand => Some(format!("(~(({}) & ({})))", xs, ys)),
Op::BitNor => Some(format!("(~(({}) | ({})))", xs, ys)),
Op::As => Some(xs),
_ => None, }
}
ProtoExpression::Ternary {
cond,
true_expr,
false_expr,
..
} => {
let c = emit_expr(cond)?;
let t = emit_expr_inner(true_expr, needs_clean)?;
let f = emit_expr_inner(false_expr, needs_clean)?;
Some(format!("(({}) ? ({}) : ({}))", c, t, f))
}
ProtoExpression::Concatenation {
elements, width, ..
} => {
if *width == 0 || *width > 128 {
return None;
}
let wide_acc = *width > 64;
let first_is_bit_repeat = elements
.first()
.is_some_and(|(sub, repeat, _)| *repeat > 1 && sub.width() == 1);
if first_is_bit_repeat && elements.len() == 1 {
let sub_str = emit_expr(&elements[0].0)?;
let mask: u128 = if *width >= 128 {
!0u128
} else {
(1u128 << *width) - 1
};
if wide_acc {
let hi = (mask >> 64) as u64;
let lo = mask as u64;
return Some(format!(
"(((__uint128_t)0 - (__uint128_t)(((uint64_t)({sub})) & 0x1ULL)) & (((__uint128_t)0x{hi:x}ULL << 64) | (__uint128_t)0x{lo:x}ULL))",
sub = sub_str,
hi = hi,
lo = lo,
));
} else {
let mask64 = mask as u64;
return Some(format!(
"((uint64_t)(0ULL - (((uint64_t)({sub})) & 0x1ULL)) & 0x{mask64:x}ULL)",
sub = sub_str,
mask64 = mask64,
));
}
}
let mut acc = if wide_acc {
String::from("((__uint128_t)0)")
} else {
String::from("0ULL")
};
if first_is_bit_repeat && elements.len() >= 2 {
let sign_str = emit_expr(&elements[0].0)?;
let mut lower_width = 0usize;
for (sub, repeat, elem_width) in &elements[1..] {
let sub_width = sub.width();
if sub_width == 0 || sub_width > 64 {
return None;
}
let sub_str = emit_expr(sub)?;
let mask = if sub_width >= 64 {
u64::MAX
} else {
(1u64 << sub_width) - 1
};
let ew = *elem_width;
for _ in 0..*repeat {
if wide_acc {
acc = format!(
"((({acc}) << {w}) | (((__uint128_t)({sub})) & (__uint128_t)0x{mask:x}ULL))",
acc = acc,
w = ew,
sub = sub_str,
mask = mask,
);
} else {
acc = format!(
"((({acc}) << {w}) | (({sub}) & 0x{mask:x}ULL))",
acc = acc,
w = ew,
sub = sub_str,
mask = mask,
);
}
lower_width += ew;
}
}
let mask: u128 = if *width >= 128 {
!0u128
} else {
(1u128 << *width) - 1
};
if wide_acc {
let hi = (mask >> 64) as u64;
let lo = mask as u64;
return Some(format!(
"(((((__uint128_t)0 - (__uint128_t)(((uint64_t)({sign})) & 0x1ULL)) << {lw}) | ({acc})) & (((__uint128_t)0x{hi:x}ULL << 64) | (__uint128_t)0x{lo:x}ULL))",
sign = sign_str,
lw = lower_width,
acc = acc,
hi = hi,
lo = lo,
));
} else {
let mask64 = mask as u64;
return Some(format!(
"((((uint64_t)(0ULL - (((uint64_t)({sign})) & 0x1ULL)) << {lw}) | ({acc})) & 0x{mask64:x}ULL)",
sign = sign_str,
lw = lower_width,
acc = acc,
mask64 = mask64,
));
}
}
for (sub, repeat, _elem_width) in elements {
let sub_width = sub.width();
if sub_width == 0 || sub_width > 64 {
return None;
}
let sub_str = emit_expr(sub)?;
let mask = if sub_width >= 64 {
u64::MAX
} else {
(1u64 << sub_width) - 1
};
for _ in 0..*repeat {
if wide_acc {
acc = format!(
"((({acc}) << {w}) | (((__uint128_t)({sub})) & (__uint128_t)0x{mask:x}ULL))",
acc = acc,
w = sub_width,
sub = sub_str,
mask = mask,
);
} else {
acc = format!(
"((({acc}) << {w}) | (({sub}) & 0x{mask:x}ULL))",
acc = acc,
w = sub_width,
sub = sub_str,
mask = mask,
);
}
}
}
Some(acc)
}
ProtoExpression::DynamicVariable {
base_offset,
stride,
index_expr,
num_elements,
select,
dynamic_select,
width,
..
} => {
if dynamic_select.is_some() {
return None; }
if *num_elements == 0 || *width == 0 || *width > 64 {
return None;
}
let read_bits = match select {
Some((hi, _lo)) => hi.checked_add(1)?,
None => *width,
};
if read_bits > 64 {
return None;
}
let nb_read = native_bytes(read_bits);
let cty = native_c_type(nb_read)?;
let (buf, base_off) = match base_offset {
VarOffset::Ff(o) => ("ff_values", *o),
VarOffset::Comb(o) => ("comb_values", *o),
};
let idx_str = emit_expr(index_expr)?;
let max_idx = num_elements.saturating_sub(1);
let load_expr = format!(
"({{ uint64_t _idx_raw = (uint64_t)({idx}); \
uint64_t _idx = _idx_raw < {max} ? _idx_raw : {max}; \
(uint64_t)*((const {ct}*)({b} + {off:#x} + (intptr_t){stride} * (intptr_t)_idx)); }})",
idx = idx_str,
max = max_idx,
ct = cty,
b = buf,
off = base_off,
stride = stride,
);
if let Some((hi, lo)) = select {
let nbits = hi.checked_sub(*lo)?.checked_add(1)?;
if nbits > 64 {
return None;
}
let mask = if nbits >= 64 {
u64::MAX
} else {
(1u64 << nbits) - 1
};
Some(format!(
"((({load}) >> {lo}) & 0x{mask:x}ULL)",
load = load_expr,
lo = lo,
mask = mask,
))
} else {
Some(load_expr)
}
}
}
}
fn emit_var_load(var_offset: &VarOffset, width: usize) -> Option<String> {
if width > 128 {
return None; }
if width == 0 {
return Some("((uint64_t)0)".to_string());
}
let nb = native_bytes(width);
let cty = native_c_type(nb)?;
let (buf, off) = match var_offset {
VarOffset::Ff(o) => ("ff_values", *o),
VarOffset::Comb(o) => ("comb_values", *o),
};
let result_ty = expr_c_type(width)?;
Some(format!(
"(({rt})*((const {ct}*)({b} + {o:#x})))",
rt = result_ty,
ct = cty,
b = buf,
o = off,
))
}
fn emit_value(value: &Value, width: usize) -> Option<String> {
if width > 128 {
return None;
}
match value {
Value::U64(v) => {
let payload: u128 = if v.width == 0 && v.payload != 0 && v.mask_xz == 0 && width > 0 {
if width >= 128 {
!0u128
} else {
(1u128 << width) - 1
}
} else {
v.payload as u128
};
let masked: u128 = if width == 0 {
0
} else if width >= 128 {
payload
} else {
payload & ((1u128 << width) - 1)
};
if width > 64 {
let hi = (masked >> 64) as u64;
let lo = masked as u64;
Some(format!(
"(((__uint128_t)0x{:x}ULL << 64) | (__uint128_t)0x{:x}ULL)",
hi, lo
))
} else {
Some(format!("0x{:x}ULL", masked as u64))
}
}
Value::BigUint(_) => None, }
}
fn native_c_type(nb: usize) -> Option<&'static str> {
match nb {
1 => Some("uint8_t"),
2 => Some("uint16_t"),
4 => Some("uint32_t"),
8 => Some("uint64_t"),
16 => Some("veryl_u128_ua"),
_ => None, }
}
fn expr_c_type(width: usize) -> Option<&'static str> {
if width == 0 || width <= 64 {
Some("uint64_t")
} else if width <= 128 {
Some("__uint128_t")
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::ChunkArtifact;
use crate::ir::{ExpressionContext, ProtoAssignStatement, ProtoSystemFunctionCall};
use veryl_analyzer::value::ValueU64;
use veryl_parser::token_range::TokenRange;
fn dummy_token() -> TokenRange {
TokenRange::default()
}
fn ctx(width: usize, signed: bool) -> ExpressionContext {
ExpressionContext { width, signed }
}
fn val_u64(payload: u64, width: usize) -> Value {
Value::U64(ValueU64 {
payload,
mask_xz: 0,
width: width as u32,
signed: false,
})
}
fn const_expr(payload: u64, width: usize) -> ProtoExpression {
ProtoExpression::Value {
value: val_u64(payload, width),
width,
expr_context: ctx(width, false),
}
}
fn var_expr(var_offset: VarOffset, width: usize) -> ProtoExpression {
ProtoExpression::Variable {
var_offset,
select: None,
dynamic_select: None,
width,
var_full_width: width,
expr_context: ctx(width, false),
}
}
#[test]
fn comb_fallback_reason_names_uncovered_stmt() {
let stmts = vec![ProtoStatement::SystemFunctionCall(
ProtoSystemFunctionCall::Finish,
)];
assert!(emit_function(&stmts).is_none()); assert_eq!(comb_fallback_reason(&stmts), "SysFn");
}
#[test]
fn emit_value_u64() {
let v = val_u64(0x1234, 32);
assert_eq!(emit_value(&v, 32).as_deref(), Some("0x1234ULL"));
}
#[test]
fn emit_value_truncates_to_width() {
let v = val_u64(0xff, 4);
assert_eq!(emit_value(&v, 4).as_deref(), Some("0xfULL"));
}
#[test]
fn emit_value_rejects_wide() {
let v = val_u64(0, 65);
let s = emit_value(&v, 65).unwrap();
assert!(s.contains("__uint128_t"));
assert!(emit_value(&v, 129).is_none());
}
#[test]
fn emit_value_width_zero_emits_zero() {
let v = val_u64(0, 0);
assert_eq!(emit_value(&v, 0).as_deref(), Some("0x0ULL"));
}
#[test]
fn emit_var_comb_u32() {
assert_eq!(
emit_var_load(&VarOffset::Comb(0x100), 16).as_deref(),
Some("((uint64_t)*((const uint32_t*)(comb_values + 0x100)))"),
);
}
#[test]
fn emit_var_ff_u64() {
assert_eq!(
emit_var_load(&VarOffset::Ff(0x40), 64).as_deref(),
Some("((uint64_t)*((const uint64_t*)(ff_values + 0x40)))"),
);
}
#[test]
fn emit_expr_binary_add() {
let e = ProtoExpression::Binary {
x: Box::new(var_expr(VarOffset::Ff(0), 32)),
op: Op::Add,
y: Box::new(const_expr(1, 32)),
width: 32,
expr_context: ctx(32, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains("ff_values + 0x0"));
assert!(s.contains("0x1ULL"));
assert!(s.contains(") + ("));
}
#[test]
fn emit_expr_ternary() {
let e = ProtoExpression::Ternary {
cond: Box::new(var_expr(VarOffset::Comb(8), 1)),
true_expr: Box::new(const_expr(0xa, 32)),
false_expr: Box::new(const_expr(0xb, 32)),
width: 32,
expr_context: ctx(32, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains(" ? "));
assert!(s.contains(" : "));
assert!(s.contains("0xaULL"));
assert!(s.contains("0xbULL"));
}
#[test]
fn emit_expr_arith_shift_right_uses_signed_cast() {
let e = ProtoExpression::Binary {
x: Box::new(var_expr(VarOffset::Ff(16), 32)),
op: Op::ArithShiftR,
y: Box::new(const_expr(2, 32)),
width: 32,
expr_context: ctx(32, true),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains("(int64_t)"));
assert!(s.contains(">>"));
}
#[test]
fn emit_expr_bit_select() {
let e = ProtoExpression::Variable {
var_offset: VarOffset::Comb(0x10),
select: Some((7, 4)), dynamic_select: None,
width: 4,
var_full_width: 32,
expr_context: ctx(4, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains(">> 4"));
assert!(s.contains("0xf"));
}
#[test]
fn emit_stmt_assign_comb() {
let a = ProtoAssignStatement {
dst: VarOffset::Comb(0x20),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xdeadbeef, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
};
let s = emit_stmt(&ProtoStatement::Assign(a)).unwrap();
assert!(s.contains("comb_values + 0x20"));
assert!(s.contains("uint32_t"));
assert!(s.contains("0xdeadbeefULL"));
}
#[test]
fn emit_stmt_assign_ff_dual_slot_stores_and_logs() {
let a = ProtoAssignStatement {
dst: VarOffset::Ff(0x48),
dst_width: 64,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0x1234, 64),
dst_ff_current_offset: 0x40,
token: dummy_token(),
};
let s = emit_stmt(&ProtoStatement::Assign(a)).unwrap();
assert!(s.contains("ff_values + 0x48")); assert!(s.contains("write_log")); assert!(s.contains("0x40")); }
#[test]
fn emit_stmt_assign_comb_bit_select_single() {
let a = ProtoAssignStatement {
dst: VarOffset::Comb(0x20),
dst_width: 32,
select: Some((5, 5)),
dynamic_select: None,
rhs_select: None,
expr: const_expr(1, 1),
dst_ff_current_offset: 0,
token: dummy_token(),
};
let s = emit_stmt(&ProtoStatement::Assign(a)).unwrap();
assert!(s.contains("comb_values + 0x20"));
assert!(s.contains("0x20"));
assert!(s.contains("0x1ULL"));
assert!(s.contains("<< 5"));
}
#[test]
fn emit_stmt_assign_comb_bit_select_slice() {
let a = ProtoAssignStatement {
dst: VarOffset::Comb(0x10),
dst_width: 32,
select: Some((11, 8)),
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xa, 4),
dst_ff_current_offset: 0,
token: dummy_token(),
};
let s = emit_stmt(&ProtoStatement::Assign(a)).unwrap();
assert!(s.contains("comb_values + 0x10"));
assert!(s.contains("0xfULL"));
assert!(s.contains("0xf00"));
assert!(s.contains("<< 8"));
}
#[test]
fn emit_stmt_assign_ff_bit_select_rmw_logs() {
let a = ProtoAssignStatement {
dst: VarOffset::Ff(0x40),
dst_width: 32,
select: Some((3, 0)),
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xf, 4),
dst_ff_current_offset: 0x40,
token: dummy_token(),
};
let s = emit_stmt(&ProtoStatement::Assign(a)).unwrap();
assert!(s.contains("write_log")); assert!(s.contains("ff_values + 0x40")); }
#[test]
fn emit_stmt_if_else() {
use crate::ir::ProtoIfStatement;
let inner_assign = ProtoAssignStatement {
dst: VarOffset::Comb(0x10),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(1, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
};
let if_stmt = ProtoIfStatement {
cond: Some(var_expr(VarOffset::Comb(0), 1)),
true_side: vec![ProtoStatement::Assign(inner_assign.clone())],
false_side: vec![ProtoStatement::Assign(ProtoAssignStatement {
expr: const_expr(2, 32),
..inner_assign
})],
};
let s = emit_stmt(&ProtoStatement::If(if_stmt)).unwrap();
assert!(s.starts_with("if ("));
assert!(s.contains("} else {"));
assert!(s.contains("0x1ULL"));
assert!(s.contains("0x2ULL"));
}
#[test]
fn emit_stmt_if_no_cond_runs_false_side() {
use crate::ir::ProtoIfStatement;
let f_assign = ProtoAssignStatement {
dst: VarOffset::Comb(0x10),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xabc, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
};
let if_stmt = ProtoIfStatement {
cond: None,
true_side: vec![],
false_side: vec![ProtoStatement::Assign(f_assign)],
};
let s = emit_stmt(&ProtoStatement::If(if_stmt)).unwrap();
assert!(s.starts_with("{ "));
assert!(s.contains("0xabcULL"));
assert!(!s.contains("if ("));
}
#[test]
fn emit_stmt_sequential_block() {
let assigns: Vec<ProtoStatement> = (0..3)
.map(|i| {
ProtoStatement::Assign(ProtoAssignStatement {
dst: VarOffset::Comb(0x10 + i * 4),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(i as u64, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
})
})
.collect();
let s = emit_stmt(&ProtoStatement::SequentialBlock(assigns)).unwrap();
assert!(s.starts_with("{ "));
assert!(s.contains("comb_values + 0x10"));
assert!(s.contains("comb_values + 0x14"));
assert!(s.contains("comb_values + 0x18"));
}
#[test]
fn emit_expr_concatenation_two_vars() {
let a = var_expr(VarOffset::Comb(0), 8);
let b = var_expr(VarOffset::Comb(8), 8);
let e = ProtoExpression::Concatenation {
elements: vec![(Box::new(a), 1, 8), (Box::new(b), 1, 8)],
width: 16,
expr_context: ctx(16, false),
};
let s = emit_expr(&e).unwrap();
assert_eq!(s.matches("<< 8").count(), 2);
assert_eq!(s.matches("0xffULL").count(), 2);
assert!(s.contains("comb_values + 0x0"));
assert!(s.contains("comb_values + 0x8"));
}
#[test]
fn emit_expr_concatenation_replicate() {
let a = var_expr(VarOffset::Comb(0), 4);
let e = ProtoExpression::Concatenation {
elements: vec![(Box::new(a), 3, 4)],
width: 12,
expr_context: ctx(12, false),
};
let s = emit_expr(&e).unwrap();
assert_eq!(s.matches("<< 4").count(), 3);
assert_eq!(s.matches("0xfULL").count(), 3);
}
#[test]
fn emit_expr_concatenation_65_to_128_emits_u128() {
let a = var_expr(VarOffset::Comb(0), 32);
let b = const_expr(0, 33);
let e = ProtoExpression::Concatenation {
elements: vec![(Box::new(a), 1, 32), (Box::new(b), 1, 33)],
width: 65,
expr_context: ctx(65, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains("__uint128_t"));
assert!(s.contains("(__uint128_t)0)"));
}
#[test]
fn emit_expr_concatenation_rejects_wider_than_128() {
let a = var_expr(VarOffset::Comb(0), 64);
let b = const_expr(0, 65);
let e = ProtoExpression::Concatenation {
elements: vec![(Box::new(a), 1, 64), (Box::new(b), 1, 65)],
width: 129,
expr_context: ctx(129, false),
};
assert!(emit_expr(&e).is_none());
}
#[test]
fn emit_expr_variable_with_dynamic_select() {
use crate::ir::ProtoDynamicBitSelect;
let idx = var_expr(VarOffset::Comb(0), 8);
let e = ProtoExpression::Variable {
var_offset: VarOffset::Comb(0x80),
select: None,
dynamic_select: Some(ProtoDynamicBitSelect {
index_expr: Box::new(idx),
elem_width: 4,
num_elements: 8,
}),
width: 4,
var_full_width: 32,
expr_context: ctx(4, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains("_idx_raw"));
assert!(s.contains("_idx_raw < 7 ?"));
assert!(s.contains("comb_values + 0x80"));
assert!(s.contains("_idx * 4"));
assert!(s.contains("0xfULL"));
}
#[test]
fn emit_expr_variable_dynamic_select_wide_var_rejects() {
use crate::ir::ProtoDynamicBitSelect;
let idx = const_expr(0, 4);
let e = ProtoExpression::Variable {
var_offset: VarOffset::Comb(0),
select: None,
dynamic_select: Some(ProtoDynamicBitSelect {
index_expr: Box::new(idx),
elem_width: 4,
num_elements: 4,
}),
width: 4,
var_full_width: 96,
expr_context: ctx(4, false),
};
assert!(emit_expr(&e).is_none());
}
#[test]
fn emit_expr_dynamic_variable_no_select() {
let idx = const_expr(2, 4);
let e = ProtoExpression::DynamicVariable {
base_offset: VarOffset::Comb(0x100),
stride: 4,
element_native_bytes: 4,
index_expr: Box::new(idx),
num_elements: 4,
select: None,
dynamic_select: None,
width: 32,
expr_context: ctx(32, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains("_idx_raw"));
assert!(s.contains("_idx_raw < 3 ?"));
assert!(s.contains("comb_values + 0x100"));
assert!(s.contains("uint32_t"));
assert!(s.contains("(intptr_t)4 * (intptr_t)_idx"));
}
#[test]
fn emit_expr_dynamic_variable_with_select() {
let idx = var_expr(VarOffset::Comb(0), 4);
let e = ProtoExpression::DynamicVariable {
base_offset: VarOffset::Ff(0x40),
stride: 1,
element_native_bytes: 1,
index_expr: Box::new(idx),
num_elements: 8,
select: Some((3, 0)),
dynamic_select: None,
width: 4,
expr_context: ctx(4, false),
};
let s = emit_expr(&e).unwrap();
assert!(s.contains("ff_values + 0x40"));
assert!(s.contains(">> 0"));
assert!(s.contains("0xfULL"));
}
#[test]
fn emit_expr_dynamic_variable_zero_elements_rejects() {
let idx = const_expr(0, 4);
let e = ProtoExpression::DynamicVariable {
base_offset: VarOffset::Comb(0),
stride: 4,
element_native_bytes: 4,
index_expr: Box::new(idx),
num_elements: 0,
select: None,
dynamic_select: None,
width: 32,
expr_context: ctx(32, false),
};
assert!(emit_expr(&e).is_none());
}
#[test]
fn emit_stmt_assign_dynamic_comb() {
use crate::ir::ProtoAssignDynamicStatement;
let idx = const_expr(2, 4);
let a = ProtoAssignDynamicStatement {
dst_base: VarOffset::Comb(0x100),
dst_stride: 4,
dst_num_elements: 4,
dst_index_expr: idx,
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xdeadbeef, 32),
dst_ff_current_base_offset: 0,
};
let s = emit_stmt(&ProtoStatement::AssignDynamic(a)).unwrap();
assert!(s.contains("_idx_raw"));
assert!(s.contains("_idx_raw < 3 ?"));
assert!(s.contains("comb_values + 0x100"));
assert!(s.contains("uint32_t"));
assert!(s.contains("0xdeadbeefULL"));
}
#[test]
fn emit_stmt_assign_dynamic_ff_rejects() {
use crate::ir::ProtoAssignDynamicStatement;
let a = ProtoAssignDynamicStatement {
dst_base: VarOffset::Ff(0x40),
dst_stride: 4,
dst_num_elements: 4,
dst_index_expr: const_expr(0, 4),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0, 32),
dst_ff_current_base_offset: 0x40,
};
assert!(emit_stmt(&ProtoStatement::AssignDynamic(a)).is_none());
}
#[test]
fn emit_stmt_compiled_block_inlines_original_stmts() {
use crate::ir::CompiledBlockStatement;
let inner_a = ProtoStatement::Assign(ProtoAssignStatement {
dst: VarOffset::Comb(0x10),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0x1111, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
});
let inner_b = ProtoStatement::Assign(ProtoAssignStatement {
dst: VarOffset::Comb(0x20),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0x2222, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
});
let cb = CompiledBlockStatement {
artifact: bogus_artifact(),
ff_delta_bytes: 0,
comb_delta_bytes: 0,
input_offsets: vec![],
output_offsets: vec![],
ff_canonical_offsets: vec![],
stmt_deps: vec![],
original_stmts: vec![inner_a, inner_b],
};
let s = emit_stmt(&ProtoStatement::CompiledBlock(cb)).unwrap();
assert!(s.starts_with("{ "));
assert!(s.contains("comb_values + 0x10"));
assert!(s.contains("comb_values + 0x20"));
assert!(s.contains("0x1111ULL"));
assert!(s.contains("0x2222ULL"));
}
#[test]
fn emit_stmt_compiled_block_applies_comb_delta() {
use crate::ir::CompiledBlockStatement;
let inner = ProtoStatement::Assign(ProtoAssignStatement {
dst: VarOffset::Comb(0x10),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xabc, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
});
let cb = CompiledBlockStatement {
artifact: bogus_artifact(),
ff_delta_bytes: 0,
comb_delta_bytes: 0x100,
input_offsets: vec![],
output_offsets: vec![],
ff_canonical_offsets: vec![],
stmt_deps: vec![],
original_stmts: vec![inner],
};
let s = emit_stmt(&ProtoStatement::CompiledBlock(cb)).unwrap();
assert!(s.contains("comb_values + 0x110"));
assert!(!s.contains("comb_values + 0x10 ")); }
fn bogus_artifact() -> Arc<ChunkArtifact> {
unsafe extern "system" fn stub(_: *const u8, _: *const u8, _: *mut u8) {}
Arc::new(ChunkArtifact {
func: stub,
keepalive: None,
})
}
#[test]
fn emit_stmt_for_const_forward() {
let body_assign = ProtoStatement::Assign(ProtoAssignStatement {
dst: VarOffset::Comb(0x100),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(0xa, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
});
let for_stmt = ProtoForStatement {
var_offset: VarOffset::Comb(0),
var_width: 32,
var_native_bytes: 4,
var_signed: false,
range: ProtoForRange::Forward {
start: ProtoForBound::Const(0),
end: ProtoForBound::Const(8),
inclusive: false,
step: 1,
},
body: vec![body_assign],
};
let s = emit_stmt(&ProtoStatement::For(for_stmt)).unwrap();
assert!(s.contains("for (uint64_t _it = 0ULL"));
assert!(s.contains("_it < 8ULL"));
assert!(s.contains("_it += 1ULL"));
assert!(s.contains("comb_values + 0x0"));
assert!(s.contains("0xaULL"));
}
#[test]
fn emit_stmt_for_inclusive_bumps_end() {
let for_stmt = ProtoForStatement {
var_offset: VarOffset::Comb(0),
var_width: 8,
var_native_bytes: 1,
var_signed: false,
range: ProtoForRange::Forward {
start: ProtoForBound::Const(0),
end: ProtoForBound::Const(7),
inclusive: true, step: 1,
},
body: vec![],
};
let s = emit_stmt(&ProtoStatement::For(for_stmt)).unwrap();
assert!(s.contains("_it < 8ULL"));
}
#[test]
fn emit_stmt_for_dynamic_bound_rejects() {
let for_stmt = ProtoForStatement {
var_offset: VarOffset::Comb(0),
var_width: 32,
var_native_bytes: 4,
var_signed: false,
range: ProtoForRange::Forward {
start: ProtoForBound::Const(0),
end: ProtoForBound::Dynamic(const_expr(8, 32)),
inclusive: false,
step: 1,
},
body: vec![],
};
assert!(emit_stmt(&ProtoStatement::For(for_stmt)).is_none());
}
#[test]
fn emit_stmt_break() {
assert_eq!(emit_stmt(&ProtoStatement::Break).as_deref(), Some("break;"));
}
#[test]
fn emit_function_simple_assign() {
let a = ProtoAssignStatement {
dst: VarOffset::Comb(0x10),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: const_expr(7, 32),
dst_ff_current_offset: 0,
token: dummy_token(),
};
let src = emit_function(&[ProtoStatement::Assign(a)]).unwrap();
assert!(src.contains("#include <stdint.h>"));
assert!(src.contains("veryl_aot_eval"));
assert!(src.contains("comb_values + 0x10"));
}
fn compile_for_test(src: &str, what: &str) -> Option<EmittedModule> {
match compile_source(src) {
Ok(m) => Some(m),
Err(e) if e.starts_with("dlopen") || e.starts_with("dlsym") => {
eprintln!("{what}: shared object not loadable on this host ({e}); skipping");
None
}
Err(e) => panic!("{what}: {e}"),
}
}
#[test]
fn emit_function_dynamic_variable_compiles() {
if Command::new(std::env::var("VERYL_AOT_CC").unwrap_or_else(|_| "cc".to_string()))
.arg("--version")
.output()
.is_err()
{
eprintln!("emit_function_dynamic_variable_compiles: cc unavailable, skipping");
return;
}
let idx = var_expr(VarOffset::Comb(16), 32);
let dyn_read = ProtoExpression::DynamicVariable {
base_offset: VarOffset::Comb(0),
stride: 4,
element_native_bytes: 4,
index_expr: Box::new(idx),
num_elements: 4,
select: None,
dynamic_select: None,
width: 32,
expr_context: ctx(32, false),
};
let assign = ProtoAssignStatement {
dst: VarOffset::Comb(20),
dst_width: 32,
select: None,
dynamic_select: None,
rhs_select: None,
expr: dyn_read,
dst_ff_current_offset: 0,
token: dummy_token(),
};
let src = emit_function(&[ProtoStatement::Assign(assign)]).unwrap();
let tmp = std::env::temp_dir().join(format!("veryl_aot_dv_{}", std::process::id()));
unsafe {
std::env::set_var("VERYL_AOT_CACHE_DIR", &tmp);
}
let Some(module) = compile_for_test(&src, "emit_function_dynamic_variable_compiles") else {
return;
};
let mut ff = vec![0u8; 16];
let mut comb = vec![0u8; 32];
comb[0..4].copy_from_slice(&0xaaaau32.to_le_bytes());
comb[4..8].copy_from_slice(&0xbbbbu32.to_le_bytes());
comb[8..12].copy_from_slice(&0xccccu32.to_le_bytes());
comb[12..16].copy_from_slice(&0xddddu32.to_le_bytes());
comb[16..20].copy_from_slice(&2u32.to_le_bytes()); let mut log = vec![0u64; 16];
unsafe {
(module.func)(
ff.as_mut_ptr(),
comb.as_mut_ptr(),
log.as_mut_ptr() as *mut u8,
);
}
let written = u32::from_le_bytes(comb[20..24].try_into().unwrap());
assert_eq!(
written, 0xcccc,
"DynamicVariable read should fetch element 2"
);
comb[16..20].copy_from_slice(&99u32.to_le_bytes());
unsafe {
(module.func)(
ff.as_mut_ptr(),
comb.as_mut_ptr(),
log.as_mut_ptr() as *mut u8,
);
}
let written = u32::from_le_bytes(comb[20..24].try_into().unwrap());
assert_eq!(
written, 0xdddd,
"out-of-range idx should clamp to last element"
);
let _ = std::fs::remove_dir_all(&tmp);
unsafe {
std::env::remove_var("VERYL_AOT_CACHE_DIR");
}
}
#[test]
fn fnv1a_64_hex_stable() {
let a = fnv1a_64_hex_parts(&["hello"]);
let b = fnv1a_64_hex_parts(&["hello"]);
let c = fnv1a_64_hex_parts(&["world"]);
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.len(), 16);
assert_ne!(
fnv1a_64_hex_parts(&["ab", "c"]),
fnv1a_64_hex_parts(&["a", "bc"]),
);
assert_ne!(
fnv1a_64_hex_parts(&["v1", "gcc", "-O3", "SRC"]),
fnv1a_64_hex_parts(&["v1", "clang", "-O3", "SRC"]),
);
}
#[test]
fn compile_source_round_trip() {
if Command::new(std::env::var("VERYL_AOT_CC").unwrap_or_else(|_| "cc".to_string()))
.arg("--version")
.output()
.is_err()
{
eprintln!("compile_source_round_trip: cc unavailable, skipping");
return;
}
let src = "\
#include <stdint.h>\n\
__attribute__((visibility(\"default\")))\n\
void veryl_aot_eval(uint8_t *ff, uint8_t *comb, uint64_t *log) {\n\
(void)ff; (void)log;\n\
*(uint32_t*)(comb + 0) = 0xdeadbeef;\n\
}\n";
let tmp = std::env::temp_dir().join(format!("veryl_aot_test_{}", std::process::id()));
unsafe {
std::env::set_var("VERYL_AOT_CACHE_DIR", &tmp);
}
let Some(module) = compile_for_test(src, "compile_source_round_trip") else {
return;
};
let mut ff = vec![0u8; 16];
let mut comb = vec![0u8; 16];
let mut log = vec![0u64; 16];
unsafe {
(module.func)(
ff.as_mut_ptr(),
comb.as_mut_ptr(),
log.as_mut_ptr() as *mut u8,
);
}
let written = u32::from_le_bytes(comb[0..4].try_into().unwrap());
assert_eq!(written, 0xdeadbeef, "comb[0..4] should be 0xdeadbeef");
let _ = std::fs::remove_dir_all(&tmp);
unsafe {
std::env::remove_var("VERYL_AOT_CACHE_DIR");
}
}
}