Conversation
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.
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).
…enVAF) The unsupported-function note and the crash-report message linked to the unmaintained upstream github.com/pascalkuthe/openvaf; update both to the active fork.
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.
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.
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).
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.
… 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.
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.
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[<k>]`, 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.
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.
…ext_state `@(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).
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.
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.
…r 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.
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.)
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.
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.
There was a problem hiding this comment.
Code Review
This pull request introduces several major features to OpenVAF, including support for fixed-size arrays, @(cross) state retention across timesteps, indirect branch assignments, compile-time genvar loop unrolling, and a standalone Verilog-A runner. Feedback on these changes highlights several critical issues: a mathematical bug in lower_laplace_nd that ignores the last coefficient and lacks proper direct feedthrough support; potential compile-time panics during division operations in constant evaluation; silent clamping of out-of-bounds constant array indices during reads and writes; and silent truncation of unrolled loops when exceeding iteration limits.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // y = Σ_k num[k] x_k. | ||
| let mut out = F_ZERO; | ||
| for (k, &nk) in num.iter().enumerate() { | ||
| if k < n { | ||
| let term = self.ctx.ins().fmul(nk, states[k].1); | ||
| out = self.ctx.ins().fadd(out, term); | ||
| } | ||
| } | ||
| out |
There was a problem hiding this comment.
The current implementation of lower_laplace_nd completely ignores the last coefficient num[n] (where n = den.len() - 1) because of the loop condition if k < n. Furthermore, for proper (but not strictly proper) transfer functions where num.len() == den.len(), the output formula y = Σ_k num[k] x_k is mathematically incorrect because it lacks the direct feedthrough term and does not adjust the state coefficients. This leads to completely incorrect simulation results for many standard filters (e.g., high-pass, band-stop, all-pass).
To fix this, we should implement the controllable canonical form with direct feedthrough support by checking if num.len() > n and adjusting the output and state coefficients accordingly.
// y = b_n * input + Σ_{k<n} (num[k] - b_n * den[k]) * x_k.
let mut out = F_ZERO;
let has_bn = num.len() > n;
let b_n = if has_bn {
let bn_val = self.ctx.ins().fdiv(num[n], den[n]);
out = self.ctx.ins().fmul(bn_val, input);
Some(bn_val)
} else {
None
};
for k in 0..n {
let nk = if k < num.len() { num[k] } else { F_ZERO };
let coef = if let Some(bn_val) = b_n {
let term = self.ctx.ins().fmul(bn_val, den[k]);
self.ctx.ins().fsub(nk, term)
} else {
nk
};
let term = self.ctx.ins().fmul(coef, states[k].1);
out = self.ctx.ins().fadd(out, term);
}
out| BinaryOp::Division if r != 0 => Some(l / r), | ||
| BinaryOp::Remainder if r != 0 => Some(l % r), |
There was a problem hiding this comment.
Division and remainder operations (/ and %) can panic at compile time with an overflow if l is i64::MIN and r is -1. This causes the compiler to crash on invalid/overflowing inputs. Use checked_div and checked_rem to safely handle division and remainder without panicking.
| BinaryOp::Division if r != 0 => Some(l / r), | |
| BinaryOp::Remainder if r != 0 => Some(l % r), | |
| BinaryOp::Division => l.checked_div(r), | |
| BinaryOp::Remainder => l.checked_rem(r), |
| 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), |
| 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)); | ||
| } |
There was a problem hiding this comment.
If a constant array index is out of bounds (e.g., < 0 or >= len), the code silently clamps it to 0 or len - 1 and reads that element. This can hide bugs and lead to unexpected behavior. It should instead return F_ZERO (the default for invalid reads).
if let Some(c) = self.body.as_literalint(&index) {
if c >= 0 && c < len as i32 {
return self.ctx.use_place(PlaceKind::VarElement(var, c as u32));
} else {
return F_ZERO;
}
}| 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; | ||
| } |
There was a problem hiding this comment.
If a constant array index is out of bounds (e.g., < 0 or >= len), the code silently clamps it to 0 or len - 1 and writes to that element. This is extremely dangerous as it silently overwrites valid array elements (e.g., writing to g[3] on a 3-element array will silently overwrite g[2]). Check if the constant index is within bounds, and skip the assignment if it is out of bounds.
| 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; | |
| } | |
| if let Some(c) = self.body.as_literalint(&index) { | |
| if c >= 0 && c < len as i32 { | |
| self.ctx.def_place(PlaceKind::VarElement(var, c as u32), val); | |
| } | |
| return; | |
| } |
| if guard > 1_000_000 { | ||
| break; | ||
| } |
There was a problem hiding this comment.
If the loop unrolling guard exceeds 1,000,000 iterations, the loop is silently broken out of and a partially unrolled block is returned. This can lead to silent correctness bugs where the compiled model behaves incorrectly without any warning or error. Return None (falling back to a runtime loop or failing gracefully) instead of silently truncating the loop.
| if guard > 1_000_000 { | |
| break; | |
| } | |
| if guard > 1_000_000 { | |
| return None; | |
| } |
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.
This branch extends OpenVAF's Verilog-A frontend to compile and correctly simulate a set of language features it previously rejected, taking the "Writing Verilog-A Code" SimetriX scorecard to 9/9, and additionally implements indirect branch assignments (issue #80).
Highlights
Indirect branch assignments —
V(out) : f(...) == 0(issue #80)The canonical ideal op-amp from the VAMS LRM (
V(out):V(pin,nin) == 0;) previously errored withunexpected token ':'. Now parsed and lowered onto the existing implicit-equation/DAE machinery: a fresh implicit unknown drives the target branch (voltage- or current-form) while an auxiliary equation pins the constraintlhs - rhs == 0. Verified end-to-end in VACASK on an inverting amplifier (out = -R2/R1·vin, inverting input held at virtual ground).Arrays
real x[a:b]→Type::Array).laplace_ndState-space (controllable-canonical) realization reusing the
idt/implicit-equation DAE machinery — Butterworth filter compiles and simulates. Integer coefficient literals widened to real.genvar+ bus/vectored nodesCompile-time loop unrolling for genvar
forloops and bus nodes expanded to scalar nodes — RC ladder compiles.Digital / event features
@(cross)and@(timer)events;@(cross)state retention across timesteps (prev/next latch); real-typed and array variables in retained cross state.transition(): continuous slew-limited (Level-2) realization, accepts real first argument.input [0:3] in).Tooling / robustness
openvaf-r --run) that executes the analog body so@(initial_step) $strobeprints.Verification
Full workspace test suite green; new behavioral regression tests for arrays,
@(cross)retention, and the indirect op-amp; real circuit solves checked in VACASK.