From 8bb27f42140f272bbee435cff00bbeff96379228 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 09:18:29 +0200 Subject: [PATCH 01/19] Add standalone VerilogA runner (openvaf-r --run) Adds an imperative execution lane separate from the OSDI/DAE compiler: a module's module-scope `initial`/`final` procedural blocks are lowered to MIR and interpreted with mir_interpret, with host implementations of the output and control system tasks. No LLVM, linking, or simulator is involved. module hello_world; initial begin $display("Hello, Verilog-A world!"); end endmodule $ openvaf-r --run hello_world.va Hello, Verilog-A world! Pipeline: - Grammar: new ProceduralBlock node ('initial'|'final') in veriloga.ungram + a new 'final' keyword; regenerated AST/token tables; parser arm in grammar/items/module.rs. - HIR: ModuleBodyKind enum (Analog/AnalogInitial/Procedural) replaces the initial:bool on DefWithBodyId::ModuleId; procedural body collection in hir_def/body.rs; Module::procedural_block(); BodyCtx::ProceduralBlock validation (allows var refs, forbids contributions/analog operators). - Lowering: MirBuilder::with_procedural() lowers all initial-then-final blocks into one self-contained MIR function (no DAE/outputs). - Runner: openvaf::run() wires CompilationDB -> MirBuilder -> mir_interpret with a callback table (Print -> stdout/stderr with a printf-style renderer, SetRetFlag -> process exit code). mir_interpret gains an early-exit signal so $finish/$fatal can stop the run. New --run CLI flag. Verified: hello-world prints; format args (%d/%g), initial/final ordering, $finish (exit 0, early-stop) and $fatal (stderr, exit 1) all work; the analog->OSDI path is unchanged; full workspace tests 354 passed. v1 limitations: $random isn't lowered by the front-end; module `parameter` defaults aren't evaluated (treated as 0); engineering %r formatting is best-effort. --- Cargo.lock | 5 + openvaf/hir/src/diagnostics.rs | 15 +- openvaf/hir/src/lib.rs | 20 +- openvaf/hir_def/src/body.rs | 23 ++- openvaf/hir_def/src/item_tree/lower.rs | 5 + openvaf/hir_def/src/lib.rs | 13 +- openvaf/hir_def/tests/data_tests.rs | 5 +- openvaf/hir_lower/src/callbacks.rs | 2 +- openvaf/hir_lower/src/lib.rs | 41 +++- openvaf/hir_ty/src/validation/body.rs | 13 +- openvaf/mir_interpret/src/lib.rs | 30 ++- openvaf/openvaf-driver/src/cli_def.rs | 12 ++ openvaf/openvaf-driver/src/main.rs | 10 +- openvaf/openvaf/Cargo.toml | 5 + openvaf/openvaf/src/lib.rs | 3 + openvaf/openvaf/src/run.rs | 212 +++++++++++++++++++++ openvaf/parser/src/grammar/items/module.rs | 9 + openvaf/syntax/src/ast/generated/nodes.rs | 36 +++- openvaf/syntax/src/ast/node_ext.rs | 17 +- openvaf/syntax/veriloga.ungram | 4 + openvaf/tokens/src/parser/generated.rs | 8 +- sourcegen/src/ast/src.rs | 2 + 22 files changed, 450 insertions(+), 40 deletions(-) create mode 100644 openvaf/openvaf/src/run.rs diff --git a/Cargo.lock b/Cargo.lock index cc8e9135..90c227c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1058,7 +1058,9 @@ dependencies = [ "expect-test", "float-cmp", "hir", + "hir_lower", "indexmap 2.14.0", + "lasso", "libc", "libloading", "linker", @@ -1068,6 +1070,8 @@ dependencies = [ "llvm-sys 211.0.1", "md5", "mini_harness", + "mir", + "mir_interpret", "mir_llvm", "osdi", "paths", @@ -1076,6 +1080,7 @@ dependencies = [ "target", "termcolor", "tokens", + "typed-index-collections", ] [[package]] diff --git a/openvaf/hir/src/diagnostics.rs b/openvaf/hir/src/diagnostics.rs index 5686a846..154c135e 100644 --- a/openvaf/hir/src/diagnostics.rs +++ b/openvaf/hir/src/diagnostics.rs @@ -4,7 +4,7 @@ pub use basedb::{BaseDB, FileId}; use hir_def::db::HirDefDB; use hir_def::nameres::diagnostics::DefDiagnosticWrapped; use hir_def::nameres::{DefMap, LocalScopeId, ScopeDefItem, ScopeOrigin}; -use hir_def::DefWithBodyId; +use hir_def::{DefWithBodyId, ModuleBodyKind}; use hir_ty::diagnostics::InferenceDiagnosticWrapped; use hir_ty::validation::{ self, BodyValidationDiagnostic, BodyValidationDiagnosticWrapped, @@ -48,7 +48,7 @@ pub(crate) fn collect(db: &CompilationDB, root_file: FileId, sink: &mut impl Dia collect_body_diagnostcs( db, sink, - DefWithBodyId::ModuleId { initial: true, module }, + DefWithBodyId::ModuleId { kind: ModuleBodyKind::AnalogInitial, module }, &parse, &sm, root_file, @@ -57,7 +57,16 @@ pub(crate) fn collect(db: &CompilationDB, root_file: FileId, sink: &mut impl Dia collect_body_diagnostcs( db, sink, - DefWithBodyId::ModuleId { initial: false, module }, + DefWithBodyId::ModuleId { kind: ModuleBodyKind::Analog, module }, + &parse, + &sm, + root_file, + &ast_id_map, + ); + collect_body_diagnostcs( + db, + sink, + DefWithBodyId::ModuleId { kind: ModuleBodyKind::Procedural, module }, &parse, &sm, root_file, diff --git a/openvaf/hir/src/lib.rs b/openvaf/hir/src/lib.rs index d446d48c..892509fe 100644 --- a/openvaf/hir/src/lib.rs +++ b/openvaf/hir/src/lib.rs @@ -22,8 +22,8 @@ pub use hir_def::nameres::diagnostics::PathResolveError; use hir_def::nameres::{DefMap, LocalScopeId, ScopeDefItem}; use hir_def::{ AliasParamId, BlockId, BlockLoc, BranchId, DefWithBodyId, DisciplineId, FunctionId, - LocalFunctionArgId, Lookup, ModuleId, ModuleLoc, NatureAttrId, NatureId, NodeId, ParamId, - VarId, + LocalFunctionArgId, Lookup, ModuleBodyKind, ModuleId, ModuleLoc, NatureAttrId, NatureId, + NodeId, ParamId, VarId, }; pub use hir_def::{BuiltIn, Case, Literal, ParamSysFun, Path, Type}; pub use hir_ty::builtin; @@ -162,11 +162,23 @@ impl Module { } pub fn analog_initial_block(&self, db: &CompilationDB) -> Body { - Body::new(DefWithBodyId::ModuleId { initial: true, module: self.id }, db) + Body::new( + DefWithBodyId::ModuleId { kind: ModuleBodyKind::AnalogInitial, module: self.id }, + db, + ) } pub fn analog_block(&self, db: &CompilationDB) -> Body { - Body::new(DefWithBodyId::ModuleId { initial: false, module: self.id }, db) + Body::new(DefWithBodyId::ModuleId { kind: ModuleBodyKind::Analog, module: self.id }, db) + } + + /// The imperative `initial`/`final` procedural body executed by the standalone + /// VerilogA runner (`openvaf-r run`). Empty for ordinary device models. + pub fn procedural_block(&self, db: &CompilationDB) -> Body { + Body::new( + DefWithBodyId::ModuleId { kind: ModuleBodyKind::Procedural, module: self.id }, + db, + ) } // todo: just temporary for VAE, this needs to be cleaned up diff --git a/openvaf/hir_def/src/body.rs b/openvaf/hir_def/src/body.rs index 4847977f..48651eff 100644 --- a/openvaf/hir_def/src/body.rs +++ b/openvaf/hir_def/src/body.rs @@ -14,7 +14,8 @@ use crate::item_tree::{DisciplineAttr, ItemTreeId, ItemTreeNode, NatureAttr}; use crate::nameres::{DefMapSource, LocalScopeId}; use crate::{ DefWithBodyId, DisciplineAttrLoc, DisciplineLoc, Expr, ExprId, FunctionLoc, Literal, Lookup, - ModuleLoc, NatureAttrLoc, NatureLoc, ParamId, ParamLoc, ScopeId, Stmt, StmtId, Type, VarLoc, + ModuleBodyKind, ModuleLoc, NatureAttrLoc, NatureLoc, ParamId, ParamLoc, ScopeId, Stmt, StmtId, + Type, VarLoc, }; mod lower; @@ -67,7 +68,7 @@ impl Body { let (body, sm, _) = db.param_body_with_sourcemap(param); return (body, sm); } - DefWithBodyId::ModuleId { initial, module } => { + DefWithBodyId::ModuleId { kind, module } => { let ModuleLoc { scope, id: item_tree } = module.lookup(db); let ast_id = tree[item_tree].ast_id(); @@ -82,10 +83,20 @@ impl Body { curr_scope, registry: ®istry, }; - body.entry_stmts = if initial { - ast.analog_initial_behaviour().map(|stmt| ctx.collect_stmt(stmt)).collect() - } else { - ast.analog_behaviour().map(|stmt| ctx.collect_stmt(stmt)).collect() + body.entry_stmts = match kind { + ModuleBodyKind::AnalogInitial => { + ast.analog_initial_behaviour().map(|stmt| ctx.collect_stmt(stmt)).collect() + } + ModuleBodyKind::Analog => { + ast.analog_behaviour().map(|stmt| ctx.collect_stmt(stmt)).collect() + } + // Procedural runner lane: all `initial` blocks (source order) then + // all `final` blocks, as one imperative sequence. + ModuleBodyKind::Procedural => ast + .initial_behaviour() + .chain(ast.final_behaviour()) + .map(|stmt| ctx.collect_stmt(stmt)) + .collect(), }; } diff --git a/openvaf/hir_def/src/item_tree/lower.rs b/openvaf/hir_def/src/item_tree/lower.rs index 05e5e668..8a8c2645 100644 --- a/openvaf/hir_def/src/item_tree/lower.rs +++ b/openvaf/hir_def/src/item_tree/lower.rs @@ -289,6 +289,11 @@ impl Ctx { self.lower_stmt(stmt, dst); } } + ast::ModuleItem::ProceduralBlock(block) => { + if let Some(stmt) = block.stmt() { + self.lower_stmt(stmt, dst); + } + } ast::ModuleItem::VarDecl(var) => { self.lower_var(var, dst); } diff --git a/openvaf/hir_def/src/lib.rs b/openvaf/hir_def/src/lib.rs index a65473c2..42e16a28 100644 --- a/openvaf/hir_def/src/lib.rs +++ b/openvaf/hir_def/src/lib.rs @@ -405,10 +405,21 @@ impl NatureAttrLoc { impl_intern!(NatureAttrId, NatureAttrLoc, intern_nature_attr, lookup_intern_nature_attr); +/// Which behavioural body of a module is being lowered. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ModuleBodyKind { + /// `analog` block (the DAE device behaviour). + Analog, + /// `analog initial` block. + AnalogInitial, + /// Standalone `initial`/`final` procedural blocks (imperative runner lane). + Procedural, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DefWithBodyId { ParamId(ParamId), - ModuleId { initial: bool, module: ModuleId }, + ModuleId { kind: ModuleBodyKind, module: ModuleId }, FunctionId(FunctionId), VarId(VarId), NatureAttrId(NatureAttrId), diff --git a/openvaf/hir_def/tests/data_tests.rs b/openvaf/hir_def/tests/data_tests.rs index 1946e71d..26d71514 100644 --- a/openvaf/hir_def/tests/data_tests.rs +++ b/openvaf/hir_def/tests/data_tests.rs @@ -6,7 +6,7 @@ use basedb::{AbsPathBuf, BaseDB, BaseDatabase, FileId, Vfs, VfsEntry, VfsPath, V use expect_test::expect_file; use hir_def::db::{HirDefDB, HirDefDatabase, InternDatabase}; use hir_def::nameres::{DefMap, LocalScopeId, ScopeDefItem, ScopeOrigin}; -use hir_def::DefWithBodyId; +use hir_def::{DefWithBodyId, ModuleBodyKind}; use mini_harness::{harness, Result}; use parking_lot::RwLock; use stdx::{ignore_dev_tests, ignore_never, is_va_file, openvaf_test_data, project_root, Upcast}; @@ -108,7 +108,8 @@ fn body_test(file: &Path) -> Result { let mut actual = String::new(); for (_, scope) in &def_map[def_map.entry()].children { if let ScopeOrigin::Module(module) = def_map[*scope].origin { - let analog_block = DefWithBodyId::ModuleId { initial: false, module }; + let analog_block = + DefWithBodyId::ModuleId { kind: ModuleBodyKind::Analog, module }; actual.push_str(&db.body(analog_block).dump(&db)); for (_, scope) in &def_map[*scope].children { if let ScopeOrigin::Function(func) = def_map[*scope].origin { diff --git a/openvaf/hir_lower/src/callbacks.rs b/openvaf/hir_lower/src/callbacks.rs index 466e4149..5c17e700 100644 --- a/openvaf/hir_lower/src/callbacks.rs +++ b/openvaf/hir_lower/src/callbacks.rs @@ -17,7 +17,7 @@ pub enum ParamInfoKind { MaxExclusive, } -#[derive(Debug, Clone, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] pub enum RetFlag { Abort, Finish, diff --git a/openvaf/hir_lower/src/lib.rs b/openvaf/hir_lower/src/lib.rs index 1f54f72e..1e0f4286 100644 --- a/openvaf/hir_lower/src/lib.rs +++ b/openvaf/hir_lower/src/lib.rs @@ -421,6 +421,9 @@ pub struct MirBuilder<'a> { tag_writes: bool, ctx: Option<&'a mut FunctionBuilderContext>, lower_equations: bool, + /// When set, lower the module's imperative `initial`/`final` procedural body + /// (the standalone runner lane) instead of the analog DAE bodies. + procedural: bool, } impl<'a> MirBuilder<'a> { @@ -439,9 +442,17 @@ impl<'a> MirBuilder<'a> { ctx: None, lower_equations: false, tag_writes: false, + procedural: false, } } + /// Lower the module's standalone `initial`/`final` procedural body instead of the + /// analog DAE bodies. Used by the VerilogA runner (`openvaf-r run`). + pub fn with_procedural(mut self) -> Self { + self.procedural = true; + self + } + pub fn tag_reads(&mut self, var: Variable) -> bool { self.tagged_reads.insert(var) } @@ -494,19 +505,29 @@ impl<'a> MirBuilder<'a> { let builder: FunctionBuilder<'_> = FunctionBuilder::new(&mut func, literals, ctx, self.tag_writes); let path = self.module.name(self.db); - let analog_initial_body = self.module.analog_initial_block(self.db); - let analog_body = self.module.analog_block(self.db); let mut ctx = LoweringCtx::new(self.db, builder, !self.lower_equations, &mut interner) .with_tagged_vars(self.tagged_reads); - let mut body_ctx = - BodyLoweringCtx { ctx: &mut ctx, body: analog_initial_body.borrow(), path: &path }; - - // lower analog initial blocks first - body_ctx.lower_entry_stmts(); - // ... and normal analog blocks afterwards - body_ctx.body = analog_body.borrow(); - body_ctx.lower_entry_stmts(); + + if self.procedural { + // Runner lane: lower only the imperative procedural body (all `initial` + // blocks in source order, then all `final` blocks). No analog/DAE bodies. + let procedural_body = self.module.procedural_block(self.db); + let mut body_ctx = + BodyLoweringCtx { ctx: &mut ctx, body: procedural_body.borrow(), path: &path }; + body_ctx.lower_entry_stmts(); + } else { + let analog_initial_body = self.module.analog_initial_block(self.db); + let analog_body = self.module.analog_block(self.db); + let mut body_ctx = + BodyLoweringCtx { ctx: &mut ctx, body: analog_initial_body.borrow(), path: &path }; + + // lower analog initial blocks first + body_ctx.lower_entry_stmts(); + // ... and normal analog blocks afterwards + body_ctx.body = analog_body.borrow(); + body_ctx.lower_entry_stmts(); + } for var in self.required_vars { ctx.dec_place(PlaceKind::Var(var)); diff --git a/openvaf/hir_ty/src/validation/body.rs b/openvaf/hir_ty/src/validation/body.rs index 25517352..76900d81 100644 --- a/openvaf/hir_ty/src/validation/body.rs +++ b/openvaf/hir_ty/src/validation/body.rs @@ -4,7 +4,7 @@ use ahash::{HashMap, HashSet}; use hir_def::body::Body; use hir_def::{ BranchId, BuiltIn, DefWithBodyId, DisciplineId, Expr, ExprId, FunctionArgLoc, Literal, Lookup, - NatureId, NodeId, ParamId, Path, Stmt, StmtId, VarId, + ModuleBodyKind, NatureId, NodeId, ParamId, Path, Stmt, StmtId, VarId, }; use stdx::impl_display; use syntax::ast::AssignOp; @@ -104,8 +104,13 @@ impl BodyValidationDiagnostic { let infere = db.inference_result(def); let ctx = match def { - DefWithBodyId::ModuleId { initial: false, .. } => BodyCtx::AnalogBlock, - DefWithBodyId::ModuleId { initial: true, .. } => BodyCtx::AnalogInitialBlock, + DefWithBodyId::ModuleId { kind: ModuleBodyKind::Analog, .. } => BodyCtx::AnalogBlock, + DefWithBodyId::ModuleId { kind: ModuleBodyKind::AnalogInitial, .. } => { + BodyCtx::AnalogInitialBlock + } + DefWithBodyId::ModuleId { kind: ModuleBodyKind::Procedural, .. } => { + BodyCtx::ProceduralBlock + } DefWithBodyId::FunctionId(_) => BodyCtx::Function, _ => BodyCtx::Const, }; @@ -144,6 +149,7 @@ impl BodyValidationDiagnostic { pub enum BodyCtx { AnalogBlock, AnalogInitialBlock, + ProceduralBlock, Conditional, EventControl, Function, @@ -177,6 +183,7 @@ impl_display! { match BodyCtx{ BodyCtx::AnalogBlock => "analog block"; BodyCtx::AnalogInitialBlock => "analog initial block"; + BodyCtx::ProceduralBlock => "procedural block"; BodyCtx::Conditional => "conditions"; BodyCtx::EventControl => "events"; BodyCtx::Function => "analog functions"; diff --git a/openvaf/mir_interpret/src/lib.rs b/openvaf/mir_interpret/src/lib.rs index c41b13e3..ddb9f47f 100644 --- a/openvaf/mir_interpret/src/lib.rs +++ b/openvaf/mir_interpret/src/lib.rs @@ -13,6 +13,9 @@ pub struct InterpreterState { vals: TiVec, prev_bb: Block, next_inst: Option, + /// Set by a callback (e.g. `$finish`/`$stop`/`$fatal` in the runner) to request + /// early termination of `run()` with the given process exit code. + exit: Option, } impl InterpreterState { @@ -23,6 +26,19 @@ impl InterpreterState { pub fn read>(&self, val: Value) -> T { self.vals[val].into() } + + /// Request early termination of the interpreter with `code`. The first request + /// wins (mirrors `$finish` semantics — later tasks don't override it). + pub fn request_exit(&mut self, code: i32) { + if self.exit.is_none() { + self.exit = Some(code); + } + } + + /// The requested exit code, if any. + pub fn exit_code(&self) -> Option { + self.exit + } } pub type Func<'a> = fn(&mut InterpreterState, &[Value], &[Value], *mut c_void); @@ -56,15 +72,23 @@ impl<'a> Interpreter<'a> { let entry = func.layout.entry_block().expect("Function without entry block can not be interpreted"); - let state = - InterpreterState { vals, prev_bb: entry, next_inst: func.layout.first_inst(entry) }; + let state = InterpreterState { + vals, + prev_bb: entry, + next_inst: func.layout.first_inst(entry), + exit: None, + }; Interpreter { state, calls, func } } pub fn run(&mut self) { while let Some(inst) = self.state.next_inst { - self.eval(inst) + self.eval(inst); + // A callback (e.g. `$finish`/`$fatal`) may request early termination. + if self.state.exit.is_some() { + break; + } } } diff --git a/openvaf/openvaf-driver/src/cli_def.rs b/openvaf/openvaf-driver/src/cli_def.rs index 1aeb7738..d8215f04 100644 --- a/openvaf/openvaf-driver/src/cli_def.rs +++ b/openvaf/openvaf-driver/src/cli_def.rs @@ -38,6 +38,7 @@ pub fn main_command() -> Command { codegen_opts(), interface(), expand(), + run_mode(), dump_json(), input(), ]) @@ -64,6 +65,7 @@ pub const CACHE_DIR: &str = "cache-dir"; pub const OPT_LVL: &str = "opt_lvl"; pub const DEFINE: &str = "define"; pub const PRINT_EXPANSION: &str = "print-expansion"; +pub const RUN: &str = "run"; pub const DUMP_JSON: &str = "dump-json"; pub const ALLOW: &str = "allow"; pub const WARN: &str = "warn"; @@ -289,6 +291,16 @@ fn opt_lvl() -> Arg { .default_value("3").required(false) } +fn run_mode() -> Arg { + flag(RUN, "run") + .help("Run the module's imperative initial/final procedural blocks.") + .long_help( + "Lower the module's standalone `initial`/`final` procedural blocks to MIR and +interpret them, executing system tasks such as $display/$strobe/$finish. +No shared library is produced and no circuit is simulated; this is the +standalone VerilogA runner lane.", + ) +} fn expand() -> Arg { flag(PRINT_EXPANSION, "print-expansion") .help("Abort after preprocessing and print expanded sourcecode.") diff --git a/openvaf/openvaf-driver/src/main.rs b/openvaf/openvaf-driver/src/main.rs index 5c6fbc98..88cb7ff7 100644 --- a/openvaf/openvaf-driver/src/main.rs +++ b/openvaf/openvaf-driver/src/main.rs @@ -7,10 +7,10 @@ use camino::Utf8PathBuf; use clap::ArgMatches; use cli_def::{main_command, INPUT}; use mimalloc::MiMalloc; -use openvaf::{compile, expand, CompilationDestination, CompilationTermination, Opts}; +use openvaf::{compile, expand, run, CompilationDestination, CompilationTermination, Opts}; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; -use crate::cli_def::{DUMP_JSON, PRINT_EXPANSION}; +use crate::cli_def::{DUMP_JSON, PRINT_EXPANSION, RUN}; use crate::cli_process::matches_to_opts; mod cli_def; @@ -61,8 +61,14 @@ pub const DATA_ERROR: i32 = 65; fn wrapped_main(matches: ArgMatches) -> Result { let print_expansion = matches.get_flag(PRINT_EXPANSION); let dump_json_ = matches.get_flag(DUMP_JSON); + let run_mode = matches.get_flag(RUN); let opts = matches_to_opts(matches)?; *ARGS.lock().unwrap() = Some(opts.clone()); + if run_mode { + // Standalone VerilogA runner: interpret the procedural blocks and propagate + // the program's own exit code (e.g. from $finish/$fatal). + return run(&opts); + } if print_expansion { let res = match expand(&opts)? { CompilationTermination::Compiled { .. } => 0, diff --git a/openvaf/openvaf/Cargo.toml b/openvaf/openvaf/Cargo.toml index 7cf64846..2bf90c89 100644 --- a/openvaf/openvaf/Cargo.toml +++ b/openvaf/openvaf/Cargo.toml @@ -28,6 +28,9 @@ llvm-sys-191 = { package = "llvm-sys", version = "191.0.0", optional = true } llvm-sys-201 = { package = "llvm-sys", version = "201.0.1", optional = true } llvm-sys-211 = { package = "llvm-sys", version = "211.0.0", optional = true } mir_llvm = { version = "0.0.0", path = "../mir_llvm", default-features = false } +mir = { version = "0.0.0", path = "../mir" } +hir_lower = { version = "0.0.0", path = "../hir_lower" } +mir_interpret = { version = "0.0.0", path = "../mir_interpret" } hir = { version = "0.0.0", path = "../hir" } target = { version = "0.0.0", path = "../target" } linker = { version = "0.0.0", path = "../linker" } @@ -40,6 +43,8 @@ md5 = "0.8" anyhow = "1" termcolor = "1.2" camino = "1.1.4" +lasso = { version = "0.7", features = ["ahash"] } +typed-index-collections = "3.1" [dev-dependencies] libloading = "0.9" diff --git a/openvaf/openvaf/src/lib.rs b/openvaf/openvaf/src/lib.rs index a2bb15f2..fdc69269 100644 --- a/openvaf/openvaf/src/lib.rs +++ b/openvaf/openvaf/src/lib.rs @@ -28,6 +28,9 @@ pub use target::spec::{get_target_names, Target}; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; mod cache; +mod run; + +pub use run::run; #[derive(Debug, Clone)] pub enum CompilationDestination { diff --git a/openvaf/openvaf/src/run.rs b/openvaf/openvaf/src/run.rs new file mode 100644 index 00000000..33750d19 --- /dev/null +++ b/openvaf/openvaf/src/run.rs @@ -0,0 +1,212 @@ +//! Standalone VerilogA runner (`openvaf-r run `). +//! +//! This is a separate execution lane from the OSDI/DAE compiler: instead of +//! emitting a shared library for a circuit simulator, it lowers a module's +//! imperative `initial`/`final` procedural blocks to MIR and *interprets* them +//! with `mir_interpret`, providing host implementations of the output/control +//! system tasks (`$display`, `$strobe`, `$finish`, `$fatal`, ...). +//! +//! No LLVM, no linking, no simulator is involved. + +use std::ffi::c_void; +use std::io::Write; + +use anyhow::{bail, Context, Result}; +use basedb::diagnostics::ConsoleSink; +use hir::CompilationDB; +use hir_lower::fmt::{DisplayKind, FmtArg, FmtArgKind}; +use hir_lower::{CallBackKind, MirBuilder, PlaceKind, RetFlag}; +use lasso::{Rodeo, Spur}; +use mir::{FuncRef, Value}; +use mir_interpret::{Data, Func, Interpreter, InterpreterState}; +use paths::AbsPathBuf; +use sim_back::collect_modules; +use typed_index_collections::{TiSlice, TiVec}; + +use crate::Opts; + +/// Host context for a single interpreter callback, stored behind the `*mut c_void` +/// the interpreter hands back on every call. +struct CbCtx { + kind: CbCtxKind, + /// Borrowed for the whole interpreter run (the `Rodeo` outlives the run). + literals: *const Rodeo, +} + +enum CbCtxKind { + Print { kind: DisplayKind, arg_tys: Box<[FmtArg]> }, + SetRetFlag(RetFlag), + /// A callback the runner does not implement (e.g. simulator-only tasks). It is + /// ignored at runtime after a one-line warning. + Unsupported(String), +} + +/// Run a module's `initial`/`final` procedural blocks and return the process exit +/// code (0 unless `$finish`/`$fatal`/`$stop` requested otherwise). +pub fn run(opts: &Opts) -> Result { + let input = + opts.input.canonicalize().with_context(|| format!("failed to resolve {}", opts.input))?; + let input = AbsPathBuf::assert(input); + let db = CompilationDB::new_fs(input, &opts.include, &opts.defines, &opts.lints)?; + + let modules = match collect_modules(&db, false, &mut ConsoleSink::new(&db)) { + Some(modules) => modules, + // Front-end emitted fatal diagnostics already. + None => return Ok(1), + }; + let module = match modules.first() { + Some(module) => module, + None => bail!("no module found to run in `{}`", opts.input), + }; + + // Lower only the imperative procedural body to a self-contained MIR function. + let mut literals = Rodeo::new(); + let is_output = |_: PlaceKind| false; + let (func, intern) = MirBuilder::new(&db, module.module, &is_output, &mut std::iter::empty()) + .with_procedural() + .build(&mut literals); + + // Build the interpreter callback table: one host context per MIR callback. + let mut ctxs: Vec> = Vec::with_capacity(intern.callbacks.len()); + let mut calls: TiVec = TiVec::with_capacity(intern.callbacks.len()); + for (_func_ref, kind) in intern.callbacks.iter_enumerated() { + let cb_kind = match kind { + CallBackKind::Print { kind, arg_tys } => { + CbCtxKind::Print { kind: *kind, arg_tys: arg_tys.clone() } + } + CallBackKind::SetRetFlag(flag) => CbCtxKind::SetRetFlag(*flag), + other => CbCtxKind::Unsupported(format!("{other:?}")), + }; + let mut boxed = Box::new(CbCtx { kind: cb_kind, literals: &literals }); + let ptr: *mut c_void = (&mut *boxed as *mut CbCtx).cast(); + ctxs.push(boxed); + calls.push((host_callback as Func, ptr)); + } + + // Provide an entry value for every MIR parameter. The procedural body has no + // circuit inputs, but module variables contribute a `HiddenState` param for their + // value on entry; uninitialised VerilogA variables default to 0. (Module + // `parameter` defaults are not yet evaluated here — a v1 limitation.) + let zero = Data::from(0.0f64); + let args: TiVec = + std::iter::repeat(zero).take(intern.params.len()).collect(); + let mut interpreter = Interpreter::new(&func, calls.as_slice(), args.as_slice()); + interpreter.run(); + + // `ctxs` (and therefore the raw pointers in `calls`) stay alive until here. + drop(ctxs); + Ok(interpreter.state.exit_code().unwrap_or(0)) +} + +/// The single `fn` dispatched for every interpreter callback; behaviour is selected +/// by the [`CbCtx`] behind `data`. +fn host_callback(state: &mut InterpreterState, args: &[Value], _rets: &[Value], data: *mut c_void) { + // SAFETY: `data` points at the `CbCtx` we stored for this `FuncRef`, which lives + // for the whole `run()`; `literals` likewise outlives the interpreter. + let ctx = unsafe { &*(data as *const CbCtx) }; + let literals = unsafe { &*ctx.literals }; + + match &ctx.kind { + CbCtxKind::Print { kind, arg_tys } => { + let fmt = literals.resolve(&state.read::(args[0])); + let rendered = render_format(fmt, &args[1..], arg_tys, state, literals); + match kind { + // Diagnostic-flavoured tasks go to stderr, the rest to stdout. + DisplayKind::Error | DisplayKind::Fatal | DisplayKind::Warn => { + let _ = write!(std::io::stderr(), "{rendered}"); + } + _ => { + let _ = write!(std::io::stdout(), "{rendered}"); + } + } + } + CbCtxKind::SetRetFlag(flag) => { + // `$finish`/`$stop` are normal termination; `$fatal` is an error. + let code = match flag { + RetFlag::Abort => 1, + _ => 0, + }; + state.request_exit(code); + } + CbCtxKind::Unsupported(name) => { + let _ = writeln!( + std::io::stderr(), + "openvaf run: system task/function `{name}` is not supported by the runner; ignored" + ); + } + } +} + +/// Minimal C-`printf`-style renderer. `fmt` has already been normalised to C +/// conversions by `hir_lower::fmt::ins_display`, and `arg_tys`/`args` line up in +/// order with the `%` conversions in `fmt`. +fn render_format( + fmt: &str, + args: &[Value], + arg_tys: &[FmtArg], + state: &InterpreterState, + literals: &Rodeo, +) -> String { + let mut out = String::with_capacity(fmt.len()); + let mut chars = fmt.chars().peekable(); + let mut ai = 0usize; + + while let Some(c) = chars.next() { + if c != '%' { + out.push(c); + continue; + } + // Consume the conversion specifier: flags/width/precision/length up to the + // terminating conversion character. + if chars.peek() == Some(&'%') { + chars.next(); + out.push('%'); + continue; + } + let mut conv = '\0'; + for cc in chars.by_ref() { + if cc.is_ascii_alphabetic() { + conv = cc; + break; + } + } + // A dangling conversion with no matching argument (e.g. the engineering `%c` + // suffix produced for `%r`): emit nothing. + if ai >= arg_tys.len() { + continue; + } + let arg = args[ai]; + let ty = &arg_tys[ai]; + ai += 1; + out.push_str(&render_arg(conv, ty, arg, state, literals)); + } + out +} + +fn render_arg( + conv: char, + ty: &FmtArg, + arg: Value, + state: &InterpreterState, + literals: &Rodeo, +) -> String { + if ty.kind == FmtArgKind::Binary { + return format!("{:b}", state.read::(arg)); + } + match conv { + 'd' | 'i' => format!("{}", state.read::(arg)), + 'x' => format!("{:x}", state.read::(arg)), + 'X' => format!("{:X}", state.read::(arg)), + 'o' => format!("{:o}", state.read::(arg)), + 'c' => char::from_u32(state.read::(arg) as u32).map_or(String::new(), String::from), + 's' => literals.resolve(&state.read::(arg)).to_owned(), + 'e' | 'E' => format!("{:e}", state.read::(arg)), + 'f' | 'F' | 'g' | 'G' => format!("{}", state.read::(arg)), + // Best-effort fallback driven by the inferred argument type. + _ => match ty.ty { + hir::Type::Integer => format!("{}", state.read::(arg)), + hir::Type::String => literals.resolve(&state.read::(arg)).to_owned(), + _ => format!("{}", state.read::(arg)), + }, + } +} diff --git a/openvaf/parser/src/grammar/items/module.rs b/openvaf/parser/src/grammar/items/module.rs index 3ca4e230..31ef0294 100644 --- a/openvaf/parser/src/grammar/items/module.rs +++ b/openvaf/parser/src/grammar/items/module.rs @@ -5,6 +5,7 @@ const MODULE_ITEM_RECOVERY: TokenSet = DIRECTION_TS.union(TokenSet::new(&[ NET_TYPE, ANALOG_KW, INITIAL_KW, + FINAL_KW, BRANCH_KW, STRING_KW, REAL_KW, @@ -116,6 +117,13 @@ fn module_items(p: &mut Parser) { stmt_with_attrs(p); m.complete(p, ANALOG_BEHAVIOUR); } + // Standalone (non-analog) procedural blocks: `initial ` / `final `. + // These are imperative blocks executed by the standalone VerilogA runner. + INITIAL_KW | FINAL_KW => { + p.bump_any(); + stmt_with_attrs(p); + m.complete(p, PROCEDURAL_BLOCK); + } NET_TYPE => { net_decl::(p, m); } @@ -147,6 +155,7 @@ fn module_items(p: &mut Parser) { PORT_DECL, NET_DECL, ANALOG_BEHAVIOUR, + PROCEDURAL_BLOCK, ]); p.error(err); p.bump_any(); diff --git a/openvaf/syntax/src/ast/generated/nodes.rs b/openvaf/syntax/src/ast/generated/nodes.rs index cc2f471d..02478dd5 100644 --- a/openvaf/syntax/src/ast/generated/nodes.rs +++ b/openvaf/syntax/src/ast/generated/nodes.rs @@ -416,6 +416,16 @@ impl AnalogBehaviour { pub fn stmt(&self) -> Option { support::child(&self.syntax) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ProceduralBlock { + pub(crate) syntax: SyntaxNode, +} +impl ast::AttrsOwner for ProceduralBlock {} +impl ProceduralBlock { + pub fn initial_token(&self) -> Option { support::token(&self.syntax, T![initial]) } + pub fn final_token(&self) -> Option { support::token(&self.syntax, T![final]) } + pub fn stmt(&self) -> Option { support::child(&self.syntax) } +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Function { pub(crate) syntax: SyntaxNode, } @@ -584,6 +594,7 @@ pub enum ModuleItem { BodyPortDecl(BodyPortDecl), NetDecl(NetDecl), AnalogBehaviour(AnalogBehaviour), + ProceduralBlock(ProceduralBlock), Function(Function), BranchDecl(BranchDecl), VarDecl(VarDecl), @@ -1059,6 +1070,17 @@ impl AstNode for AnalogBehaviour { } fn syntax(&self) -> &SyntaxNode { &self.syntax } } +impl AstNode for ProceduralBlock { + fn can_cast(kind: SyntaxKind) -> bool { kind == PROCEDURAL_BLOCK } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { &self.syntax } +} impl AstNode for Function { fn can_cast(kind: SyntaxKind) -> bool { kind == FUNCTION } fn cast(syntax: SyntaxNode) -> Option { @@ -1409,6 +1431,9 @@ impl From for ModuleItem { impl From for ModuleItem { fn from(node: AnalogBehaviour) -> ModuleItem { ModuleItem::AnalogBehaviour(node) } } +impl From for ModuleItem { + fn from(node: ProceduralBlock) -> ModuleItem { ModuleItem::ProceduralBlock(node) } +} impl From for ModuleItem { fn from(node: Function) -> ModuleItem { ModuleItem::Function(node) } } @@ -1427,8 +1452,8 @@ impl From for ModuleItem { impl AstNode for ModuleItem { fn can_cast(kind: SyntaxKind) -> bool { match kind { - BODY_PORT_DECL | NET_DECL | ANALOG_BEHAVIOUR | FUNCTION | BRANCH_DECL | VAR_DECL - | PARAM_DECL | ALIAS_PARAM => true, + BODY_PORT_DECL | NET_DECL | ANALOG_BEHAVIOUR | PROCEDURAL_BLOCK | FUNCTION + | BRANCH_DECL | VAR_DECL | PARAM_DECL | ALIAS_PARAM => true, _ => false, } } @@ -1437,6 +1462,7 @@ impl AstNode for ModuleItem { BODY_PORT_DECL => ModuleItem::BodyPortDecl(BodyPortDecl { syntax }), NET_DECL => ModuleItem::NetDecl(NetDecl { syntax }), ANALOG_BEHAVIOUR => ModuleItem::AnalogBehaviour(AnalogBehaviour { syntax }), + PROCEDURAL_BLOCK => ModuleItem::ProceduralBlock(ProceduralBlock { syntax }), FUNCTION => ModuleItem::Function(Function { syntax }), BRANCH_DECL => ModuleItem::BranchDecl(BranchDecl { syntax }), VAR_DECL => ModuleItem::VarDecl(VarDecl { syntax }), @@ -1451,6 +1477,7 @@ impl AstNode for ModuleItem { ModuleItem::BodyPortDecl(it) => &it.syntax, ModuleItem::NetDecl(it) => &it.syntax, ModuleItem::AnalogBehaviour(it) => &it.syntax, + ModuleItem::ProceduralBlock(it) => &it.syntax, ModuleItem::Function(it) => &it.syntax, ModuleItem::BranchDecl(it) => &it.syntax, ModuleItem::VarDecl(it) => &it.syntax, @@ -1802,6 +1829,11 @@ impl std::fmt::Display for AnalogBehaviour { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for ProceduralBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for Function { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/openvaf/syntax/src/ast/node_ext.rs b/openvaf/syntax/src/ast/node_ext.rs index 7176af57..a3dbcf2f 100644 --- a/openvaf/syntax/src/ast/node_ext.rs +++ b/openvaf/syntax/src/ast/node_ext.rs @@ -7,7 +7,7 @@ use stdx::impl_debug; use super::{ AnalogBehaviour, ArgListOwner, Assign, AstChildTokens, AstChildren, Constraint, EventStmt, - Expr, ForStmt, Function, ModulePortKind, Path, PortFlow, Range, Stmt, StrLit, + Expr, ForStmt, Function, ModulePortKind, Path, PortFlow, ProceduralBlock, Range, Stmt, StrLit, }; use crate::ast::{self, support, AstNode}; use crate::SyntaxKind::{IDENT, ROOT_KW}; @@ -163,6 +163,21 @@ impl ast::ModuleDecl { pub fn body_ports(&self) -> AstChildren { support::children(self.syntax()) } + + /// Statements of standalone `initial` procedural blocks (the imperative runner + /// lane), in source order. Distinct from `analog initial` blocks. + pub fn initial_behaviour(&self) -> impl Iterator { + support::children::(self.syntax()) + .filter(|it| it.initial_token().is_some()) + .filter_map(|it| it.stmt()) + } + + /// Statements of standalone `final` procedural blocks, in source order. + pub fn final_behaviour(&self) -> impl Iterator { + support::children::(self.syntax()) + .filter(|it| it.final_token().is_some()) + .filter_map(|it| it.stmt()) + } } impl ast::ModulePort { diff --git a/openvaf/syntax/veriloga.ungram b/openvaf/syntax/veriloga.ungram index 0fccf808..5b2c7cb8 100644 --- a/openvaf/syntax/veriloga.ungram +++ b/openvaf/syntax/veriloga.ungram @@ -190,6 +190,7 @@ ModuleItem = BodyPortDecl | NetDecl | AnalogBehaviour +| ProceduralBlock | Function | BranchDecl | VarDecl @@ -203,6 +204,9 @@ ModulePortKind = PortDecl| Name AnalogBehaviour = AttrList* 'analog' 'initial'? Stmt +ProceduralBlock = + AttrList* ('initial' | 'final') Stmt + VarDecl = AttrList* Type (Var (',' Var)*) ';' diff --git a/openvaf/tokens/src/parser/generated.rs b/openvaf/tokens/src/parser/generated.rs index 0a59b3ad..c6aacb7f 100644 --- a/openvaf/tokens/src/parser/generated.rs +++ b/openvaf/tokens/src/parser/generated.rs @@ -89,6 +89,7 @@ pub enum SyntaxKind { INITIAL_STEP_KW, INITIAL_KW, FINAL_STEP_KW, + FINAL_KW, ALIASPARAM_KW, INT_NUMBER, STD_REAL_NUMBER, @@ -101,6 +102,7 @@ pub enum SyntaxKind { WHITESPACE, COMMENT, ANALOG_BEHAVIOUR, + PROCEDURAL_BLOCK, ARG, ARG_LIST, ARRAY_EXPR, @@ -168,7 +170,7 @@ impl SyntaxKind { | ENDMODULE_KW | ENDNATURE_KW | EXCLUDE_KW | FOR_KW | FROM_KW | FUNCTION_KW | IF_KW | INF_KW | INOUT_KW | INPUT_KW | INTEGER_KW | MODULE_KW | NATURE_KW | OUTPUT_KW | PARAMETER_KW | LOCALPARAM_KW | REAL_KW | STRING_KW | WHILE_KW | ROOT_KW - | INITIAL_STEP_KW | INITIAL_KW | FINAL_STEP_KW | ALIASPARAM_KW => true, + | INITIAL_STEP_KW | INITIAL_KW | FINAL_STEP_KW | FINAL_KW | ALIASPARAM_KW => true, _ => false, } } @@ -225,6 +227,7 @@ impl SyntaxKind { "initial_step" => INITIAL_STEP_KW, "initial" => INITIAL_KW, "final_step" => FINAL_STEP_KW, + "final" => FINAL_KW, "aliasparam" => ALIASPARAM_KW, "reg" | "wreal" | "wire" | "uwire" | "wand" | "wor" | "ground" => NET_TYPE, _ => return None, @@ -349,6 +352,7 @@ impl std::fmt::Display for SyntaxKind { Self::INITIAL_STEP_KW => "'initial_step'", Self::INITIAL_KW => "'initial'", Self::FINAL_STEP_KW => "'final_step'", + Self::FINAL_KW => "'final'", Self::ALIASPARAM_KW => "'aliasparam'", Self::INT_NUMBER => "integer", Self::STD_REAL_NUMBER | Self::SI_REAL_NUMBER => "real number", @@ -368,4 +372,4 @@ impl std::fmt::Display for SyntaxKind { } } #[macro_export] -macro_rules ! T { [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; ["_"] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<<] => { $ crate :: SyntaxKind :: ASHL } ; [>>>] => { $ crate :: SyntaxKind :: ASHR } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; ["(*"] => { $ crate :: SyntaxKind :: L_ATTR_PAREN } ; ["*)"] => { $ crate :: SyntaxKind :: R_ATTR_PAREN } ; ["'{"] => { $ crate :: SyntaxKind :: ARR_START } ; [<+] => { $ crate :: SyntaxKind :: CONTR } ; [**] => { $ crate :: SyntaxKind :: POW } ; [~^] => { $ crate :: SyntaxKind :: L_NXOR } ; [^~] => { $ crate :: SyntaxKind :: R_NXOR } ; [analog] => { $ crate :: SyntaxKind :: ANALOG_KW } ; [begin] => { $ crate :: SyntaxKind :: BEGIN_KW } ; [branch] => { $ crate :: SyntaxKind :: BRANCH_KW } ; [case] => { $ crate :: SyntaxKind :: CASE_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [disable] => { $ crate :: SyntaxKind :: DISABLE_KW } ; [discipline] => { $ crate :: SyntaxKind :: DISCIPLINE_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [end] => { $ crate :: SyntaxKind :: END_KW } ; [endcase] => { $ crate :: SyntaxKind :: ENDCASE_KW } ; [enddiscipline] => { $ crate :: SyntaxKind :: ENDDISCIPLINE_KW } ; [endfunction] => { $ crate :: SyntaxKind :: ENDFUNCTION_KW } ; [endmodule] => { $ crate :: SyntaxKind :: ENDMODULE_KW } ; [endnature] => { $ crate :: SyntaxKind :: ENDNATURE_KW } ; [exclude] => { $ crate :: SyntaxKind :: EXCLUDE_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [from] => { $ crate :: SyntaxKind :: FROM_KW } ; [function] => { $ crate :: SyntaxKind :: FUNCTION_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [inf] => { $ crate :: SyntaxKind :: INF_KW } ; [inout] => { $ crate :: SyntaxKind :: INOUT_KW } ; [input] => { $ crate :: SyntaxKind :: INPUT_KW } ; [integer] => { $ crate :: SyntaxKind :: INTEGER_KW } ; [module] => { $ crate :: SyntaxKind :: MODULE_KW } ; [nature] => { $ crate :: SyntaxKind :: NATURE_KW } ; [output] => { $ crate :: SyntaxKind :: OUTPUT_KW } ; [parameter] => { $ crate :: SyntaxKind :: PARAMETER_KW } ; [localparam] => { $ crate :: SyntaxKind :: LOCALPARAM_KW } ; [real] => { $ crate :: SyntaxKind :: REAL_KW } ; [string] => { $ crate :: SyntaxKind :: STRING_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [root] => { $ crate :: SyntaxKind :: ROOT_KW } ; [initial_step] => { $ crate :: SyntaxKind :: INITIAL_STEP_KW } ; [initial] => { $ crate :: SyntaxKind :: INITIAL_KW } ; [final_step] => { $ crate :: SyntaxKind :: FINAL_STEP_KW } ; [aliasparam] => { $ crate :: SyntaxKind :: ALIASPARAM_KW } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [net_type] => { $ crate :: SyntaxKind :: NET_TYPE } ; [sysfun] => { $ crate :: SyntaxKind :: SYSFUN } ; } +macro_rules ! T { [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; ["_"] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<<] => { $ crate :: SyntaxKind :: ASHL } ; [>>>] => { $ crate :: SyntaxKind :: ASHR } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; ["(*"] => { $ crate :: SyntaxKind :: L_ATTR_PAREN } ; ["*)"] => { $ crate :: SyntaxKind :: R_ATTR_PAREN } ; ["'{"] => { $ crate :: SyntaxKind :: ARR_START } ; [<+] => { $ crate :: SyntaxKind :: CONTR } ; [**] => { $ crate :: SyntaxKind :: POW } ; [~^] => { $ crate :: SyntaxKind :: L_NXOR } ; [^~] => { $ crate :: SyntaxKind :: R_NXOR } ; [analog] => { $ crate :: SyntaxKind :: ANALOG_KW } ; [begin] => { $ crate :: SyntaxKind :: BEGIN_KW } ; [branch] => { $ crate :: SyntaxKind :: BRANCH_KW } ; [case] => { $ crate :: SyntaxKind :: CASE_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [disable] => { $ crate :: SyntaxKind :: DISABLE_KW } ; [discipline] => { $ crate :: SyntaxKind :: DISCIPLINE_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [end] => { $ crate :: SyntaxKind :: END_KW } ; [endcase] => { $ crate :: SyntaxKind :: ENDCASE_KW } ; [enddiscipline] => { $ crate :: SyntaxKind :: ENDDISCIPLINE_KW } ; [endfunction] => { $ crate :: SyntaxKind :: ENDFUNCTION_KW } ; [endmodule] => { $ crate :: SyntaxKind :: ENDMODULE_KW } ; [endnature] => { $ crate :: SyntaxKind :: ENDNATURE_KW } ; [exclude] => { $ crate :: SyntaxKind :: EXCLUDE_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [from] => { $ crate :: SyntaxKind :: FROM_KW } ; [function] => { $ crate :: SyntaxKind :: FUNCTION_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [inf] => { $ crate :: SyntaxKind :: INF_KW } ; [inout] => { $ crate :: SyntaxKind :: INOUT_KW } ; [input] => { $ crate :: SyntaxKind :: INPUT_KW } ; [integer] => { $ crate :: SyntaxKind :: INTEGER_KW } ; [module] => { $ crate :: SyntaxKind :: MODULE_KW } ; [nature] => { $ crate :: SyntaxKind :: NATURE_KW } ; [output] => { $ crate :: SyntaxKind :: OUTPUT_KW } ; [parameter] => { $ crate :: SyntaxKind :: PARAMETER_KW } ; [localparam] => { $ crate :: SyntaxKind :: LOCALPARAM_KW } ; [real] => { $ crate :: SyntaxKind :: REAL_KW } ; [string] => { $ crate :: SyntaxKind :: STRING_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [root] => { $ crate :: SyntaxKind :: ROOT_KW } ; [initial_step] => { $ crate :: SyntaxKind :: INITIAL_STEP_KW } ; [initial] => { $ crate :: SyntaxKind :: INITIAL_KW } ; [final_step] => { $ crate :: SyntaxKind :: FINAL_STEP_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [aliasparam] => { $ crate :: SyntaxKind :: ALIASPARAM_KW } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [net_type] => { $ crate :: SyntaxKind :: NET_TYPE } ; [sysfun] => { $ crate :: SyntaxKind :: SYSFUN } ; } diff --git a/sourcegen/src/ast/src.rs b/sourcegen/src/ast/src.rs index c1880e2c..8c69b3cd 100644 --- a/sourcegen/src/ast/src.rs +++ b/sourcegen/src/ast/src.rs @@ -94,12 +94,14 @@ pub(crate) const KINDS_SRC: KindsSrc = KindsSrc { "initial_step", "initial", "final_step", + "final", "aliasparam", ], literals: &["INT_NUMBER", "STD_REAL_NUMBER", "SI_REAL_NUMBER", "STR_LIT"], tokens: &["ERROR", "IDENT", "SYSFUN", "NET_TYPE", "WHITESPACE", "COMMENT"], nodes: &[ "ANALOG_BEHAVIOUR", + "PROCEDURAL_BLOCK", "ARG", "ARG_LIST", "ARRAY_EXPR", From 51e8654deecfdc9e2f180560228748d7da2d09e8 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 09:58:13 +0200 Subject: [PATCH 02/19] runner: also execute the analog body so @(initial_step) $strobe prints The canonical VerilogA 'hello world' puts $strobe inside the analog block guarded by @(initial_step): module hello_world; analog begin @(initial_step) $strobe("Hello World!"); end endmodule @(initial_step) is currently lowered unconditionally (stmt.rs EventControl), so the $strobe is always present in the analog MIR. Have `openvaf-r --run` lower and interpret the analog body (then the procedural initial/final body), sharing one callback table via interpret_body(). A $finish/$fatal in the analog body stops before the procedural body runs. Now both the analog @(initial_step) form and the standalone `initial` form print under --run; device models with ports/contributions also run (the contributions are harmless writes). Known limitation unchanged: module `parameter` defaults are not evaluated (read as 0). --- openvaf/openvaf/src/run.rs | 65 +++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/openvaf/openvaf/src/run.rs b/openvaf/openvaf/src/run.rs index 33750d19..5b445226 100644 --- a/openvaf/openvaf/src/run.rs +++ b/openvaf/openvaf/src/run.rs @@ -15,7 +15,7 @@ use anyhow::{bail, Context, Result}; use basedb::diagnostics::ConsoleSink; use hir::CompilationDB; use hir_lower::fmt::{DisplayKind, FmtArg, FmtArgKind}; -use hir_lower::{CallBackKind, MirBuilder, PlaceKind, RetFlag}; +use hir_lower::{CallBackKind, HirInterner, MirBuilder, PlaceKind, RetFlag}; use lasso::{Rodeo, Spur}; use mir::{FuncRef, Value}; use mir_interpret::{Data, Func, Interpreter, InterpreterState}; @@ -41,8 +41,13 @@ enum CbCtxKind { Unsupported(String), } -/// Run a module's `initial`/`final` procedural blocks and return the process exit -/// code (0 unless `$finish`/`$fatal`/`$stop` requested otherwise). +/// Run a module's behaviour and return the process exit code (0 unless +/// `$finish`/`$fatal`/`$stop` requested otherwise). +/// +/// Two bodies are executed, in order: the `analog` behaviour (so the idiomatic +/// `analog begin @(initial_step) $strobe(...) end` form prints — `@(initial_step)` +/// is currently lowered unconditionally), then any standalone `initial`/`final` +/// procedural blocks. A `$finish`/`$fatal` in the first stops before the second. pub fn run(opts: &Opts) -> Result { let input = opts.input.canonicalize().with_context(|| format!("failed to resolve {}", opts.input))?; @@ -59,16 +64,41 @@ pub fn run(opts: &Opts) -> Result { None => bail!("no module found to run in `{}`", opts.input), }; - // Lower only the imperative procedural body to a self-contained MIR function. let mut literals = Rodeo::new(); let is_output = |_: PlaceKind| false; - let (func, intern) = MirBuilder::new(&db, module.module, &is_output, &mut std::iter::empty()) - .with_procedural() - .build(&mut literals); + // Lower both behavioural bodies up front (both extend `literals`). + let (analog_func, analog_intern) = + MirBuilder::new(&db, module.module, &is_output, &mut std::iter::empty()) + .build(&mut literals); + let (proc_func, proc_intern) = + MirBuilder::new(&db, module.module, &is_output, &mut std::iter::empty()) + .with_procedural() + .build(&mut literals); + + // analog behaviour first, then procedural blocks; an early-exit request from the + // first body skips the second. + if let Some(code) = interpret_body(&analog_func, &analog_intern, &literals) { + return Ok(code); + } + if let Some(code) = interpret_body(&proc_func, &proc_intern, &literals) { + return Ok(code); + } + Ok(0) +} + +/// Interpret one lowered MIR body, wiring its callbacks to host implementations. +/// Returns `Some(code)` if the body requested early termination (`$finish`/`$fatal`), +/// `None` otherwise. +fn interpret_body( + func: &mir::Function, + intern: &hir_lower::HirInterner, + literals: &Rodeo, +) -> Option { // Build the interpreter callback table: one host context per MIR callback. let mut ctxs: Vec> = Vec::with_capacity(intern.callbacks.len()); - let mut calls: TiVec = TiVec::with_capacity(intern.callbacks.len()); + let mut calls: TiVec = + TiVec::with_capacity(intern.callbacks.len()); for (_func_ref, kind) in intern.callbacks.iter_enumerated() { let cb_kind = match kind { CallBackKind::Print { kind, arg_tys } => { @@ -77,25 +107,24 @@ pub fn run(opts: &Opts) -> Result { CallBackKind::SetRetFlag(flag) => CbCtxKind::SetRetFlag(*flag), other => CbCtxKind::Unsupported(format!("{other:?}")), }; - let mut boxed = Box::new(CbCtx { kind: cb_kind, literals: &literals }); + let mut boxed = Box::new(CbCtx { kind: cb_kind, literals }); let ptr: *mut c_void = (&mut *boxed as *mut CbCtx).cast(); ctxs.push(boxed); calls.push((host_callback as Func, ptr)); } - // Provide an entry value for every MIR parameter. The procedural body has no - // circuit inputs, but module variables contribute a `HiddenState` param for their - // value on entry; uninitialised VerilogA variables default to 0. (Module - // `parameter` defaults are not yet evaluated here — a v1 limitation.) + // Provide an entry value for every MIR parameter. The runner has no circuit, so + // all inputs (node voltages, temperature, module-variable entry state, ...) default + // to 0. (Module `parameter` defaults are not yet evaluated here — a v1 limitation.) let zero = Data::from(0.0f64); - let args: TiVec = - std::iter::repeat(zero).take(intern.params.len()).collect(); - let mut interpreter = Interpreter::new(&func, calls.as_slice(), args.as_slice()); + let args: TiVec = std::iter::repeat(zero).take(intern.params.len()).collect(); + let mut interpreter = Interpreter::new(func, calls.as_slice(), args.as_slice()); interpreter.run(); - // `ctxs` (and therefore the raw pointers in `calls`) stay alive until here. + // `ctxs` (and the raw pointers in `calls`) stay alive until here. + let code = interpreter.state.exit_code(); drop(ctxs); - Ok(interpreter.state.exit_code().unwrap_or(0)) + code } /// The single `fn` dispatched for every interpreter callback; behaviour is selected From 7b40cdcbdebdbd7ee916978537306fbff24952a4 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 10:15:53 +0200 Subject: [PATCH 03/19] Point issue/crash-report URLs at the maintained fork (arpadbuermen/OpenVAF) The unsupported-function note and the crash-report message linked to the unmaintained upstream github.com/pascalkuthe/openvaf; update both to the active fork. --- openvaf/hir_ty/src/validation.rs | 2 +- openvaf/openvaf-driver/src/crash_report.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openvaf/hir_ty/src/validation.rs b/openvaf/hir_ty/src/validation.rs index 37410907..c2ff481a 100644 --- a/openvaf/hir_ty/src/validation.rs +++ b/openvaf/hir_ty/src/validation.rs @@ -392,7 +392,7 @@ impl Diagnostic for BodyValidationDiagnosticWrapped<'_> { }]); res = res.with_notes(vec![ - "This function is part of the Verilog-A standard but currently not implemented by OpenVAF\nIf this function is important to your application, create an issue:\nhttps://github.com/pascalkuthe/openvaf/issues/new".to_owned(), + "This function is part of the Verilog-A standard but currently not implemented by OpenVAF\nIf this function is important to your application, create an issue:\nhttps://github.com/arpadbuermen/OpenVAF/issues/new".to_owned(), ]); res diff --git a/openvaf/openvaf-driver/src/crash_report.rs b/openvaf/openvaf-driver/src/crash_report.rs index 5dc56b1c..0b42a6a2 100644 --- a/openvaf/openvaf-driver/src/crash_report.rs +++ b/openvaf/openvaf-driver/src/crash_report.rs @@ -78,8 +78,8 @@ pub fn print_msg>(file_path: Option

) -> io::Result<()> { writeln!( stderr, "\nA log file has been generated at \"{}\". -To help us fix the problem, please open an issue at https://github.com/pascalkuthe/OpenVAF/ -or send an email to pascal.kuthe@semimod.de and attach the log file. +To help us fix the problem, please open an issue at https://github.com/arpadbuermen/OpenVAF/ +and attach the log file. If possible please also attach the source file that OpenVAF was compiling.", match file_path { Some(fp) => format!("{}", fp.as_ref().display()), From fee06b57ca8e6778445a3e36a76b2b9fdc7d906f Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 10:16:03 +0200 Subject: [PATCH 04/19] Implement digital gate features: @(cross/timer) events + transition() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the SimetriX digital AND-gate example compile and run. - Parser: `@(...)` now accepts a monitored-event call expression (`@(cross(expr,dir,tol))`, `@(timer(...))`) in addition to initial_step/final_step. The HIR already degrades an unrecognised event to running its guarded body (collect_event_stmt), matching the existing EventControl behaviour. - transition(): removed from the UNSUPPORTED builtin list so its existing stub lowering is used; the stub now casts its Integer first argument to Real to match the operator's Real return type (fixes an LLVM type mismatch when the result is contributed to a real branch). Semantics (level 1): the event condition is not used for scheduling — the guarded body is always evaluated — and transition() is instantaneous (delay/rise/fall ignored). True time-domain event detection and transition filtering would require simulator-side bound-step/event support. Verified: the AND gate compiles to OSDI and runs via --run with correct combinational logic; full workspace tests 354 passed. --- openvaf/hir_def/src/builtin.rs | 1 - openvaf/hir_lower/src/expr.rs | 9 +++++++-- openvaf/parser/src/grammar/stmts.rs | 30 +++++++++++++++++------------ sourcegen/src/hir_builtins.rs | 3 +-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/openvaf/hir_def/src/builtin.rs b/openvaf/hir_def/src/builtin.rs index 5729cfb5..1c3b6ece 100644 --- a/openvaf/hir_def/src/builtin.rs +++ b/openvaf/hir_def/src/builtin.rs @@ -189,7 +189,6 @@ impl BuiltIn { | BuiltIn::laplace_zp | BuiltIn::last_crossing | BuiltIn::slew - | BuiltIn::transition | BuiltIn::fclose | BuiltIn::fopen | BuiltIn::fdisplay diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index 92f43fe0..cdaa0b76 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -716,9 +716,14 @@ impl BodyLoweringCtx<'_, '_, '_> { res }*/ - BuiltIn::slew | BuiltIn::transition | BuiltIn::limit | BuiltIn::absdelay => { - self.lower_expr(args[0]) + BuiltIn::transition => { + // Instantaneous approximation: ignore delay/rise/fall and return the + // target value. `transition`'s first argument is typed Integer but the + // operator returns Real, so cast to keep the MIR well-typed. + let val = self.lower_expr(args[0]); + self.ctx.insert_cast(val, &Type::Integer, &Type::Real) } + BuiltIn::slew | BuiltIn::limit | BuiltIn::absdelay => self.lower_expr(args[0]), _ => unreachable!(), } diff --git a/openvaf/parser/src/grammar/stmts.rs b/openvaf/parser/src/grammar/stmts.rs index 1c569b0f..8e6349df 100644 --- a/openvaf/parser/src/grammar/stmts.rs +++ b/openvaf/parser/src/grammar/stmts.rs @@ -60,21 +60,27 @@ fn assign_or_expr(p: &mut Parser) -> bool { fn event_stmt(p: &mut Parser, m: Marker) { p.bump(T![@]); p.expect(T!['(']); - p.expect_ts_r( - TokenSet::new(&[INITIAL_STEP_KW, FINAL_STEP_KW]), - TokenSet::new(&[T![')'], T!['(']]), - ); - if p.eat(T!['(']) { - while !p.at_ts(TokenSet::new(&[T![')'], T![begin], ENDMODULE_KW])) { - let mut succ = p.expect(STR_LIT); - if !p.at(T![')']) { - succ |= p.expect_with(T![,], &[T![')'], T![,]]); - if !succ { - p.bump_any() + if p.at_ts(TokenSet::new(&[INITIAL_STEP_KW, FINAL_STEP_KW])) { + // Global events: `@(initial_step)` / `@(final_step)` with optional sim phases. + p.bump_any(); + if p.eat(T!['(']) { + while !p.at_ts(TokenSet::new(&[T![')'], T![begin], ENDMODULE_KW])) { + let mut succ = p.expect(STR_LIT); + if !p.at(T![')']) { + succ |= p.expect_with(T![,], &[T![')'], T![,]]); + if !succ { + p.bump_any() + } } } + p.eat(T![')']); } - p.eat(T![')']); + } else { + // Monitored events: `@(cross(expr, dir, tol))`, `@(timer(...))`, ... parsed as + // a call expression. Currently the event condition is not used for scheduling + // (the guarded body is always evaluated, see hir_lower EventControl), so we + // only need to accept and consume it. + expr(p); } p.expect(T![')']); stmt_with_attrs(p); diff --git a/sourcegen/src/hir_builtins.rs b/sourcegen/src/hir_builtins.rs index 79e5637b..4ca02b3f 100644 --- a/sourcegen/src/hir_builtins.rs +++ b/sourcegen/src/hir_builtins.rs @@ -25,7 +25,7 @@ const ANALOG_OPERATORS: [&str; 17] = [ "transition", ]; -const UNSUPPORTED: [&str; 50] = [ +const UNSUPPORTED: [&str; 49] = [ "simprobe", "analog_node_alias", "analog_port_alias", @@ -41,7 +41,6 @@ const UNSUPPORTED: [&str; 50] = [ "laplace_zp", "last_crossing", "slew", - "transition", "fclose", "fopen", "fdisplay", From 3b32c8dc7494a6b2f8c194f7df090dc28052ebc0 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 11:04:51 +0200 Subject: [PATCH 05/19] parser: don't panic on unsupported module-port syntax module_ports() called port_decl() whenever a port name wasn't found, but port_decl() asserts a direction keyword is present (bump_ts(DIRECTION_TS)). Input like the vectored-node port `module m(inode[0], inode[n])` (not yet supported) hit neither branch and panicked the parser; the recovery path could also leave a childless MODULE_PORT that later panicked ModulePort::kind(). Only take the port_decl path when actually at a direction/attribute; otherwise abandon the empty port node, emit an 'expected port name or direction' error and recover. Vectored-node models now fail with diagnostics instead of crashing. Verified: directioned and directionless ports still parse; full tests 354 passed. --- openvaf/parser/src/grammar/items/module.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openvaf/parser/src/grammar/items/module.rs b/openvaf/parser/src/grammar/items/module.rs index 31ef0294..f08da921 100644 --- a/openvaf/parser/src/grammar/items/module.rs +++ b/openvaf/parser/src/grammar/items/module.rs @@ -40,12 +40,22 @@ const MODULE_PORTS_RECOVERY: TokenSet = TokenSet::new(&[T![;], T![')'], ENDMODUL fn module_ports(p: &mut Parser) { while !p.at_ts(MODULE_PORTS_RECOVERY) { let m = p.start(); - if !eat_name(p) { - let m = p.start(); + if eat_name(p) { + m.complete(p, MODULE_PORT); + } else if p.at_ts(DIRECTION_TS) || p.at(T!["(*"]) { + let inner = p.start(); attrs(p, MODULE_PORTS_RECOVERY.union(DIRECTION_TS)); - port_decl::(p, m) + port_decl::(p, inner); + m.complete(p, MODULE_PORT); + } else { + // Neither a port name nor a port direction. This happens e.g. for the + // currently unsupported vectored-node port syntax `inode[0]`. Abandon the + // (empty) port node, report an error and recover instead of asserting + // inside `port_decl` or producing a childless MODULE_PORT. + m.abandon(p); + let err = p.unexpected_tokens_msg(vec![IDENT, INPUT_KW, OUTPUT_KW, INOUT_KW]); + p.err_recover(err, MODULE_PORTS_RECOVERY.union(TokenSet::unique(T![,]))); } - m.complete(p, MODULE_PORT); if !p.at(T![')']) { p.expect_with(T![,], &[T![,], T![')']]); } From c73a4eedaa172b2dc83ec0f949986208805d8060 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 11:41:03 +0200 Subject: [PATCH 06/19] Arrays/indexing: grammar + HIR plumbing (foundation for scorecard 9/9) First increment toward supporting the Butterworth (#8) and RC-ladder (#9) SimetriX examples. Adds the syntactic + HIR scaffolding for array/bus indexing; semantic lowering (const-eval, array places, laplace_nd) follows. - Grammar: IndexExpr (base[index]) and a Dimension node for array-variable declarations (real x[a:b]); re-enable array literals and accept the plain Verilog-A {..} spelling in addition to '{..}. Regenerated AST/tokens. - Parser: postfix [index] on path expressions; optional [msb:lsb] dimension on var declarations; working array_expr. - AST helpers: IndexExpr::base/index and Dimension::msb/lsb (expr_ext.rs), since the generator can't disambiguate same-type children. - HIR: Expr::Index{base,index} threaded through hir_def (collect, walk, pretty) and hir_ty inference (array element type). Butterworth's den[order:0] / den[k] / {1.0} now parse; remaining errors are semantic (array-element lvalue + laplace_nd lowering), the next increment. Full workspace tests still 354 passed (core parsing/inference unaffected). --- openvaf/hir_def/src/body/lower.rs | 6 +++ openvaf/hir_def/src/body/pretty.rs | 6 +++ openvaf/hir_def/src/expr.rs | 9 ++++ openvaf/hir_ty/src/inference.rs | 10 ++++ openvaf/parser/src/grammar/expressions.rs | 50 +++++++++++------- openvaf/parser/src/grammar/items.rs | 12 ++++- openvaf/syntax/src/ast/expr_ext.rs | 24 +++++++++ openvaf/syntax/src/ast/generated/nodes.rs | 62 ++++++++++++++++++++++- openvaf/syntax/veriloga.ungram | 9 +++- openvaf/tokens/src/parser/generated.rs | 2 + sourcegen/src/ast/src.rs | 2 + 11 files changed, 169 insertions(+), 23 deletions(-) diff --git a/openvaf/hir_def/src/body/lower.rs b/openvaf/hir_def/src/body/lower.rs index 1bacc01a..49567519 100644 --- a/openvaf/hir_def/src/body/lower.rs +++ b/openvaf/hir_def/src/body/lower.rs @@ -77,6 +77,12 @@ impl LowerCtx<'_> { Expr::Select { cond, then_val, else_val } } + ast::Expr::IndexExpr(e) => { + let base = self.collect_opt_expr(e.base()); + let index = self.collect_opt_expr(e.index()); + Expr::Index { base, index } + } + // TODO refactor with if let binding and default case is missing expression // BLOCK ast::Expr::PathExpr(path) => { diff --git a/openvaf/hir_def/src/body/pretty.rs b/openvaf/hir_def/src/body/pretty.rs index a5aca0e0..9e153b99 100644 --- a/openvaf/hir_def/src/body/pretty.rs +++ b/openvaf/hir_def/src/body/pretty.rs @@ -183,6 +183,12 @@ impl Printer<'_> { } w!(self, "}}"); } + Expr::Index { base, index } => { + self.pretty_print_expr(base); + w!(self, "["); + self.pretty_print_expr(index); + w!(self, "]"); + } Expr::Literal(ref lit) => w!(self, "{:?}", lit), } } diff --git a/openvaf/hir_def/src/expr.rs b/openvaf/hir_def/src/expr.rs index 1fe73c5e..d0d1d650 100644 --- a/openvaf/hir_def/src/expr.rs +++ b/openvaf/hir_def/src/expr.rs @@ -82,6 +82,11 @@ pub enum Expr { args: Vec, }, Array(Vec), + /// Array element / bus node access `base[index]`. + Index { + base: ExprId, + index: ExprId, + }, Literal(Literal), } @@ -100,6 +105,10 @@ impl Expr { f(then_val); f(else_val); } + Expr::Index { base, index } => { + f(base); + f(index); + } Expr::Call { args: ref exprs, .. } | Expr::Array(ref exprs) => { for e in exprs { f(*e) diff --git a/openvaf/hir_ty/src/inference.rs b/openvaf/hir_ty/src/inference.rs index 5c134b31..d381df0c 100755 --- a/openvaf/hir_ty/src/inference.rs +++ b/openvaf/hir_ty/src/inference.rs @@ -391,6 +391,16 @@ impl Ctx<'_> { } Expr::Array(ref args) if args.is_empty() => Ty::Val(Type::EmptyArray), Expr::Array(ref args) => self.infere_array(stmt, args)?, + Expr::Index { base, index } => { + // The index is an integer value. + self.infere_expr(stmt, index); + // The result is the element type of the indexed array. + let base_ty = self.infere_expr(stmt, base)?; + match base_ty.to_value() { + Some(Type::Array { ty, .. }) => Ty::Val(*ty), + _ => Ty::Val(Type::Err), + } + } Expr::Literal(Literal::Float(_)) => Ty::Literal(Type::Real), Expr::Literal(Literal::Int(_)) => Ty::Literal(Type::Integer), // +/- inf can only appear in param bounds. diff --git a/openvaf/parser/src/grammar/expressions.rs b/openvaf/parser/src/grammar/expressions.rs index 35282c50..ed087607 100644 --- a/openvaf/parser/src/grammar/expressions.rs +++ b/openvaf/parser/src/grammar/expressions.rs @@ -101,7 +101,7 @@ fn atom_expr(p: &mut Parser) -> Option { let done = match p.current() { T!['('] => paren_expr(p), - // T!["'{"] => array_expr(p), TODO properly implement arrays + T!["'{"] | T!['{'] => array_expr(p), T![~] | T![!] | T![-] | T![+] => { let m = p.start(); p.bump_ts(TokenSet::new(&[T![~], T![!], T![-], T![+]])); @@ -112,6 +112,10 @@ fn atom_expr(p: &mut Parser) -> Option { let m = path(p); if p.at(T!('(')) { call(p, m) + } else if p.at(T!['[']) { + // Index expression `base[index]` (array element or bus node access). + let base = m.precede(p).complete(p, PATH_EXPR); + index_expr(p, base) } else { let m = m.precede(p); m.complete(p, PATH_EXPR) @@ -159,21 +163,29 @@ fn paren_expr(p: &mut Parser) -> CompletedMarker { m.complete(p, PAREN_EXPR) } -// fn array_expr(p: &mut Parser) -> CompletedMarker { -// let m = p.start(); -// p.bump(T!["'{"]); -// while !p.at(EOF) && !p.at(T![']']) { -// // test array_attrs -// // const A: &[i64] = &[1, #[cfg(test)] 2]; -// if expr(p).is_none() { -// break; -// } - -// if !p.at(T!['}']) && !p.expect(T![,]) { -// break; -// } -// } -// p.expect(T!['}']); - -// m.complete(p, ARRAY_EXPR) -// } +/// `'{ e0, e1, ... }` (or plain `{ ... }`) array literal. +fn array_expr(p: &mut Parser) -> CompletedMarker { + let m = p.start(); + // Accept both the SystemVerilog-style `'{` and the plain `{` Verilog-A spelling. + p.bump_ts(TokenSet::new(&[T!["'{"], T!['{']])); + while !p.at(EOF) && !p.at(T!['}']) { + if expr(p).is_none() { + break; + } + if !p.at(T!['}']) && !p.expect(T![,]) { + break; + } + } + p.expect(T!['}']); + + m.complete(p, ARRAY_EXPR) +} + +/// `base[index]` — `base` is the already-parsed/completed base expression. +fn index_expr(p: &mut Parser, base: CompletedMarker) -> CompletedMarker { + let m = base.precede(p); + p.bump(T!['[']); + expr(p); + p.expect(T![']']); + m.complete(p, INDEX_EXPR) +} diff --git a/openvaf/parser/src/grammar/items.rs b/openvaf/parser/src/grammar/items.rs index 28152a7b..ca30756c 100755 --- a/openvaf/parser/src/grammar/items.rs +++ b/openvaf/parser/src/grammar/items.rs @@ -86,7 +86,17 @@ pub(super) fn var_decl(p: &mut Parser, m: Marker) { fn var(p: &mut Parser) -> bool { let m = p.start(); - name_r(p, TokenSet::new(&[T![,], T![=], T![;]])); + name_r(p, TokenSet::new(&[T!['['], T![,], T![=], T![;]])); + // Optional array dimension, e.g. `real den[order:0];`. + if p.at(T!['[']) { + let dim = p.start(); + p.bump(T!['[']); + expr(p); + p.expect(T![:]); + expr(p); + p.expect(T![']']); + dim.complete(p, DIMENSION); + } if p.eat(T![=]) { expr(p); } diff --git a/openvaf/syntax/src/ast/expr_ext.rs b/openvaf/syntax/src/ast/expr_ext.rs index 3bf62c58..ee1bc277 100644 --- a/openvaf/syntax/src/ast/expr_ext.rs +++ b/openvaf/syntax/src/ast/expr_ext.rs @@ -366,6 +366,30 @@ impl ast::SelectExpr { } } +impl ast::IndexExpr { + /// The indexed expression (e.g. the array/bus name in `den[k]`). + pub fn base(&self) -> Option { + support::children(self.syntax()).next() + } + + /// The index expression (e.g. `k` in `den[k]`). + pub fn index(&self) -> Option { + support::children(self.syntax()).nth(1) + } +} + +impl ast::Dimension { + /// The most-significant bound of `[msb:lsb]`. + pub fn msb(&self) -> Option { + support::children(self.syntax()).next() + } + + /// The least-significant bound of `[msb:lsb]`. + pub fn lsb(&self) -> Option { + support::children(self.syntax()).nth(1) + } +} + pub enum AsssigmentOp { /// a variable assignment stmt /// lhs must be an identifier (example `I = V(a,c)/R;`) diff --git a/openvaf/syntax/src/ast/generated/nodes.rs b/openvaf/syntax/src/ast/generated/nodes.rs index 02478dd5..0881415f 100644 --- a/openvaf/syntax/src/ast/generated/nodes.rs +++ b/openvaf/syntax/src/ast/generated/nodes.rs @@ -251,6 +251,15 @@ impl ArrayExpr { pub fn r_curly_token(&self) -> Option { support::token(&self.syntax, T!['}']) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct IndexExpr { + pub(crate) syntax: SyntaxNode, +} +impl IndexExpr { + pub fn expr(&self) -> Option { support::child(&self.syntax) } + pub fn l_brack_token(&self) -> Option { support::token(&self.syntax, T!['[']) } + pub fn r_brack_token(&self) -> Option { support::token(&self.syntax, T![']']) } +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Call { pub(crate) syntax: SyntaxNode, } @@ -492,10 +501,21 @@ pub struct Var { } impl Var { pub fn name(&self) -> Option { support::child(&self.syntax) } + pub fn dimension(&self) -> Option { support::child(&self.syntax) } pub fn eq_token(&self) -> Option { support::token(&self.syntax, T![=]) } pub fn default(&self) -> Option { support::child(&self.syntax) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Dimension { + pub(crate) syntax: SyntaxNode, +} +impl Dimension { + pub fn l_brack_token(&self) -> Option { support::token(&self.syntax, T!['[']) } + pub fn expr(&self) -> Option { support::child(&self.syntax) } + pub fn colon_token(&self) -> Option { support::token(&self.syntax, T![:]) } + pub fn r_brack_token(&self) -> Option { support::token(&self.syntax, T![']']) } +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Param { pub(crate) syntax: SyntaxNode, } @@ -551,6 +571,7 @@ pub enum Expr { BinExpr(BinExpr), ParenExpr(ParenExpr), ArrayExpr(ArrayExpr), + IndexExpr(IndexExpr), Call(Call), SelectExpr(SelectExpr), PathExpr(PathExpr), @@ -894,6 +915,17 @@ impl AstNode for ArrayExpr { } fn syntax(&self) -> &SyntaxNode { &self.syntax } } +impl AstNode for IndexExpr { + fn can_cast(kind: SyntaxKind) -> bool { kind == INDEX_EXPR } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { &self.syntax } +} impl AstNode for Call { fn can_cast(kind: SyntaxKind) -> bool { kind == CALL } fn cast(syntax: SyntaxNode) -> Option { @@ -1147,6 +1179,17 @@ impl AstNode for Var { } fn syntax(&self) -> &SyntaxNode { &self.syntax } } +impl AstNode for Dimension { + fn can_cast(kind: SyntaxKind) -> bool { kind == DIMENSION } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { &self.syntax } +} impl AstNode for Param { fn can_cast(kind: SyntaxKind) -> bool { kind == PARAM } fn cast(syntax: SyntaxNode) -> Option { @@ -1214,6 +1257,9 @@ impl From for Expr { impl From for Expr { fn from(node: ArrayExpr) -> Expr { Expr::ArrayExpr(node) } } +impl From for Expr { + fn from(node: IndexExpr) -> Expr { Expr::IndexExpr(node) } +} impl From for Expr { fn from(node: Call) -> Expr { Expr::Call(node) } } @@ -1232,8 +1278,8 @@ impl From for Expr { impl AstNode for Expr { fn can_cast(kind: SyntaxKind) -> bool { match kind { - PREFIX_EXPR | BIN_EXPR | PAREN_EXPR | ARRAY_EXPR | CALL | SELECT_EXPR | PATH_EXPR - | PORT_FLOW => true, + PREFIX_EXPR | BIN_EXPR | PAREN_EXPR | ARRAY_EXPR | INDEX_EXPR | CALL | SELECT_EXPR + | PATH_EXPR | PORT_FLOW => true, _ => Literal::can_cast(kind), } } @@ -1243,6 +1289,7 @@ impl AstNode for Expr { BIN_EXPR => Expr::BinExpr(BinExpr { syntax }), PAREN_EXPR => Expr::ParenExpr(ParenExpr { syntax }), ARRAY_EXPR => Expr::ArrayExpr(ArrayExpr { syntax }), + INDEX_EXPR => Expr::IndexExpr(IndexExpr { syntax }), CALL => Expr::Call(Call { syntax }), SELECT_EXPR => Expr::SelectExpr(SelectExpr { syntax }), PATH_EXPR => Expr::PathExpr(PathExpr { syntax }), @@ -1257,6 +1304,7 @@ impl AstNode for Expr { Expr::BinExpr(it) => &it.syntax, Expr::ParenExpr(it) => &it.syntax, Expr::ArrayExpr(it) => &it.syntax, + Expr::IndexExpr(it) => &it.syntax, Expr::Call(it) => &it.syntax, Expr::SelectExpr(it) => &it.syntax, Expr::PathExpr(it) => &it.syntax, @@ -1749,6 +1797,11 @@ impl std::fmt::Display for ArrayExpr { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for IndexExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for Call { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) @@ -1864,6 +1917,11 @@ impl std::fmt::Display for Var { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for Dimension { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for Param { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/openvaf/syntax/veriloga.ungram b/openvaf/syntax/veriloga.ungram index 5b2c7cb8..159286ea 100644 --- a/openvaf/syntax/veriloga.ungram +++ b/openvaf/syntax/veriloga.ungram @@ -108,11 +108,15 @@ Expr = | BinExpr | ParenExpr | ArrayExpr +| IndexExpr | Call | SelectExpr | PathExpr | PortFlow +IndexExpr = + Expr '[' Expr ']' + PathExpr = Path // Required to allow PortFlow = '<' port: Path '>' @@ -211,7 +215,10 @@ VarDecl = AttrList* Type (Var (',' Var)*) ';' Var = - Name ('=' default:Expr)? + Name Dimension? ('=' default:Expr)? + +Dimension = + '[' Expr ':' Expr ']' diff --git a/openvaf/tokens/src/parser/generated.rs b/openvaf/tokens/src/parser/generated.rs index c6aacb7f..dc02a366 100644 --- a/openvaf/tokens/src/parser/generated.rs +++ b/openvaf/tokens/src/parser/generated.rs @@ -103,6 +103,8 @@ pub enum SyntaxKind { COMMENT, ANALOG_BEHAVIOUR, PROCEDURAL_BLOCK, + INDEX_EXPR, + DIMENSION, ARG, ARG_LIST, ARRAY_EXPR, diff --git a/sourcegen/src/ast/src.rs b/sourcegen/src/ast/src.rs index 8c69b3cd..72949544 100644 --- a/sourcegen/src/ast/src.rs +++ b/sourcegen/src/ast/src.rs @@ -102,6 +102,8 @@ pub(crate) const KINDS_SRC: KindsSrc = KindsSrc { nodes: &[ "ANALOG_BEHAVIOUR", "PROCEDURAL_BLOCK", + "INDEX_EXPR", + "DIMENSION", "ARG", "ARG_LIST", "ARRAY_EXPR", From 811752ef9eb4e7a93775b2dbd270c6d500a8c317 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 11:50:20 +0200 Subject: [PATCH 07/19] Arrays: const-eval array dimensions (real x[a:b] -> Type::Array) Adds a best-effort compile-time integer const-evaluator (literals, +-*/ arithmetic, and parameter references resolved against the enclosing module) and uses it to size array-variable declarations: real den[order:0] now lowers to Type::Array{Real, order+1}. Regression-safe (no existing model uses array dimensions); workspace builds and the 7 working examples still compile. Next increment: array element read/write lowering (Expr::Index through the hir layer + hir_lower element places, const index then runtime mux), then laplace_nd. --- openvaf/hir_def/src/item_tree/lower.rs | 66 +++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/openvaf/hir_def/src/item_tree/lower.rs b/openvaf/hir_def/src/item_tree/lower.rs index 8a8c2645..0a8f38c8 100644 --- a/openvaf/hir_def/src/item_tree/lower.rs +++ b/openvaf/hir_def/src/item_tree/lower.rs @@ -562,13 +562,29 @@ impl Ctx { } fn lower_var>>(&mut self, decl: ast::VarDecl, dst: &mut Vec) { - let ty = decl.ty().as_type(); + let base_ty = decl.ty().as_type(); + let module = decl.syntax().ancestors().find_map(ast::ModuleDecl::cast); for var in decl.vars() { if let Some(name) = var.name() { + // `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()) { + (Some(dim), Some(module)) => { + let len = dim + .msb() + .and_then(|m| eval_const_int(&m, module)) + .zip(dim.lsb().and_then(|l| eval_const_int(&l, module))) + .map(|(msb, lsb)| (msb - lsb).unsigned_abs() as u32 + 1) + .unwrap_or(0); + Type::Array { ty: Box::new(base_ty.clone()), len } + } + _ => base_ty.clone(), + }; let var = Var { name: name.as_name(), ast_id: self.source_ast_id_map.ast_id(&var), - ty: ty.clone(), + ty, }; let id = self.tree.data.variables.push_and_get_key(var); dst.push(id.into()) @@ -616,3 +632,49 @@ impl Ctx { } } } + +/// Best-effort compile-time evaluation of an integer constant expression, resolving +/// parameter references against the enclosing module's parameter declarations. Used +/// to size array/bus dimensions like `real den[order:0]` / `electrical [0:n] x`. +fn eval_const_int(expr: &ast::Expr, module: &ast::ModuleDecl) -> Option { + use syntax::ast::{BinaryOp, LiteralKind, UnaryOp}; + match expr { + ast::Expr::Literal(lit) => match lit.kind() { + LiteralKind::IntNumber(i) => Some(i.value() as i64), + _ => None, + }, + ast::Expr::PrefixExpr(p) => { + let v = eval_const_int(&p.expr()?, module)?; + match p.op_kind()? { + UnaryOp::Neg => Some(-v), + UnaryOp::Identity => Some(v), + _ => None, + } + } + ast::Expr::ParenExpr(p) => eval_const_int(&p.expr()?, module), + ast::Expr::BinExpr(b) => { + let l = eval_const_int(&b.lhs()?, module)?; + let r = eval_const_int(&b.rhs()?, module)?; + match b.op_kind()? { + BinaryOp::Addition => Some(l.wrapping_add(r)), + BinaryOp::Subtraction => Some(l.wrapping_sub(r)), + BinaryOp::Multiplication => Some(l.wrapping_mul(r)), + BinaryOp::Division if r != 0 => Some(l / r), + _ => None, + } + } + ast::Expr::PathExpr(pe) => { + let ident = pe.path()?.as_raw_ident()?; + let name = ident.text(); + for pdecl in module.syntax().descendants().filter_map(ast::ParamDecl::cast) { + for para in pdecl.paras() { + if para.name().map_or(false, |n| n.text() == name) { + return eval_const_int(¶.default()?, module); + } + } + } + None + } + _ => None, + } +} From 32f9d5c60a23fd0ed6ffda17e78e14aaccbbf2e6 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 12:12:30 +0200 Subject: [PATCH 08/19] Arrays: element read/write lowering (const + runtime index) + two bug fixes Arrays are now functional end to end. A fixed-size array variable lowers to one MIR place per element (PlaceKind::VarElement); reads/writes with a constant index hit the element directly, and runtime indices use an N-way select/mux (reads) or per-element conditional writes, staying in pure SSA. Threaded through the layers: hir_ty AssignDst::VarElement (array element is a valid lvalue), hir Expr::Index + AssignmentLhs::ArrayElement, hir_lower PlaceKind::VarElement + lower_index/assign_array_element, and the array-var default desugar. Fixes two pre-existing latent bugs that only triggered once arrays became usable: - Type::base_type() looped forever (matched on instead of the running ). - inference panicked rendering the array var's scalar default (checked against the array type instead of the element type). Verified via --run: const-index r/w, runtime-index r/w in a loop, and compile-time param-sized arrays (real den[order:0]) all produce correct results. --- openvaf/hir/src/body.rs | 9 ++++++++ openvaf/hir_def/src/body.rs | 8 +++++-- openvaf/hir_def/src/types.rs | 2 +- openvaf/hir_lower/src/ctx.rs | 3 +++ openvaf/hir_lower/src/expr.rs | 38 +++++++++++++++++++++++++++++++++ openvaf/hir_lower/src/lib.rs | 10 +++++++++ openvaf/hir_lower/src/stmt.rs | 30 +++++++++++++++++++++++++- openvaf/hir_ty/src/inference.rs | 28 +++++++++++++++++++++++- 8 files changed, 123 insertions(+), 5 deletions(-) diff --git a/openvaf/hir/src/body.rs b/openvaf/hir/src/body.rs index 36f39a50..f5e44791 100644 --- a/openvaf/hir/src/body.rs +++ b/openvaf/hir/src/body.rs @@ -167,6 +167,7 @@ impl<'a> BodyRef<'a> { Expr::Call { fun, args } } hir_def::Expr::Array(ref args) => Expr::Array(args), + hir_def::Expr::Index { base, index } => Expr::Index { base, index }, hir_def::Expr::Literal(ref literal) => Expr::Literal(literal), _ => panic!("invalid HIR: {:?}", self.body.exprs[expr]), } @@ -192,6 +193,10 @@ impl<'a> BodyRef<'a> { inference::AssignDst::Var(id) => { Stmt::Assignment { lhs: AssignmentLhs::Variable(Variable { id }), rhs: val } } + inference::AssignDst::VarElement { var, index } => Stmt::Assignment { + lhs: AssignmentLhs::ArrayElement { var: Variable { id: var }, index }, + rhs: val, + }, inference::AssignDst::FunVar { fun, arg: None } => Stmt::Assignment { lhs: AssignmentLhs::FunctionReturn(Function { id: fun }), rhs: val, @@ -231,6 +236,8 @@ pub enum AssignmentLhs { Variable(Variable), FunctionReturn(Function), FunctionArg(FunctionArg), + /// `arr[index] = …` — assignment to an array element. + ArrayElement { var: Variable, index: ExprId }, } #[derive(Debug, Clone, Eq, PartialEq)] @@ -270,6 +277,8 @@ pub enum Expr<'a> { Select { cond: ExprId, then_val: ExprId, else_val: ExprId }, Call { fun: ResolvedFun, args: &'a [ExprId] }, Array(&'a [ExprId]), + /// Array element access `base[index]`. + Index { base: ExprId, index: ExprId }, Literal(&'a Literal), } impl Expr<'_> { diff --git a/openvaf/hir_def/src/body.rs b/openvaf/hir_def/src/body.rs index 48651eff..18b12380 100644 --- a/openvaf/hir_def/src/body.rs +++ b/openvaf/hir_def/src/body.rs @@ -144,9 +144,13 @@ impl Body { ctx.collect_expr(expr) } else { let default_val = match db.var_data(var).ty { - Type::Real => Literal::Float(Ieee64::with_float(0.0)), Type::Integer => Literal::Int(0), - _ => unreachable!("invalid var type (TODO arrays)"), + // 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)) + } + _ => unreachable!("invalid var type"), }; ctx.alloc_expr_desugared(Expr::Literal(default_val)) }; diff --git a/openvaf/hir_def/src/types.rs b/openvaf/hir_def/src/types.rs index 4c6fd533..4831d6f9 100644 --- a/openvaf/hir_def/src/types.rs +++ b/openvaf/hir_def/src/types.rs @@ -95,7 +95,7 @@ impl Type { pub fn base_type(&self) -> &Type { let mut curr = self; - while let Type::Array { ty, .. } = self { + while let Type::Array { ty, .. } = curr { curr = ty } curr diff --git a/openvaf/hir_lower/src/ctx.rs b/openvaf/hir_lower/src/ctx.rs index 958c6132..9aab3a84 100644 --- a/openvaf/hir_lower/src/ctx.rs +++ b/openvaf/hir_lower/src/ctx.rs @@ -80,6 +80,9 @@ impl<'a, 'c> LoweringCtx<'a, 'c> { | PlaceKind::ParamMax(_) => return place, PlaceKind::Var(var) => self.use_param(ParamKind::HiddenState(var)), + // Array elements default to 0 (typically written before read in the + // initial block; a per-element HiddenState would be more precise). + PlaceKind::VarElement(..) => F_ZERO, PlaceKind::ImplicitResidual { .. } | PlaceKind::Contribute { .. } => F_ZERO, PlaceKind::CollapseImplicitEquation(_) => TRUE, PlaceKind::IsVoltageSrc(_) => FALSE, diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index cdaa0b76..6cc4d991 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -49,6 +49,7 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.ins().phi(&[then_src, else_src]) } + Expr::Index { base, index } => self.lower_index(base, index), Expr::Call { args, fun } => match fun { ResolvedFun::User { func, limit } => self.lower_user_fun(func, limit, args), ResolvedFun::BuiltIn(builtin) => self.lower_builtin(expr, builtin, args), @@ -260,6 +261,43 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.use_place(PlaceKind::FunctionReturn(fun)) } + /// The number of elements of an array-typed variable (0 if not an array). + pub(crate) fn array_len(&self, var: hir::Variable) -> u32 { + match var.ty(self.ctx.db) { + Type::Array { len, .. } => len, + _ => 0, + } + } + + /// Lower an array element read `base[index]`. A fixed-size array is one MIR place + /// per element; a constant index reads it directly, a runtime index builds a + /// select chain over all elements. + fn lower_index(&mut self, base: ExprId, index: ExprId) -> Value { + let var = match self.body.get_expr(base) { + Expr::Read(Ref::Variable(var)) => var, + // only array-variable indexing is supported + _ => return F_ZERO, + }; + let len = self.array_len(var); + if len == 0 { + return F_ZERO; + } + 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 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 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 }); + } + res + } + fn lower_builtin(&mut self, expr: ExprId, builtin: BuiltIn, args: &[ExprId]) -> Value { let signature = self.body.get_call_signature(expr); match builtin { diff --git a/openvaf/hir_lower/src/lib.rs b/openvaf/hir_lower/src/lib.rs index 1e0f4286..b337fbc6 100644 --- a/openvaf/hir_lower/src/lib.rs +++ b/openvaf/hir_lower/src/lib.rs @@ -154,6 +154,9 @@ impl IdtKind { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum PlaceKind { Var(Variable), + /// Element `idx` of a fixed-size array variable (compile-time array lowered to + /// one place per element). + VarElement(Variable, u32), FunctionReturn(hir::Function), FunctionArg(hir::FunctionArg), Contribute { @@ -178,6 +181,10 @@ impl PlaceKind { pub fn ty(&self, db: &CompilationDB) -> Type { match *self { PlaceKind::Var(var) => var.ty(db), + PlaceKind::VarElement(var, _) => match var.ty(db) { + Type::Array { ty, .. } => *ty, + ty => ty, + }, PlaceKind::FunctionReturn(fun) => fun.return_ty(db), PlaceKind::FunctionArg(arg) => arg.ty(db), @@ -202,6 +209,9 @@ impl From for PlaceKind { hir::AssignmentLhs::Variable(var) => PlaceKind::Var(var), hir::AssignmentLhs::FunctionReturn(fun) => PlaceKind::FunctionReturn(fun), hir::AssignmentLhs::FunctionArg(arg) => PlaceKind::FunctionArg(arg), + hir::AssignmentLhs::ArrayElement { .. } => { + unreachable!("array element assignment is lowered directly, not via PlaceKind") + } } } } diff --git a/openvaf/hir_lower/src/stmt.rs b/openvaf/hir_lower/src/stmt.rs index 6407f43c..e032d5f7 100644 --- a/openvaf/hir_lower/src/stmt.rs +++ b/openvaf/hir_lower/src/stmt.rs @@ -23,7 +23,12 @@ impl BodyLoweringCtx<'_, '_, '_> { } Stmt::Assignment { lhs, rhs } => { let val_ = self.lower_expr(rhs); - self.ctx.def_place(lhs.into(), val_); + match lhs { + hir::AssignmentLhs::ArrayElement { var, index } => { + self.assign_array_element(var, index, val_) + } + _ => self.ctx.def_place(lhs.into(), val_), + } } Stmt::Contribute { kind, branch, rhs } => { self.contribute(kind == ContributeKind::Potential, branch, rhs) @@ -119,6 +124,29 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.switch_to_block(end); } + /// 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. + fn assign_array_element(&mut self, var: hir::Variable, index: ExprId, val: mir::Value) { + let len = self.array_len(var); + if len == 0 { + return; + } + 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); + 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 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); + } + } + fn lower_loop(&mut self, cond: ExprId, lower_body: impl FnOnce(&mut Self)) { let loop_cond_head = self.ctx.create_block(); let loop_body_head = self.ctx.create_block(); diff --git a/openvaf/hir_ty/src/inference.rs b/openvaf/hir_ty/src/inference.rs index d381df0c..6d6e425e 100755 --- a/openvaf/hir_ty/src/inference.rs +++ b/openvaf/hir_ty/src/inference.rs @@ -42,6 +42,8 @@ pub enum ResolvedFun { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum AssignDst { Var(VarId), + /// `arr[index] = …` — assignment to an array element. + VarElement { var: VarId, index: ExprId }, FunVar { fun: FunctionId, arg: Option }, Flow(BranchWrite), Potential(BranchWrite), @@ -92,7 +94,12 @@ impl InferenceResult { .infere_expr(body.entry_stmts[0], db.param_exprs(param).default) .and_then(|ty| ty.to_value()), }, - DefWithBodyId::VarId(var) => Some(db.var_data(var).ty.clone()), + DefWithBodyId::VarId(var) => Some(match db.var_data(var).ty.clone() { + // An array variable's desugared default is a scalar placeholder; check + // it against the element type rather than the array type. + Type::Array { ty, .. } => *ty, + ty => ty, + }), _ => None, }; @@ -196,6 +203,25 @@ impl Ctx<'_> { ) -> Option { let e = self.infere_expr(stmt, expr); + // Array element assignment `den[index] = …`. The base must be an array variable. + if let Expr::Index { base, index } = self.body.exprs[expr] { + if let Ty::Var(Type::Array { ty, .. }, var) = self.result.expr_types[base].clone() { + let elem = *ty; + if assignment_kind == ast::AssignOp::Contribute { + self.result.diagnostics.push(InferenceDiagnostic::InvalidAssignDst { + e: expr, + maybe_different_operand: Some(ast::AssignOp::Assign), + assignment_kind, + }); + } else { + self.result + .assignment_destination + .insert(stmt, AssignDst::VarElement { var, index }); + } + return Some(elem); + } + } + let (dst, ty) = match e? { Ty::Var(ty, var) => (AssignDst::Var(var), ty), Ty::FunctionVar { fun, ty, arg } => (AssignDst::FunVar { fun, arg }, ty), From 6d648031440ed4f91a9a7085b28c9f13ad6ee392 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 12:21:37 +0200 Subject: [PATCH 09/19] laplace_nd: state-space realization -> Butterworth filter compiles (8/9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements laplace_nd(input, num, den) as a controllable-canonical-form state space using den.len()-1 integrator states (implicit equations), reusing the existing idt/DAE machinery (no simulator changes). Coefficients may be runtime values (read from array variables/literals), so the const-arg validation is relaxed for laplace_nd and it is moved from UNSUPPORTED to a supported analog operator. Also makes array literals type as arrays (infere_array now returns Type::Array, not the element type) and lets an array *variable* satisfy the ArrayAnyLength requirement (Ty::Var(Array) coerces to a value), which the laplace coefficient arguments need. The SimetriX Butterworth example (#8) — array vars real den[order:0], the {1.0} array literal, runtime den[k] writes, and laplace_nd — now compiles to a working OSDI device model. Scorecard: 8/9. --- openvaf/hir_def/src/builtin.rs | 1 - openvaf/hir_lower/src/expr.rs | 72 +++++++++++++++++++++++++++ openvaf/hir_ty/src/inference.rs | 3 +- openvaf/hir_ty/src/types.rs | 3 +- openvaf/hir_ty/src/validation/body.rs | 5 +- sourcegen/src/hir_builtins.rs | 3 +- 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/openvaf/hir_def/src/builtin.rs b/openvaf/hir_def/src/builtin.rs index 1c3b6ece..10424a05 100644 --- a/openvaf/hir_def/src/builtin.rs +++ b/openvaf/hir_def/src/builtin.rs @@ -183,7 +183,6 @@ impl BuiltIn { | BuiltIn::zi_np | BuiltIn::zi_zd | BuiltIn::zi_zp - | BuiltIn::laplace_nd | BuiltIn::laplace_np | BuiltIn::laplace_zd | BuiltIn::laplace_zp diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index 6cc4d991..78549637 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -568,6 +568,10 @@ impl BodyLoweringCtx<'_, '_, '_> { } } + // Without equation lowering (e.g. op-var contexts) a filter is a no-op. + BuiltIn::laplace_nd if self.ctx.no_equations => F_ZERO, + BuiltIn::laplace_nd => self.lower_laplace_nd(args), + BuiltIn::idt => { let kind = match_signature! { signature: @@ -829,6 +833,74 @@ impl BodyLoweringCtx<'_, '_, '_> { val } + /// Read the coefficient values of an array-valued argument (an array variable's + /// elements or an array literal's entries), lowest index first. + fn array_coeffs(&mut self, arg: ExprId) -> Vec { + match self.body.get_expr(arg) { + Expr::Read(Ref::Variable(var)) => { + let len = self.array_len(var); + (0..len).map(|i| self.ctx.use_place(PlaceKind::VarElement(var, i))).collect() + } + Expr::Array(elems) => elems.iter().map(|&e| self.lower_expr(e)).collect(), + _ => vec![self.lower_expr(arg)], + } + } + + /// Lower `laplace_nd(input, num, den)` (coefficients in ascending powers of `s`) + /// as a controllable-canonical-form state space using `den.len()-1` integrator + /// states (implicit equations), reusing the existing DAE machinery. Coefficients + /// may be runtime values. + fn lower_laplace_nd(&mut self, args: &[ExprId]) -> Value { + let input = self.lower_expr(args[0]); + let num = self.array_coeffs(args[1]); + let den = self.array_coeffs(args[2]); + let n = den.len().saturating_sub(1); // filter order + if n == 0 { + if num.is_empty() || den.is_empty() { + return input; + } + let g = self.ctx.ins().fdiv(num[0], den[0]); + return self.ctx.ins().fmul(g, input); + } + + // States x_0..x_{n-1} with x_i = s^i w where D(s) w = input. + let mut states = Vec::with_capacity(n); + for _ in 0..n { + states.push(self.ctx.implicit_equation(ImplicitEquationKind::Idt(IdtKind::Basic))); + } + + // dx_i/dt = x_{i+1} for i in 0..n-1. + for i in 0..n - 1 { + let eq = states[i].0; + let next = states[i + 1].1; + let neg = self.ctx.ins().fneg(next); + self.ctx.def_resist_residual(neg, eq); + self.ctx.def_react_residual(states[i].1, eq); + } + + // dx_{n-1}/dt = (input - Σ_{i Type { self.body .needs_cast(expr) diff --git a/openvaf/hir_ty/src/inference.rs b/openvaf/hir_ty/src/inference.rs index 6d6e425e..52557f2e 100755 --- a/openvaf/hir_ty/src/inference.rs +++ b/openvaf/hir_ty/src/inference.rs @@ -1003,7 +1003,8 @@ impl Ctx<'_> { } } - Some(Ty::Val(ty)) + // An array literal `{e0, e1, ...}` has an array type (element type `ty`). + Some(Ty::Val(Type::Array { ty: Box::new(ty), len: args.len() as u32 })) } fn infere_bin_op( diff --git a/openvaf/hir_ty/src/types.rs b/openvaf/hir_ty/src/types.rs index bd267f60..cb803403 100644 --- a/openvaf/hir_ty/src/types.rs +++ b/openvaf/hir_ty/src/types.rs @@ -203,7 +203,8 @@ impl Ty { // TODO merge these match arms when there are box/deref patterns (not any time soon) ( - Ty::Val(Type::Array { ty: ref ty1, .. }), + Ty::Val(Type::Array { ty: ref ty1, .. }) + | Ty::Var(Type::Array { ty: ref ty1, .. }, _), TyRequirement::ArrayAnyLength { ty: ty2 }, ) => equiv.compare_ty(ty1, ty2), diff --git a/openvaf/hir_ty/src/validation/body.rs b/openvaf/hir_ty/src/validation/body.rs index 76900d81..b6de1f0b 100644 --- a/openvaf/hir_ty/src/validation/body.rs +++ b/openvaf/hir_ty/src/validation/body.rs @@ -734,8 +734,9 @@ impl ExprValidator<'_, '_> { } ( - BuiltIn::laplace_nd - | BuiltIn::laplace_np + // laplace_nd accepts runtime-computed coefficient arrays (realized as + // a state-space filter), so its coefficient args are not const-checked. + BuiltIn::laplace_np | BuiltIn::laplace_zp | BuiltIn::laplace_zd | BuiltIn::zi_nd diff --git a/sourcegen/src/hir_builtins.rs b/sourcegen/src/hir_builtins.rs index 4ca02b3f..a7ab25db 100644 --- a/sourcegen/src/hir_builtins.rs +++ b/sourcegen/src/hir_builtins.rs @@ -25,7 +25,7 @@ const ANALOG_OPERATORS: [&str; 17] = [ "transition", ]; -const UNSUPPORTED: [&str; 49] = [ +const UNSUPPORTED: [&str; 48] = [ "simprobe", "analog_node_alias", "analog_port_alias", @@ -35,7 +35,6 @@ const UNSUPPORTED: [&str; 49] = [ "zi_np", "zi_zd", "zi_zp", - "laplace_nd", "laplace_np", "laplace_zd", "laplace_zp", From 31c632ead901eec786947da596cd49a559ad8853 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 12:49:21 +0200 Subject: [PATCH 10/19] genvar + bus nodes: RC ladder compiles -> scorecard 9/9 Phase 2 of the SimetriX "Writing Verilog-A Code" scorecard. Implements the features the RC ladder (#9) needs, completing all 9 examples: - genvar declarations and compile-time `for`-loop unrolling. A `for` loop whose control variable is a genvar with constant bounds is unrolled in HIR body lowering into a flat block of body copies, with the genvar folded to its per-iteration constant value. - Vectored/bus nets `electrical [0:n] inode;` expand at item-tree lowering into n+1 ordinary scalar nodes named `inode[0]`..`inode[n]` (sized via the existing compile-time const-evaluator). The node model stays scalar internally. - Bus port references `module rc_ladder(inode[0], inode[n])` in the module header resolve their constant index and mark the matching expanded scalar node a port. - Bus element access `inode[i]` (constant index after unrolling) rewrites to a path to the expanded scalar node `inode[]`, so V()/I()/ddt() work unchanged. Grammar: new `genvar` keyword + GENVAR_DECL, PORT_REF node, optional Dimension on NetDecl; regenerated AST/tokens. Verified: rc_ladder (n=16) compiles to a 34KB OSDI with 17 nodes (2 ports); a 3-stage variant produces exactly 3 series resistors + 3 shunt capacitors with a correct symmetric Jacobian. All 9 scorecard examples compile. Full compiler test suite green (216 passed, 0 failed), no snapshot drift. --- openvaf/hir_def/src/body.rs | 39 +++++ openvaf/hir_def/src/body/lower.rs | 192 ++++++++++++++++++++- openvaf/hir_def/src/item_tree/lower.rs | 95 ++++++++-- openvaf/parser/src/grammar/items/module.rs | 33 +++- openvaf/syntax/src/ast/generated/nodes.rs | 69 +++++++- openvaf/syntax/veriloga.ungram | 11 +- openvaf/tokens/src/parser/generated.rs | 16 +- sourcegen/src/ast/src.rs | 3 + 8 files changed, 428 insertions(+), 30 deletions(-) diff --git a/openvaf/hir_def/src/body.rs b/openvaf/hir_def/src/body.rs index 18b12380..0a1c671a 100644 --- a/openvaf/hir_def/src/body.rs +++ b/openvaf/hir_def/src/body.rs @@ -7,6 +7,7 @@ use basedb::lints::{Lint, LintSrc}; use basedb::{AttrDiagnostic, LintAttrs}; use lower::LowerCtx; use stdx::Ieee64; +use syntax::name::AsName; use syntax::{ast, AstNode, AstPtr}; use crate::db::HirDefDB; @@ -82,6 +83,24 @@ impl Body { ast_id_map: &ast_id_map, curr_scope, registry: ®istry, + genvar_names: ast + .module_items() + .filter_map(|it| match it { + ast::ModuleItem::GenvarDecl(g) => Some(g), + _ => None, + }) + .flat_map(|g| g.names().map(|n| n.as_name())) + .collect(), + bus_names: ast + .module_items() + .filter_map(|it| match it { + ast::ModuleItem::NetDecl(net) if net.dimension().is_some() => Some(net), + _ => None, + }) + .flat_map(|net| net.names().map(|n| n.as_name())) + .collect(), + module: Some(ast.clone()), + genvars: Vec::new(), }; body.entry_stmts = match kind { ModuleBodyKind::AnalogInitial => { @@ -121,6 +140,10 @@ impl Body { ast_id_map: &ast_id_map, curr_scope, registry: ®istry, + genvar_names: Vec::new(), + bus_names: Vec::new(), + module: None, + genvars: Vec::new(), }; body.entry_stmts = ast.body().map(|stmt| ctx.collect_stmt(stmt)).collect(); } @@ -138,6 +161,10 @@ impl Body { ast_id_map: &ast_id_map, curr_scope, registry: ®istry, + genvar_names: Vec::new(), + bus_names: Vec::new(), + module: None, + genvars: Vec::new(), }; let expr = if let Some(expr) = ast.default() { @@ -175,6 +202,10 @@ impl Body { ast_id_map: &ast_id_map, curr_scope, registry: ®istry, + genvar_names: Vec::new(), + bus_names: Vec::new(), + module: None, + genvars: Vec::new(), }; let expr = ctx.collect_opt_expr(ast.val()); let stmt = ctx.alloc_stmt_desugared(Stmt::Expr(expr)); @@ -197,6 +228,10 @@ impl Body { ast_id_map: &ast_id_map, curr_scope, registry: ®istry, + genvar_names: Vec::new(), + bus_names: Vec::new(), + module: None, + genvars: Vec::new(), }; let expr = ctx.collect_opt_expr(ast.val()); let stmt = ctx.alloc_stmt_desugared(Stmt::Expr(expr)); @@ -231,6 +266,10 @@ impl Body { ast_id_map: &ast_id_map, curr_scope: (scope, ast_id.into()), registry: ®istry, + genvar_names: Vec::new(), + bus_names: Vec::new(), + module: None, + genvars: Vec::new(), }; let default = ctx.collect_opt_expr(ast.default()); diff --git a/openvaf/hir_def/src/body/lower.rs b/openvaf/hir_def/src/body/lower.rs index 49567519..47c4aa4c 100644 --- a/openvaf/hir_def/src/body/lower.rs +++ b/openvaf/hir_def/src/body/lower.rs @@ -4,7 +4,7 @@ use basedb::lints::LintRegistry; use basedb::{AstIdMap, ErasedAstId, LintAttrs}; use syntax::ast::{self, ArgListOwner, AttrIter, AttrsOwner, FunctionRef}; use syntax::name::AsName; -use syntax::AstPtr; +use syntax::{AstNode, AstPtr}; // use tracing::debug; use super::{Body, BodySourceMap}; @@ -20,6 +20,15 @@ pub(super) struct LowerCtx<'a> { pub(super) ast_id_map: &'a AstIdMap, pub(super) curr_scope: (ScopeId, ErasedAstId), pub(super) registry: &'a LintRegistry, + /// Enclosing module (for compile-time constant evaluation of genvar/bus + /// expressions against module parameters). `None` for function/var/param bodies. + pub(super) module: Option, + /// Names declared `genvar` in the enclosing module. + pub(super) genvar_names: Vec, + /// Net names declared as a vectored/bus (`electrical [0:n] inode;`). + pub(super) bus_names: Vec, + /// Currently-bound genvar values during compile-time loop unrolling. + pub(super) genvars: Vec<(syntax::name::Name, i64)>, } impl LowerCtx<'_> { @@ -78,6 +87,11 @@ impl LowerCtx<'_> { } ast::Expr::IndexExpr(e) => { + // Vectored/bus node element `inode[i]` with a compile-time-constant + // index resolves to the expanded scalar node `inode[]`. + if let Some(id) = self.try_bus_index(e, &expr) { + return id; + } let base = self.collect_opt_expr(e.base()); let index = self.collect_opt_expr(e.index()); Expr::Index { base, index } @@ -86,6 +100,10 @@ impl LowerCtx<'_> { // TODO refactor with if let binding and default case is missing expression // BLOCK ast::Expr::PathExpr(path) => { + // A reference to a bound genvar folds to its current constant value. + if let Some(id) = self.try_genvar_path(path, &expr) { + return id; + } if let Some(path) = path.path().and_then(Path::resolve) { Expr::Path { path, port: false } } else { @@ -144,6 +162,11 @@ impl LowerCtx<'_> { Stmt::WhileLoop { cond, body } } ast::Stmt::ForStmt(stmt) => { + // A `for` loop over a genvar with compile-time bounds is unrolled into + // a flat block of body copies (one per iteration, genvar substituted). + if let Some(id) = self.try_unroll_genvar_for(stmt) { + return id; + } let cond = self.collect_opt_expr(stmt.condition()); let init = self.collect_opt_stmt(stmt.init()); let incr = self.collect_opt_stmt(stmt.incr()); @@ -219,6 +242,173 @@ impl LowerCtx<'_> { Stmt::Block { body } } + /// Evaluate a compile-time integer expression in the current genvar/parameter + /// environment (literals, integer arithmetic, bound genvars and module + /// parameter defaults). Returns `None` if it is not a compile-time constant. + fn eval_genvar_const(&self, expr: &ast::Expr) -> Option { + use syntax::ast::{BinaryOp, LiteralKind, UnaryOp}; + match expr { + ast::Expr::Literal(lit) => match lit.kind() { + LiteralKind::IntNumber(i) => Some(i.value() as i64), + _ => None, + }, + ast::Expr::PrefixExpr(p) => { + let v = self.eval_genvar_const(&p.expr()?)?; + match p.op_kind()? { + UnaryOp::Neg => Some(-v), + UnaryOp::Identity => Some(v), + _ => None, + } + } + ast::Expr::ParenExpr(p) => self.eval_genvar_const(&p.expr()?), + ast::Expr::BinExpr(b) => { + let l = self.eval_genvar_const(&b.lhs()?)?; + let r = self.eval_genvar_const(&b.rhs()?)?; + match b.op_kind()? { + BinaryOp::Addition => Some(l.wrapping_add(r)), + BinaryOp::Subtraction => Some(l.wrapping_sub(r)), + BinaryOp::Multiplication => Some(l.wrapping_mul(r)), + BinaryOp::Division if r != 0 => Some(l / r), + BinaryOp::Remainder if r != 0 => Some(l % r), + _ => None, + } + } + ast::Expr::PathExpr(pe) => { + let ident = pe.path()?.as_raw_ident()?; + let tname = ident.text(); + // genvar binding takes precedence over parameters + if let Some((_, val)) = self.genvars.iter().rev().find(|(gv, _)| tname == &**gv) { + return Some(*val); + } + let module = self.module.as_ref()?; + for pdecl in module.syntax().descendants().filter_map(ast::ParamDecl::cast) { + for para in pdecl.paras() { + if para.name().map_or(false, |n| n.text() == tname) { + return self.eval_genvar_const(¶.default()?); + } + } + } + None + } + _ => None, + } + } + + /// Evaluate a compile-time boolean loop condition (a comparison of two + /// compile-time integers). Returns `None` if it cannot be evaluated. + fn eval_genvar_cond(&self, expr: &ast::Expr) -> Option { + use syntax::ast::BinaryOp; + match expr { + ast::Expr::ParenExpr(p) => self.eval_genvar_cond(&p.expr()?), + ast::Expr::BinExpr(b) => { + let l = self.eval_genvar_const(&b.lhs()?)?; + let r = self.eval_genvar_const(&b.rhs()?)?; + match b.op_kind()? { + BinaryOp::LesserTest => Some(l < r), + BinaryOp::GreaterTest => Some(l > r), + BinaryOp::LesserEqualTest => Some(l <= r), + BinaryOp::GreaterEqualTest => Some(l >= r), + BinaryOp::EqualityTest => Some(l == r), + BinaryOp::NegatedEqualityTest => Some(l != r), + _ => None, + } + } + _ => None, + } + } + + /// If `expr` is a single identifier path, return its name. + fn single_ident(expr: &ast::Expr) -> Option { + match expr { + ast::Expr::PathExpr(pe) => { + let ident = pe.path()?.as_raw_ident()?; + Some(syntax::name::Name::resolve(ident.text().as_ref())) + } + _ => None, + } + } + + /// Fold a reference to a bound genvar into its current constant value. + fn try_genvar_path(&mut self, path: &ast::PathExpr, expr: &ast::Expr) -> Option { + let ident = path.path()?.as_raw_ident()?; + let tname = ident.text(); + let val = self.genvars.iter().rev().find(|(gv, _)| tname == &**gv).map(|(_, v)| *v)?; + Some(self.alloc_expr(Expr::Literal(Literal::Int(val as i32)), AstPtr::new(expr))) + } + + /// Resolve `inode[i]` (bus net element, compile-time-constant index) to the + /// expanded scalar node `inode[]`. + fn try_bus_index(&mut self, e: &ast::IndexExpr, expr: &ast::Expr) -> Option { + let base = e.base()?; + let pe = match &base { + ast::Expr::PathExpr(pe) => pe, + _ => return None, + }; + let ident = pe.path()?.as_raw_ident()?; + let bname = ident.text(); + if !self.bus_names.iter().any(|b| bname == &**b) { + return None; + } + let k = self.eval_genvar_const(&e.index()?)?; + let synth = syntax::name::Name::resolve(&format!("{}[{}]", bname, k)); + let path = Path::new_ident(synth); + Some(self.alloc_expr(Expr::Path { path, port: false }, AstPtr::new(expr))) + } + + /// Unroll a genvar `for` loop with compile-time bounds into a flat block of + /// body copies (genvar substituted per iteration). Returns `None` for ordinary + /// runtime loops, which are lowered normally. + fn try_unroll_genvar_for(&mut self, stmt: &ast::ForStmt) -> Option { + let init = stmt.init()?; + let init_assign = match &init { + ast::Stmt::AssignStmt(a) => a.assign()?, + _ => return None, + }; + let gv = Self::single_ident(&init_assign.lval()?)?; + if !self.genvar_names.contains(&gv) { + return None; + } + let start = self.eval_genvar_const(&init_assign.rval()?)?; + let cond = stmt.condition()?; + let incr = stmt.incr()?; + let incr_assign = match &incr { + ast::Stmt::AssignStmt(a) => a.assign()?, + _ => return None, + }; + let incr_rval = incr_assign.rval()?; + + let mut bodies = Vec::new(); + let mut val = start; + let mut guard = 0u64; + loop { + self.genvars.push((gv.clone(), val)); + match self.eval_genvar_cond(&cond) { + Some(true) => {} + Some(false) => { + self.genvars.pop(); + break; + } + None => { + self.genvars.pop(); + return None; + } + } + let body_id = self.collect_opt_stmt(stmt.for_body()); + bodies.push(body_id); + let next = self.eval_genvar_const(&incr_rval); + self.genvars.pop(); + match next { + Some(n) => val = n, + None => return None, + } + guard += 1; + if guard > 1_000_000 { + break; + } + } + Some(self.alloc_stmt_desugared(Stmt::Block { body: bodies })) + } + fn alloc_expr(&mut self, expr: Expr, ptr: AstPtr) -> ExprId { let id = self.make_expr(expr, Some(ptr.clone())); self.source_map.expr_map.insert(ptr, id); diff --git a/openvaf/hir_def/src/item_tree/lower.rs b/openvaf/hir_def/src/item_tree/lower.rs index 0a8f38c8..9baf803a 100644 --- a/openvaf/hir_def/src/item_tree/lower.rs +++ b/openvaf/hir_def/src/item_tree/lower.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use arena::IdxRange; use basedb::{AstId, AstIdMap, FileId}; use syntax::ast::{self, ParamRef, PathSegmentKind}; -use syntax::name::{kw, AsIdent, AsName}; +use syntax::name::{kw, AsIdent, AsName, Name}; use syntax::{match_ast, AstNode, ConstExprValue, WalkEvent}; use typed_index_collections::TiVec; @@ -305,6 +305,10 @@ impl Ctx { } ast::ModuleItem::BranchDecl(branch) => self.lower_branch(branch, dst), ast::ModuleItem::AliasParam(alias) => self.lower_alias_param(alias, dst), + // Genvars are compile-time loop variables; they carry no item-tree + // entity. The genvar `for` loop is unrolled during body lowering, so + // there is nothing to lower here. + ast::ModuleItem::GenvarDecl(_) => {} }; } } @@ -395,6 +399,7 @@ impl Ctx { nodes: &mut TiVec, dst: &mut Vec, ) { + let module = ports.syntax().ancestors().find_map(ast::ModuleDecl::cast); for port in ports.ports() { let ast_id = self.source_ast_id_map.ast_id(&port); match port.kind() { @@ -410,6 +415,34 @@ impl Ctx { dst.push(node.into()) } } + // Vectored/bus port reference `inode[k]` in the module header. The + // index is a compile-time constant; the referenced element is one of + // the scalar nodes expanded from the bus net declaration (named + // `inode[k]`), which we mark as a port here. + ast::ModulePortKind::PortRef(port_ref) => { + let base = match port_ref.name() { + Some(name) => name.as_name(), + None => continue, + }; + let idx = port_ref + .expr() + .as_ref() + .zip(module.as_ref()) + .and_then(|(e, m)| eval_const_int(e, m)); + let name = match idx { + Some(idx) => Name::resolve(&format!("{}[{}]", base, idx)), + None => continue, + }; + if nodes.iter().all(|node| node.name != name) { + let node = nodes.push_and_get_key(Node { + name, + is_port: true, + ast_id: ast_id.into(), + decls: Vec::new(), + }); + dst.push(node.into()) + } + } ast::ModulePortKind::PortDecl(decl) => { self.lower_port_decl(decl, nodes, dst); } @@ -465,26 +498,50 @@ impl Ctx { let ast_id = self.source_ast_id_map.ast_id(&decl); let is_gnd = decl.net_type_token().map_or(false, |it| it.text() == kw::raw::ground); + + // Vectored/bus net declaration `electrical [msb:lsb] inode;` expands into one + // scalar node per index, named `inode[msb]`..`inode[lsb]`. Indices are + // compile-time constants. All later access goes through those scalar nodes. + let bus_range = decl.dimension().and_then(|dim| { + let module = decl.syntax().ancestors().find_map(ast::ModuleDecl::cast)?; + let msb = eval_const_int(&dim.msb()?, &module)?; + let lsb = eval_const_int(&dim.lsb()?, &module)?; + Some((msb, lsb)) + }); + for (name_idx, name) in decl.names().enumerate() { - let name = name.as_name(); - let id = self.tree.data.nets.push_and_get_key(Net { - name: name.clone(), - discipline: discipline.clone(), - ast_id, - is_gnd, - name_idx, - }); + let base = name.as_name(); + let indices: Vec> = match bus_range { + Some((msb, lsb)) => { + let (lo, hi) = if msb <= lsb { (msb, lsb) } else { (lsb, msb) }; + (lo..=hi).map(Some).collect() + } + None => vec![None], + }; + for idx in indices { + let name = match idx { + Some(idx) => Name::resolve(&format!("{}[{}]", base, idx)), + None => base.clone(), + }; + let id = self.tree.data.nets.push_and_get_key(Net { + name: name.clone(), + discipline: discipline.clone(), + ast_id, + is_gnd, + name_idx, + }); - match nodes.iter_mut().find(|node| node.name == name) { - Some(node) => node.decls.push(id.into()), - None => { - let node = nodes.push_and_get_key(Node { - name, - is_port: false, - ast_id: ast_id.into(), - decls: vec![id.into()], - }); - dst.push(node.into()); + match nodes.iter_mut().find(|node| node.name == name) { + Some(node) => node.decls.push(id.into()), + None => { + let node = nodes.push_and_get_key(Node { + name, + is_port: false, + ast_id: ast_id.into(), + decls: vec![id.into()], + }); + dst.push(node.into()); + } } } } diff --git a/openvaf/parser/src/grammar/items/module.rs b/openvaf/parser/src/grammar/items/module.rs index f08da921..dd5837a3 100644 --- a/openvaf/parser/src/grammar/items/module.rs +++ b/openvaf/parser/src/grammar/items/module.rs @@ -10,6 +10,7 @@ const MODULE_ITEM_RECOVERY: TokenSet = DIRECTION_TS.union(TokenSet::new(&[ STRING_KW, REAL_KW, INTEGER_KW, + GENVAR_KW, PARAMETER_KW, LOCALPARAM_KW, ENDMODULE_KW, @@ -40,7 +41,18 @@ const MODULE_PORTS_RECOVERY: TokenSet = TokenSet::new(&[T![;], T![')'], ENDMODUL fn module_ports(p: &mut Parser) { while !p.at_ts(MODULE_PORTS_RECOVERY) { let m = p.start(); - if eat_name(p) { + if p.at(IDENT) && p.nth_at(1, T!['[']) { + // Vectored/bus port reference in the module header, e.g. + // `rc_ladder(inode[0], inode[n])`. The referenced element is resolved + // against the expanded scalar nodes of the bus net declaration. + let pr = p.start(); + name(p); + p.bump(T!['[']); + expr(p); + p.expect(T![']']); + pr.complete(p, PORT_REF); + m.complete(p, MODULE_PORT); + } else if eat_name(p) { m.complete(p, MODULE_PORT); } else if p.at_ts(DIRECTION_TS) || p.at(T!["(*"]) { let inner = p.start(); @@ -150,6 +162,7 @@ fn module_items(p: &mut Parser) { branch_decl(p, m); } INTEGER_KW | REAL_KW | STRING_KW => var_decl(p, m), + GENVAR_KW => genvar_decl(p, m), INPUT_KW | OUTPUT_KW | INOUT_KW => port_decl::(p, m), _ => { error_range = if let Some(error_range) = error_range { @@ -179,6 +192,13 @@ fn module_items(p: &mut Parser) { } } +fn genvar_decl(p: &mut Parser, m: Marker) { + p.bump(GENVAR_KW); + decl_list(p, T![;], decl_name, MODULE_ITEM_OR_ATTR_RECOVERY); + p.eat(T![;]); + m.complete(p, GENVAR_DECL); +} + fn net_decl(p: &mut Parser, m: Marker) { //direction and type ar both optional since only one is required if NET_TYPE_FIRST { @@ -190,6 +210,17 @@ fn net_decl(p: &mut Parser, m: Marker) { name_ref_r(p, MODULE_ITEM_OR_ATTR_RECOVERY.union(TokenSet::unique(T![;]))) } + // Optional vectored/bus range, e.g. `electrical [0:n] inode;`. + if p.at(T!['[']) { + let dim = p.start(); + p.bump(T!['[']); + expr(p); + p.expect(T![:]); + expr(p); + p.expect(T![']']); + dim.complete(p, DIMENSION); + } + net_dec_list(p); p.eat(T![;]); m.complete(p, NET_DECL); diff --git a/openvaf/syntax/src/ast/generated/nodes.rs b/openvaf/syntax/src/ast/generated/nodes.rs index 0881415f..04a925d7 100644 --- a/openvaf/syntax/src/ast/generated/nodes.rs +++ b/openvaf/syntax/src/ast/generated/nodes.rs @@ -411,6 +411,7 @@ impl NetDecl { pub fn net_type_token(&self) -> Option { support::token(&self.syntax, T![net_type]) } + pub fn dimension(&self) -> Option { support::child(&self.syntax) } pub fn names(&self) -> AstChildren { support::children(&self.syntax) } pub fn semicolon_token(&self) -> Option { support::token(&self.syntax, T![;]) } } @@ -478,6 +479,16 @@ impl AliasParam { pub fn semicolon_token(&self) -> Option { support::token(&self.syntax, T![;]) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct GenvarDecl { + pub(crate) syntax: SyntaxNode, +} +impl ast::AttrsOwner for GenvarDecl {} +impl GenvarDecl { + pub fn genvar_token(&self) -> Option { support::token(&self.syntax, T![genvar]) } + pub fn names(&self) -> AstChildren { support::children(&self.syntax) } + pub fn semicolon_token(&self) -> Option { support::token(&self.syntax, T![;]) } +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ModulePort { pub(crate) syntax: SyntaxNode, } @@ -496,6 +507,16 @@ impl PortDecl { pub fn names(&self) -> AstChildren { support::children(&self.syntax) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PortRef { + pub(crate) syntax: SyntaxNode, +} +impl PortRef { + pub fn name(&self) -> Option { support::child(&self.syntax) } + pub fn l_brack_token(&self) -> Option { support::token(&self.syntax, T!['[']) } + pub fn expr(&self) -> Option { support::child(&self.syntax) } + pub fn r_brack_token(&self) -> Option { support::token(&self.syntax, T![']']) } +} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Var { pub(crate) syntax: SyntaxNode, } @@ -621,11 +642,13 @@ pub enum ModuleItem { VarDecl(VarDecl), ParamDecl(ParamDecl), AliasParam(AliasParam), + GenvarDecl(GenvarDecl), } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ModulePortKind { PortDecl(PortDecl), Name(Name), + PortRef(PortRef), } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ParamRef { @@ -1146,6 +1169,17 @@ impl AstNode for AliasParam { } fn syntax(&self) -> &SyntaxNode { &self.syntax } } +impl AstNode for GenvarDecl { + fn can_cast(kind: SyntaxKind) -> bool { kind == GENVAR_DECL } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { &self.syntax } +} impl AstNode for ModulePort { fn can_cast(kind: SyntaxKind) -> bool { kind == MODULE_PORT } fn cast(syntax: SyntaxNode) -> Option { @@ -1168,6 +1202,17 @@ impl AstNode for PortDecl { } fn syntax(&self) -> &SyntaxNode { &self.syntax } } +impl AstNode for PortRef { + fn can_cast(kind: SyntaxKind) -> bool { kind == PORT_REF } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { &self.syntax } +} impl AstNode for Var { fn can_cast(kind: SyntaxKind) -> bool { kind == VAR } fn cast(syntax: SyntaxNode) -> Option { @@ -1497,11 +1542,14 @@ impl From for ModuleItem { impl From for ModuleItem { fn from(node: AliasParam) -> ModuleItem { ModuleItem::AliasParam(node) } } +impl From for ModuleItem { + fn from(node: GenvarDecl) -> ModuleItem { ModuleItem::GenvarDecl(node) } +} impl AstNode for ModuleItem { fn can_cast(kind: SyntaxKind) -> bool { match kind { BODY_PORT_DECL | NET_DECL | ANALOG_BEHAVIOUR | PROCEDURAL_BLOCK | FUNCTION - | BRANCH_DECL | VAR_DECL | PARAM_DECL | ALIAS_PARAM => true, + | BRANCH_DECL | VAR_DECL | PARAM_DECL | ALIAS_PARAM | GENVAR_DECL => true, _ => false, } } @@ -1516,6 +1564,7 @@ impl AstNode for ModuleItem { VAR_DECL => ModuleItem::VarDecl(VarDecl { syntax }), PARAM_DECL => ModuleItem::ParamDecl(ParamDecl { syntax }), ALIAS_PARAM => ModuleItem::AliasParam(AliasParam { syntax }), + GENVAR_DECL => ModuleItem::GenvarDecl(GenvarDecl { syntax }), _ => return None, }; Some(res) @@ -1531,6 +1580,7 @@ impl AstNode for ModuleItem { ModuleItem::VarDecl(it) => &it.syntax, ModuleItem::ParamDecl(it) => &it.syntax, ModuleItem::AliasParam(it) => &it.syntax, + ModuleItem::GenvarDecl(it) => &it.syntax, } } } @@ -1540,10 +1590,13 @@ impl From for ModulePortKind { impl From for ModulePortKind { fn from(node: Name) -> ModulePortKind { ModulePortKind::Name(node) } } +impl From for ModulePortKind { + fn from(node: PortRef) -> ModulePortKind { ModulePortKind::PortRef(node) } +} impl AstNode for ModulePortKind { fn can_cast(kind: SyntaxKind) -> bool { match kind { - PORT_DECL | NAME => true, + PORT_DECL | NAME | PORT_REF => true, _ => false, } } @@ -1551,6 +1604,7 @@ impl AstNode for ModulePortKind { let res = match syntax.kind() { PORT_DECL => ModulePortKind::PortDecl(PortDecl { syntax }), NAME => ModulePortKind::Name(Name { syntax }), + PORT_REF => ModulePortKind::PortRef(PortRef { syntax }), _ => return None, }; Some(res) @@ -1559,6 +1613,7 @@ impl AstNode for ModulePortKind { match self { ModulePortKind::PortDecl(it) => &it.syntax, ModulePortKind::Name(it) => &it.syntax, + ModulePortKind::PortRef(it) => &it.syntax, } } } @@ -1902,6 +1957,11 @@ impl std::fmt::Display for AliasParam { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for GenvarDecl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for ModulePort { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) @@ -1912,6 +1972,11 @@ impl std::fmt::Display for PortDecl { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for PortRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for Var { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/openvaf/syntax/veriloga.ungram b/openvaf/syntax/veriloga.ungram index 159286ea..e30562eb 100644 --- a/openvaf/syntax/veriloga.ungram +++ b/openvaf/syntax/veriloga.ungram @@ -200,10 +200,17 @@ ModuleItem = | VarDecl | ParamDecl | AliasParam +| GenvarDecl ModulePorts = '('ports: (ModulePort (',' ModulePort)*)? ')' ModulePort = kind: ModulePortKind -ModulePortKind = PortDecl| Name +ModulePortKind = PortDecl| Name | PortRef + +PortRef = + Name '[' Expr ']' + +GenvarDecl = + AttrList* 'genvar' (Name (',' Name)*) ';' AnalogBehaviour = AttrList* 'analog' 'initial'? Stmt @@ -241,7 +248,7 @@ Range = NetDecl = - AttrList* discipline:NameRef? 'net_type'? (Name (',' Name)*)';' + AttrList* discipline:NameRef? 'net_type'? Dimension? (Name (',' Name)*)';' BodyPortDecl = PortDecl ';' diff --git a/openvaf/tokens/src/parser/generated.rs b/openvaf/tokens/src/parser/generated.rs index dc02a366..f230276e 100644 --- a/openvaf/tokens/src/parser/generated.rs +++ b/openvaf/tokens/src/parser/generated.rs @@ -72,6 +72,7 @@ pub enum SyntaxKind { FOR_KW, FROM_KW, FUNCTION_KW, + GENVAR_KW, IF_KW, INF_KW, INOUT_KW, @@ -133,6 +134,8 @@ pub enum SyntaxKind { MODULE_DECL, MODULE_PORT, MODULE_PORTS, + PORT_REF, + GENVAR_DECL, NAME, NAME_REF, SYS_FUN, @@ -169,10 +172,11 @@ impl SyntaxKind { match self { ANALOG_KW | BEGIN_KW | BRANCH_KW | CASE_KW | DEFAULT_KW | DISABLE_KW | DISCIPLINE_KW | ELSE_KW | END_KW | ENDCASE_KW | ENDDISCIPLINE_KW | ENDFUNCTION_KW - | ENDMODULE_KW | ENDNATURE_KW | EXCLUDE_KW | FOR_KW | FROM_KW | FUNCTION_KW | IF_KW - | INF_KW | INOUT_KW | INPUT_KW | INTEGER_KW | MODULE_KW | NATURE_KW | OUTPUT_KW - | PARAMETER_KW | LOCALPARAM_KW | REAL_KW | STRING_KW | WHILE_KW | ROOT_KW - | INITIAL_STEP_KW | INITIAL_KW | FINAL_STEP_KW | FINAL_KW | ALIASPARAM_KW => true, + | ENDMODULE_KW | ENDNATURE_KW | EXCLUDE_KW | FOR_KW | FROM_KW | FUNCTION_KW + | GENVAR_KW | IF_KW | INF_KW | INOUT_KW | INPUT_KW | INTEGER_KW | MODULE_KW + | NATURE_KW | OUTPUT_KW | PARAMETER_KW | LOCALPARAM_KW | REAL_KW | STRING_KW + | WHILE_KW | ROOT_KW | INITIAL_STEP_KW | INITIAL_KW | FINAL_STEP_KW | FINAL_KW + | ALIASPARAM_KW => true, _ => false, } } @@ -212,6 +216,7 @@ impl SyntaxKind { "for" => FOR_KW, "from" => FROM_KW, "function" => FUNCTION_KW, + "genvar" => GENVAR_KW, "if" => IF_KW, "inf" => INF_KW, "inout" => INOUT_KW, @@ -337,6 +342,7 @@ impl std::fmt::Display for SyntaxKind { Self::FOR_KW => "'for'", Self::FROM_KW => "'from'", Self::FUNCTION_KW => "'function'", + Self::GENVAR_KW => "'genvar'", Self::IF_KW => "'if'", Self::INF_KW => "'inf'", Self::INOUT_KW => "'inout'", @@ -374,4 +380,4 @@ impl std::fmt::Display for SyntaxKind { } } #[macro_export] -macro_rules ! T { [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; ["_"] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<<] => { $ crate :: SyntaxKind :: ASHL } ; [>>>] => { $ crate :: SyntaxKind :: ASHR } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; ["(*"] => { $ crate :: SyntaxKind :: L_ATTR_PAREN } ; ["*)"] => { $ crate :: SyntaxKind :: R_ATTR_PAREN } ; ["'{"] => { $ crate :: SyntaxKind :: ARR_START } ; [<+] => { $ crate :: SyntaxKind :: CONTR } ; [**] => { $ crate :: SyntaxKind :: POW } ; [~^] => { $ crate :: SyntaxKind :: L_NXOR } ; [^~] => { $ crate :: SyntaxKind :: R_NXOR } ; [analog] => { $ crate :: SyntaxKind :: ANALOG_KW } ; [begin] => { $ crate :: SyntaxKind :: BEGIN_KW } ; [branch] => { $ crate :: SyntaxKind :: BRANCH_KW } ; [case] => { $ crate :: SyntaxKind :: CASE_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [disable] => { $ crate :: SyntaxKind :: DISABLE_KW } ; [discipline] => { $ crate :: SyntaxKind :: DISCIPLINE_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [end] => { $ crate :: SyntaxKind :: END_KW } ; [endcase] => { $ crate :: SyntaxKind :: ENDCASE_KW } ; [enddiscipline] => { $ crate :: SyntaxKind :: ENDDISCIPLINE_KW } ; [endfunction] => { $ crate :: SyntaxKind :: ENDFUNCTION_KW } ; [endmodule] => { $ crate :: SyntaxKind :: ENDMODULE_KW } ; [endnature] => { $ crate :: SyntaxKind :: ENDNATURE_KW } ; [exclude] => { $ crate :: SyntaxKind :: EXCLUDE_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [from] => { $ crate :: SyntaxKind :: FROM_KW } ; [function] => { $ crate :: SyntaxKind :: FUNCTION_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [inf] => { $ crate :: SyntaxKind :: INF_KW } ; [inout] => { $ crate :: SyntaxKind :: INOUT_KW } ; [input] => { $ crate :: SyntaxKind :: INPUT_KW } ; [integer] => { $ crate :: SyntaxKind :: INTEGER_KW } ; [module] => { $ crate :: SyntaxKind :: MODULE_KW } ; [nature] => { $ crate :: SyntaxKind :: NATURE_KW } ; [output] => { $ crate :: SyntaxKind :: OUTPUT_KW } ; [parameter] => { $ crate :: SyntaxKind :: PARAMETER_KW } ; [localparam] => { $ crate :: SyntaxKind :: LOCALPARAM_KW } ; [real] => { $ crate :: SyntaxKind :: REAL_KW } ; [string] => { $ crate :: SyntaxKind :: STRING_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [root] => { $ crate :: SyntaxKind :: ROOT_KW } ; [initial_step] => { $ crate :: SyntaxKind :: INITIAL_STEP_KW } ; [initial] => { $ crate :: SyntaxKind :: INITIAL_KW } ; [final_step] => { $ crate :: SyntaxKind :: FINAL_STEP_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [aliasparam] => { $ crate :: SyntaxKind :: ALIASPARAM_KW } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [net_type] => { $ crate :: SyntaxKind :: NET_TYPE } ; [sysfun] => { $ crate :: SyntaxKind :: SYSFUN } ; } +macro_rules ! T { [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; ["_"] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<<] => { $ crate :: SyntaxKind :: ASHL } ; [>>>] => { $ crate :: SyntaxKind :: ASHR } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; ["(*"] => { $ crate :: SyntaxKind :: L_ATTR_PAREN } ; ["*)"] => { $ crate :: SyntaxKind :: R_ATTR_PAREN } ; ["'{"] => { $ crate :: SyntaxKind :: ARR_START } ; [<+] => { $ crate :: SyntaxKind :: CONTR } ; [**] => { $ crate :: SyntaxKind :: POW } ; [~^] => { $ crate :: SyntaxKind :: L_NXOR } ; [^~] => { $ crate :: SyntaxKind :: R_NXOR } ; [analog] => { $ crate :: SyntaxKind :: ANALOG_KW } ; [begin] => { $ crate :: SyntaxKind :: BEGIN_KW } ; [branch] => { $ crate :: SyntaxKind :: BRANCH_KW } ; [case] => { $ crate :: SyntaxKind :: CASE_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [disable] => { $ crate :: SyntaxKind :: DISABLE_KW } ; [discipline] => { $ crate :: SyntaxKind :: DISCIPLINE_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [end] => { $ crate :: SyntaxKind :: END_KW } ; [endcase] => { $ crate :: SyntaxKind :: ENDCASE_KW } ; [enddiscipline] => { $ crate :: SyntaxKind :: ENDDISCIPLINE_KW } ; [endfunction] => { $ crate :: SyntaxKind :: ENDFUNCTION_KW } ; [endmodule] => { $ crate :: SyntaxKind :: ENDMODULE_KW } ; [endnature] => { $ crate :: SyntaxKind :: ENDNATURE_KW } ; [exclude] => { $ crate :: SyntaxKind :: EXCLUDE_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [from] => { $ crate :: SyntaxKind :: FROM_KW } ; [function] => { $ crate :: SyntaxKind :: FUNCTION_KW } ; [genvar] => { $ crate :: SyntaxKind :: GENVAR_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [inf] => { $ crate :: SyntaxKind :: INF_KW } ; [inout] => { $ crate :: SyntaxKind :: INOUT_KW } ; [input] => { $ crate :: SyntaxKind :: INPUT_KW } ; [integer] => { $ crate :: SyntaxKind :: INTEGER_KW } ; [module] => { $ crate :: SyntaxKind :: MODULE_KW } ; [nature] => { $ crate :: SyntaxKind :: NATURE_KW } ; [output] => { $ crate :: SyntaxKind :: OUTPUT_KW } ; [parameter] => { $ crate :: SyntaxKind :: PARAMETER_KW } ; [localparam] => { $ crate :: SyntaxKind :: LOCALPARAM_KW } ; [real] => { $ crate :: SyntaxKind :: REAL_KW } ; [string] => { $ crate :: SyntaxKind :: STRING_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [root] => { $ crate :: SyntaxKind :: ROOT_KW } ; [initial_step] => { $ crate :: SyntaxKind :: INITIAL_STEP_KW } ; [initial] => { $ crate :: SyntaxKind :: INITIAL_KW } ; [final_step] => { $ crate :: SyntaxKind :: FINAL_STEP_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [aliasparam] => { $ crate :: SyntaxKind :: ALIASPARAM_KW } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [net_type] => { $ crate :: SyntaxKind :: NET_TYPE } ; [sysfun] => { $ crate :: SyntaxKind :: SYSFUN } ; } diff --git a/sourcegen/src/ast/src.rs b/sourcegen/src/ast/src.rs index 72949544..5b233fa6 100644 --- a/sourcegen/src/ast/src.rs +++ b/sourcegen/src/ast/src.rs @@ -77,6 +77,7 @@ pub(crate) const KINDS_SRC: KindsSrc = KindsSrc { "for", "from", "function", + "genvar", "if", "inf", "inout", @@ -132,6 +133,8 @@ pub(crate) const KINDS_SRC: KindsSrc = KindsSrc { "MODULE_DECL", "MODULE_PORT", "MODULE_PORTS", + "PORT_REF", + "GENVAR_DECL", "NAME", "NAME_REF", "SYS_FUN", From 1eb16da7c64c247f35da181a86441c7036fef833 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 14:12:04 +0200 Subject: [PATCH 11/19] transition(): continuous slew-limited realization (Level-2) The Level-1 `transition()` returned the target value instantaneously, producing a time discontinuity that aborts transient analysis ("timestep too small") the moment an input crosses a threshold -- so event-driven mixed-signal models (gates, comparators) compiled and loaded but could not be simulated. Realize `transition(expr, delay, rise, fall)` as a first-order lag (slew-limited follower) via an implicit equation: dx/dt = (target - x)/tau, with tau = rise when the target is increasing and fall when decreasing (floored to avoid /0). The output is continuous, so the transient integrator steps through switching events cleanly, at the requested transition speed. The `no_equations` path (AC/noise/op-var setup) still passes the target through directly. Verified in VACASK: the SimetriX AND gate now simulates a full transient -- output tracks A AND B, toggling with smooth continuous edges and no abort (previously "timestep too small" at the first threshold crossing). Scorecard still 9/9 compile+boot (the AND gate gains one transition state node); compiler suite green (190 passed, 0 failed). Note: sequential event logic (e.g. Schmitt-trigger hysteresis) now also runs without aborting, but correct hysteresis additionally needs true edge-triggered @(cross) with cross-timestep state retention, which is not yet implemented (an integer set in a @(cross) handler does not persist across timesteps, so such models behave as a plain comparator). That is the remaining Level-2 @(cross) piece. --- openvaf/hir_lower/src/expr.rs | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index 78549637..eee95e13 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -759,11 +759,39 @@ impl BodyLoweringCtx<'_, '_, '_> { res }*/ BuiltIn::transition => { - // Instantaneous approximation: ignore delay/rise/fall and return the - // target value. `transition`'s first argument is typed Integer but the - // operator returns Real, so cast to keep the MIR well-typed. - let val = self.lower_expr(args[0]); - self.ctx.insert_cast(val, &Type::Integer, &Type::Real) + // `transition`'s first argument is typed Integer but the operator + // returns Real, so cast to keep the MIR well-typed. + let target = self.lower_expr(args[0]); + let target = self.ctx.insert_cast(target, &Type::Integer, &Type::Real); + if self.ctx.no_equations { + // No DAE context (AC/noise setup, op-vars): pass the target through. + target + } else { + // Continuous (slew-limited) realization. The ideal `transition` is a + // piecewise-linear ramp from the old value to the new one over the + // rise/fall time; emitting it as an instantaneous jump produces a + // time discontinuity the transient integrator cannot step across + // ("timestep too small"). We realize it as a first-order lag whose + // time constant is the rise time when the target is increasing and + // the fall time when decreasing — a continuous output the solver + // integrates through, with the requested transition speed. + let eps = self.ctx.fconst(1e-12); + let rise = if args.len() > 2 { self.lower_expr(args[2]) } else { eps }; + let fall = if args.len() > 3 { self.lower_expr(args[3]) } else { rise }; + let (eq, x) = + self.ctx.implicit_equation(ImplicitEquationKind::Idt(IdtKind::Basic)); + // tau = (target >= x) ? rise : fall, floored to eps to avoid /0. + let rising = self.ctx.ins().fge(target, x); + let tau = self.ctx.make_select(rising, |_s, b| if b { rise } else { fall }); + let tau_ok = self.ctx.ins().fge(tau, eps); + let tau = self.ctx.make_select(tau_ok, |_s, b| if b { tau } else { eps }); + // dx/dt = (target - x)/tau -> react = x, resist = (x - target)/tau. + let diff = self.ctx.ins().fsub(x, target); + let resist = self.ctx.ins().fdiv(diff, tau); + self.ctx.def_resist_residual(resist, eq); + self.ctx.def_react_residual(x, eq); + x + } } BuiltIn::slew | BuiltIn::limit | BuiltIn::absdelay => self.lower_expr(args[0]), From 39c657f282aa4bf734ae83328a474e6c84868ac2 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 16:37:34 +0200 Subject: [PATCH 12/19] @(cross) state retention: cross-timestep latch state via prev_state/next_state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@(cross)` event handlers were lowered unconditionally with no memory, so a variable they assign could not hold its value between events — hysteretic / sequential mixed-signal models (Schmitt trigger, comparators with memory) compiled but behaved as plain comparators. Give each variable assigned inside an `@(cross)` handler a retained state backed by the OSDI `prev_state`/`next_state` arrays (the same per-instance, cross-timestep store the limiting machinery uses): - Preserve `@(cross)`/`@(timer)` as an `Event::Cross` `EventControl` in HIR (previously the monitored-event body was inlined and the event discarded). - In MIR lowering, detect the variables such handlers assign; for each, allocate a retained limit-state slot, initialise the variable's place from its previous accepted value (`PrevState`), and store the final value back (`StoreLimit`) at the end of the analog block. The slot reuses all existing OSDI state codegen (`num_states`, `state_idx`, `store_lim`) but is keyed on a synthetic constant and marked retained, so the limit-specific derivative/value passes (1 in hir_lower, the limit-rhs pass in sim_back) skip it. - An `@(initial_step)` assignment to a retained variable becomes its initial value (loaded from the state) instead of a per-evaluation reset; non-retained `@(initial_step)` work (e.g. Butterworth's filter coefficients) is unchanged. - `StoreLimit` is now marked side-effecting: it writes `next_state`, so it must not be eliminated when the stored value is otherwise unused (retained state). The limit path already used the return value, so its behaviour is unchanged (only the callback's `const`-ness in MIR dumps differs; snapshots updated). Verified in VACASK: the Schmitt trigger now shows true hysteresis — output switches HIGH as V(in) rises past vhi (0.6) and LOW only as it falls past vlo (0.4), holding in the dead-band across timesteps (previously it toggled at 0.6 both ways). The AND gate still computes A&B, Butterworth AC is still exact (0.0000 dB), the full scorecard is 9/9 compile+boot, and the compiler suite is green (190 passed). --- openvaf/hir/src/body.rs | 2 +- openvaf/hir/src/lib.rs | 3 +- openvaf/hir_def/src/body/lower.rs | 10 ++- openvaf/hir_def/src/expr.rs | 3 + openvaf/hir_lower/src/body.rs | 75 +++++++++++++++++++- openvaf/hir_lower/src/callbacks.rs | 5 +- openvaf/hir_lower/src/ctx.rs | 39 +++++++++- openvaf/hir_lower/src/lib.rs | 13 +++- openvaf/hir_lower/src/stmt.rs | 34 ++++++++- openvaf/sim_back/src/dae/builder.rs | 5 ++ openvaf/test_data/dae/lim_rhs_mir.snap | 2 +- openvaf/test_data/dae/lim_rhs_react_mir.snap | 2 +- openvaf/test_data/dae/lim_rhs_sign_mir.snap | 2 +- 13 files changed, 181 insertions(+), 14 deletions(-) diff --git a/openvaf/hir/src/body.rs b/openvaf/hir/src/body.rs index f5e44791..2dc6639d 100644 --- a/openvaf/hir/src/body.rs +++ b/openvaf/hir/src/body.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use hir_def::db::HirDefDB; -pub use hir_def::expr::Event; +pub use hir_def::expr::{Event, GlobalEvent}; use hir_def::DefWithBodyId; pub use hir_def::{/*expr::CaseCond,*/ BuiltIn, Case, ExprId, Literal, ParamSysFun, StmtId, Type,}; use hir_ty::db::HirTyDB; diff --git a/openvaf/hir/src/lib.rs b/openvaf/hir/src/lib.rs index 892509fe..f10f9538 100644 --- a/openvaf/hir/src/lib.rs +++ b/openvaf/hir/src/lib.rs @@ -37,7 +37,8 @@ pub use syntax::name::Name; pub use crate::attributes::AstCache; pub use crate::body::{ - AssignmentLhs, Body, BodyRef, ContributeKind, Expr, ExprId, Ref, ResolvedFun, Stmt, StmtId, + AssignmentLhs, Body, BodyRef, ContributeKind, Event, Expr, ExprId, GlobalEvent, Ref, + ResolvedFun, Stmt, StmtId, }; pub use crate::db::CompilationDB; diff --git a/openvaf/hir_def/src/body/lower.rs b/openvaf/hir_def/src/body/lower.rs index 47c4aa4c..f8798ec9 100644 --- a/openvaf/hir_def/src/body/lower.rs +++ b/openvaf/hir_def/src/body/lower.rs @@ -186,7 +186,15 @@ impl LowerCtx<'_> { } else if event_stmt.final_step_token().is_some() { GlobalEvent::FinalStep } else { - return self.collect_opt_stmt(event_stmt.stmt()); + // Monitored event (`@(cross(...))` / `@(timer(...))`): preserve it so MIR + // lowering can give the variables it assigns cross-timestep retention. + let body = self.collect_opt_stmt(event_stmt.stmt()); + let stmt = Stmt::EventControl { event: Event::Cross, body }; + return self.alloc_stmt( + stmt, + AstPtr::new(event_stmt).cast().unwrap(), + event_stmt.attrs(), + ); }; let phases = event_stmt.sim_phases().map(|lit| lit.unescaped_value()).collect(); diff --git a/openvaf/hir_def/src/expr.rs b/openvaf/hir_def/src/expr.rs index d0d1d650..7ac41aeb 100644 --- a/openvaf/hir_def/src/expr.rs +++ b/openvaf/hir_def/src/expr.rs @@ -153,6 +153,9 @@ pub enum GlobalEvent { #[non_exhaustive] pub enum Event { Global { kind: GlobalEvent, phases: Vec }, + /// A monitored analog event such as `@(cross(...))` / `@(timer(...))`. Variables + /// assigned inside its body are given cross-timestep retention during lowering. + Cross, } #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/openvaf/hir_lower/src/body.rs b/openvaf/hir_lower/src/body.rs index 6a5b04e8..aa9ab535 100644 --- a/openvaf/hir_lower/src/body.rs +++ b/openvaf/hir_lower/src/body.rs @@ -1,10 +1,10 @@ -use hir::{BodyRef, ExprId, Node}; +use hir::{AssignmentLhs, BodyRef, Event, ExprId, Node, Stmt, StmtId, Type, Variable}; use mir::builder::InstBuilder; use mir::{Block, Value}; use stdx::iter::zip; use crate::ctx::LoweringCtx; -use crate::ParamKind; +use crate::{CallBackKind, ParamKind, PlaceKind}; pub struct BodyLoweringCtx<'a, 'c1, 'c2> { pub ctx: &'a mut LoweringCtx<'c1, 'c2>, @@ -14,9 +14,80 @@ pub struct BodyLoweringCtx<'a, 'c1, 'c2> { impl<'c1, 'c2> BodyLoweringCtx<'_, 'c1, 'c2> { pub fn lower_entry_stmts(&mut self) { + // Pre-pass: find variables assigned inside `@(cross)` handlers. Each is backed + // by a retained limit-state slot so it holds its value across timesteps (true + // latch/event semantics, e.g. a Schmitt trigger). The variable starts each + // evaluation at its previous accepted value. + let mut retained: Vec = Vec::new(); + for &stmnt in self.body.entry() { + self.collect_cross_assigned(stmnt, false, &mut retained); + } + let mut seen = ahash::AHashSet::new(); + retained.retain(|v| seen.insert(*v)); + + if !self.ctx.no_equations { + for &var in &retained { + let state = self.ctx.alloc_retained_state(); + let prev = self.ctx.retained_prev(state); + let ty = var.ty(self.ctx.db); + let init = self.ctx.insert_cast(prev, &Type::Real, &ty); + self.ctx.def_place(PlaceKind::Var(var), init); + self.ctx.retained_states.insert(var, state); + } + } + for &stmnt in self.body.entry() { self.lower_stmt(stmnt) } + + // Post-pass: store each retained variable's final value for the next timestep. + if !self.ctx.no_equations && !self.ctx.retained_states.is_empty() { + let states: Vec<(Variable, crate::LimitState)> = + self.ctx.retained_states.iter().map(|(&v, &s)| (v, s)).collect(); + for (var, state) in states { + let ty = var.ty(self.ctx.db); + let v_final = self.ctx.use_place(PlaceKind::Var(var)); + let as_real = self.ctx.insert_cast(v_final, &ty, &Type::Real); + self.ctx.store_retained(state, as_real); + } + } + } + + /// Recursively collect variables assigned inside `@(cross)` event handlers. + fn collect_cross_assigned(&self, stmnt: StmtId, in_cross: bool, dst: &mut Vec) { + let stmt = match self.body.get_stmt(stmnt) { + Some(stmt) => stmt, + None => return, + }; + match stmt { + Stmt::Assignment { lhs, .. } if in_cross => match lhs { + AssignmentLhs::Variable(var) => dst.push(var), + AssignmentLhs::ArrayElement { var, .. } => dst.push(var), + _ => {} + }, + Stmt::Assignment { .. } | Stmt::Expr(_) | Stmt::Contribute { .. } => {} + Stmt::EventControl { event, body } => { + let inner = in_cross || matches!(event, Event::Cross); + self.collect_cross_assigned(body, inner, dst); + } + Stmt::Block { body } => { + for &s in body { + self.collect_cross_assigned(s, in_cross, dst); + } + } + Stmt::If { then_branch, else_branch, .. } => { + self.collect_cross_assigned(then_branch, in_cross, dst); + self.collect_cross_assigned(else_branch, in_cross, dst); + } + Stmt::ForLoop { body, .. } | Stmt::WhileLoop { body, .. } => { + self.collect_cross_assigned(body, in_cross, dst); + } + Stmt::Case { case_arms, .. } => { + for arm in case_arms { + self.collect_cross_assigned(arm.body, in_cross, dst); + } + } + } } pub fn nodes_from_args( diff --git a/openvaf/hir_lower/src/callbacks.rs b/openvaf/hir_lower/src/callbacks.rs index 5c17e700..82720f75 100644 --- a/openvaf/hir_lower/src/callbacks.rs +++ b/openvaf/hir_lower/src/callbacks.rs @@ -119,7 +119,10 @@ impl CallBackKind { name: format!("$store[{state:?}]"), params: 1, returns: 1, - has_sideeffects: false, + // Writing `next_state` is a side effect: when the stored value is not + // otherwise used (retained `@(cross)` state) the call must not be + // eliminated. The limit path still uses the return value as before. + has_sideeffects: true, }, CallBackKind::LimDiscontinuity => FunctionSignature { name: "$discontinuty[-1]".to_owned(), diff --git a/openvaf/hir_lower/src/ctx.rs b/openvaf/hir_lower/src/ctx.rs index 9aab3a84..4dfccbd6 100644 --- a/openvaf/hir_lower/src/ctx.rs +++ b/openvaf/hir_lower/src/ctx.rs @@ -1,4 +1,4 @@ -use ahash::AHashSet; +use ahash::{AHashMap, AHashSet}; use hir::{CompilationDB, Node, Type, Variable}; use mir::builder::{InsertBuilder, InstBuilder}; use mir::{ @@ -25,8 +25,19 @@ pub struct LoweringCtx<'a, 'c> { /// but necessary to avoid accidental correlation/opimization. /// For example white_noise(x) - white_noise(x) is not zero. pub num_noise_sources: u32, + /// Variables assigned inside `@(cross)` handlers, each mapped to the limit-state + /// slot that stores its value across timesteps (latch / event retention). + pub retained_states: AHashMap, + /// True while lowering an `@(initial_step)` body: resets of retained variables + /// there are their initial value (read from the retained state), not a + /// per-evaluation reset. + pub in_initial_step: bool, } +/// Synthetic constant base used as the (non-parameter) `lim_state` key for retained +/// `@(cross)` slots, chosen to not collide with ordinary integer literals. +const RETAINED_STATE_KEY_BASE: i32 = 0x5E7A_0000; + impl<'a, 'c> LoweringCtx<'a, 'c> { pub fn new( db: &'a CompilationDB, @@ -43,6 +54,8 @@ impl<'a, 'c> LoweringCtx<'a, 'c> { inside_lim: false, intern, num_noise_sources: 0, + retained_states: AHashMap::default(), + in_initial_step: false, } } @@ -223,6 +236,30 @@ impl<'a, 'c> LoweringCtx<'a, 'c> { val } + /// Allocate a limit-state slot used purely to retain a value across timesteps + /// (the latch state of an `@(cross)` variable). It reuses the limit state-array + /// machinery (`prev_state`/`next_state`) but is keyed on a synthetic constant and + /// marked retained, so the limit-specific passes skip it. + pub fn alloc_retained_state(&mut self) -> LimitState { + let idx = self.intern.lim_state.len() as i32; + let key = self.iconst(RETAINED_STATE_KEY_BASE.wrapping_add(idx)); + let dst = self.intern.lim_state.raw.entry(key); + let state = LimitState::from(dst.index()); + dst.or_default().push((F_ZERO, false)); + self.intern.retained_lim_states.insert(state); + state + } + + /// Read the value retained from the previous accepted timestep. + pub fn retained_prev(&mut self, state: LimitState) -> Value { + self.use_param(ParamKind::PrevState(state)) + } + + /// Store `val` as the retained value for the next timestep. + pub fn store_retained(&mut self, state: LimitState, val: Value) { + self.call1(CallBackKind::StoreLimit(state), &[val]); + } + pub fn implicit_equation(&mut self, kind: ImplicitEquationKind) -> (ImplicitEquation, Value) { let equation = self.intern.implicit_equations.push_and_get_key(kind); let place = self.dec_place(PlaceKind::CollapseImplicitEquation(equation)); diff --git a/openvaf/hir_lower/src/lib.rs b/openvaf/hir_lower/src/lib.rs index b337fbc6..267d094c 100644 --- a/openvaf/hir_lower/src/lib.rs +++ b/openvaf/hir_lower/src/lib.rs @@ -241,6 +241,11 @@ pub struct HirInterner { pub tagged_reads: IndexMap>, pub implicit_equations: TiVec, pub lim_state: TiMap>, + /// Limit-state slots that actually back `@(cross)` retained variables (latch + /// state stored across timesteps). They reuse the limit state-array machinery + /// but carry no limit function, so the limit-specific derivative/value passes + /// must skip them. + pub retained_lim_states: ahash::AHashSet, } pub type LiveParams<'a> = FilterMap< @@ -258,6 +263,7 @@ impl Default for HirInterner { tagged_reads: IndexMap::with_hasher(BuildHasherDefault::::default()), implicit_equations: TiVec::default(), lim_state: TiMap::default(), + retained_lim_states: ahash::AHashSet::default(), } } } @@ -329,7 +335,12 @@ impl HirInterner { } } - for (param, vals) in self.lim_state.iter() { + for (state, (param, vals)) in self.lim_state.iter_enumerated() { + // Retained `@(cross)` slots are not limited node voltages; their key is a + // synthetic constant, so skip the limit derivative handling for them. + if self.retained_lim_states.contains(&state) { + continue; + } for &(val, neg) in vals { let param = func.dfg.value_def(*param).unwrap_param(); diff --git a/openvaf/hir_lower/src/stmt.rs b/openvaf/hir_lower/src/stmt.rs index e032d5f7..1f031cb2 100644 --- a/openvaf/hir_lower/src/stmt.rs +++ b/openvaf/hir_lower/src/stmt.rs @@ -17,11 +17,39 @@ impl BodyLoweringCtx<'_, '_, '_> { Stmt::Expr(expr) => { self.lower_expr(expr); } - Stmt::EventControl { body, .. } => { - // TODO handle porperly - self.lower_stmt(body); + Stmt::EventControl { event, body } => { + // Track `@(initial_step)` so resets of retained (`@cross`) variables + // inside it are treated as initial values (read from the retained + // state) rather than per-evaluation resets. Other events lower their + // body directly; their effect is gated by guards in the body. + if matches!( + event, + hir::Event::Global { kind: hir::GlobalEvent::InitialStep, .. } + ) { + let prev = self.ctx.in_initial_step; + self.ctx.in_initial_step = true; + self.lower_stmt(body); + self.ctx.in_initial_step = prev; + } else { + self.lower_stmt(body); + } } Stmt::Assignment { lhs, rhs } => { + // A retained variable's `@(initial_step)` reset is its initial value + // (already loaded from the retained state); skip it so it is not + // re-applied on every evaluation. + if self.ctx.in_initial_step { + let retained = match &lhs { + hir::AssignmentLhs::Variable(var) + | hir::AssignmentLhs::ArrayElement { var, .. } => { + self.ctx.retained_states.contains_key(var) + } + _ => false, + }; + if retained { + return; + } + } let val_ = self.lower_expr(rhs); match lhs { hir::AssignmentLhs::ArrayElement { var, index } => { diff --git a/openvaf/sim_back/src/dae/builder.rs b/openvaf/sim_back/src/dae/builder.rs index 3c6b6d4b..847ffa65 100644 --- a/openvaf/sim_back/src/dae/builder.rs +++ b/openvaf/sim_back/src/dae/builder.rs @@ -224,6 +224,11 @@ impl<'a> Builder<'a> { ) { for residual in &mut self.system.residual { for (state, (unchanged, lim_vals)) in self.intern.lim_state.iter_enumerated() { + // Retained `@(cross)` slots reuse the state array but carry no limit + // function; they take no part in limit-rhs construction. + if self.intern.retained_lim_states.contains(&state) { + continue; + } for &(val, neg) in lim_vals { let unknown = if let Some(unknown) = derivative_info.unknowns.index(&val) { unknown diff --git a/openvaf/test_data/dae/lim_rhs_mir.snap b/openvaf/test_data/dae/lim_rhs_mir.snap index 6297143b..cfc218f6 100644 --- a/openvaf/test_data/dae/lim_rhs_mir.snap +++ b/openvaf/test_data/dae/lim_rhs_mir.snap @@ -1,6 +1,6 @@ function %(v16, v17, v18, v19, v20, v36, v46) { inst0 = const fn %$limit[Spur(1)](2) -> 1 - inst1 = const fn %$store[lim_state0](1) -> 1 + inst1 = fn %$store[lim_state0](1) -> 1 block5: @0009 br v20, block2, block4 diff --git a/openvaf/test_data/dae/lim_rhs_react_mir.snap b/openvaf/test_data/dae/lim_rhs_react_mir.snap index 9b9753c2..cd6d3264 100644 --- a/openvaf/test_data/dae/lim_rhs_react_mir.snap +++ b/openvaf/test_data/dae/lim_rhs_react_mir.snap @@ -1,6 +1,6 @@ function %(v16, v17, v18, v19, v20, v37, v45) { inst0 = const fn %$limit[Spur(1)](2) -> 1 - inst1 = const fn %$store[lim_state0](1) -> 1 + inst1 = fn %$store[lim_state0](1) -> 1 inst2 = const fn %ddt(1) -> 1 block5: diff --git a/openvaf/test_data/dae/lim_rhs_sign_mir.snap b/openvaf/test_data/dae/lim_rhs_sign_mir.snap index 99723dbf..f2f82af9 100644 --- a/openvaf/test_data/dae/lim_rhs_sign_mir.snap +++ b/openvaf/test_data/dae/lim_rhs_sign_mir.snap @@ -1,6 +1,6 @@ function %(v16, v19, v20, v21, v25, v30, v44, v64) { inst0 = const fn %$limit[Spur(1)](2) -> 1 - inst1 = const fn %$store[lim_state0](1) -> 1 + inst1 = fn %$store[lim_state0](1) -> 1 v3 = fconst 0.0 v6 = fconst 0x1.0000000000000p0 From b3bc1efa562042010655fb0cf857268201f995fa Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Wed, 24 Jun 2026 22:53:05 +0200 Subject: [PATCH 13/19] transition(): accept real first argument The Verilog-A LRM allows transition()'s input expression to be real, not just integer (e.g. SimetriX/issue #71's inverter that smooths a real 'target' between logic levels). The builtin signature typed the first argument as Integer, rejecting real-valued targets. Type all TRANSITION overloads' first argument as Real; integer/bool arguments still coerce via the normal widening path in lower_expr, so the manual Integer->Real cast in the transition lowering is removed (it would otherwise double-cast a coerced integer arg and emit an ifcast on a real). Verified: issue #71's inverter_va compiles and runs in VACASK with a smooth slew-limited output (no discontinuity); the integer-arg Schmitt/AND models still compile; full suite green. --- openvaf/hir_lower/src/expr.rs | 6 +++--- openvaf/hir_ty/src/builtin.rs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index eee95e13..22abff22 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -759,10 +759,10 @@ impl BodyLoweringCtx<'_, '_, '_> { res }*/ BuiltIn::transition => { - // `transition`'s first argument is typed Integer but the operator - // returns Real, so cast to keep the MIR well-typed. + // `transition` accepts an integer or real first argument; the + // builtin signature types it as Real, so `lower_expr` already widens + // an integer/bool argument to Real for us (the operator returns Real). let target = self.lower_expr(args[0]); - let target = self.ctx.insert_cast(target, &Type::Integer, &Type::Real); if self.ctx.no_equations { // No DAE context (AC/noise setup, op-vars): pass the target through. target diff --git a/openvaf/hir_ty/src/builtin.rs b/openvaf/hir_ty/src/builtin.rs index 1cf2cef3..51f5abef 100644 --- a/openvaf/hir_ty/src/builtin.rs +++ b/openvaf/hir_ty/src/builtin.rs @@ -263,11 +263,11 @@ bultins! { TRANSITION = const { - fn TRANSITION_NO_ARGS(Val(Integer)) -> Real; - fn TRANSITION_DELAY(Val(Integer),Val(Real)) -> Real; - fn TRANSITION_DELAY_RISET(Val(Integer),Val(Real)) -> Real; - fn TRANSITION_DELAY_RISET_FALLT(Val(Integer),Val(Real),Val(Real)) -> Real; - fn TRANSITION_DELAY_RISET_FALLT_TOL(Val(Integer),Val(Real),Val(Real), Val(Real)) -> Real; + fn TRANSITION_NO_ARGS(Val(Real)) -> Real; + fn TRANSITION_DELAY(Val(Real),Val(Real)) -> Real; + fn TRANSITION_DELAY_RISET(Val(Real),Val(Real)) -> Real; + fn TRANSITION_DELAY_RISET_FALLT(Val(Real),Val(Real),Val(Real)) -> Real; + fn TRANSITION_DELAY_RISET_FALLT_TOL(Val(Real),Val(Real),Val(Real), Val(Real)) -> Real; } From df8ea39201bf05caa42b99df8f8190333326018c Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Thu, 25 Jun 2026 05:19:51 +0200 Subject: [PATCH 14/19] tests: behavioral regression tests for arrays and @(cross) retention The recently added features (fixed-size arrays, laplace_nd, genvar/bus nodes, transition, @(cross) state retention) had no in-repo coverage -- only manual VACASK checks -- so a refactor could silently break them. Add two behavioral tests built on the existing in-process OSDI harness (compile -> dlopen -> MockSimulation), which asserts loaded DAE residuals and Jacobian without needing an external simulator: - arrays.va / test_arrays: array declaration, constant- and runtime-index read/write all assemble one conductance (G=21); the loaded residual and Jacobian must match exactly, proving the array lowering computes correctly. - cross_latch.va / test_cross_latch: a @(cross) latch whose state is assigned only inside cross handlers. Using the harness's prev/next state swap (next_iter) to advance timesteps, it drives high/dead-band/low and checks the output is retained across steps (-1,-1,0,0,-1) -- exercising the prev_state/next_state retention mechanism end to end. The descriptor snapshot records '1 states' for the retained slot. Both run in-process under the existing integration harness; full suite green. --- openvaf/openvaf/tests/integration.rs | 73 ++++++++++++++++++++++++- openvaf/test_data/osdi/arrays.snap | 14 +++++ openvaf/test_data/osdi/arrays.va | 34 ++++++++++++ openvaf/test_data/osdi/cross_latch.snap | 9 +++ openvaf/test_data/osdi/cross_latch.va | 21 +++++++ 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 openvaf/test_data/osdi/arrays.snap create mode 100644 openvaf/test_data/osdi/arrays.va create mode 100644 openvaf/test_data/osdi/cross_latch.snap create mode 100644 openvaf/test_data/osdi/cross_latch.va diff --git a/openvaf/openvaf/tests/integration.rs b/openvaf/openvaf/tests/integration.rs index df94470b..36b40cce 100644 --- a/openvaf/openvaf/tests/integration.rs +++ b/openvaf/openvaf/tests/integration.rs @@ -10,7 +10,7 @@ use openvaf::{CompilationDestination, CompilationTermination, LLVMCodeGenOptLeve use stdx::{ignore_dev_tests, openvaf_test_data, project_root}; use target::spec::Target; -use crate::load::{load_osdi_lib, EvalFlags, OsdiDescriptor}; +use crate::load::{load_osdi_lib, EvalFlags, OsdiDescriptor, OsdiInstance, OsdiModel}; use crate::mock_sim::{MockSimulation, ALPHA}; mod load; @@ -255,6 +255,75 @@ fn test_noise() -> Result<()> { Ok(()) } +/// Fixed-size arrays: declaration, constant- and runtime-index read/write all +/// feed a single conductance. See `arrays.va`; with the default `sel=1` the +/// assembled conductance is G = 21, so the loaded DAE residual/Jacobian must +/// match exactly if every array access lowered correctly. +fn test_arrays() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + + let desc = test_descriptor(&openvaf_test_data("osdi").join("arrays.va"))?; + let model = desc.new_model(); + model.process_params()?; + let mut instance = model.new_instance(); + let mut sim = instance.mock_simulation(&model, desc.num_terminals, 300.0)?; + + sim.set_voltage("p", 1.0); + sim.set_voltage("n", 0.0); + instance.eval(&model, &mut sim, EvalFlags::empty()); + instance.load_dae(&model, &mut sim); + + // G = gsum (13) + gx (8) = 21, with I(p,n) = G * V(p,n). + float_cmp::assert_approx_eq!(f64, sim.read_residual("p").0, 21.0, epsilon = 1e-9); + float_cmp::assert_approx_eq!(f64, sim.read_residual("n").0, -21.0, epsilon = 1e-9); + float_cmp::assert_approx_eq!(f64, sim.read_jacobian("p", "p").0, 21.0, epsilon = 1e-9); + Ok(()) +} + +/// `@(cross)` state retention: `state` is assigned only inside cross handlers, so +/// it must hold across timesteps. The mock simulator's `next_iter` swaps the +/// prev/next state arrays, exactly as a real simulator advances a timestep. We +/// drive the input high/low/dead-band and check the latched output is retained. +/// See `cross_latch.va`; residual at q equals `-state` (with V(q)=0). +fn test_cross_latch() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + + let desc = test_descriptor(&openvaf_test_data("osdi").join("cross_latch.va"))?; + let model = desc.new_model(); + model.process_params()?; + let mut instance = model.new_instance(); + let mut sim = instance.mock_simulation(&model, desc.num_terminals, 300.0)?; + + // Advance one timestep: swap prev/next state, re-apply the node voltages + // (next_iter zeroes the solution), evaluate, and load the DAE residual. + let step = |instance: &OsdiInstance, model: &OsdiModel, sim: &mut MockSimulation, vd: f64, first: bool| { + if !first { + sim.next_iter(); + } + sim.set_voltage("q", 0.0); + sim.set_voltage("d", vd); + instance.eval(model, sim, EvalFlags::ENABLE_LIM | EvalFlags::INIT_LIM); + instance.load_dae(model, sim); + sim.read_residual("q").0 + }; + + // d high -> latch sets state=1 (residual = -1). + float_cmp::assert_approx_eq!(f64, step(&instance, &model, &mut sim, 1.0, true), -1.0, epsilon = 1e-9); + // dead-band -> state 1 retained. + float_cmp::assert_approx_eq!(f64, step(&instance, &model, &mut sim, 0.5, false), -1.0, epsilon = 1e-9); + // d low -> latch clears state=0 (residual = 0). + float_cmp::assert_approx_eq!(f64, step(&instance, &model, &mut sim, 0.0, false), 0.0, epsilon = 1e-9); + // dead-band -> state 0 retained. + float_cmp::assert_approx_eq!(f64, step(&instance, &model, &mut sim, 0.5, false), 0.0, epsilon = 1e-9); + // d high again -> latch flips back to state=1. + float_cmp::assert_approx_eq!(f64, step(&instance, &model, &mut sim, 1.0, false), -1.0, epsilon = 1e-9); + Ok(()) +} + harness! { // TODO: run this in CI, somehow this test is flakey tough regarding the linker invocation (and really slow) Test::from_dir("integration", &integration_test, &ignore_dev_tests, &project_root().join("integration_tests")), @@ -264,5 +333,5 @@ harness! { Test::from_dir_filtered("vacask_spice", &vacask_spice_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice")), // VACASK simplified SPICE models Test::from_dir_filtered("vacask_spice_sn", &vacask_spice_sn_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice/sn")), - [Test::new("$limit", &test_limit),Test::new("noise", &test_noise)] + [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch)] } diff --git a/openvaf/test_data/osdi/arrays.snap b/openvaf/test_data/osdi/arrays.snap new file mode 100644 index 00000000..496ec49f --- /dev/null +++ b/openvaf/test_data/osdi/arrays.snap @@ -0,0 +1,14 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) +param "sel" +units = "", desc = "", flags = ParameterFlags(PARA_TY_INT) + +2 terminals +node "p" units = "V", runits = "A" +node "n" units = "V", runits = "A" +jacobian (p, p) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (p, n) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (n, p) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (n, n) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +0 states +has bound_step false diff --git a/openvaf/test_data/osdi/arrays.va b/openvaf/test_data/osdi/arrays.va new file mode 100644 index 00000000..1398e1fc --- /dev/null +++ b/openvaf/test_data/osdi/arrays.va @@ -0,0 +1,34 @@ +`include "disciplines.vams" + +// Exercises fixed-size array support: declaration, constant-index read/write, +// and runtime-index (parameter-driven) read/write. The module behaves as a +// linear conductance whose value is assembled entirely through array accesses, +// so the loaded DAE residual/Jacobian directly reflect that the array lowering +// computed the right number. +// +// With sel = 1 the conductance works out to: +// g = [1, 2, 4] (constant-index writes) +// g[sel] = 8 -> g[1]=8 (runtime-index write) => g = [1, 8, 4] +// gsum = g[0]+g[1]+g[2] = 13 (constant-index reads) +// gx = g[sel] = g[1] = 8 (runtime-index read) +// G = gsum + gx = 21 +module arrays(p, n); + inout p, n; + electrical p, n; + + parameter integer sel = 1; + + real g[0:2]; + real gsum; + real gx; + + analog begin + g[0] = 1.0; + g[1] = 2.0; + g[2] = 4.0; + g[sel] = 8.0; + gsum = g[0] + g[1] + g[2]; + gx = g[sel]; + I(p, n) <+ (gsum + gx) * V(p, n); + end +endmodule diff --git a/openvaf/test_data/osdi/cross_latch.snap b/openvaf/test_data/osdi/cross_latch.snap new file mode 100644 index 00000000..5f48ec8f --- /dev/null +++ b/openvaf/test_data/osdi/cross_latch.snap @@ -0,0 +1,9 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) + +2 terminals +node "d" units = "V", runits = "A" +node "q" units = "V", runits = "A" +jacobian (q, q) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +1 states +has bound_step false diff --git a/openvaf/test_data/osdi/cross_latch.va b/openvaf/test_data/osdi/cross_latch.va new file mode 100644 index 00000000..fe0c0da0 --- /dev/null +++ b/openvaf/test_data/osdi/cross_latch.va @@ -0,0 +1,21 @@ +`include "disciplines.vams" + +// Exercises `@(cross)` state retention across timesteps. `state` is assigned only +// inside cross handlers, so it must hold its value between evaluations (a latch). +// A rising crossing past 0.7 sets it; a falling crossing past 0.3 clears it; in +// the 0.3..0.7 dead-band neither guard fires and the previous value is retained. +// +// The output drives q toward `state` (residual at q is V(q) - state), so reading +// the q residual with V(q)=0 yields exactly -state -- a direct, retained readout. +module cross_latch(d, q); + inout d, q; + electrical d, q; + + integer state; + + analog begin + @(cross(V(d) - 0.7, +1)) if (V(d) > 0.7) state = 1; + @(cross(V(d) - 0.3, -1)) if (V(d) < 0.3) state = 0; + I(q) <+ V(q) - state; + end +endmodule From 86ae31d3c7090f4dbf0f5ca234a9a5bff021b326 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Thu, 25 Jun 2026 05:53:49 +0200 Subject: [PATCH 15/19] laplace_nd: widen integer coefficient literals to real (fixes compiler panic) Following the Verilog-AMS LRM 2.4 analog-operator examples through OpenVAF: the LRM's own laplace_nd example writes coefficients as bare integer literals (H(s)=s/(s^2-1) is 'laplace_nd(V(in), '{0,1}, '{-1,0,1})'). Those lower to integer MIR values, but the controllable-canonical state-space realization does real arithmetic on them (fmul/fsub against the float integrator states), so the optimizer hit 'fsub Float, Int' and the compiler panicked with an internal error. Our Butterworth test only ever used real coefficients ('{1.0,...}'), so this never surfaced. Widen each coefficient to real in array_coeffs (covering anonymous array literals, array variables via their element type, and the scalar fallback). Regression test laplace_nd_int.va: a stable first-order low-pass with integer coefficients '{1},'{1,1} -- compiling+loading its descriptor would have panicked before this fix. Full suite green. --- openvaf/hir_lower/src/expr.rs | 38 ++++++++++++++++++++-- openvaf/openvaf/tests/integration.rs | 14 +++++++- openvaf/test_data/osdi/laplace_nd_int.snap | 15 +++++++++ openvaf/test_data/osdi/laplace_nd_int.va | 18 ++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 openvaf/test_data/osdi/laplace_nd_int.snap create mode 100644 openvaf/test_data/osdi/laplace_nd_int.va diff --git a/openvaf/hir_lower/src/expr.rs b/openvaf/hir_lower/src/expr.rs index 22abff22..2c27437e 100644 --- a/openvaf/hir_lower/src/expr.rs +++ b/openvaf/hir_lower/src/expr.rs @@ -864,13 +864,45 @@ impl BodyLoweringCtx<'_, '_, '_> { /// Read the coefficient values of an array-valued argument (an array variable's /// elements or an array literal's entries), lowest index first. fn array_coeffs(&mut self, arg: ExprId) -> Vec { + // Laplace coefficients feed real-valued state-space arithmetic, but an + // anonymous array literal of integer constants (the LRM's own examples use + // `'{-1,0,1}`) lowers to integer values. Widen each coefficient to real so + // the residual math stays well-typed. match self.body.get_expr(arg) { Expr::Read(Ref::Variable(var)) => { let len = self.array_len(var); - (0..len).map(|i| self.ctx.use_place(PlaceKind::VarElement(var, i))).collect() + let elem_ty = match var.ty(self.ctx.db) { + Type::Array { ty, .. } => *ty, + other => other, + }; + (0..len) + .map(|i| { + let v = self.ctx.use_place(PlaceKind::VarElement(var, i)); + self.coeff_to_real(v, &elem_ty) + }) + .collect() + } + Expr::Array(elems) => elems + .iter() + .map(|&e| { + let v = self.lower_expr(e); + let ty = self.body.expr_type(e); + self.coeff_to_real(v, &ty) + }) + .collect(), + _ => { + let v = self.lower_expr(arg); + let ty = self.body.expr_type(arg); + vec![self.coeff_to_real(v, &ty)] } - Expr::Array(elems) => elems.iter().map(|&e| self.lower_expr(e)).collect(), - _ => vec![self.lower_expr(arg)], + } + } + + /// Widen an integer/bool coefficient value to real; reals pass through. + fn coeff_to_real(&mut self, v: Value, ty: &Type) -> Value { + match ty { + Type::Integer | Type::Bool => self.ctx.insert_cast(v, ty, &Type::Real), + _ => v, } } diff --git a/openvaf/openvaf/tests/integration.rs b/openvaf/openvaf/tests/integration.rs index 36b40cce..1ec04b37 100644 --- a/openvaf/openvaf/tests/integration.rs +++ b/openvaf/openvaf/tests/integration.rs @@ -324,6 +324,18 @@ fn test_cross_latch() -> Result<()> { Ok(()) } +/// Regression: `laplace_nd` with anonymous integer coefficient literals (the form +/// the LRM examples use) must compile without the optimizer panicking on mixed +/// int/float arithmetic. Compiling + loading the descriptor is enough to guard the +/// crash. See `laplace_nd_int.va`. +fn test_laplace_nd_int() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + test_descriptor(&openvaf_test_data("osdi").join("laplace_nd_int.va"))?; + Ok(()) +} + harness! { // TODO: run this in CI, somehow this test is flakey tough regarding the linker invocation (and really slow) Test::from_dir("integration", &integration_test, &ignore_dev_tests, &project_root().join("integration_tests")), @@ -333,5 +345,5 @@ harness! { Test::from_dir_filtered("vacask_spice", &vacask_spice_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice")), // VACASK simplified SPICE models Test::from_dir_filtered("vacask_spice_sn", &vacask_spice_sn_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice/sn")), - [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch)] + [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int)] } diff --git a/openvaf/test_data/osdi/laplace_nd_int.snap b/openvaf/test_data/osdi/laplace_nd_int.snap new file mode 100644 index 00000000..577828d5 --- /dev/null +++ b/openvaf/test_data/osdi/laplace_nd_int.snap @@ -0,0 +1,15 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) + +2 terminals +node "in" units = "V", runits = "A" +node "out" units = "V", runits = "A" +node(flow) "flow(out)" units = "A", runits = "V" +node "implicit_equation_0" units = "", runits = "" +jacobian (out, flow(out)) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), out) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_0, in) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_0, implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +0 states +has bound_step false diff --git a/openvaf/test_data/osdi/laplace_nd_int.va b/openvaf/test_data/osdi/laplace_nd_int.va new file mode 100644 index 00000000..d9150554 --- /dev/null +++ b/openvaf/test_data/osdi/laplace_nd_int.va @@ -0,0 +1,18 @@ +`include "disciplines.vams" + +// Regression: `laplace_nd` with anonymous *integer* coefficient literals. The +// Verilog-AMS LRM's own examples write coefficients as bare integers (e.g. +// `'{-1,0,1}`), which lower to integer MIR values. The state-space realization +// does real arithmetic on them, so the coefficients must be widened to real -- +// otherwise the optimizer hits `fsub Float, Int` and the compiler panics. +// +// Here H(s) = 1 / (1 + s): a stable first-order low-pass with integer +// coefficients, exercising exactly that widening path. +module laplace_nd_int(in, out); + inout in, out; + electrical in, out; + + analog begin + V(out) <+ laplace_nd(V(in), '{1}, '{1, 1}); + end +endmodule From 47c97a9a2dd9eece5774ffea8bbc8b70495f887f Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Thu, 25 Jun 2026 07:50:43 +0200 Subject: [PATCH 16/19] vector ports: support ranged port declarations (input [0:3] in) The Verilog-AMS LRM declares vectored ports with a range on the direction/ discipline declaration -- e.g. the transition() QAM and A/D examples use 'input [0:3] in; voltage [0:3] in;'. We supported ranged *net* declarations (bus nodes, from the RC-ladder work) but not ranged *port* declarations, so the LRM examples failed to parse at 'unexpected token [['. - Grammar/parser: add an optional Dimension to PortDecl (mirrors NetDecl); regenerate the AST (PortDecl now exposes dimension()). - HIR: lower_port_decl expands a ranged port into one scalar port/node per index (in[0]..in[3]), exactly as bus nets expand. - Head/body reconciliation: a port is commonly listed bare in the module head ('module m(in, out);') and ranged only in the body ('input [0:3] in;'). Added collect_bus_ranges to pre-scan the body, so a bare head name expands to the same in[0]..in[3] scalar nodes the body decl produces and the two reconcile by name. Tests (in-process compile -> dlopen -> MockSimulation): - vector_ports.va: bare-head + body-ranged bus input summed with distinct weights; descriptor shows 5 terminals in[0..3]/out and the residual asserts -(1+2+3+4) = -10, proving each bit is a distinct, correctly-indexed node. - qam16.va: the LRM transition() Example 1, compile+load guard. Full suite green. (The LRM A/D-converter example additionally needs retained @(cross) *array* state, a separate gap, so it is not yet covered.) --- openvaf/hir_def/src/item_tree/lower.rs | 134 ++++++++++++++++----- openvaf/openvaf/tests/integration.rs | 40 +++++- openvaf/parser/src/grammar/items/module.rs | 11 ++ openvaf/syntax/src/ast/generated/nodes.rs | 1 + openvaf/syntax/veriloga.ungram | 2 +- openvaf/test_data/osdi/qam16.snap | 30 +++++ openvaf/test_data/osdi/qam16.va | 28 +++++ openvaf/test_data/osdi/vector_ports.snap | 16 +++ openvaf/test_data/osdi/vector_ports.va | 20 +++ 9 files changed, 250 insertions(+), 32 deletions(-) create mode 100644 openvaf/test_data/osdi/qam16.snap create mode 100644 openvaf/test_data/osdi/qam16.va create mode 100644 openvaf/test_data/osdi/vector_ports.snap create mode 100644 openvaf/test_data/osdi/vector_ports.va diff --git a/openvaf/hir_def/src/item_tree/lower.rs b/openvaf/hir_def/src/item_tree/lower.rs index 9baf803a..40c8efa7 100644 --- a/openvaf/hir_def/src/item_tree/lower.rs +++ b/openvaf/hir_def/src/item_tree/lower.rs @@ -393,6 +393,37 @@ impl Ctx { } } + /// Collect bus base names declared with a vectored range anywhere in the module + /// body (`input [msb:lsb] x;` or `electrical [msb:lsb] x;`), mapping each name to + /// its constant `(msb, lsb)` bounds. Used to expand bare head port names. + fn collect_bus_ranges(&self, module: &ast::ModuleDecl) -> ahash::AHashMap { + let mut ranges = ahash::AHashMap::new(); + let mut add = |dim: Option, names: ast::AstChildren| { + if let Some(dim) = dim { + if let (Some(msb), Some(lsb)) = ( + dim.msb().and_then(|e| eval_const_int(&e, module)), + dim.lsb().and_then(|e| eval_const_int(&e, module)), + ) { + for name in names { + ranges.insert(name.as_name(), (msb, lsb)); + } + } + } + }; + for item in module.module_items() { + match item { + ast::ModuleItem::BodyPortDecl(bpd) => { + if let Some(decl) = bpd.port_decl() { + add(decl.dimension(), decl.names()); + } + } + ast::ModuleItem::NetDecl(decl) => add(decl.dimension(), decl.names()), + _ => {} + } + } + ranges + } + fn lower_module_ports( &mut self, ports: ast::ModulePorts, @@ -400,19 +431,39 @@ impl Ctx { dst: &mut Vec, ) { let module = ports.syntax().ancestors().find_map(ast::ModuleDecl::cast); + + // A port may be listed in the head as a bare name (`module m(in, out);`) and + // given a bus range only in the body (`input [0:3] in;`). Pre-scan the body's + // declarations so such a head name expands to the same `in[0]..in[3]` scalar + // nodes the body decl produces, letting the two reconcile by name. + let bus_ranges = module.as_ref().map(|m| self.collect_bus_ranges(m)).unwrap_or_default(); + for port in ports.ports() { let ast_id = self.source_ast_id_map.ast_id(&port); match port.kind() { ast::ModulePortKind::Name(name) => { - let name = name.as_name(); - if nodes.iter().all(|node| node.name != name) { - let node = nodes.push_and_get_key(Node { - name, - is_port: true, - ast_id: ast_id.into(), - decls: Vec::new(), - }); - dst.push(node.into()) + let base = name.as_name(); + let indices: Vec> = match bus_ranges.get(&base) { + Some(&(msb, lsb)) => { + let (lo, hi) = if msb <= lsb { (msb, lsb) } else { (lsb, msb) }; + (lo..=hi).map(Some).collect() + } + None => vec![None], + }; + for idx in indices { + let name = match idx { + Some(idx) => Name::resolve(&format!("{}[{}]", base, idx)), + None => base.clone(), + }; + if nodes.iter().all(|node| node.name != name) { + let node = nodes.push_and_get_key(Node { + name, + is_port: true, + ast_id: ast_id.into(), + decls: Vec::new(), + }); + dst.push(node.into()) + } } } // Vectored/bus port reference `inode[k]` in the module header. The @@ -461,28 +512,51 @@ impl Ctx { let is_gnd = decl.net_type_token().map_or(false, |it| it.text() == kw::raw::ground); let ast_id = self.source_ast_id_map.ast_id(&decl); + + // Vectored/bus port declaration `input [msb:lsb] in;` expands into one scalar + // port/node per index, named `in[msb]`..`in[lsb]`, exactly as a bus net does. + let bus_range = decl.dimension().and_then(|dim| { + let module = decl.syntax().ancestors().find_map(ast::ModuleDecl::cast)?; + let msb = eval_const_int(&dim.msb()?, &module)?; + let lsb = eval_const_int(&dim.lsb()?, &module)?; + Some((msb, lsb)) + }); + for (name_idx, name) in decl.names().enumerate() { - let name = name.as_name(); - let id = self.tree.data.ports.push_and_get_key(Port { - name: name.clone(), - discipline: discipline.clone(), - is_input: is_input(&direction), - is_output: is_output(&direction), - ast_id, - name_idx, - is_gnd, - }); - - match nodes.iter_mut().find(|node| node.name == name) { - Some(node) => node.decls.push(id.into()), - None => { - let node = nodes.push_and_get_key(Node { - name, - is_port: true, - ast_id: ast_id.into(), - decls: vec![id.into()], - }); - dst.push(node.into()) + let base = name.as_name(); + let indices: Vec> = match bus_range { + Some((msb, lsb)) => { + let (lo, hi) = if msb <= lsb { (msb, lsb) } else { (lsb, msb) }; + (lo..=hi).map(Some).collect() + } + None => vec![None], + }; + for idx in indices { + let name = match idx { + Some(idx) => Name::resolve(&format!("{}[{}]", base, idx)), + None => base.clone(), + }; + let id = self.tree.data.ports.push_and_get_key(Port { + name: name.clone(), + discipline: discipline.clone(), + is_input: is_input(&direction), + is_output: is_output(&direction), + ast_id, + name_idx, + is_gnd, + }); + + match nodes.iter_mut().find(|node| node.name == name) { + Some(node) => node.decls.push(id.into()), + None => { + let node = nodes.push_and_get_key(Node { + name, + is_port: true, + ast_id: ast_id.into(), + decls: vec![id.into()], + }); + dst.push(node.into()) + } } } } diff --git a/openvaf/openvaf/tests/integration.rs b/openvaf/openvaf/tests/integration.rs index 1ec04b37..112538aa 100644 --- a/openvaf/openvaf/tests/integration.rs +++ b/openvaf/openvaf/tests/integration.rs @@ -336,6 +336,44 @@ fn test_laplace_nd_int() -> Result<()> { Ok(()) } +/// Vectored/bus ports: a port declared bare in the head and ranged in the body +/// (`input [0:3] in`) must expand to in[0]..in[3] and index correctly. The output +/// sums the four bits with distinct weights, so the loaded residual proves each bit +/// is a distinct node. See `vector_ports.va`. +fn test_vector_ports() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + let desc = test_descriptor(&openvaf_test_data("osdi").join("vector_ports.va"))?; + let model = desc.new_model(); + model.process_params()?; + let mut instance = model.new_instance(); + let mut sim = instance.mock_simulation(&model, desc.num_terminals, 300.0)?; + + sim.set_voltage("out", 0.0); + sim.set_voltage("in[0]", 1.0); + sim.set_voltage("in[1]", 1.0); + sim.set_voltage("in[2]", 1.0); + sim.set_voltage("in[3]", 1.0); + instance.eval(&model, &mut sim, EvalFlags::empty()); + instance.load_dae(&model, &mut sim); + + // residual(out) = V(out) - (1+2+3+4) = -10. + float_cmp::assert_approx_eq!(f64, sim.read_residual("out").0, -10.0, epsilon = 1e-9); + Ok(()) +} + +/// LRM 2.4 transition() Example 1 (QAM modulator): vectored input ports declared +/// bare in the head and ranged in the body, bus indexing, transition, $abstime. +/// Compile+load guard. +fn test_qam16() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + test_descriptor(&openvaf_test_data("osdi").join("qam16.va"))?; + Ok(()) +} + harness! { // TODO: run this in CI, somehow this test is flakey tough regarding the linker invocation (and really slow) Test::from_dir("integration", &integration_test, &ignore_dev_tests, &project_root().join("integration_tests")), @@ -345,5 +383,5 @@ harness! { Test::from_dir_filtered("vacask_spice", &vacask_spice_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice")), // VACASK simplified SPICE models Test::from_dir_filtered("vacask_spice_sn", &vacask_spice_sn_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice/sn")), - [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int)] + [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int),Test::new("vector_ports", &test_vector_ports),Test::new("qam16", &test_qam16)] } diff --git a/openvaf/parser/src/grammar/items/module.rs b/openvaf/parser/src/grammar/items/module.rs index dd5837a3..9f3d91fe 100644 --- a/openvaf/parser/src/grammar/items/module.rs +++ b/openvaf/parser/src/grammar/items/module.rs @@ -106,6 +106,17 @@ fn port_decl(p: &mut Parser, m: Marker) { } p.eat(NET_TYPE); + // Optional vectored/bus range, e.g. `input [0:3] in;` / `output [0:bits-1] out;`. + if p.at(T!['[']) { + let dim = p.start(); + p.bump(T!['[']); + expr(p); + p.expect(T![:]); + expr(p); + p.expect(T![']']); + dim.complete(p, DIMENSION); + } + if MODULE_HEAD { decl_list(p, T![')'], module_port, MODULE_PORT_RECOVERY); } else { diff --git a/openvaf/syntax/src/ast/generated/nodes.rs b/openvaf/syntax/src/ast/generated/nodes.rs index 04a925d7..127af9e8 100644 --- a/openvaf/syntax/src/ast/generated/nodes.rs +++ b/openvaf/syntax/src/ast/generated/nodes.rs @@ -504,6 +504,7 @@ impl PortDecl { pub fn net_type_token(&self) -> Option { support::token(&self.syntax, T![net_type]) } + pub fn dimension(&self) -> Option { support::child(&self.syntax) } pub fn names(&self) -> AstChildren { support::children(&self.syntax) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/openvaf/syntax/veriloga.ungram b/openvaf/syntax/veriloga.ungram index e30562eb..cc50fd56 100644 --- a/openvaf/syntax/veriloga.ungram +++ b/openvaf/syntax/veriloga.ungram @@ -254,7 +254,7 @@ BodyPortDecl = PortDecl ';' PortDecl = - AttrList* Direction discipline:NameRef? 'net_type'? (Name (',' Name)*) + AttrList* Direction discipline:NameRef? 'net_type'? Dimension? (Name (',' Name)*) Direction = 'inout' | 'input' | 'output' diff --git a/openvaf/test_data/osdi/qam16.snap b/openvaf/test_data/osdi/qam16.snap new file mode 100644 index 00000000..ec043f9c --- /dev/null +++ b/openvaf/test_data/osdi/qam16.snap @@ -0,0 +1,30 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) +param "freq" +units = "", desc = "", flags = ParameterFlags(0x0) +param "ampl" +units = "", desc = "", flags = ParameterFlags(0x0) +param "thresh" +units = "", desc = "", flags = ParameterFlags(0x0) +param "tdelay" +units = "", desc = "", flags = ParameterFlags(0x0) +param "ttransit" +units = "", desc = "", flags = ParameterFlags(0x0) + +5 terminals +node "in[0]" units = "V", runits = "" +node "in[1]" units = "V", runits = "" +node "in[2]" units = "V", runits = "" +node "in[3]" units = "V", runits = "" +node "out" units = "V", runits = "" +node(flow) "flow(out)" units = "", runits = "V" +node "implicit_equation_0" units = "", runits = "" +node "implicit_equation_1" units = "", runits = "" +jacobian (out, flow(out)) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), out) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), implicit_equation_1) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_0, implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_1, implicit_equation_1) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +0 states +has bound_step false diff --git a/openvaf/test_data/osdi/qam16.va b/openvaf/test_data/osdi/qam16.va new file mode 100644 index 00000000..45542fa7 --- /dev/null +++ b/openvaf/test_data/osdi/qam16.va @@ -0,0 +1,28 @@ +`include "constants.vams" +`include "disciplines.vams" + +// Verilog-AMS LRM 2.4 section 4.5.8, transition() Example 1 (QAM modulator). +// Exercises vectored input ports declared bare in the head and ranged in the body +// (`input [0:3] in; voltage [0:3] in;`), bus indexing `V(in[k])`, transition(), and +// $abstime. Kept as a compile+load regression for vector ports. +module qam16(in, out); + input [0:3] in; + output out; + voltage [0:3] in; + voltage out; + parameter real freq = 1.0 from (0:inf); + parameter real ampl = 1.0; + parameter real thresh = 2.5; + parameter real tdelay = 0 from [0:inf); + localparam real ttransit = 1/freq; + real x, y, phi; + integer row, col; + analog begin + row = 2 * (V(in[3]) > thresh) + (V(in[2]) > thresh); + col = 2 * (V(in[1]) > thresh) + (V(in[0]) > thresh); + x = transition(row - 1.5, tdelay, ttransit); + y = transition(col - 1.5, tdelay, ttransit); + phi = `M_TWO_PI * freq * $abstime; + V(out) <+ ampl * (x * cos(phi) + y * sin(phi)); + end +endmodule diff --git a/openvaf/test_data/osdi/vector_ports.snap b/openvaf/test_data/osdi/vector_ports.snap new file mode 100644 index 00000000..b1c4b323 --- /dev/null +++ b/openvaf/test_data/osdi/vector_ports.snap @@ -0,0 +1,16 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) + +5 terminals +node "in[0]" units = "V", runits = "A" +node "in[1]" units = "V", runits = "A" +node "in[2]" units = "V", runits = "A" +node "in[3]" units = "V", runits = "A" +node "out" units = "V", runits = "A" +jacobian (out, in[0]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out, in[1]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out, in[2]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out, in[3]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out, out) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +0 states +has bound_step false diff --git a/openvaf/test_data/osdi/vector_ports.va b/openvaf/test_data/osdi/vector_ports.va new file mode 100644 index 00000000..8f4fa327 --- /dev/null +++ b/openvaf/test_data/osdi/vector_ports.va @@ -0,0 +1,20 @@ +`include "disciplines.vams" + +// Exercises vectored/bus PORTS: `in` is declared as a bare name in the module head +// and given its [0:3] range only in the body (the LRM style), so the head name must +// reconcile with the body bus expansion into in[0]..in[3]. The output current sums +// the four bus bits with distinct weights, so the loaded residual/Jacobian prove +// each bit expanded and indexes to a distinct node. +// +// I(out) = V(out) - (1*V(in0) + 2*V(in1) + 3*V(in2) + 4*V(in3)) +// With V(out)=0 and V(in)= [1,1,1,1], residual(out) = -(1+2+3+4) = -10. +module vbus(in, out); + input [0:3] in; + output out; + electrical [0:3] in; + electrical out; + analog begin + I(out) <+ V(out) + - (1.0*V(in[0]) + 2.0*V(in[1]) + 3.0*V(in[2]) + 4.0*V(in[3])); + end +endmodule From 9015777cdebb8a6cfbbda5395b037f4581b9b9a0 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Thu, 25 Jun 2026 08:06:18 +0200 Subject: [PATCH 17/19] retained @(cross): support real-typed and array variables Retained @(cross) state previously only worked for integer scalars (the Schmitt latch). The LRM A/D-converter example exposed two gaps: 1. Real-typed retained scalars panicked: the init/store path always emitted an integer<->real cast, so a real variable hit insert_cast(Real -> Real) -> unreachable. (The ADC's sample/thresh are real.) 2. Array retained variables were unhandled: a single state slot and a Var place were allocated for the whole array instead of one slot/VarElement place per element. Rework the retained-state machinery (hir_lower) to allocate one retained slot per scalar, or one per element for arrays, and to widen to/from real only when the element type isn't already real. retained_states now maps a variable to its vector of per-element states. Also fix the test harness: MockSimulation never initialized the per-instance state_idx map (logical limit-state -> physical state slot). With one state the default 0 works, but several retained states all aliased slot 0. A real simulator assigns these; the mock now writes the identity mapping too. Tests: - cross_array.va / test_cross_array: two array elements assigned in @(cross) retain independently across timesteps (-1,-2 / retained / 0,0 / retained / flips back). - adc.va / test_adc: the LRM transition() Example 2 (8-bit SAR ADC) -- vector ports + genvar unroll + retained @(cross) array + transition together. Verified in real VACASK: the ADC quantizes V(in)=0.7 to 10110011 = 0.6992 (a mix of bits, so the 8 retained states do not collide). Full suite green. --- openvaf/hir_lower/src/body.rs | 67 +++++++++++++++++------ openvaf/hir_lower/src/ctx.rs | 5 +- openvaf/openvaf/tests/integration.rs | 54 ++++++++++++++++++- openvaf/openvaf/tests/mock_sim/mod.rs | 12 +++++ openvaf/test_data/osdi/adc.snap | 72 +++++++++++++++++++++++++ openvaf/test_data/osdi/adc.va | 31 +++++++++++ openvaf/test_data/osdi/cross_array.snap | 11 ++++ openvaf/test_data/osdi/cross_array.va | 25 +++++++++ 8 files changed, 258 insertions(+), 19 deletions(-) create mode 100644 openvaf/test_data/osdi/adc.snap create mode 100644 openvaf/test_data/osdi/adc.va create mode 100644 openvaf/test_data/osdi/cross_array.snap create mode 100644 openvaf/test_data/osdi/cross_array.va diff --git a/openvaf/hir_lower/src/body.rs b/openvaf/hir_lower/src/body.rs index aa9ab535..c386dc31 100644 --- a/openvaf/hir_lower/src/body.rs +++ b/openvaf/hir_lower/src/body.rs @@ -15,9 +15,10 @@ pub struct BodyLoweringCtx<'a, 'c1, 'c2> { impl<'c1, 'c2> BodyLoweringCtx<'_, 'c1, 'c2> { pub fn lower_entry_stmts(&mut self) { // Pre-pass: find variables assigned inside `@(cross)` handlers. Each is backed - // by a retained limit-state slot so it holds its value across timesteps (true + // by retained limit-state slots so it holds its value across timesteps (true // latch/event semantics, e.g. a Schmitt trigger). The variable starts each - // evaluation at its previous accepted value. + // evaluation at its previous accepted value. An array variable retains every + // element (one slot each), so e.g. an ADC's sampled bit vector survives. let mut retained: Vec = Vec::new(); for &stmnt in self.body.entry() { self.collect_cross_assigned(stmnt, false, &mut retained); @@ -25,14 +26,21 @@ impl<'c1, 'c2> BodyLoweringCtx<'_, 'c1, 'c2> { let mut seen = ahash::AHashSet::new(); retained.retain(|v| seen.insert(*v)); + // (place, state, element type) for every retained slot, in store order. + let mut slots: Vec<(PlaceKind, crate::LimitState, Type)> = Vec::new(); + if !self.ctx.no_equations { for &var in &retained { - let state = self.ctx.alloc_retained_state(); - let prev = self.ctx.retained_prev(state); - let ty = var.ty(self.ctx.db); - let init = self.ctx.insert_cast(prev, &Type::Real, &ty); - self.ctx.def_place(PlaceKind::Var(var), init); - self.ctx.retained_states.insert(var, state); + let (elem_ty, places) = self.retained_layout(var); + let mut states = Vec::with_capacity(places.len()); + for place in places { + let state = self.ctx.alloc_retained_state(); + let init = self.retained_load(state, &elem_ty); + self.ctx.def_place(place, init); + states.push(state); + slots.push((place, state, elem_ty.clone())); + } + self.ctx.retained_states.insert(var, states); } } @@ -40,19 +48,44 @@ impl<'c1, 'c2> BodyLoweringCtx<'_, 'c1, 'c2> { self.lower_stmt(stmnt) } - // Post-pass: store each retained variable's final value for the next timestep. - if !self.ctx.no_equations && !self.ctx.retained_states.is_empty() { - let states: Vec<(Variable, crate::LimitState)> = - self.ctx.retained_states.iter().map(|(&v, &s)| (v, s)).collect(); - for (var, state) in states { - let ty = var.ty(self.ctx.db); - let v_final = self.ctx.use_place(PlaceKind::Var(var)); - let as_real = self.ctx.insert_cast(v_final, &ty, &Type::Real); - self.ctx.store_retained(state, as_real); + // Post-pass: store each retained slot's final value for the next timestep. + for (place, state, elem_ty) in slots { + let v_final = self.ctx.use_place(place); + self.retained_save(state, v_final, &elem_ty); + } + } + + /// The element type and the per-element places that back a retained variable: a + /// scalar has one `Var` place, an array one `VarElement` place per index. + fn retained_layout(&self, var: Variable) -> (Type, Vec) { + match var.ty(self.ctx.db) { + Type::Array { ty, len } => { + let places = (0..len).map(|i| PlaceKind::VarElement(var, i)).collect(); + (*ty, places) } + ty => (ty, vec![PlaceKind::Var(var)]), } } + /// Read a retained slot's previous-timestep value (stored as real) back into the + /// variable's element type. A real element needs no cast. + fn retained_load(&mut self, state: crate::LimitState, elem_ty: &Type) -> Value { + let prev = self.ctx.retained_prev(state); + match elem_ty { + Type::Real => prev, + _ => self.ctx.insert_cast(prev, &Type::Real, elem_ty), + } + } + + /// Store a retained slot's final value (cast to real) for the next timestep. + fn retained_save(&mut self, state: crate::LimitState, val: Value, elem_ty: &Type) { + let as_real = match elem_ty { + Type::Real => val, + _ => self.ctx.insert_cast(val, elem_ty, &Type::Real), + }; + self.ctx.store_retained(state, as_real); + } + /// Recursively collect variables assigned inside `@(cross)` event handlers. fn collect_cross_assigned(&self, stmnt: StmtId, in_cross: bool, dst: &mut Vec) { let stmt = match self.body.get_stmt(stmnt) { diff --git a/openvaf/hir_lower/src/ctx.rs b/openvaf/hir_lower/src/ctx.rs index 4dfccbd6..bf51deaf 100644 --- a/openvaf/hir_lower/src/ctx.rs +++ b/openvaf/hir_lower/src/ctx.rs @@ -27,7 +27,10 @@ pub struct LoweringCtx<'a, 'c> { pub num_noise_sources: u32, /// Variables assigned inside `@(cross)` handlers, each mapped to the limit-state /// slot that stores its value across timesteps (latch / event retention). - pub retained_states: AHashMap, + /// Variables assigned inside `@(cross)` handlers that must retain their value + /// across timesteps. Each is backed by one retained limit-state slot per scalar, + /// or one per element for an array variable (in element order). + pub retained_states: AHashMap>, /// True while lowering an `@(initial_step)` body: resets of retained variables /// there are their initial value (read from the retained state), not a /// per-evaluation reset. diff --git a/openvaf/openvaf/tests/integration.rs b/openvaf/openvaf/tests/integration.rs index 112538aa..7b4ac857 100644 --- a/openvaf/openvaf/tests/integration.rs +++ b/openvaf/openvaf/tests/integration.rs @@ -374,6 +374,58 @@ fn test_qam16() -> Result<()> { Ok(()) } +/// Retained `@(cross)` ARRAY state: each array element assigned inside a cross +/// handler must retain independently across timesteps. Drives the input +/// high/dead-band/low and checks both elements hold and flip via the prev/next +/// state swap. See `cross_array.va`; residual at q0/q1 equals -s[0]/-s[1]. +fn test_cross_array() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + + let desc = test_descriptor(&openvaf_test_data("osdi").join("cross_array.va"))?; + let model = desc.new_model(); + model.process_params()?; + let mut instance = model.new_instance(); + let mut sim = instance.mock_simulation(&model, desc.num_terminals, 300.0)?; + + let step = |instance: &OsdiInstance, model: &OsdiModel, sim: &mut MockSimulation, vd: f64, first: bool| { + if !first { + sim.next_iter(); + } + sim.set_voltage("q0", 0.0); + sim.set_voltage("q1", 0.0); + sim.set_voltage("d", vd); + instance.eval(model, sim, EvalFlags::ENABLE_LIM | EvalFlags::INIT_LIM); + instance.load_dae(model, sim); + (sim.read_residual("q0").0, sim.read_residual("q1").0) + }; + + let check = |(a, b): (f64, f64), ea: f64, eb: f64| { + float_cmp::assert_approx_eq!(f64, a, ea, epsilon = 1e-9); + float_cmp::assert_approx_eq!(f64, b, eb, epsilon = 1e-9); + }; + + check(step(&instance, &model, &mut sim, 1.0, true), -1.0, -2.0); // set s=[1,2] + check(step(&instance, &model, &mut sim, 0.5, false), -1.0, -2.0); // dead-band: retained + check(step(&instance, &model, &mut sim, 0.0, false), 0.0, 0.0); // clear s=[0,0] + check(step(&instance, &model, &mut sim, 0.5, false), 0.0, 0.0); // dead-band: retained + check(step(&instance, &model, &mut sim, 1.0, false), -1.0, -2.0); // flips back + Ok(()) +} + +/// LRM 2.4 transition() Example 2 (N-bit A/D converter), legal form (continuous +/// contributions outside the discrete @(cross) sampler). Exercises the whole new +/// stack at once: vector ports + genvar unroll + retained @(cross) array + +/// transition. Compile+load guard. +fn test_adc() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + test_descriptor(&openvaf_test_data("osdi").join("adc.va"))?; + Ok(()) +} + harness! { // TODO: run this in CI, somehow this test is flakey tough regarding the linker invocation (and really slow) Test::from_dir("integration", &integration_test, &ignore_dev_tests, &project_root().join("integration_tests")), @@ -383,5 +435,5 @@ harness! { Test::from_dir_filtered("vacask_spice", &vacask_spice_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice")), // VACASK simplified SPICE models Test::from_dir_filtered("vacask_spice_sn", &vacask_spice_sn_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice/sn")), - [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int),Test::new("vector_ports", &test_vector_ports),Test::new("qam16", &test_qam16)] + [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int),Test::new("vector_ports", &test_vector_ports),Test::new("qam16", &test_qam16),Test::new("cross_array", &test_cross_array),Test::new("adc", &test_adc)] } diff --git a/openvaf/openvaf/tests/mock_sim/mod.rs b/openvaf/openvaf/tests/mock_sim/mod.rs index 8fb354fb..03d05433 100644 --- a/openvaf/openvaf/tests/mock_sim/mod.rs +++ b/openvaf/openvaf/tests/mock_sim/mod.rs @@ -178,6 +178,18 @@ impl OsdiInstance { sim.state_1.resize(self.descriptor.num_states as usize, 0.0); sim.state_2.resize(self.descriptor.num_states as usize, 0.0); sim.noise_dense.resize(self.descriptor.num_noise_src as usize, 0.0); + + // Initialize the per-instance state_idx map (logical limit-state -> physical + // slot in prev_state/next_state). A real simulator assigns these; with a + // single state the default 0 happens to work, but with several they would all + // alias slot 0. Use the identity mapping. + unsafe { + let data = self.data as *mut u8; + let state_idx = data.add(self.descriptor.state_idx_off as usize).cast::(); + for i in 0..self.descriptor.num_states { + state_idx.add(i as usize).write(i); + } + } Ok(sim) } diff --git a/openvaf/test_data/osdi/adc.snap b/openvaf/test_data/osdi/adc.snap new file mode 100644 index 00000000..b2ea60d4 --- /dev/null +++ b/openvaf/test_data/osdi/adc.snap @@ -0,0 +1,72 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) +param "bits" +units = "", desc = "", flags = ParameterFlags(PARA_TY_INT) +param "fullscale" +units = "", desc = "", flags = ParameterFlags(0x0) +param "dly" +units = "", desc = "", flags = ParameterFlags(PARA_TY_INT) +param "ttime" +units = "", desc = "", flags = ParameterFlags(0x0) + +10 terminals +node "in" units = "V", runits = "A" +node "clk" units = "V", runits = "A" +node "out[0]" units = "V", runits = "A" +node "out[1]" units = "V", runits = "A" +node "out[2]" units = "V", runits = "A" +node "out[3]" units = "V", runits = "A" +node "out[4]" units = "V", runits = "A" +node "out[5]" units = "V", runits = "A" +node "out[6]" units = "V", runits = "A" +node "out[7]" units = "V", runits = "A" +node(flow) "flow(out[0])" units = "A", runits = "V" +node(flow) "flow(out[1])" units = "A", runits = "V" +node(flow) "flow(out[2])" units = "A", runits = "V" +node(flow) "flow(out[3])" units = "A", runits = "V" +node(flow) "flow(out[4])" units = "A", runits = "V" +node(flow) "flow(out[5])" units = "A", runits = "V" +node(flow) "flow(out[6])" units = "A", runits = "V" +node(flow) "flow(out[7])" units = "A", runits = "V" +node "implicit_equation_0" units = "", runits = "" +node "implicit_equation_1" units = "", runits = "" +node "implicit_equation_2" units = "", runits = "" +node "implicit_equation_3" units = "", runits = "" +node "implicit_equation_4" units = "", runits = "" +node "implicit_equation_5" units = "", runits = "" +node "implicit_equation_6" units = "", runits = "" +node "implicit_equation_7" units = "", runits = "" +jacobian (out[0], flow(out[0])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[1], flow(out[1])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[2], flow(out[2])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[3], flow(out[3])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[4], flow(out[4])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[5], flow(out[5])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[6], flow(out[6])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (out[7], flow(out[7])) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[0]), out[0]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[0]), implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[1]), out[1]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[1]), implicit_equation_1) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[2]), out[2]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[2]), implicit_equation_2) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[3]), out[3]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[3]), implicit_equation_3) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[4]), out[4]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[4]), implicit_equation_4) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[5]), out[5]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[5]), implicit_equation_5) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[6]), out[6]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[6]), implicit_equation_6) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[7]), out[7]) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out[7]), implicit_equation_7) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_0, implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_1, implicit_equation_1) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_2, implicit_equation_2) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_3, implicit_equation_3) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_4, implicit_equation_4) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_5, implicit_equation_5) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_6, implicit_equation_6) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_7, implicit_equation_7) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT | JACOBIAN_ENTRY_REACT_CONST) +10 states +has bound_step false diff --git a/openvaf/test_data/osdi/adc.va b/openvaf/test_data/osdi/adc.va new file mode 100644 index 00000000..b1a5b01a --- /dev/null +++ b/openvaf/test_data/osdi/adc.va @@ -0,0 +1,31 @@ +`include "constants.vams" +`include "disciplines.vams" +module adc(in, clk, out); + parameter bits = 8, fullscale = 1.0, dly = 0, ttime = 10n; + input in, clk; + output [0:bits-1] out; + electrical in, clk; + electrical [0:bits-1] out; + real sample, thresh; + integer result[0:bits-1]; + genvar i; + analog begin + @(cross(V(clk)-2.5, +1)) begin + sample = V(in); + thresh = fullscale/2.0; + for (i = bits - 1; i >= 0; i = i - 1) begin + if (sample > thresh) begin + result[i] = 1.0; + sample = sample - thresh; + end + else begin + result[i] = 0.0; + end + sample = 2.0*sample; + end + end + for (i = 0; i < bits; i = i + 1) begin + V(out[i]) <+ transition(result[i], dly, ttime); + end + end +endmodule diff --git a/openvaf/test_data/osdi/cross_array.snap b/openvaf/test_data/osdi/cross_array.snap new file mode 100644 index 00000000..0ba47cb7 --- /dev/null +++ b/openvaf/test_data/osdi/cross_array.snap @@ -0,0 +1,11 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) + +3 terminals +node "d" units = "V", runits = "A" +node "q0" units = "V", runits = "A" +node "q1" units = "V", runits = "A" +jacobian (q0, q0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (q1, q1) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +2 states +has bound_step false diff --git a/openvaf/test_data/osdi/cross_array.va b/openvaf/test_data/osdi/cross_array.va new file mode 100644 index 00000000..9d446ee3 --- /dev/null +++ b/openvaf/test_data/osdi/cross_array.va @@ -0,0 +1,25 @@ +`include "disciplines.vams" + +// Retained `@(cross)` ARRAY state: s[0]/s[1] are assigned only inside cross +// handlers, so each element must hold its value across timesteps independently +// (the ADC sampled-bit-vector pattern, minimal form). Residual at q0/q1 is +// V(q) - s[k], so with V(q)=0 it reads back -s[0] / -s[1]. +module cross_array(d, q0, q1); + inout d, q0, q1; + electrical d, q0, q1; + + integer s[0:1]; + + analog begin + @(cross(V(d) - 0.7, +1)) if (V(d) > 0.7) begin + s[0] = 1; + s[1] = 2; + end + @(cross(V(d) - 0.3, -1)) if (V(d) < 0.3) begin + s[0] = 0; + s[1] = 0; + end + I(q0) <+ V(q0) - s[0]; + I(q1) <+ V(q1) - s[1]; + end +endmodule From 942eeb7a1d867c9c03bf886c47558908c3749a14 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Thu, 25 Jun 2026 12:01:15 +0200 Subject: [PATCH 18/19] indirect branch assignments (V(out) : f(...) == 0) Implements Verilog-AMS indirect branch assignments (issue #80). The canonical ideal op-amp V(out) : V(pin,nin) == 0; now parses and lowers correctly instead of erroring on the ':' operator. The construct "solve the source value of branch `out` so the constraint holds" maps directly onto the existing implicit-equation/DAE machinery (the same path behind idt/laplace_nd): a fresh implicit unknown drives the target branch as a voltage (V form) or current (I form) source, and an auxiliary equation whose residual is `lhs - rhs` of the `==` pins that unknown so the constraint is zero at the solution. Threaded through the whole front end: - parser: `:` accepted as a third assignment operator (the lval expr is fully parsed first, so a trailing `:` is unambiguous vs `?:`). - AST: AssignOp::Indirect. - hir_ty: the rhs is inferred as its own constraint equation (not coerced to the branch type); indirect requires a branch destination like a contribution, with diagnostics for misuse and the in-event/function restriction. - hir: ContributeKind::IndirectFlow / IndirectPotential. - hir_lower: ImplicitEquationKind::IndirectBranch; the ordinary contribution path is refactored to lower the rhs at the exact original point via a closure, so normal contributions keep byte-identical MIR. Verified end-to-end in VACASK: an inverting amplifier (R1=1k, R2=2k, vin=0.5) holds the inverting input at virtual ground (0 V) and solves out = -1.0 V (= -R2/R1 * vin), with the implicit unknown resolving to the output voltage. Adds opamp_indirect.va + a behavioral integration test asserting the constraint residual (V(pin)-V(nin)) and its Jacobian. Full suite green. --- openvaf/hir/src/body.rs | 23 +++++- openvaf/hir_lower/src/lib.rs | 4 ++ openvaf/hir_lower/src/stmt.rs | 81 +++++++++++++++++++--- openvaf/hir_ty/src/diagnostics.rs | 7 +- openvaf/hir_ty/src/inference.rs | 21 +++++- openvaf/hir_ty/src/validation/body.rs | 4 +- openvaf/openvaf/tests/integration.rs | 48 ++++++++++++- openvaf/parser/src/grammar/stmts.rs | 5 +- openvaf/syntax/src/ast/node_ext.rs | 5 ++ openvaf/test_data/osdi/opamp_indirect.snap | 16 +++++ openvaf/test_data/osdi/opamp_indirect.va | 9 +++ 11 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 openvaf/test_data/osdi/opamp_indirect.snap create mode 100644 openvaf/test_data/osdi/opamp_indirect.va diff --git a/openvaf/hir/src/body.rs b/openvaf/hir/src/body.rs index 2dc6639d..9b7765c6 100644 --- a/openvaf/hir/src/body.rs +++ b/openvaf/hir/src/body.rs @@ -188,7 +188,8 @@ impl<'a> BodyRef<'a> { hir_def::Stmt::EventControl { ref event, body } => { Some(Stmt::EventControl { event, body }) } - hir_def::Stmt::Assignment { val, .. } => { + hir_def::Stmt::Assignment { val, assignment_kind, .. } => { + let indirect = assignment_kind == syntax::ast::AssignOp::Indirect; let stmt = match self.infere.assignment_destination[&stmnt] { inference::AssignDst::Var(id) => { Stmt::Assignment { lhs: AssignmentLhs::Variable(Variable { id }), rhs: val } @@ -206,12 +207,20 @@ impl<'a> BodyRef<'a> { rhs: val, }, inference::AssignDst::Flow(branch) => Stmt::Contribute { - kind: ContributeKind::Flow, + kind: if indirect { + ContributeKind::IndirectFlow + } else { + ContributeKind::Flow + }, branch: branch.into(), rhs: val, }, inference::AssignDst::Potential(branch) => Stmt::Contribute { - kind: ContributeKind::Potential, + kind: if indirect { + ContributeKind::IndirectPotential + } else { + ContributeKind::Potential + }, branch: branch.into(), rhs: val, }, @@ -244,6 +253,14 @@ pub enum AssignmentLhs { pub enum ContributeKind { Flow, Potential, + /// Indirect branch assignment `I(out) : f(...) == 0` — `out` becomes a current + /// source whose value is solved so the constraint `f == 0` holds. `rhs` is the + /// constraint equation. + IndirectFlow, + /// Indirect branch assignment `V(out) : f(...) == 0` — `out` becomes a voltage + /// source whose value is solved so the constraint `f == 0` holds. `rhs` is the + /// constraint equation. + IndirectPotential, } #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/openvaf/hir_lower/src/lib.rs b/openvaf/hir_lower/src/lib.rs index 267d094c..d4917d03 100644 --- a/openvaf/hir_lower/src/lib.rs +++ b/openvaf/hir_lower/src/lib.rs @@ -45,6 +45,10 @@ pub enum ImplicitEquationKind { Ddt, NoiseSrc, Idt(IdtKind), + /// Auxiliary unknown introduced by an indirect branch assignment + /// (`V(out) : f(...) == 0`): the source value of the target branch, solved so the + /// constraint residual is zero. + IndirectBranch, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/openvaf/hir_lower/src/stmt.rs b/openvaf/hir_lower/src/stmt.rs index 1f031cb2..0a496bce 100644 --- a/openvaf/hir_lower/src/stmt.rs +++ b/openvaf/hir_lower/src/stmt.rs @@ -1,9 +1,10 @@ -use hir::{BranchWrite, Case, CaseCond, ContributeKind, ExprId, Node, Stmt, StmtId, Type}; +use hir::{BranchWrite, Case, CaseCond, ContributeKind, Expr, ExprId, Node, Stmt, StmtId, Type}; use mir::builder::InstBuilder; -use mir::{Opcode, F_ZERO}; +use mir::{Opcode, Value, F_ZERO}; +use syntax::ast::BinaryOp; use crate::body::BodyLoweringCtx; -use crate::{CallBackKind, CurrentKind, ParamKind, PlaceKind}; +use crate::{CallBackKind, CurrentKind, ImplicitEquationKind, ParamKind, PlaceKind}; impl BodyLoweringCtx<'_, '_, '_> { pub(super) fn lower_stmt(&mut self, stmnt: StmtId) { @@ -58,9 +59,12 @@ impl BodyLoweringCtx<'_, '_, '_> { _ => self.ctx.def_place(lhs.into(), val_), } } - Stmt::Contribute { kind, branch, rhs } => { - self.contribute(kind == ContributeKind::Potential, branch, rhs) - } + Stmt::Contribute { kind, branch, rhs } => match kind { + ContributeKind::Potential => self.contribute(true, branch, rhs), + ContributeKind::Flow => self.contribute(false, branch, rhs), + ContributeKind::IndirectPotential => self.indirect_contribute(true, branch, rhs), + ContributeKind::IndirectFlow => self.indirect_contribute(false, branch, rhs), + }, Stmt::Block { body } => { for stmt in body { @@ -197,7 +201,22 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.switch_to_block(loop_end); } - fn contribute(&mut self, voltage_src: bool, mut write: BranchWrite, rhs: ExprId) { + fn contribute(&mut self, voltage_src: bool, write: BranchWrite, rhs: ExprId) { + let is_zero = self.body.get_expr(rhs).is_zero(); + self.contribute_with(voltage_src, write, is_zero, |s| s.lower_expr(rhs)); + } + + /// Shared body of a branch contribution. `lower_rhs` is invoked to produce the + /// contributed value at the exact point the old direct lowering did, so ordinary + /// contributions keep byte-identical MIR; indirect assignments supply an + /// already-computed implicit unknown instead. + fn contribute_with( + &mut self, + voltage_src: bool, + mut write: BranchWrite, + rhs_is_zero: bool, + lower_rhs: impl FnOnce(&mut Self) -> Value, + ) { let mut negate = false; if let BranchWrite::Unnamed { hi, lo } = &mut write { self.lower_contribute_unnamed_branch(&mut negate, hi, lo, voltage_src) @@ -205,8 +224,7 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.def_place(PlaceKind::IsVoltageSrc(write), voltage_src.into()); let (mut hi, mut lo) = write.nodes(self.ctx.db); - let is_zero = self.body.get_expr(rhs).is_zero(); - if voltage_src && is_zero { + if voltage_src && rhs_is_zero { if matches!(write, BranchWrite::Named(_)) { self.lower_contribute_unnamed_branch(&mut negate, &mut hi, &mut lo, voltage_src) } @@ -219,7 +237,7 @@ impl BodyLoweringCtx<'_, '_, '_> { F_ZERO, ); - let rhs = self.lower_expr(rhs); + let rhs = lower_rhs(self); if rhs == F_ZERO { return; } @@ -236,6 +254,49 @@ impl BodyLoweringCtx<'_, '_, '_> { self.ctx.def_place(place, new); } + /// Lower an indirect branch assignment `V(out) : f(...) == 0` (or the `I(out)` + /// flow form). The target branch becomes a source whose value is a fresh implicit + /// unknown `u`; an auxiliary equation pins `u` so the constraint residual is zero. + /// This reuses exactly the implicit-equation/DAE machinery behind `idt`. + fn indirect_contribute(&mut self, voltage_src: bool, write: BranchWrite, constraint: ExprId) { + let (eq, unknown) = self.ctx.implicit_equation(ImplicitEquationKind::IndirectBranch); + // Drive the branch as a source whose value is the implicit unknown. + self.contribute_with(voltage_src, write, false, |_| unknown); + // Residual of the auxiliary equation: `lhs - rhs` of the `==` constraint (== 0). + let residual = self.lower_constraint_residual(constraint); + self.ctx.def_resist_residual(residual, eq); + } + + /// Lower the constraint of an indirect branch assignment to its residual value. + /// The canonical form is `lhs == rhs`, whose residual is `lhs - rhs`; a bare + /// expression is treated leniently as `expr == 0`. + fn lower_constraint_residual(&mut self, constraint: ExprId) -> Value { + if let Expr::BinaryOp { lhs, rhs, op: BinaryOp::EqualityTest } = + self.body.get_expr(constraint) + { + let lhs = self.lower_real_operand(lhs); + let rhs = self.lower_real_operand(rhs); + self.ctx.ins().fsub(lhs, rhs) + } else { + self.lower_real_operand(constraint) + } + } + + /// Lower an expression and coerce the result to `Real` (integer/bool constraint + /// operands are widened so the residual is a floating-point quantity). + fn lower_real_operand(&mut self, expr: ExprId) -> Value { + let val = self.lower_expr(expr); + let ty = match self.body.needs_cast(expr) { + Some((_, dst)) => dst.clone(), + None => self.body.expr_type(expr), + }; + match ty { + Type::Real => val, + Type::Integer | Type::Bool => self.ctx.insert_cast(val, &ty, &Type::Real), + _ => val, + } + } + fn lower_contribute_unnamed_branch( &mut self, negate: &mut bool, diff --git a/openvaf/hir_ty/src/diagnostics.rs b/openvaf/hir_ty/src/diagnostics.rs index 29f29590..4a7ec740 100644 --- a/openvaf/hir_ty/src/diagnostics.rs +++ b/openvaf/hir_ty/src/diagnostics.rs @@ -86,6 +86,11 @@ impl Diagnostic for InferenceDiagnosticWrapped<'_> { AssignOp::Assign => res .with_message("invalid destination for assignment") .with_notes(vec!["help: expected a variable".to_owned()]), + AssignOp::Indirect => res + .with_message("invalid destination for indirect branch assignment") + .with_notes(vec![ + "help: expected nature access such as V(foo) or I(foo)".to_owned() + ]), }; match maybe_different_operand { @@ -97,7 +102,7 @@ impl Diagnostic for InferenceDiagnosticWrapped<'_> { "help: found a variable\nperhaps you meant to assign (=) a value" .to_owned(), ]), - None => res, + Some(ast::AssignOp::Indirect) | None => res, } } InferenceDiagnostic::PathResolveError { ref err, expr } => { diff --git a/openvaf/hir_ty/src/inference.rs b/openvaf/hir_ty/src/inference.rs index 52557f2e..a26a2ef9 100755 --- a/openvaf/hir_ty/src/inference.rs +++ b/openvaf/hir_ty/src/inference.rs @@ -131,7 +131,15 @@ impl Ctx<'_> { } Stmt::Assignment { dst, val, assignment_kind } => { let dst_ty = self.infere_assignment_dst(stmt, dst, assignment_kind); - self.infere_assignment(stmt, val, dst_ty); + if assignment_kind == ast::AssignOp::Indirect { + // `V(out) : f(...) == 0` — the rhs is the constraint equation, not a + // value to assign to the branch. Infer it on its own terms (an + // equality test producing bool, with its operands coerced as usual); + // MIR lowering turns it into an implicit-equation residual. + self.infere_expr(stmt, val); + } else { + self.infere_assignment(stmt, val, dst_ty); + } } Stmt::ForLoop { cond, .. } | Stmt::If { cond, .. } | Stmt::WhileLoop { cond, .. } => { self.infere_cond(stmt, cond) @@ -207,7 +215,7 @@ impl Ctx<'_> { if let Expr::Index { base, index } = self.body.exprs[expr] { if let Ty::Var(Type::Array { ty, .. }, var) = self.result.expr_types[base].clone() { let elem = *ty; - if assignment_kind == ast::AssignOp::Contribute { + if matches!(assignment_kind, ast::AssignOp::Contribute | ast::AssignOp::Indirect) { self.result.diagnostics.push(InferenceDiagnostic::InvalidAssignDst { e: expr, maybe_different_operand: Some(ast::AssignOp::Assign), @@ -288,6 +296,15 @@ impl Ctx<'_> { assignment_kind, }); } + // Indirect branch assignment (`V(out) : …`) requires a branch destination, + // exactly like a contribution. + (AssignDst::Var(_) | AssignDst::FunVar { .. }, ast::AssignOp::Indirect) => { + self.result.diagnostics.push(InferenceDiagnostic::InvalidAssignDst { + e: expr, + maybe_different_operand: Some(ast::AssignOp::Assign), + assignment_kind, + }); + } _ => { self.result.assignment_destination.insert(stmt, dst); } diff --git a/openvaf/hir_ty/src/validation/body.rs b/openvaf/hir_ty/src/validation/body.rs index b6de1f0b..5ed80c18 100644 --- a/openvaf/hir_ty/src/validation/body.rs +++ b/openvaf/hir_ty/src/validation/body.rs @@ -210,7 +210,9 @@ impl BodyValidator<'_> { Stmt::Assignment { dst, val, assignment_kind } => { self.validate_expr(val, stmt); - if assignment_kind == AssignOp::Contribute && !self.ctx.allow_contribute() { + if matches!(assignment_kind, AssignOp::Contribute | AssignOp::Indirect) + && !self.ctx.allow_contribute() + { self.diagnostics .push(BodyValidationDiagnostic::IllegalContribute { stmt, ctx: self.ctx }) } diff --git a/openvaf/openvaf/tests/integration.rs b/openvaf/openvaf/tests/integration.rs index 7b4ac857..ed97f72a 100644 --- a/openvaf/openvaf/tests/integration.rs +++ b/openvaf/openvaf/tests/integration.rs @@ -414,6 +414,52 @@ fn test_cross_array() -> Result<()> { Ok(()) } +/// Indirect branch assignment `V(out) : V(pin,nin) == 0` (ideal op-amp, issue #80). +/// Lowers to an implicit equation whose unknown drives `out` as a voltage source and +/// whose residual is the constraint `V(pin) - V(nin)`. The constraint residual (and +/// its Jacobian) is independent of the unknown, so we can check it on the isolated +/// device: with V(pin)=0.3, V(nin)=0.1 the `implicit_equation_0` row carries 0.2 with +/// d/dV(pin)=+1, d/dV(nin)=-1. See `opamp_indirect.va`. +fn test_indirect_opamp() -> Result<()> { + if stdx::IS_CI && cfg!(windows) { + return Ok(()); + } + + let desc = test_descriptor(&openvaf_test_data("osdi").join("opamp_indirect.va"))?; + let model = desc.new_model(); + model.process_params()?; + let mut instance = model.new_instance(); + let mut sim = instance.mock_simulation(&model, desc.num_terminals, 300.0)?; + + sim.set_voltage("out", 0.0); + sim.set_voltage("pin", 0.3); + sim.set_voltage("nin", 0.1); + sim.set_voltage("implicit_equation_0", 0.7); // unknown; residual must not depend on it + instance.eval(&model, &mut sim, EvalFlags::empty()); + instance.load_dae(&model, &mut sim); + + // constraint residual = V(pin) - V(nin) = 0.2, regardless of the unknown. + float_cmp::assert_approx_eq!( + f64, + sim.read_residual("implicit_equation_0").0, + 0.2, + epsilon = 1e-9 + ); + float_cmp::assert_approx_eq!( + f64, + sim.read_jacobian("pin", "implicit_equation_0").0, + 1.0, + epsilon = 1e-9 + ); + float_cmp::assert_approx_eq!( + f64, + sim.read_jacobian("nin", "implicit_equation_0").0, + -1.0, + epsilon = 1e-9 + ); + Ok(()) +} + /// LRM 2.4 transition() Example 2 (N-bit A/D converter), legal form (continuous /// contributions outside the discrete @(cross) sampler). Exercises the whole new /// stack at once: vector ports + genvar unroll + retained @(cross) array + @@ -435,5 +481,5 @@ harness! { Test::from_dir_filtered("vacask_spice", &vacask_spice_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice")), // VACASK simplified SPICE models Test::from_dir_filtered("vacask_spice_sn", &vacask_spice_sn_test, &is_va_file, &ignore_dev_tests, &vacask_devices().join("spice/sn")), - [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int),Test::new("vector_ports", &test_vector_ports),Test::new("qam16", &test_qam16),Test::new("cross_array", &test_cross_array),Test::new("adc", &test_adc)] + [Test::new("$limit", &test_limit),Test::new("noise", &test_noise),Test::new("arrays", &test_arrays),Test::new("cross_latch", &test_cross_latch),Test::new("laplace_nd_int", &test_laplace_nd_int),Test::new("vector_ports", &test_vector_ports),Test::new("qam16", &test_qam16),Test::new("cross_array", &test_cross_array),Test::new("adc", &test_adc),Test::new("indirect_opamp", &test_indirect_opamp)] } diff --git a/openvaf/parser/src/grammar/stmts.rs b/openvaf/parser/src/grammar/stmts.rs index 8e6349df..42a89380 100644 --- a/openvaf/parser/src/grammar/stmts.rs +++ b/openvaf/parser/src/grammar/stmts.rs @@ -47,7 +47,10 @@ fn expr_or_assign_stmt(p: &mut Parser, m: Marker) { fn assign_or_expr(p: &mut Parser) -> bool { let m = p.start(); expr(p); - if p.eat_ts(TokenSet::new(&[T![<+], T![=]])) { + // `:` is the indirect branch assignment operator (`V(out) : f(...) == 0`). The + // lval expression is fully parsed above; a `:` here is unambiguous (any ternary + // `?:` was already consumed inside `expr`). + if p.eat_ts(TokenSet::new(&[T![<+], T![=], T![:]])) { expr(p); m.complete(p, ASSIGN); true diff --git a/openvaf/syntax/src/ast/node_ext.rs b/openvaf/syntax/src/ast/node_ext.rs index a3dbcf2f..bec308e2 100644 --- a/openvaf/syntax/src/ast/node_ext.rs +++ b/openvaf/syntax/src/ast/node_ext.rs @@ -200,12 +200,15 @@ impl ast::ModulePorts { pub enum AssignOp { Contribute, Assign, + /// Indirect branch assignment `V(out) : constraint == 0;` (Verilog-AMS LRM). + Indirect, } impl_debug! { match AssignOp{ AssignOp::Contribute => "<+"; AssignOp::Assign => "="; + AssignOp::Indirect => ":"; } } @@ -215,6 +218,8 @@ impl Assign { Some(AssignOp::Assign) } else if support::token(self.syntax(), T![<+]).is_some() { Some(AssignOp::Contribute) + } else if support::token(self.syntax(), T![:]).is_some() { + Some(AssignOp::Indirect) } else { None } diff --git a/openvaf/test_data/osdi/opamp_indirect.snap b/openvaf/test_data/osdi/opamp_indirect.snap new file mode 100644 index 00000000..40d93090 --- /dev/null +++ b/openvaf/test_data/osdi/opamp_indirect.snap @@ -0,0 +1,16 @@ +param "$mfactor" +units = "", desc = "Multiplier (Verilog-A $mfactor)", flags = ParameterFlags(PARA_KIND_INST) + +3 terminals +node "out" units = "V", runits = "A" +node "pin" units = "V", runits = "A" +node "nin" units = "V", runits = "A" +node(flow) "flow(out)" units = "A", runits = "V" +node "implicit_equation_0" units = "", runits = "" +jacobian (out, flow(out)) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), out) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (flow(out), implicit_equation_0) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_0, pin) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +jacobian (implicit_equation_0, nin) JacobianFlags(JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_RESIST_CONST | JACOBIAN_ENTRY_REACT_CONST) +0 states +has bound_step false diff --git a/openvaf/test_data/osdi/opamp_indirect.va b/openvaf/test_data/osdi/opamp_indirect.va new file mode 100644 index 00000000..6400621a --- /dev/null +++ b/openvaf/test_data/osdi/opamp_indirect.va @@ -0,0 +1,9 @@ +// Ideal op-amp via indirect branch assignment (Verilog-AMS LRM, issue #80). +// `V(out):V(pin,nin) == 0` solves the output so the differential input is zero. +`include "disciplines.vams" +module opamp_indirect(out, pin, nin); + inout out, pin, nin; + electrical out, pin, nin; + analog + V(out) : V(pin,nin) == 0; +endmodule From d10dd4e07043f00508cb4c6d19e928130774d69c Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Thu, 25 Jun 2026 13:08:15 +0200 Subject: [PATCH 19/19] sim_back: skip None outputs in compute_outputs (fixes #10) The non-contributes branch of compute_outputs unwrapped each output value with unwrap_unchecked(), which in release mode returns the reserved sentinel Value(u32::MAX) for a None PackedOption instead of panicking. Feeding that sentinel into output_values.insert() computes a word index of u32::MAX / 64 = 67108863, far past the few-word bitset, causing: index out of bounds: the len is 3 but the index is 67108863 at lib/bitset/src/lib.rs:133 (BitSet::insert) via sim_back::context::Context::compute_outputs A BoundStep/CollapseImplicitEquation/op-var output can legitimately have a None value when it gets eliminated (e.g. the $bound_step value in the Skywater sky130 ReRAM model under release-mode DCE). The contributes branch already guards against this with filter_map(PackedOption::expand); mirror that here by expanding and skipping None. --- openvaf/sim_back/src/context.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openvaf/sim_back/src/context.rs b/openvaf/sim_back/src/context.rs index eb518e01..3bbdf0d6 100644 --- a/openvaf/sim_back/src/context.rs +++ b/openvaf/sim_back/src/context.rs @@ -118,7 +118,13 @@ impl<'a> Context<'a> { if matches!(kind, PlaceKind::Var(var) if self.module.op_vars.contains_key(var)) || matches!(kind, PlaceKind::CollapseImplicitEquation(_) | PlaceKind::BoundStep) { - self.output_values.insert(val.unwrap_unchecked()); + // The output value may be `None` if it was never materialized (e.g. a + // `$bound_step()` whose value got eliminated). `unwrap_unchecked()` would + // otherwise yield the reserved sentinel `Value(u32::MAX)` and blow up the + // bitset insert (issue #10). + if let Some(val) = val.expand() { + self.output_values.insert(val); + } } } }