diff --git a/melange/core/src/veriloga/osdi_0_4.rs b/melange/core/src/veriloga/osdi_0_4.rs index 298db0ff..c0284ef3 100644 --- a/melange/core/src/veriloga/osdi_0_4.rs +++ b/melange/core/src/veriloga/osdi_0_4.rs @@ -62,8 +62,6 @@ pub const DOMAIN_CONTINUOUS: u32 = 2; pub const NOISE_TYPE_WHITE: u32 = 0; pub const NOISE_TYPE_FLICKER: u32 = 1; pub const NOISE_TYPE_TABLE: u32 = 2; -pub const MODULEFLAG_ABSTIME: u32 = 1; -pub const MODULEFLAG_ABSDELAY: u32 = 2; #[repr(C)] pub struct OsdiLimFunction { @@ -86,19 +84,6 @@ pub struct OsdiSimInfo { pub prev_state: *mut f64, pub next_state: *mut f64, pub flags: u32, - pub history_ctx: *mut c_void, - pub query_past_state: fn(*mut c_void, u32, f64, f64) -> f64, -} -impl OsdiSimInfo { - pub fn query_past_state( - &self, - history_ctx: *mut c_void, - delay_id: u32, - time: f64, - current_value: f64, - ) -> f64 { - (self.query_past_state)(history_ctx, delay_id, time, current_value) - } } #[repr(C)] pub union OsdiInitErrorPayload { @@ -152,9 +137,12 @@ pub struct OsdiNoiseSource { pub nodes: OsdiNodePair, } #[repr(C)] -pub struct OsdiDelayDescriptor { - pub source_offset: u32, - pub flags: u32, +pub struct OsdiAbsDelayInfo { + pub input_node_1: u32, + pub input_node_2: u32, + pub output_node: u32, + pub delay_offset: u32, + pub max_delay_offset: u32, } #[repr(C)] pub struct OsdiNatureRef { @@ -215,9 +203,8 @@ pub struct OsdiDescriptor { pub residual_nature: *mut OsdiNatureRef, pub noise_source_type: *mut u32, pub load_noise_params: fn(*mut c_void, *mut c_void, *mut f64, *mut f64), - pub module_flags: u32, - pub num_delay: u32, - pub delay: *mut OsdiDelayDescriptor, + pub absdelay_count: u32, + pub absdelay_info: *const OsdiAbsDelayInfo, } impl OsdiDescriptor { pub fn access( diff --git a/melange/core/src/veriloga/osdi_device.rs b/melange/core/src/veriloga/osdi_device.rs index 37121b32..48b8816c 100644 --- a/melange/core/src/veriloga/osdi_device.rs +++ b/melange/core/src/veriloga/osdi_device.rs @@ -20,10 +20,6 @@ use crate::veriloga::osdi_0_4::{ PARA_KIND_INST, PARA_TY_INT, PARA_TY_MASK, PARA_TY_REAL, PARA_TY_STR, }; -fn unsupported_query_past_state(_: *mut c_void, _: u32, _: f64, _: f64) -> f64 { - std::process::abort() -} - impl OsdiDescriptor { fn nodes(&self) -> &[OsdiNode] { // # SAFETY: OsdiDescriptor can only be constructed from FFI and is assumed to contain @@ -200,7 +196,7 @@ impl Drop for OsdiModel { impl ModelImpl for OsdiModel { fn process_params(&self) -> Result<()> { - if self.descriptor.num_delay != 0 { + if self.descriptor.absdelay_count != 0 { bail!( "OSDI model uses absdelay, but Melange does not implement transient delay history" ) @@ -368,7 +364,7 @@ impl InstanceImpl for OsdiInstance { sim_builder: &mut SimBuilder, terminals: &[Node], ) -> Result<()> { - if self.descriptor.num_delay != 0 { + if self.descriptor.absdelay_count != 0 { bail!( "OSDI model uses absdelay, but Melange does not implement transient delay history" ) @@ -489,8 +485,6 @@ impl InstanceImpl for OsdiInstance { prev_state: ptr::null_mut(), next_state: ptr::null_mut(), flags: sim_info.flags.bits(), - history_ctx: ptr::null_mut(), - query_past_state: unsupported_query_past_state, }; let ret_flags = self.descriptor.eval( diff --git a/openvaf/hir/src/db.rs b/openvaf/hir/src/db.rs index e92a237a..0df1f88d 100644 --- a/openvaf/hir/src/db.rs +++ b/openvaf/hir/src/db.rs @@ -68,6 +68,13 @@ impl CompilationDB { CompilationUnit { root_file: self.root_file } } + /// Filesystem directory containing the compilation root file, if the root + /// lives on disk (as opposed to a virtual/in-memory file). Used to resolve + /// relative paths referenced from source, e.g. a `noise_table` data file. + pub fn root_file_dir(&self) -> Option { + self.file_path(self.root_file).parent() + } + pub fn new<'a>( root_file: VfsPath, contents: Result, io::Error>, diff --git a/openvaf/hir/src/lib.rs b/openvaf/hir/src/lib.rs index a71b1d13..788548d6 100644 --- a/openvaf/hir/src/lib.rs +++ b/openvaf/hir/src/lib.rs @@ -29,6 +29,7 @@ pub use hir_def::{BuiltIn, Case, Literal, ParamSysFun, Path, Type}; pub use hir_ty::builtin; use hir_ty::db::HirTyDB as HirDatabase; use hir_ty::inference; +pub use hir_ty::types::Signature; pub use rec_declarations::RecDeclarations; use salsa::InternKey; use smol_str::SmolStr; @@ -444,6 +445,11 @@ impl Variable { db.var_data(self.id).ty.clone() } + /// Lowest declared index of an array variable (`real g[2:5]` -> 2). + pub fn array_lo(self, db: &CompilationDB) -> i32 { + db.var_data(self.id).array_lo + } + pub fn init(self, db: &CompilationDB) -> Body { Body::new(self.id.into(), db) } @@ -463,6 +469,11 @@ impl Parameter { db.param_data(self.id).name.to_string() } + /// Whether this parameter is a `localparam` (never externally overridable). + pub fn is_local(self, db: &CompilationDB) -> bool { + db.param_data(self.id).is_local + } + pub fn default(self, db: &CompilationDB) -> ExprId { db.param_exprs(self.id).default } diff --git a/openvaf/hir_def/src/body.rs b/openvaf/hir_def/src/body.rs index 37894482..ab9b5d58 100644 --- a/openvaf/hir_def/src/body.rs +++ b/openvaf/hir_def/src/body.rs @@ -175,6 +175,7 @@ impl Body { // Arrays have no scalar default (their elements are managed // per-element during lowering); use 0.0 as a placeholder. Type::Real | Type::Array { .. } => Literal::Float(Ieee64::with_float(0.0)), + Type::String => Literal::String(Box::from("")), _ => unreachable!("invalid var type"), }; ctx.alloc_expr_desugared(Expr::Literal(default_val)) diff --git a/openvaf/hir_def/src/data.rs b/openvaf/hir_def/src/data.rs index 70d100c3..d3988575 100644 --- a/openvaf/hir_def/src/data.rs +++ b/openvaf/hir_def/src/data.rs @@ -122,13 +122,15 @@ impl NatureData { pub struct VarData { pub name: Name, pub ty: Type, + /// Lowest declared index of an array variable (`real g[2:5]` -> 2). + pub array_lo: i32, } impl VarData { pub fn var_data_query(db: &dyn HirDefDB, id: VarId) -> Arc { let loc = id.lookup(db); let var = &loc.item_tree(db)[loc.id]; - Arc::new(VarData { name: var.name.clone(), ty: var.ty.clone() }) + Arc::new(VarData { name: var.name.clone(), ty: var.ty.clone(), array_lo: var.array_lo }) } } @@ -136,13 +138,19 @@ impl VarData { pub struct ParamData { pub name: Name, pub ty: Option, + /// `localparam` declarations are never externally overridable (LRM). + pub is_local: bool, } impl ParamData { pub fn param_data_query(db: &dyn HirDefDB, id: ParamId) -> Arc { let loc = id.lookup(db); let param = &loc.item_tree(db)[loc.id]; - Arc::new(ParamData { name: param.name.clone(), ty: param.ty.clone() }) + Arc::new(ParamData { + name: param.name.clone(), + ty: param.ty.clone(), + is_local: param.is_local, + }) } } diff --git a/openvaf/hir_def/src/item_tree.rs b/openvaf/hir_def/src/item_tree.rs index b40a07c4..f281308a 100644 --- a/openvaf/hir_def/src/item_tree.rs +++ b/openvaf/hir_def/src/item_tree.rs @@ -288,6 +288,9 @@ pub struct Net { pub struct Var { pub name: Name, pub ty: Type, + /// Lowest declared index of an array variable (`real g[2:5]` -> 2). + /// 0 for scalars and zero-based arrays. + pub array_lo: i32, pub ast_id: AstId, } diff --git a/openvaf/hir_def/src/item_tree/lower.rs b/openvaf/hir_def/src/item_tree/lower.rs index d8596140..c13eaa0d 100644 --- a/openvaf/hir_def/src/item_tree/lower.rs +++ b/openvaf/hir_def/src/item_tree/lower.rs @@ -700,20 +700,28 @@ impl Ctx { // `real den[msb:lsb];` -> a fixed-size array. The bounds are // compile-time integer constants (literals, arithmetic, or parameter // references resolved against the enclosing module). - let ty = match (var.dimension(), module.as_ref()) { + let (ty, array_lo) = match (var.dimension(), module.as_ref()) { (Some(dim), Some(module)) => { - let len = dim + let bounds = dim .msb() .and_then(|m| eval_const_int(&m, module)) - .zip(dim.lsb().and_then(|l| eval_const_int(&l, module))) + .zip(dim.lsb().and_then(|l| eval_const_int(&l, module))); + let len = bounds .map(|(msb, lsb)| (msb - lsb).unsigned_abs() as u32 + 1) .unwrap_or(0); - Type::Array { ty: Box::new(base_ty.clone()), len } + // The declared range may run either way (`[2:5]` or `[5:2]`); + // element positions are offset by the lower bound. + let lo = bounds.map(|(msb, lsb)| msb.min(lsb) as i32).unwrap_or(0); + (Type::Array { ty: Box::new(base_ty.clone()), len }, lo) } - _ => base_ty.clone(), + _ => (base_ty.clone(), 0), + }; + let var = Var { + name: name.as_name(), + ast_id: self.source_ast_id_map.ast_id(&var), + ty, + array_lo, }; - let var = - Var { name: name.as_name(), ast_id: self.source_ast_id_map.ast_id(&var), ty }; let id = self.tree.data.variables.push_and_get_key(var); dst.push(id.into()) } diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index a5c5f262..83456545 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -1,5 +1,6 @@ use hir::builtin::{ - FLICKER_NOISE_NAME, NOISE_TABLE_FILE_NAME, NOISE_TABLE_INLINE_NAME, WHITE_NOISE_NAME, + FLICKER_NOISE_NAME, NOISE_TABLE_FILE, NOISE_TABLE_FILE_NAME, NOISE_TABLE_INLINE, + NOISE_TABLE_INLINE_NAME, WHITE_NOISE_NAME, }; use hir::signatures::{ ABSDELAY_MAX, ABS_INT, ABS_REAL, BOOL_EQ, DDX_POT, IDTMOD_IC, IDTMOD_IC_MODULUS, @@ -282,15 +283,25 @@ impl BodyLoweringCtx<'_, '_, '_> { if len == 0 { return F_ZERO; } + // Element positions are offset by the declared lower bound: `real g[2:5]` + // stores g[2] in element 0. Previously the raw index was used as the + // position (g[3] read the wrong element) and constant out-of-range + // indices were silently clamped instead of diagnosed. + let lo = var.array_lo(self.ctx.db); if let Some(c) = self.body.as_literalint(&index) { - let c = (c.max(0) as u32).min(len - 1); - return self.ctx.use_place(PlaceKind::VarElement(var, c)); + let pos = c as i64 - lo as i64; + if !(0..len as i64).contains(&pos) { + // Out of the declared range: diagnosed during type checking; + // lower to 0 so compilation can continue. + return F_ZERO; + } + return self.ctx.use_place(PlaceKind::VarElement(var, pos as u32)); } let idx_val = self.lower_expr(index); let mut res = self.ctx.use_place(PlaceKind::VarElement(var, 0)); for i in 1..len { let elem = self.ctx.use_place(PlaceKind::VarElement(var, i)); - let i_const = self.ctx.iconst(i as i32); + let i_const = self.ctx.iconst(lo + i as i32); let cond = self.ctx.ins().ieq(idx_val, i_const); let prev = res; res = self.ctx.make_select(cond, |_s, branch| if branch { elem } else { prev }); @@ -298,6 +309,77 @@ impl BodyLoweringCtx<'_, '_, '_> { res } + /// Evaluate a compile-time-constant real expression (literal, possibly + /// with a leading unary `+`/`-`). Used to read the `noise_table` inline + /// data array, whose elements must all be constants per the LRM. + fn eval_const_real(&self, expr: ExprId) -> Option { + if let Some(lit) = self.body.as_literal(expr) { + return match lit { + Literal::Float(f) => Some((*f).into()), + Literal::Int(i) => Some(*i as f64), + _ => None, + }; + } + match self.body.get_expr(expr) { + Expr::UnaryOp { expr: inner, op } => match op { + UnaryOp::Neg => Some(-self.eval_const_real(inner)?), + UnaryOp::Identity => self.eval_const_real(inner), + _ => None, + }, + _ => None, + } + } + + /// Read a whitespace-separated two-column ` ` noise table + /// file, resolved relative to the directory of the compilation root file. + /// Blank lines and `#`/`//`/`*`-prefixed comment lines are skipped. + fn read_noise_table_file(&self, fname: &str) -> Vec<(f64, f64)> { + let Some(dir) = self.ctx.db.root_file_dir() else { return Vec::new() }; + let Some(path) = dir.join(fname) else { return Vec::new() }; + let Some(abs) = path.as_path() else { return Vec::new() }; + let Ok(content) = std::fs::read_to_string(abs) else { return Vec::new() }; + let mut out = Vec::new(); + for line in content.lines() { + let line = line.trim(); + if line.is_empty() + || line.starts_with('#') + || line.starts_with("//") + || line.starts_with('*') + { + continue; + } + let mut it = line.split_whitespace(); + if let (Some(a), Some(b)) = (it.next(), it.next()) { + if let (Ok(f), Ok(p)) = (a.parse::(), b.parse::()) { + out.push((f, p)); + } + } + } + out + } + + /// Gather the `(frequency, power)` pairs backing a `noise_table` / + /// `noise_table_log` call, either from an inline real array + /// `{f0, p0, f1, p1, ...}` or from a two-column data file. + fn noise_table_data(&self, signature: hir::Signature, args: &[ExprId]) -> Vec<(f64, f64)> { + match signature { + NOISE_TABLE_INLINE | NOISE_TABLE_INLINE_NAME => { + let elems = match self.body.get_expr(args[0]) { + Expr::Array(vals) => vals, + _ => return Vec::new(), + }; + let nums: Vec = + elems.iter().map(|&e| self.eval_const_real(e).unwrap_or(0.0)).collect(); + nums.chunks_exact(2).map(|c| (c[0], c[1])).collect() + } + NOISE_TABLE_FILE | NOISE_TABLE_FILE_NAME => { + let fname = self.body.as_literal(args[0]).unwrap().unwrap_str(); + self.read_noise_table_file(fname) + } + _ => Vec::new(), + } + } + fn lower_builtin(&mut self, expr: ExprId, builtin: BuiltIn, args: &[ExprId]) -> Value { let signature = self.body.get_call_signature(expr); match builtin { @@ -555,7 +637,8 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.func.interner.get_or_intern(name) }; let log = builtin == BuiltIn::noise_table_log; - let noise_table = NoiseTable::new([(0.0, 0.0)], log, name, idx); + let table_vals = self.noise_table_data(signature, args); + let noise_table = NoiseTable::new(table_vals, log, name, idx); self.ctx.call1(CallBackKind::NoiseTable(Box::new(noise_table)), &[]) } @@ -811,6 +894,13 @@ impl BodyLoweringCtx<'_, '_, '_> { } BuiltIn::slew | BuiltIn::limit => self.lower_expr(args[0]), + // `ac_stim` is an AC small-signal stimulus: it is defined to be zero in the + // large-signal (DC/transient) domain, which is what a contribution lowers. + // Previously only the `no_equations` guard above matched, so a contributing + // use (`V(a,b) <+ ac_stim(...)`) fell through to `unreachable!()` and + // crashed the compiler. Actual AC-analysis injection is not implemented yet. + BuiltIn::ac_stim => F_ZERO, + _ => unreachable!(), } } @@ -833,37 +923,22 @@ impl BodyLoweringCtx<'_, '_, '_> { self.lower_multi_select(enable_integral, |mut ctx, branch| { if branch { - if kind.has_modulus() { - let modulus = ctx.lower_expr(args[2]); - let (min, max) = if kind.has_offset() { - let offset = ctx.lower_expr(args[2]); - (offset, ctx.ctx.ins().fadd(offset, modulus)) - } else { - (F_ZERO, modulus) - }; - let too_large = ctx.ctx.ins().fgt(val, max); - ctx.lower_multi_select(too_large, |mut ctx, too_large| { - if too_large { - [ctx.ctx.ins().fsub(val, min), F_ZERO] - } else { - let too_small = ctx.ctx.ins().flt(val, min); - ctx.lower_multi_select(too_small, |mut ctx, too_small| { - if too_small { - [ctx.ctx.ins().fsub(val, min), F_ZERO] - } else { - let arg = ctx.lower_expr(args[0]); - [ctx.ctx.ins().fneg(arg), val] - } - }) - } - }) - } else { - let arg = ctx.lower_expr(args[0]); - [ctx.ctx.ins().fneg(arg), val] - } + // Always integrate the DAE state unbounded; for `idtmod` the modulo + // wrap is applied to the *returned value* (below), not the state. + // Wrapping the state inside the residual makes the reactive residual + // jump by `modulus` at each wrap, so the transient integrator's d/dt + // term (based on the previous charge, ~modulus) diverges at the wrap. + let arg = ctx.lower_expr(args[0]); + [ctx.ctx.ins().fneg(arg), val] } else { + // During the IC/DC phase the stored charge (reactive residual) must + // be `ic`, not zero: `val - ic` pins `val = ic` at DC, but a zero + // charge makes the integrator restart from 0 once transient + // integration turns on, silently dropping the initial condition. + // Storing charge = `ic` lets the transient continue from `ic` (and + // an `assert` reset likewise restores the integrator to `ic`). let ic = ctx.lower_expr(args[1]); - [ctx.ctx.ins().fsub(val, ic), F_ZERO] + [ctx.ctx.ins().fsub(val, ic), ic] } }) } else { @@ -874,7 +949,23 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.def_resist_residual(residual[0], equation); self.ctx.def_react_residual(residual[1], equation); - val + // `idtmod` returns the (unbounded) integral wrapped into `[offset, offset+modulus)`: + // offset + floor_mod(val - offset, modulus), where floor_mod(x, m) = x - m*floor(x/m) + // stays in `[0, m)` even for negative x. Only the returned value wraps; the DAE state + // keeps integrating smoothly (above). This also fixes the offset argument, which + // previously read `args[2]` (the modulus) instead of `args[3]`. + if kind.has_modulus() { + let modulus = self.lower_expr(args[2]); + let offset = if kind.has_offset() { self.lower_expr(args[3]) } else { F_ZERO }; + let shifted = self.ctx.ins().fsub(val, offset); + let quot = self.ctx.ins().fdiv(shifted, modulus); + let whole = self.ctx.ins().floor(quot); + let whole_mod = self.ctx.ins().fmul(whole, modulus); + let rem = self.ctx.ins().fsub(shifted, whole_mod); + self.ctx.ins().fadd(rem, offset) + } else { + val + } } /// Read the coefficient values of an array-valued argument (an array variable's @@ -902,13 +993,17 @@ impl BodyLoweringCtx<'_, '_, '_> { .iter() .map(|&e| { let v = self.lower_expr(e); - let ty = self.body.expr_type(e); + // `lower_expr` already applies any inference-inserted cast + // (`needs_cast`), so consult the *resolved* type here — using the + // pre-cast type would insert a second `ifcast` on an already-real + // value, which the constant folder rejects. + let ty = self.resolved_ty(e); self.coeff_to_real(v, &ty) }) .collect(), _ => { let v = self.lower_expr(arg); - let ty = self.body.expr_type(arg); + let ty = self.resolved_ty(arg); vec![self.coeff_to_real(v, &ty)] } } @@ -966,14 +1061,31 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.def_resist_residual(neg, eq_last); self.ctx.def_react_residual(x_last, eq_last); - // y = Σ_k num[k] x_k. + // Direct feedthrough d = num[n]/den[n], present only when deg(num) == deg(den). + // Since s^n w = (input - Σ_{i { + let d_ak = self.ctx.ins().fmul(d, den[k]); + self.ctx.ins().fsub(nk, d_ak) + } + None => nk, + }; + let term = self.ctx.ins().fmul(ck, states[k].1); out = self.ctx.ins().fadd(out, term); } } + if let Some(d) = d { + let du = self.ctx.ins().fmul(d, input); + out = self.ctx.ins().fadd(out, du); + } out } diff --git a/openvaf/hir_lower/src/stmt.rs b/openvaf/hir_lower/src/stmt.rs index 37d8d454..a35b29cf 100644 --- a/openvaf/hir_lower/src/stmt.rs +++ b/openvaf/hir_lower/src/stmt.rs @@ -48,6 +48,14 @@ impl BodyLoweringCtx<'_, '_, '_> { return; } } + // Whole-array assignment (`g = '{1.0, 2.0};` or `g = h;`) writes the + // element places directly: an array is not a single MIR value. + if let hir::AssignmentLhs::Variable(var) = lhs { + if matches!(var.ty(self.ctx.db), Type::Array { .. }) { + self.assign_whole_array(var, rhs); + return; + } + } let val_ = self.lower_expr(rhs); match lhs { hir::AssignmentLhs::ArrayElement { var, index } => { @@ -153,6 +161,40 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.switch_to_block(end); } + /// Lower `arr = rhs` where `arr` is an array variable. The right-hand side can + /// only be an array literal or another array variable (the type checker rejects + /// everything else); both are written element by element. + fn assign_whole_array(&mut self, var: hir::Variable, rhs: ExprId) { + let len = self.array_len(var); + // A cast recorded on the whole array expression (e.g. `'{0, 1}` assigned to + // a real array) applies to every element. + let elem_cast = self.body.needs_cast(rhs).and_then(|(src, dst)| match (src, dst.clone()) { + (Type::Array { ty: src, .. }, Type::Array { ty: dst, .. }) => Some((*src, *dst)), + _ => None, + }); + match self.body.get_expr(rhs) { + Expr::Array(vals) => { + for (i, val) in vals.iter().enumerate().take(len as usize) { + let mut elem = self.lower_expr(*val); + if let Some((src, dst)) = &elem_cast { + elem = self.ctx.insert_cast(elem, src, dst); + } + self.ctx.def_place(PlaceKind::VarElement(var, i as u32), elem); + } + } + Expr::Read(hir::Ref::Variable(src_var)) => { + for i in 0..len.min(self.array_len(src_var)) { + let mut elem = self.ctx.use_place(PlaceKind::VarElement(src_var, i)); + if let Some((src, dst)) = &elem_cast { + elem = self.ctx.insert_cast(elem, src, dst); + } + self.ctx.def_place(PlaceKind::VarElement(var, i), elem); + } + } + _ => unreachable!("unsupported whole-array assignment source"), + } + } + /// Lower `arr[index] = val`. A constant index writes the element place directly; /// a runtime index conditionally rewrites every element (`elem_i = (index==i) ? /// val : elem_i`), keeping the array in pure SSA. @@ -161,15 +203,22 @@ impl BodyLoweringCtx<'_, '_, '_> { if len == 0 { return; } + // Element positions are offset by the declared lower bound (see + // `lower_index`): `real g[2:5]` stores g[2] in element 0. + let lo = var.array_lo(self.ctx.db); if let Some(c) = self.body.as_literalint(&index) { - let c = (c.max(0) as u32).min(len - 1); - self.ctx.def_place(PlaceKind::VarElement(var, c), val); + let pos = c as i64 - lo as i64; + if !(0..len as i64).contains(&pos) { + // Out of the declared range: diagnosed during type checking. + return; + } + self.ctx.def_place(PlaceKind::VarElement(var, pos as u32), val); return; } let idx_val = self.lower_expr(index); for i in 0..len { let current = self.ctx.use_place(PlaceKind::VarElement(var, i)); - let i_const = self.ctx.iconst(i as i32); + let i_const = self.ctx.iconst(lo + i as i32); let cond = self.ctx.ins().ieq(idx_val, i_const); let new = self.ctx.make_select(cond, |_s, branch| if branch { val } else { current }); self.ctx.def_place(PlaceKind::VarElement(var, i), new); diff --git a/openvaf/hir_ty/src/builtin.rs b/openvaf/hir_ty/src/builtin.rs index 61e8389c..6eeee682 100644 --- a/openvaf/hir_ty/src/builtin.rs +++ b/openvaf/hir_ty/src/builtin.rs @@ -303,7 +303,7 @@ bultins! { fn SIMPARAM_DEFAULT(Literal(String),Val(Real)) -> Real; } - const fn SIMPARAM_STR(Literal(String)) -> Real; + const fn SIMPARAM_STR(Literal(String)) -> String; RANDOM = const { fn RANDOM_NO_SEED() -> Integer; diff --git a/openvaf/hir_ty/src/diagnostics.rs b/openvaf/hir_ty/src/diagnostics.rs index 4a7ec740..95c3125d 100644 --- a/openvaf/hir_ty/src/diagnostics.rs +++ b/openvaf/hir_ty/src/diagnostics.rs @@ -297,6 +297,21 @@ impl Diagnostic for InferenceDiagnosticWrapped<'_> { "help: expected one of the following\nbranch current access: I(branch), I(a,b)\nnode voltage: V(x)".to_owned(), ]) } + InferenceDiagnostic::ArrayIndexOutOfBounds { e, index, lo, hi } => { + let src = self + .parse + .to_file_span(self.body_sm.expr_map_back[e].as_ref().unwrap().range(), self.sm); + + Report::error() + .with_labels(vec![Label { + style: LabelStyle::Primary, + file_id: src.file, + range: src.range.into(), + message: format!("index {index} is out of bounds"), + }]) + .with_message("array index out of bounds") + .with_notes(vec![format!("help: the declared range is [{lo}:{hi}]")]) + } InferenceDiagnostic::ExpectedProbe { e } => { let src = self .parse diff --git a/openvaf/hir_ty/src/inference.rs b/openvaf/hir_ty/src/inference.rs index 7c1c158b..6c8c34c1 100755 --- a/openvaf/hir_ty/src/inference.rs +++ b/openvaf/hir_ty/src/inference.rs @@ -445,6 +445,22 @@ impl Ctx<'_> { self.infere_expr(stmt, index); // The result is the element type of the indexed array. let base_ty = self.infere_expr(stmt, base)?; + if let Ty::Var(Type::Array { len, .. }, var) = &base_ty { + if let Some(idx) = self.const_int_value(index) { + let lo = self.db.var_data(*var).array_lo as i64; + let hi = lo + *len as i64 - 1; + if idx < lo || idx > hi { + self.result.diagnostics.push( + InferenceDiagnostic::ArrayIndexOutOfBounds { + e: index, + index: idx, + lo, + hi, + }, + ); + } + } + } match base_ty.to_value() { Some(Type::Array { ty, .. }) => Ty::Val(*ty), _ => Ty::Val(Type::Err), @@ -1255,6 +1271,21 @@ impl Ctx<'_> { } } + /// Value of an expression if it is a plain (possibly negated) integer literal. + fn const_int_value(&self, e: ExprId) -> Option { + match self.body.exprs[e] { + Expr::Literal(Literal::Int(val)) => Some(val as i64), + Expr::UnaryOp { expr, op: UnaryOp::Neg } => { + if let Expr::Literal(Literal::Int(val)) = self.body.exprs[expr] { + Some(-(val as i64)) + } else { + None + } + } + _ => None, + } + } + // fn collect_fmt_literal(&mut self, stmt: StmtId, args: &[ExprId]){ // self.body // } @@ -1282,6 +1313,13 @@ pub enum InferenceDiagnostic { e: ExprId, }, + ArrayIndexOutOfBounds { + e: ExprId, + index: i64, + lo: i64, + hi: i64, + }, + InvalidLimitFunction { expr: ExprId, func: FunctionId, diff --git a/openvaf/lexer/src/lib.rs b/openvaf/lexer/src/lib.rs index 908fc73d..cfd076b9 100644 --- a/openvaf/lexer/src/lib.rs +++ b/openvaf/lexer/src/lib.rs @@ -116,11 +116,13 @@ impl Cursor<'_> { // Three Symbol tokens '<' if self.first() == '<' && self.second() == '<' => { + self.bump(); self.bump(); ShlA } '>' if self.first() == '>' && self.second() == '>' => { + self.bump(); self.bump(); ShrA } diff --git a/openvaf/mir/src/dominators.rs b/openvaf/mir/src/dominators.rs index e84c34c4..c4802d63 100644 --- a/openvaf/mir/src/dominators.rs +++ b/openvaf/mir/src/dominators.rs @@ -47,6 +47,11 @@ pub struct DominatorTree { /// CFG post-order of all reachable blocks. postorder: Vec, stack: Vec<(Block, Successors)>, + /// Real exit blocks (no successors) seeded as independent roots by + /// `compute_reverse_postorder` when there is more than one -- see its doc comment. + /// Empty whenever the function has a single exit (the overwhelmingly common case), in + /// which case `compute_domtree::` behaves exactly as before. + reverse_roots: Vec, } impl DominatorTree { @@ -84,6 +89,7 @@ impl DominatorTree { self.nodes.clear(); self.reverse_nodes.clear(); self.postorder.clear(); + self.reverse_roots.clear(); debug_assert!(self.stack.is_empty()); // self.valid = false; } @@ -186,12 +192,36 @@ impl DominatorTree { // self.compute_reverse_cfg_postorder(func, cfg); self.reverse_nodes .resize(func.layout.num_blocks(), DomTreeNode { rpo_number: UNDEF, idom: None.into() }); - match func.layout.last_block() { - Some(block) => { + + // Post-dominance requires a single root to walk predecessors from. A function can have + // more than one real exit block (multiple `Exit`/terminating blocks with no successors, + // e.g. from independent `if`/event-control regions each ending their own control-flow + // path) -- seeding the traversal from only `func.layout.last_block()` (as this used to + // do) silently leaves every block that can't reach *that specific* block as + // `rpo_number == UNDEF`, making `ipdom()` return `None` for branches whose two arms + // don't both funnel through the layout's last block, even though they have a perfectly + // well-defined nearer common post-dominator. Seed from *every* real exit block (no + // successors) at once instead, as if they all fed into one virtual super-exit -- this + // matches the standard multi-exit post-dominator tree construction and leaves + // single-exit functions (the common case) unaffected. + for block in func.layout.blocks() { + if cfg.successors(block) == Successors(None.into(), None.into()) { self.stack.push((block, Successors(None.into(), None.into()))); self.reverse_nodes[block].rpo_number = SEEN; + self.reverse_roots.push(block); + } + } + // Fall back to the previous behaviour if the function has no real exit block at all + // (e.g. every block loops forever) so at least something is seeded. + if self.reverse_roots.is_empty() { + match func.layout.last_block() { + Some(block) => { + self.stack.push((block, Successors(None.into(), None.into()))); + self.reverse_nodes[block].rpo_number = SEEN; + self.reverse_roots.push(block); + } + None => return, } - None => return, } while let Some((block, _)) = self.stack.pop() { @@ -217,8 +247,6 @@ impl DominatorTree { _ => unreachable!(), } } - - debug_assert_eq!(self.postorder.last().copied(), func.layout.last_block()); } /// Reset all internal data structures and compute a post-order of the control flow graph. @@ -276,6 +304,22 @@ impl DominatorTree { None => return, }; + // For `REVERSE` (post-dominance), a function can have more than one real exit block -- + // `compute_reverse_postorder` seeds a walk from every one of them (see its doc comment), + // so `postorder` can contain several such "root" blocks besides `entry_block` (the one + // `split_last` happened to pick), each with zero reachable (reverse-)predecessors. + // `compute_idom_` would panic on those (`"block node must have one reachable + // predecessor"`, since it genuinely has none). Instead of computing an idom for them, + // give each one an explicit idom of `entry_block` directly -- equivalent to what a real + // virtual super-exit node feeding into every real exit would produce, without needing to + // allocate one. Blocks with a single real exit (the overwhelmingly common case) have an + // empty `reverse_roots` other than `entry_block` itself, so this is a no-op then. + let other_roots: Vec = if REVERSE { + self.reverse_roots.iter().copied().filter(|&b| b != entry_block).collect() + } else { + Vec::new() + }; + // Do a first pass where we assign RPO numbers to all reachable nodes. let nodes = if REVERSE { &mut self.reverse_nodes } else { &mut self.nodes }; nodes[entry_block].rpo_number = 2; @@ -287,11 +331,14 @@ impl DominatorTree { // function will never see an uninitialized predecessor. // // Due to the nature of the post-order traversal, every node we visit will have at - // least one predecessor that has previously been visited during this RPO. - let node = DomTreeNode { - rpo_number: rpo_idx as u32 + 3, - idom: self.compute_idom::(block, cfg).into(), + // least one predecessor that has previously been visited during this RPO -- except + // the other real-exit roots handled above, which have none by construction. + let idom = if other_roots.contains(&block) { + entry_block + } else { + self.compute_idom::(block, cfg) }; + let node = DomTreeNode { rpo_number: rpo_idx as u32 + 3, idom: idom.into() }; let nodes = if REVERSE { &mut self.reverse_nodes } else { &mut self.nodes }; nodes[block] = node; @@ -304,7 +351,12 @@ impl DominatorTree { while changed { changed = false; for &block in postorder.iter().rev() { - let idom = self.compute_idom::(block, cfg).into(); + let idom = if other_roots.contains(&block) { + entry_block + } else { + self.compute_idom::(block, cfg) + } + .into(); let nodes = if REVERSE { &mut self.reverse_nodes } else { &mut self.nodes }; if nodes[block].idom != idom { nodes[block].idom = idom; @@ -416,3 +468,52 @@ impl<'a> dot::GraphWalk<'a, Block, (Block, Block)> for DomTreeRender<'a> { edge.1 } } + +#[cfg(test)] +mod tests { + use crate::builder::InstBuilder; + use crate::cursor::{Cursor, FuncCursor}; + use crate::{ControlFlowGraph, DominatorTree, Function}; + + /// A branch whose arms rejoin and exit through a block that is *not* the + /// layout's last block used to get no post-dominator information at all: + /// the reverse walk was seeded only from `layout.last_block()`, so every + /// block that could not reach that specific exit stayed unvisited and + /// `ipdom()` returned `None` despite a well-defined common post-dominator. + #[test] + fn multi_exit_postdominators() { + let mut func = Function::new(); + let cond = func.dfg.make_param(0u32.into()); + + let entry = func.layout.append_new_block(); + let branching = func.layout.append_new_block(); + let then_arm = func.layout.append_new_block(); + let else_arm = func.layout.append_new_block(); + let rejoin_exit = func.layout.append_new_block(); + // A second real exit; deliberately the *last* block in the layout. + let other_exit = func.layout.append_new_block(); + + let mut cur = FuncCursor::new(&mut func).at_bottom(entry); + cur.ins().branch(cond, branching, other_exit, false); + cur = cur.at_bottom(branching); + cur.ins().branch(cond, then_arm, else_arm, false); + cur = cur.at_bottom(then_arm); + cur.ins().jump(rejoin_exit); + cur = cur.at_bottom(else_arm); + cur.ins().jump(rejoin_exit); + cur = cur.at_bottom(rejoin_exit); + cur.ins().exit(); + cur = cur.at_bottom(other_exit); + cur.ins().exit(); + + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&func); + let mut domtree = DominatorTree::default(); + domtree.compute(&func, &cfg, false, true, false); + + assert_eq!(domtree.ipdom(then_arm), Some(rejoin_exit)); + assert_eq!(domtree.ipdom(else_arm), Some(rejoin_exit)); + assert_eq!(domtree.ipdom(branching), Some(rejoin_exit)); + assert!(domtree.post_dominates(branching, rejoin_exit)); + } +} diff --git a/openvaf/mir/src/layout.rs b/openvaf/mir/src/layout.rs index 1aedb966..ceb64c97 100644 --- a/openvaf/mir/src/layout.rs +++ b/openvaf/mir/src/layout.rs @@ -237,6 +237,42 @@ impl Layout { self.last_block } + /// Unlink `block` from its current layout position and re-insert it as the + /// last block. Used when a block is merged away and it was the layout's + /// last block, so its surviving predecessor must take over that position + /// -- otherwise `last_block()` would silently start pointing at whatever + /// block happened to be physically last, which is not necessarily the + /// function's true logical exit anymore. + pub fn move_block_to_end(&mut self, block: Block) { + if self.last_block == Some(block) { + return; + } + debug_assert!(self.is_block_inserted(block), "block not in the layout"); + let prev = self.blocks[block].prev; + let next = self.blocks[block].next; + match prev.expand() { + None => self.first_block = next.expand(), + Some(p) => self.blocks[p].next = next, + } + match next.expand() { + None => self.last_block = prev.expand(), + Some(n) => self.blocks[n].prev = prev, + } + + let last = self.last_block; + { + let node = &mut self.blocks[block]; + node.prev = last.into(); + node.next = None.into(); + } + if let Some(last) = last { + self.blocks[last].next = block.into(); + } else { + self.first_block = Some(block); + } + self.last_block = Some(block); + } + /// Get the block preceding `block` in the layout order. pub fn prev_block(&self, block: Block) -> Option { self.blocks[block].prev.expand() diff --git a/openvaf/mir/src/layout/tests.rs b/openvaf/mir/src/layout/tests.rs index b32befa5..808326f6 100644 --- a/openvaf/mir/src/layout/tests.rs +++ b/openvaf/mir/src/layout/tests.rs @@ -474,3 +474,27 @@ fn merge_block() { "#]] .assert_eq(&func.to_debug_string()); } + +#[test] +fn move_block_to_end() { + let mut layout = Layout::new(); + let e0 = layout.append_new_block(); + let e1 = layout.append_new_block(); + let e2 = layout.append_new_block(); + assert_eq!(layout.last_block(), Some(e2)); + + // Moving the block that already is last is a no-op. + layout.move_block_to_end(e2); + verify(&mut layout, &[(e0, &[]), (e1, &[]), (e2, &[])]); + + // Move a middle block to the end. + layout.move_block_to_end(e1); + verify(&mut layout, &[(e0, &[]), (e2, &[]), (e1, &[])]); + assert_eq!(layout.last_block(), Some(e1)); + + // Move the first block to the end. + layout.move_block_to_end(e0); + verify(&mut layout, &[(e2, &[]), (e1, &[]), (e0, &[])]); + assert_eq!(layout.last_block(), Some(e0)); + assert_eq!(layout.entry_block(), Some(e2)); +} diff --git a/openvaf/mir_opt/src/simplify_cfg.rs b/openvaf/mir_opt/src/simplify_cfg.rs index c0978044..7c4aa98f 100644 --- a/openvaf/mir_opt/src/simplify_cfg.rs +++ b/openvaf/mir_opt/src/simplify_cfg.rs @@ -262,6 +262,7 @@ impl<'a> SimplifyCfg<'a> { { return false; } + let bb_was_last = self.func.layout.last_block() == Some(bb); // in case the terminator is an unoptimized br _, bb, bb if let Some(terminator) = self.func.layout.last_inst(pred) { @@ -281,6 +282,17 @@ impl<'a> SimplifyCfg<'a> { } self.func.layout.merge_blocks(pred, bb); + // `merge_blocks` keeps `pred` at its own original layout position and + // discards `bb` entirely. If `bb` used to be the function's last + // block, `pred` (which now holds `bb`'s merged-in contents, including + // its terminator) must take over that position, or `last_block()` + // would silently start pointing at some earlier, unrelated block -- + // this broke `mir::cursor::goto_exit`, which several `sim_back` + // passes (e.g. `dae/builder.rs`'s `ensure_optbarriers`) rely on to + // find the true function exit. + if bb_was_last { + self.func.layout.move_block_to_end(pred); + } self.local_changed = true; // update sucessors/predecessors @@ -632,9 +644,26 @@ impl<'a> SimplifyCfg<'a> { // Remove basic blocks that have no predecessors (except the entry block)... // or that just have themself as a predecessor. These are unreachable. // Do not remove last block in layout + // + // A block can look CFG-unreachable from this pass's point of view while one of + // its instructions' results is still genuinely referenced by an instruction that + // hasn't been spliced into the layout yet (e.g. a not-yet-inserted derivative/ + // Jacobian instruction created by `mir_autodiff`, which references existing + // values as raw operands before its own instructions are placed into a block). + // Blindly `zap_inst`-ing every instruction in such a block -- unlike this file's + // other phi-removal helpers (`simplify_trivial_phis`, `simplify_duplicates_phis_naive`), + // which call `replace_uses` before zapping -- would leave that later instruction + // with a permanently dangling reference to a removed instruction, since there is + // no live/reachable use at this point to redirect it to. So: only actually treat + // the block as removable once none of its instructions' results still have any + // outstanding uses. + let has_live_results = self.func.layout.block_insts(bb).any(|inst| { + self.func.dfg.inst_results(inst).iter().any(|&val| !self.func.dfg.value_dead(val)) + }); if (self.cfg[bb].predecessors.is_empty() || self.cfg.self_loop(bb)) && Some(bb) != self.func.layout.entry_block() && self.func.layout.last_block() != Some(bb) + && !has_live_results { // remove phi phi_edges for succ in self.cfg.succ_iter(bb) { diff --git a/openvaf/mir_opt/src/simplify_cfg/tests.rs b/openvaf/mir_opt/src/simplify_cfg/tests.rs index 4e9d9bf6..9a58c250 100644 --- a/openvaf/mir_opt/src/simplify_cfg/tests.rs +++ b/openvaf/mir_opt/src/simplify_cfg/tests.rs @@ -238,3 +238,37 @@ pub fn duplicate_phis_set() { "#]]; expect.assert_eq(&func.to_debug_string()) } + +/// An unreachable block whose instruction results still have outstanding uses +/// (in real compilations e.g. from a not-yet-inserted derivative instruction +/// created by `mir_autodiff`) must not be zapped: that would leave the user +/// with a permanently dangling reference. It may only be removed once its +/// results are dead. +#[test] +pub fn keep_unreachable_block_with_live_results() { + let raw = r##" + function %foo(v10) { + block0: + v11 = iadd v10, v10 + jmp block2 + block1: + v12 = imul v10, v10 + jmp block2 + block2: + v13 = iadd v11, v12 + } + "##; + + let (mut func, _) = parse_function(raw).unwrap(); + let mut cfg = ControlFlowGraph::new(); + cfg.compute(&func); + simplify_cfg(&mut func, &mut cfg); + + // block1 is unreachable, but its result v12 still has a live use in + // block2; removing the block would leave that use dangling. + let printed = func.to_debug_string(); + assert!( + printed.contains("imul"), + "unreachable block was removed despite live uses:\n{printed}" + ); +} diff --git a/openvaf/openvaf/tests/load/osdi_0_4.rs b/openvaf/openvaf/tests/load/osdi_0_4.rs index bd473f7d..c0284ef3 100644 --- a/openvaf/openvaf/tests/load/osdi_0_4.rs +++ b/openvaf/openvaf/tests/load/osdi_0_4.rs @@ -62,6 +62,7 @@ pub const DOMAIN_CONTINUOUS: u32 = 2; pub const NOISE_TYPE_WHITE: u32 = 0; pub const NOISE_TYPE_FLICKER: u32 = 1; pub const NOISE_TYPE_TABLE: u32 = 2; + #[repr(C)] pub struct OsdiLimFunction { pub name: *mut c_char, diff --git a/openvaf/osdi/src/load.rs b/openvaf/osdi/src/load.rs index 46ed9862..89b51928 100644 --- a/openvaf/osdi/src/load.rs +++ b/openvaf/osdi/src/load.rs @@ -2,10 +2,11 @@ use core::ffi::c_uint; use std::ptr::NonNull; use llvm_sys::core::{ - LLVMAppendBasicBlockInContext, LLVMBuildCall2, LLVMBuildFAdd, LLVMBuildFDiv, LLVMBuildFMul, - LLVMBuildFSub, LLVMBuildGEP2, LLVMBuildRetVoid, LLVMBuildStore, LLVMCreateBuilderInContext, - LLVMDisposeBuilder, LLVMGetParam, LLVMPositionBuilderAtEnd, + LLVMAppendBasicBlockInContext, LLVMBuildCall2, LLVMBuildFAdd, LLVMBuildFCmp, LLVMBuildFDiv, + LLVMBuildFMul, LLVMBuildFSub, LLVMBuildGEP2, LLVMBuildRetVoid, LLVMBuildSelect, LLVMBuildStore, + LLVMCreateBuilderInContext, LLVMDisposeBuilder, LLVMGetParam, LLVMPositionBuilderAtEnd, }; +use llvm_sys::LLVMRealPredicate; use mir_llvm::UNNAMED; use sim_back::dae::NoiseSourceKind; use stdx::iter::zip; @@ -42,6 +43,99 @@ impl JacobianLoadType { } impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { + /// Emit the LLVM IR that evaluates a `noise_table`/`noise_table_log` power + /// spectral density at run time for a given `freq`. + /// + /// `vals` are the sorted `(x, power)` pairs produced by + /// `hir_lower::NoiseTable::new`, where `x` is already in `log10(frequency)` + /// space (linear-input tables are `log10`-ed at build time; `_log` tables + /// are stored as-is). The lookup key is therefore `log10(freq)`, and the + /// power is obtained by piecewise-linear interpolation over `x`, clamped to + /// the table's endpoints outside `[x[0], x[n-1]]`. + /// + /// The interpolation is fully unrolled into `select`s: every segment's + /// slope/intercept is a compile-time constant, so each segment costs one + /// `fmul`, one `fadd`, one `fcmp` and one `select`. + unsafe fn build_noise_table_interp( + &self, + llbuilder: llvm_sys::prelude::LLVMBuilderRef, + freq: llvm_sys::prelude::LLVMValueRef, + vals: &[(stdx::Ieee64, stdx::Ieee64)], + ) -> &'ll llvm_sys::LLVMValue { + let cx = self.cx; + let n = vals.len(); + if n == 0 { + return cx.const_real(0.0); + } + let x: Vec = vals.iter().map(|v| f64::from(v.0)).collect(); + let y: Vec = vals.iter().map(|v| f64::from(v.1)).collect(); + if n == 1 { + return cx.const_real(y[0]); + } + + // lx = log10(freq) + let (log_ty, log_fn) = self + .cx + .intrinsic("llvm.log10.f64") + .unwrap_or_else(|| unreachable!("intrinsic llvm.log10.f64 not found")); + let mut log_args: [llvm_sys::prelude::LLVMValueRef; 1] = [freq]; + let lx = LLVMBuildCall2( + llbuilder, + NonNull::from(log_ty).as_ptr(), + NonNull::from(log_fn).as_ptr(), + log_args.as_mut_ptr(), + 1, + UNNAMED, + ); + + // Default (lx >= x[n-1]): clamp to the last point. + let mut result = NonNull::from(cx.const_real(y[n - 1])).as_ptr(); + + // Walk the segments from the top down. After the loop, the surviving + // `select` is the one for the lowest segment whose upper bound exceeds + // `lx`, i.e. exactly the bracketing segment. + for i in (0..n - 1).rev() { + let slope = (y[i + 1] - y[i]) / (x[i + 1] - x[i]); + let intercept = y[i] - slope * x[i]; + // seg = slope * lx + intercept + let seg = + LLVMBuildFMul(llbuilder, NonNull::from(cx.const_real(slope)).as_ptr(), lx, UNNAMED); + let seg = LLVMBuildFAdd( + llbuilder, + seg, + NonNull::from(cx.const_real(intercept)).as_ptr(), + UNNAMED, + ); + let cond = LLVMBuildFCmp( + llbuilder, + LLVMRealPredicate::LLVMRealOLT, + lx, + NonNull::from(cx.const_real(x[i + 1])).as_ptr(), + UNNAMED, + ); + result = LLVMBuildSelect(llbuilder, cond, seg, result, UNNAMED); + } + + // Clamp below x[0] to the first point (otherwise segment 0 would + // extrapolate below the table). + let cond0 = LLVMBuildFCmp( + llbuilder, + LLVMRealPredicate::LLVMRealOLT, + lx, + NonNull::from(cx.const_real(x[0])).as_ptr(), + UNNAMED, + ); + result = LLVMBuildSelect( + llbuilder, + cond0, + NonNull::from(cx.const_real(y[0])).as_ptr(), + result, + UNNAMED, + ); + + &*result + } + pub fn load_noise(&self) -> &'ll llvm_sys::LLVMValue { let OsdiCompilationUnit { cx, module, .. } = self; let void_ptr = cx.ty_ptr(); @@ -120,7 +214,10 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { pwr } - NoiseSourceKind::NoiseTable { .. } => unimplemented!("noise tables"), + NoiseSourceKind::NoiseTable { ref vals, .. } => { + let freq_ptr = freq as *const llvm_sys::LLVMValue as *mut _; + self.build_noise_table_interp(llbuilder, freq_ptr, vals) + } }; // Multiply with squared factor because factor is in terms of signal, but @@ -197,7 +294,10 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { NoiseSourceKind::FlickerNoise { .. } => { self.load_eval_output(eval_outputs.args[0], &*inst, &*model, &*llbuilder) } - NoiseSourceKind::NoiseTable { .. } => unimplemented!("noise tables"), + // A frequency-dependent table has no single scalar power; the + // per-frequency `load_noise` entry point is the real evaluator + // (this ABI slot is unused by ngspice's OSDI noise path). + NoiseSourceKind::NoiseTable { .. } => cx.const_real(0.0), }; // Multiply with squared factor because factor is in terms of signal, but @@ -224,7 +324,7 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { NoiseSourceKind::FlickerNoise { .. } => { self.load_eval_output(eval_outputs.args[1], &*inst, &*model, &*llbuilder) } - NoiseSourceKind::NoiseTable { .. } => unimplemented!("noise tables"), + NoiseSourceKind::NoiseTable { .. } => cx.const_real(0.0), }; // Store power diff --git a/openvaf/osdi/src/metadata.rs b/openvaf/osdi/src/metadata.rs index 85881a0f..7cf7514c 100644 --- a/openvaf/osdi/src/metadata.rs +++ b/openvaf/osdi/src/metadata.rs @@ -442,6 +442,7 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { } }) .collect(); + let absdelay_count = absdelay_info.len() as u32; OsdiDescriptor { name: module.info.module.name(db), @@ -499,6 +500,7 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { residual_nature: rvec, noise_source_type, load_noise_params: self.load_noise_params(), + absdelay_count, absdelay_info, } } diff --git a/openvaf/osdi/src/metadata/osdi_0_4.rs b/openvaf/osdi/src/metadata/osdi_0_4.rs index efe82cde..e7aae96a 100644 --- a/openvaf/osdi/src/metadata/osdi_0_4.rs +++ b/openvaf/osdi/src/metadata/osdi_0_4.rs @@ -447,6 +447,7 @@ pub struct OsdiDescriptor<'ll> { pub residual_nature: Vec, pub noise_source_type: Vec, pub load_noise_params: &'ll llvm_sys::LLVMValue, + pub absdelay_count: u32, pub absdelay_info: Vec, } impl<'ll> OsdiDescriptor<'ll> { @@ -517,7 +518,7 @@ impl<'ll> OsdiDescriptor<'ll> { ctx.const_arr_ptr(tys.osdi_nature_ref, &arr_47), ctx.const_arr_ptr(ctx.ty_int(), &arr_48), self.load_noise_params, - ctx.const_unsigned_int(self.absdelay_info.len() as u32), + ctx.const_unsigned_int(self.absdelay_count), ctx.const_arr_ptr(tys.osdi_abs_delay_info, &arr_51), ]; let ty = tys.osdi_descriptor; diff --git a/openvaf/osdi/src/setup.rs b/openvaf/osdi/src/setup.rs index fd2ac388..e03ffeae 100644 --- a/openvaf/osdi/src/setup.rs +++ b/openvaf/osdi/src/setup.rs @@ -162,8 +162,15 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { builder.params[dst] = BuilderVal::Load(Box::new(loc)); let dst = intern.params.unwrap_index(&ParamKind::ParamGiven { param }); - let is_given = - unsafe { model_data.is_nth_param_given(cx, i, &*model, builder.llbuilder) }; + // A `localparam` is never externally overridable (Verilog-AMS LRM): + // force its "given" flag to a constant false so the parameter-init + // code always stores the declared default expression, unconditionally + // overwriting any value the simulator wrote into the parameter slot. + let is_given = if param.is_local(self.db) { + cx.const_bool(false) + } else { + unsafe { model_data.is_nth_param_given(cx, i, &*model, builder.llbuilder) } + }; builder.params[dst] = BuilderVal::Eager(is_given); } @@ -200,6 +207,9 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { // Debug: Destination index for user param: dst builder.params[dst] = BuilderVal::Eager(val); let dst = intern.params.unwrap_index(&ParamKind::ParamGiven { param }); + // localparams are never externally overridable (see above) + let is_given = + if param.is_local(self.db) { cx.const_bool(false) } else { is_given }; builder.params[dst] = BuilderVal::Eager(is_given); } } @@ -427,6 +437,9 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { inst_data.store_nth_param(i, instance, val, builder.llbuilder); } let dst = intern.params.unwrap_index(&ParamKind::ParamGiven { param }); + // localparams are never externally overridable + let is_given = + if param.is_local(self.db) { cx.const_bool(false) } else { is_given }; builder.params[dst] = BuilderVal::Eager(is_given); } } @@ -441,8 +454,12 @@ impl<'ll> OsdiCompilationUnit<'_, '_, 'll> { } if let Some(dst) = intern.params.index(&ParamKind::ParamGiven { param }) { - let is_given = - unsafe { model_data.is_nth_param_given(cx, i, model, builder.llbuilder) }; + // localparams are never externally overridable + let is_given = if param.is_local(self.db) { + cx.const_bool(false) + } else { + unsafe { model_data.is_nth_param_given(cx, i, model, builder.llbuilder) } + }; builder.params[dst] = BuilderVal::Eager(is_given); } } diff --git a/openvaf/osdi/stdlib.c b/openvaf/osdi/stdlib.c index 8d58bdc7..a7989426 100644 --- a/openvaf/osdi/stdlib.c +++ b/openvaf/osdi/stdlib.c @@ -88,13 +88,19 @@ extern int strcmp(const char *__s1, const char *__s2); char *simparam_str(void *params_, void *handle, uint32_t *flags, char *name) { OsdiSimParas *params = params_; - for (int i = 0; params->names[i]; i++) { - char *p1, *p2; - int eq; - SCMP(p1, p2, params->names_str[i], name, eq); - // if (strcmp(params->names_str[i], name) == 0) { - if (eq) { - return params->names_str[i]; + // Walk the *string* parameter list (`names_str`, NULL-terminated) and return + // the matching *value* (`vals_str`). Previously this loop iterated using the + // numeric `names` array as its bound (an out-of-bounds read once the string + // list is shorter) and returned the name itself instead of the value, so + // `$simparam$str` never worked. + if (params->names_str) { + for (int i = 0; params->names_str[i]; i++) { + char *p1, *p2; + int eq; + SCMP(p1, p2, params->names_str[i], name, eq); + if (eq) { + return params->vals_str[i]; + } } } *flags |= EVAL_RET_FLAG_FATAL; diff --git a/openvaf/parser/src/grammar/items/module.rs b/openvaf/parser/src/grammar/items/module.rs index 9f3d91fe..d6788d4f 100644 --- a/openvaf/parser/src/grammar/items/module.rs +++ b/openvaf/parser/src/grammar/items/module.rs @@ -218,7 +218,10 @@ fn net_decl(p: &mut Parser, m: Marker) { eat_name_ref(p); } } else { - name_ref_r(p, MODULE_ITEM_OR_ATTR_RECOVERY.union(TokenSet::unique(T![;]))) + name_ref_r(p, MODULE_ITEM_OR_ATTR_RECOVERY.union(TokenSet::unique(T![;]))); + // Allow an optional net-type after the discipline, e.g. + // `electrical ground gnd;`, mirroring the `ground electrical gnd;` form. + p.eat(NET_TYPE); } // Optional vectored/bus range, e.g. `electrical [0:n] inode;`. diff --git a/openvaf/sim_back/src/topology/lineralize.rs b/openvaf/sim_back/src/topology/lineralize.rs index e3adfd7b..e13d2b8a 100644 --- a/openvaf/sim_back/src/topology/lineralize.rs +++ b/openvaf/sim_back/src/topology/lineralize.rs @@ -42,7 +42,11 @@ impl<'a> super::Builder<'a> { ) { let mut ssa_builder = mir_build::SSAVariableBuilder::new(self.cfg); for (operator_inst, evaluation) in analog_operators { - let arg0 = self.func.dfg.instr_args(operator_inst)[0]; + // `noise_table`/`noise_table_log` carry their data in the callback + // and take no MIR value args, so guard against an empty arg list. + // `arg0` is only consumed by the non-noise (ddt) branch below, + // which always has an argument. + let arg0 = self.func.dfg.instr_args(operator_inst).first().copied().unwrap_or(F_ZERO); let cb = self.func.dfg.func_ref(operator_inst).unwrap(); let is_noise = intern.callbacks[cb].is_noise(); match evaluation { diff --git a/openvaf/syntax/src/name.rs b/openvaf/syntax/src/name.rs index 703351f8..4ee57422 100644 --- a/openvaf/syntax/src/name.rs +++ b/openvaf/syntax/src/name.rs @@ -436,11 +436,11 @@ pub mod sysfun { #[allow(bad_style, dead_code)] pub const value_plusargs:&str = "$value$plusargs"; #[allow(bad_style, dead_code)] - pub const simparam_str: &str ="$simpara$str"; + pub const simparam_str: &str = "$simparam$str"; } pub fn is_known(name: &str) -> bool{ - matches!(name,$(concat!("$",stringify!($ident)) |)* "$test$plusargs" | "$value$plusargs" | "$simpara$str") + matches!(name,$(concat!("$",stringify!($ident)) |)* "$test$plusargs" | "$value$plusargs" | "$simparam$str") } }; } diff --git a/sourcegen/src/osdi.rs b/sourcegen/src/osdi.rs index a2a68761..5939cf88 100644 --- a/sourcegen/src/osdi.rs +++ b/sourcegen/src/osdi.rs @@ -188,7 +188,13 @@ impl<'a> HeaderParser<'a> { } fn parse_ty(&mut self) -> Ty<'a> { - let base_ty = match self.eat_ident().unwrap() { + let mut ident = self.eat_ident().unwrap(); + let is_const = ident == "const"; + if is_const { + ident = self.eat_ident().unwrap(); + } + + let base_ty = match ident { "double" => BaseTy::F64, "int" | "int32_t" => BaseTy::I32, "uint32_t" => BaseTy::U32, @@ -210,7 +216,7 @@ impl<'a> HeaderParser<'a> { while self.eat("*") { indirection += 1; } - Ty { indirection, base: base_ty, func_args: None } + Ty { indirection, base: base_ty, func_args: None, is_const } } fn parse_struct(&mut self, is_union: bool) { @@ -293,6 +299,7 @@ struct Ty<'a> { base: BaseTy<'a>, indirection: u32, func_args: Option)>>, + is_const: bool, } struct BaseTyInterpolater<'b, 'a> { @@ -658,6 +665,7 @@ impl ToTokens for RustStruct<'_> { ret_ty: RustReturnTy(RustBasicTy { base: ty.base, indirection: ty.indirection, + is_const: ty.is_const, }), args: ty.func_args.as_ref()?, }) @@ -677,11 +685,12 @@ impl ToTokens for RustStruct<'_> { struct RustBasicTy<'a> { base: BaseTy<'a>, indirection: u32, + is_const: bool, } impl ToTokens for RustBasicTy<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - let RustBasicTy { base, indirection } = *self; + let RustBasicTy { base, indirection, is_const } = *self; let ident = match base { BaseTy::F64 => "f64", BaseTy::I32 => "i32", @@ -695,8 +704,15 @@ impl ToTokens for RustBasicTy<'_> { }; let base = Ident::new(ident, Span::call_site()); - let ptr = (0..indirection).map(|_| quote!(*mut)); - quote!(#(#ptr)* #base).to_tokens(tokens) + let ptr = (1..indirection).map(|_| quote!(*mut)); + let inner_ptr = if indirection == 0 { + quote!() + } else if is_const { + quote!(*const) + } else { + quote!(*mut) + }; + quote!(#(#ptr)* #inner_ptr #base).to_tokens(tokens) } } @@ -714,8 +730,8 @@ struct RustTy<'a>(&'a Ty<'a>); impl ToTokens for RustTy<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - let Ty { base, indirection, ref func_args } = *self.0; - let base = RustBasicTy { base, indirection }; + let Ty { base, indirection, ref func_args, is_const } = *self.0; + let base = RustBasicTy { base, indirection, is_const }; match func_args { Some(args) => { let base = RustReturnTy(base); diff --git a/verilogae/verilogae/src/back.rs b/verilogae/verilogae/src/back.rs index 61463845..90060515 100644 --- a/verilogae/verilogae/src/back.rs +++ b/verilogae/verilogae/src/back.rs @@ -72,8 +72,7 @@ pub fn stub_callbacks<'ll>( | CallBackKind::StoreLimit(_) | CallBackKind::LimDiscontinuity | CallBackKind::CollapseHint(_, _) - | CallBackKind::SetRetFlag { .. } - | CallBackKind::QueryPastState(_) => return None, + | CallBackKind::SetRetFlag { .. } => return None, CallBackKind::Analysis => { CallbackFun::Prebuilt(cx.const_callback(&[cx.ty_ptr()], cx.const_int(1))) }