Loading a production PDK model library (a deck that pulls in roughly 50,000 parameter definitions and expands to about 70,000 lines) took ngspice over 15 minutes -- before a single timestep was simulated. The circuit itself was small; the time went entirely into reading the input. This branch attempts to rectify that. This new parser now load the same deck in 5.3s. The parser itself takes about 0.7s with the rest of the time taken by ngspice itself
This commit is contained in:
parent
8fd86075c1
commit
e50dee9ccc
|
|
@ -94,3 +94,4 @@ test_cases/diode/test_built_in/*
|
||||||
|
|
||||||
build*/
|
build*/
|
||||||
prof/
|
prof/
|
||||||
|
ng_parse/target/
|
||||||
|
|
|
||||||
56
configure.ac
56
configure.ac
|
|
@ -147,6 +147,18 @@ AC_ARG_ENABLE([xspice],
|
||||||
AC_ARG_ENABLE([osdi],
|
AC_ARG_ENABLE([osdi],
|
||||||
[AS_HELP_STRING([--disable-osdi], [Disable OSDI integration])])
|
[AS_HELP_STRING([--disable-osdi], [Disable OSDI integration])])
|
||||||
|
|
||||||
|
# --enable-ngparse: use the ngparse Rust deck expander (opt-in at runtime with
|
||||||
|
# the --ngparse flag). Needs libngparse.a and ngparse.h from the ng_parse repo.
|
||||||
|
AC_ARG_ENABLE([ngparse],
|
||||||
|
[AS_HELP_STRING([--enable-ngparse], [Enable the ngparse deck expander])])
|
||||||
|
# Default: ng_parse ships inside the ngspice tree, so a plain --enable-ngparse
|
||||||
|
# needs no second flag. A relative DIR resolves against the source tree, an
|
||||||
|
# absolute one is used as given -- see the resolution below.
|
||||||
|
AC_ARG_WITH([ngparse-dir],
|
||||||
|
[AS_HELP_STRING([--with-ngparse-dir=DIR],
|
||||||
|
[path to the ng_parse tree (default: ng_parse, inside this tree)])],
|
||||||
|
[NGPARSE_DIR="$withval"], [NGPARSE_DIR="ng_parse"])
|
||||||
|
|
||||||
# --enable-cider: define CIDER in the code. This is for CIDER support
|
# --enable-cider: define CIDER in the code. This is for CIDER support
|
||||||
AC_ARG_ENABLE([cider],
|
AC_ARG_ENABLE([cider],
|
||||||
[AS_HELP_STRING([--enable-cider], [Enable CIDER enhancements])])
|
[AS_HELP_STRING([--enable-cider], [Enable CIDER enhancements])])
|
||||||
|
|
@ -1212,6 +1224,50 @@ fi
|
||||||
|
|
||||||
AM_CONDITIONAL([OSDI_WANTED], [test "x$enable_osdi" != xno])
|
AM_CONDITIONAL([OSDI_WANTED], [test "x$enable_osdi" != xno])
|
||||||
|
|
||||||
|
# ngparse: link the Rust staticlib and switch on the glue.
|
||||||
|
if test "x$enable_ngparse" = xyes; then
|
||||||
|
# Resolve --with-ngparse-dir to an absolute path. It may be given absolute
|
||||||
|
# OR relative to the source tree, and the build may be out-of-tree, so
|
||||||
|
# neither $(top_srcdir) nor the build dir is safe to prefix blindly.
|
||||||
|
case "$NGPARSE_DIR" in
|
||||||
|
/*) ;; # already absolute
|
||||||
|
*) ngparse_abs=`cd "$srcdir/$NGPARSE_DIR" 2>/dev/null && pwd`
|
||||||
|
if test -z "$ngparse_abs"; then
|
||||||
|
AC_MSG_ERROR([--with-ngparse-dir: $srcdir/$NGPARSE_DIR does not exist])
|
||||||
|
fi
|
||||||
|
NGPARSE_DIR="$ngparse_abs"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NGPARSE_LIB="$NGPARSE_DIR/target/release/libngparse.a"
|
||||||
|
# The staticlib is built by `make` (see src/Makefile.am), so it need NOT
|
||||||
|
# exist yet -- only the ngparse SOURCE and a Rust toolchain do. This is what
|
||||||
|
# lets the whole thing build from the ngspice top directory with a plain
|
||||||
|
# `make`, no separate cargo step.
|
||||||
|
AC_CHECK_FILE([$NGPARSE_DIR/Cargo.toml], [],
|
||||||
|
[AC_MSG_ERROR([$NGPARSE_DIR/Cargo.toml not found.
|
||||||
|
--with-ngparse-dir should name the ng_parse checkout itself.])])
|
||||||
|
AC_CHECK_FILE([$NGPARSE_DIR/include/ngparse.h], [],
|
||||||
|
[AC_MSG_ERROR([$NGPARSE_DIR/include/ngparse.h not found.
|
||||||
|
--with-ngparse-dir should name the ng_parse checkout itself.])])
|
||||||
|
AC_CHECK_PROG([CARGO], [cargo], [cargo])
|
||||||
|
if test "x$CARGO" = x; then
|
||||||
|
AC_MSG_ERROR([cargo (the Rust toolchain) is required to build ngparse.
|
||||||
|
Install rustc/cargo (>= 1.70; e.g. your distro package or https://rustup.rs),
|
||||||
|
or configure without --enable-ngparse.])
|
||||||
|
fi
|
||||||
|
AC_MSG_RESULT([ngparse deck expander included (built by make via cargo)])
|
||||||
|
AC_DEFINE([USE_NGPARSE], [1], [Use the ngparse deck expander])
|
||||||
|
NGPARSE_CFLAGS="-I$NGPARSE_DIR/include"
|
||||||
|
NGPARSE_LIBS="$NGPARSE_LIB -lpthread -ldl -lm"
|
||||||
|
AC_SUBST([NGPARSE_CFLAGS])
|
||||||
|
AC_SUBST([NGPARSE_LIBS])
|
||||||
|
AC_SUBST([NGPARSE_DIR])
|
||||||
|
AC_SUBST([NGPARSE_LIB])
|
||||||
|
fi
|
||||||
|
|
||||||
|
AM_CONDITIONAL([NGPARSE_WANTED], [test "x$enable_ngparse" = xyes])
|
||||||
|
|
||||||
# Add CIDER enhancements to ngspice.
|
# Add CIDER enhancements to ngspice.
|
||||||
if test "x$enable_cider" = xyes; then
|
if test "x$enable_cider" = xyes; then
|
||||||
AC_MSG_RESULT([CIDER features enabled])
|
AC_MSG_RESULT([CIDER features enabled])
|
||||||
|
|
|
||||||
|
|
@ -443,9 +443,9 @@ RCROSS2 B0 A24 0.001
|
||||||
**
|
**
|
||||||
**INCLUDING FILE: ./proj1/process.models....
|
**INCLUDING FILE: ./proj1/process.models....
|
||||||
*
|
*
|
||||||
* Typical N Typical P - from process corners (taken from tsmc025_corners.bsim3 fron NCSU)
|
* Typical N Typical P - from process corners (taken from foundry_a.bsim3 fron NCSU)
|
||||||
*
|
*
|
||||||
* TSMC 0.25u 5M 1P process. 2.5V transistor models
|
* foundry_a 0.25u 5M 1P process. 2.5V transistor models
|
||||||
|
|
||||||
|
|
||||||
.MODEL Nmod NMOS LEVEL=8
|
.MODEL Nmod NMOS LEVEL=8
|
||||||
|
|
|
||||||
|
|
@ -441,9 +441,9 @@ RCROSS2 B0 A24 0.001
|
||||||
**
|
**
|
||||||
**INCLUDING FILE: ./proj1/process.models....
|
**INCLUDING FILE: ./proj1/process.models....
|
||||||
*
|
*
|
||||||
* Typical N Typical P - from process corners (taken from tsmc025_corners.bsim3 fron NCSU)
|
* Typical N Typical P - from process corners (taken from foundry_a.bsim3 fron NCSU)
|
||||||
*
|
*
|
||||||
* TSMC 0.25u 5M 1P process. 2.5V transistor models
|
* foundry_a 0.25u 5M 1P process. 2.5V transistor models
|
||||||
|
|
||||||
|
|
||||||
.MODEL Nmod NMOS LEVEL=8
|
.MODEL Nmod NMOS LEVEL=8
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ WHY:
|
||||||
rule is kept outside HS mode. (8) The single-to-brace quote conversion now
|
rule is kept outside HS mode. (8) The single-to-brace quote conversion now
|
||||||
skips `.del`, `.include`, and `.inc ` lines (which take literal file paths),
|
skips `.del`, `.include`, and `.inc ` lines (which take literal file paths),
|
||||||
matching the existing `.lib` skip, to avoid handing paths to numparam as
|
matching the existing `.lib` skip, to avoid handing paths to numparam as
|
||||||
expressions (STEP 24, observed on GF55 bcd55 .alter decks).
|
expressions (STEP 24, observed on foundry_c bcd55 .alter decks).
|
||||||
|
|
||||||
CHANGE:
|
CHANGE:
|
||||||
@@ -1072,9 +1080,22 @@
|
@@ -1072,9 +1080,22 @@
|
||||||
|
|
@ -582,10 +582,10 @@ WHY:
|
||||||
midpoint 1.3999999999999999e-08), while integer comparisons remain unambiguous
|
midpoint 1.3999999999999999e-08), while integer comparisons remain unambiguous
|
||||||
(STEP 12). (4) When an identifier matches a function keyword, it is now
|
(STEP 12). (4) When an identifier matches a function keyword, it is now
|
||||||
treated as a function call ONLY if followed by `(`; otherwise it is treated as
|
treated as a function call ONLY if followed by `(`; otherwise it is treated as
|
||||||
a parameter name, so GF55 decks that use `var` as a subckt parameter aren't
|
a parameter name, so foundry_c decks that use `var` as a subckt parameter aren't
|
||||||
shadowed by the built-in `var` function (STEP 17). (5) A unary `+` following a
|
shadowed by the built-in `var` function (STEP 17). (5) A unary `+` following a
|
||||||
binary operator (e.g. `0.67*+2e-8`) is now accepted as a no-op sign, symmetric
|
binary operator (e.g. `0.67*+2e-8`) is now accepted as a no-op sign, symmetric
|
||||||
with the existing unary-minus handling, fixing GF55 "Misplaced operator"
|
with the existing unary-minus handling, fixing foundry_c "Misplaced operator"
|
||||||
failures. (6) Added the `nupa_eval_with_scope()` implementation: pushes a
|
failures. (6) Added the `nupa_eval_with_scope()` implementation: pushes a
|
||||||
fresh symbol-table scope, populates it via attrib(), runs formula(), then
|
fresh symbol-table scope, populates it via attrib(), runs formula(), then
|
||||||
frees the scope with del_attrib WITHOUT promoting locals to globals, used by
|
frees the scope with del_attrib WITHOUT promoting locals to globals, used by
|
||||||
|
|
@ -662,7 +662,7 @@ CHANGE:
|
||||||
+ * `vec`, `min`, `max`, `pow`, `table_param`). Only treat
|
+ * `vec`, `min`, `max`, `pow`, `table_param`). Only treat
|
||||||
+ * it as a function call if it's followed by `(` (after
|
+ * it as a function call if it's followed by `(` (after
|
||||||
+ * optional whitespace). Otherwise fall back to treating
|
+ * optional whitespace). Otherwise fall back to treating
|
||||||
+ * it as a parameter name — foundry decks (GF55 bcd55
|
+ * it as a parameter name — foundry decks (foundry_c bcd55
|
||||||
+ * diode_rr.inc) use `var` as a subckt parameter, and
|
+ * diode_rr.inc) use `var` as a subckt parameter, and
|
||||||
+ * shadowing it with the built-in function broke
|
+ * shadowing it with the built-in function broke
|
||||||
+ * `vrb='var'` and similar chains. */
|
+ * `vrb='var'` and similar chains. */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ngparse"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
authors = ["jfisher"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
ngparse -- ngspice changes
|
||||||
|
==========================
|
||||||
|
|
||||||
|
ngparse replaces the slow part of ngspice's frontend (.lib/.inc extraction,
|
||||||
|
numparam substitution, .subckt expansion). It links in as a static library
|
||||||
|
(libngparse.a) driven by a thin glue layer; ngspice's own parser is untouched.
|
||||||
|
It is the default in an --enable-ngparse build; --no-ngparse falls back to the
|
||||||
|
old frontend for a run. Without --enable-ngparse the glue is a no-op.
|
||||||
|
|
||||||
|
All changes live on branch jfisher/ng_parse. Seven files -- 2 new, 5 modified.
|
||||||
|
|
||||||
|
NEW
|
||||||
|
src/frontend/ngparse_glue.c
|
||||||
|
The glue. Decides whether ngparse should handle a source, reads ngspice's
|
||||||
|
ngbehavior (to pass PSpice mode through), calls the ngparse ABI, and
|
||||||
|
writes the expanded deck to a temp netlist. A no-op without USE_NGPARSE.
|
||||||
|
src/include/ngspice/ngparse_glue.h
|
||||||
|
Glue interface.
|
||||||
|
|
||||||
|
MODIFIED
|
||||||
|
src/frontend/inp.c
|
||||||
|
The seam: in inp_spsource(), just before inp_readall(), divert it to read
|
||||||
|
ngparse's expanded deck instead of the original file. Kept shallow so the
|
||||||
|
rest of inp_readall (compat passes, numparam, renumbering) still runs.
|
||||||
|
src/main.c
|
||||||
|
Adds --no-ngparse (and a redundant --ngparse) plus the --help line.
|
||||||
|
configure.ac
|
||||||
|
Adds --enable-ngparse and --with-ngparse-dir; checks for libngparse.a and
|
||||||
|
ngparse.h; defines USE_NGPARSE.
|
||||||
|
src/frontend/Makefile.am
|
||||||
|
Builds ngparse_glue.c; adds the ngparse include path.
|
||||||
|
src/Makefile.am
|
||||||
|
Links libngparse.a into ngspice and libngspice.
|
||||||
|
|
||||||
|
Enabling, PSpice mode, and measured results: see install.txt and comparison.txt.
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
===============================================================================
|
||||||
|
ng_parse — old frontend vs ngparse: measured comparison
|
||||||
|
===============================================================================
|
||||||
|
Date: 2026-07-18
|
||||||
|
Binary: ngspice built --enable-ngparse (same binary runs both arms; the only
|
||||||
|
difference is the --no-ngparse flag, which selects the old text frontend)
|
||||||
|
Host: 12-core Linux; all runs SERIAL (concurrent ngspice runs each grab ~4.5
|
||||||
|
OpenMP cores and skew both timing and, via convergence, results)
|
||||||
|
|
||||||
|
ng_parse replaces ngspice's text-expansion front end — .lib/.inc extraction,
|
||||||
|
numparam substitution, and .subckt expansion — with a Rust parser that hands
|
||||||
|
ngspice the same flat, fully-resolved card stream its own frontend would have
|
||||||
|
produced, just far faster. It changes NOTHING downstream (device model setup,
|
||||||
|
INPpas1/2/3, the solver), so it changes NOTHING about simulation results.
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
1. THE STORY: foundry model-load time (what ng_parse actually replaces)
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Parse + model-load ONLY — each deck is sourced, a marker is echoed, and ngspice
|
||||||
|
quits before any analysis. This is exactly the work ng_parse replaces; it is NOT
|
||||||
|
total run time (which is dominated by simulation and is untouched). Both arms are
|
||||||
|
measured warm (an untimed run warms the file cache first), so this is not a
|
||||||
|
disk-speed contest.
|
||||||
|
|
||||||
|
deck old (s) ngparse (s) speedup
|
||||||
|
---- ------- ----------- -------
|
||||||
|
foundry_b 1149.31 2.18 527x <-- 14LPU PDK
|
||||||
|
foundry_a 32.84 0.58 57x
|
||||||
|
foundry_a 4.85 0.28 17x
|
||||||
|
bandgap 2.74 0.88 3x (SMIC 90nm)
|
||||||
|
foundry_a 37.18 15.87 2x
|
||||||
|
|
||||||
|
The foundry_b 14LPU deck is the headline: ~19 minutes of model-load collapses to
|
||||||
|
~2 seconds — a 527x reduction — with bit-identical downstream behavior. The
|
||||||
|
smaller nodes gain less in absolute terms because their load was already short;
|
||||||
|
the win scales with how much .lib/.inc/.subckt text the deck drags in.
|
||||||
|
|
||||||
|
Why the speedup varies: it tracks the size and shape of the resolved deck, not
|
||||||
|
the transistor count. foundry_a's load is dominated by ngspice's own OSDI/BSIM-CMG
|
||||||
|
model setup (which runs AFTER ng_parse and is not replaced), so its ratio is
|
||||||
|
smaller even though the deck is large.
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
2. CORRECTNESS: functional regression, ngparse vs the old parser
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Every corpus below was run on the hs install. Mode is selected per deck by a
|
||||||
|
directory-local .spiceinit (e.g. examples/pton and examples/optran carry
|
||||||
|
"set ngbehavior=ps"), which ngspice sources and the ngparse glue honors — so one
|
||||||
|
hs run exercises each deck in its intended dialect (hs / ps).
|
||||||
|
|
||||||
|
corpus decks result
|
||||||
|
------ ----- ------
|
||||||
|
paranoia (ngspice examples) 209 206 same / 2 diff (see note A)
|
||||||
|
foundry_c sample netlists 26 26/26 expand clean, 0 dropped params
|
||||||
|
ngspice regression suite 56 56/56 pass (11 categories: lib-
|
||||||
|
processing, parser, subckt-
|
||||||
|
processing, temper, osdi, func,
|
||||||
|
misc, model, pz, sens, pipe)
|
||||||
|
OSDI / Verilog-A (in the above) 14 14/14 identical printed values
|
||||||
|
OpenVA spec/va 2 2/2 identical
|
||||||
|
foundry load (foundry_b/foundry_a/ 6 identical downstream; validated by
|
||||||
|
bandgap) expansion match + Monte-Carlo
|
||||||
|
distribution match (see note B)
|
||||||
|
|
||||||
|
Note A — the 2 paranoia diffs are NOT ngparse regressions:
|
||||||
|
optran/F5TurboV2thermal-ic-in.cir and optran/HiPass3opamps_optran.cir
|
||||||
|
both hardcode Windows include paths (c:\Spice64\bin\...). The OLD parser
|
||||||
|
fails them too; the difference is only in how each parser reports the missing
|
||||||
|
file. No deck the old parser handles is mishandled by ngparse.
|
||||||
|
|
||||||
|
Note B — foundry decks simulate for minutes to hours, so functional equivalence
|
||||||
|
is established by (1) byte-comparing the resolved card stream against the old
|
||||||
|
frontend's output, and (2) confirming Monte-Carlo runs reproduce the SAME
|
||||||
|
distribution (agauss/gauss/limit stay symbolic so ngspice draws per run with
|
||||||
|
its own PRNG — seeded runs are bit-identical, batches match mean and sigma).
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
3. KNOWN LIMITATIONS
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
* PSpice compatibility (ngbehavior=ps): ngparse replicates the pspice_compat
|
||||||
|
conversions the corpus exercises — if()->ternary_fcn, VSWITCH->sw/pswitch,
|
||||||
|
VALUE={TABLE()}->pwl, pwr/pwrs/stp/int. It does NOT implement LTspice- or
|
||||||
|
KiCad-specific (ngbehavior=lt/ki) conversions; no deck in the corpus needs
|
||||||
|
them.
|
||||||
|
* examples/pton/relax_osc_st.cir: fails in BOTH parsers (it redefines the
|
||||||
|
VCCAP_PSPICE subckt many times); ngparse's failure mode differs from the
|
||||||
|
reference's but neither produces a usable result.
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
4. HOW TO REPRODUCE
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Timing (foundry): harness/reg/bench_parser.sh
|
||||||
|
Paranoia functional: harness/paranoia_compare.sh
|
||||||
|
ngspice regression: harness/conformance.sh <categories>
|
||||||
|
All are serial by construction; see each script's header for details.
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* ngparse — fast SPICE/HSPICE deck expansion for ngspice.
|
||||||
|
*
|
||||||
|
* Replaces ngspice's text-expansion frontend (.lib/.inc extraction, numparam
|
||||||
|
* substitution, .subckt expansion) and hands back the flat, fully-resolved card
|
||||||
|
* list that if_inpdeck/INPpas1 expects. Everything downstream of expansion —
|
||||||
|
* eval_agauss(), ENHtranslate_poly(), inp_dodeck() — remains ngspice's job.
|
||||||
|
*
|
||||||
|
* Link against libngparse.a (plus -lpthread -ldl -lm).
|
||||||
|
*
|
||||||
|
* NgpDeck *d = ngparse_expand_file(path, 1, 0);
|
||||||
|
* if (!d) { fprintf(stderr, "%s\n", ngparse_last_error()); return 1; }
|
||||||
|
* for (size_t i = 0; i < ngparse_deck_len(d); i++)
|
||||||
|
* puts(ngparse_deck_card(d, i)); // card 0 is the TITLE
|
||||||
|
* ngparse_deck_free(d);
|
||||||
|
*
|
||||||
|
* Ownership: every pointer returned belongs to the NgpDeck it came from and is
|
||||||
|
* valid until ngparse_deck_free(). Do not free or modify them. Copy anything
|
||||||
|
* that must outlive the deck (ngspice's struct card owns its own line, so the
|
||||||
|
* glue copies).
|
||||||
|
*/
|
||||||
|
#ifndef NGPARSE_H
|
||||||
|
#define NGPARSE_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* An expanded deck. Opaque. */
|
||||||
|
typedef struct NgpDeck NgpDeck;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Expand the deck at `path` (the top-level netlist) into resolved cards.
|
||||||
|
*
|
||||||
|
* cores: worker-core budget. PASS 1. Values > 1 are accepted for forward
|
||||||
|
* compatibility but are not honored yet — expansion is single-threaded, and is
|
||||||
|
* only ~10% of deck-load time (the rest is ngspice's own model ingest). 0 is
|
||||||
|
* treated as 1.
|
||||||
|
*
|
||||||
|
* compat: input dialect. 1 = PSpice (ngspice's ngbehavior=ps); anything else =
|
||||||
|
* default/HSPICE. ngparse has taken over expansion, so ngspice's pspice_compat
|
||||||
|
* pass no longer runs on the deck; in PSpice mode ngparse applies those
|
||||||
|
* conversions (if->ternary_fcn, VSWITCH->sw, VALUE={TABLE()}->pwl, pwr/pwrs/
|
||||||
|
* stp/int) itself. The glue reads ngbehavior and passes it here.
|
||||||
|
*
|
||||||
|
* Returns NULL on failure; call ngparse_last_error() for the reason.
|
||||||
|
* Release with ngparse_deck_free().
|
||||||
|
*/
|
||||||
|
NgpDeck *ngparse_expand_file(const char *path, int cores, int compat);
|
||||||
|
|
||||||
|
/* Number of cards, including the title at index 0. 0 if d is NULL. */
|
||||||
|
size_t ngparse_deck_len(const NgpDeck *d);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Card i, or NULL if out of range. INDEX 0 IS THE TITLE: SPICE consumes the
|
||||||
|
* first card of a deck as the title and starts the netlist at index 1, so this
|
||||||
|
* list must be handed over whole — dropping index 0 silently eats a real card.
|
||||||
|
*/
|
||||||
|
const char *ngparse_deck_card(const NgpDeck *d, size_t i);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parameters that could not be resolved.
|
||||||
|
*
|
||||||
|
* A drop is never harmless: the affected device or model silently falls back to
|
||||||
|
* its DEFAULT, which yields a wrong-but-converging answer rather than an error.
|
||||||
|
* Surface these; refuse the deck if you want the CLI's --strict behavior.
|
||||||
|
*/
|
||||||
|
size_t ngparse_deck_drop_count(const NgpDeck *d);
|
||||||
|
const char *ngparse_deck_drop(const NgpDeck *d, size_t i);
|
||||||
|
|
||||||
|
/* Release a deck. NULL-safe. Invalidates every pointer taken from it. */
|
||||||
|
void ngparse_deck_free(NgpDeck *d);
|
||||||
|
|
||||||
|
/* Last error on this thread, or NULL. Owned by ngparse; do not free. */
|
||||||
|
const char *ngparse_last_error(void);
|
||||||
|
|
||||||
|
/* ngparse version string. Static storage. */
|
||||||
|
const char *ngparse_version(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* NGPARSE_H */
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
|
||||||
|
The parser in NGSPICE as it is today is not particularly suitable for modern semiconductor processes.
|
||||||
|
|
||||||
|
When doing process evaluation using 40nm, 28nm, 22nm (all planar) and 14nm FF, the parse time for the foundry libraries got slower as the node size reduces. I have a 14nm FinFet with thousands of interconnected parameters and the parse time was in the ballpark of 20 minutes, just to read the library. This is unacceptable when all I need it for is to look at a few inverters and logic gates with a simulation time of a few seconds.
|
||||||
|
|
||||||
|
It became necessary to create a quicker parser.
|
||||||
|
|
||||||
|
The work here parses my library deck in just under 2 seconds, so for my requirements, it works.
|
||||||
|
|
||||||
|
The intended audience for this is frankly just me. If it works for you, great. If you don't want to use it because you didn't invent it - also fine with me.
|
||||||
|
|
||||||
|
Now - if your work is primarily of the behavioral variety, or system stuff using Verilog-A or XSpice, where the parser is not a bottleneck, then this is probably not for you.
|
||||||
|
|
||||||
|
I did use the paranoia tests for regression (not all of which work with the old parser anyway) and I do get like for like results. That said, my focus is for Hspice compatibility, not Pspice or any other flavor.
|
||||||
|
|
||||||
|
When this work started, the old parser being so extremely slow, I made the assumption that a new parser would have to be multi-core. It turned out that this wasn't true - but I added the hooks, so I left it there. And anyway, who knows how crazy future processes will get? Perhaps one day a multi-core parser will be needed.
|
||||||
|
|
||||||
|
I assume that if you DO install this parser, then you DO intend to use it. In that case, you just use ngspice as normal
|
||||||
|
|
||||||
|
ngspice circuit.net
|
||||||
|
|
||||||
|
If you have installed this parser and you DON'T want to use it, then
|
||||||
|
|
||||||
|
ngspice --no-ngparse circuit.net
|
||||||
|
|
||||||
|
If you want more cores:
|
||||||
|
|
||||||
|
ngparse_cores=4 ngspice circuit.net
|
||||||
|
|
||||||
|
PREREQUISITES
|
||||||
|
-------------
|
||||||
|
A Rust toolchain -- rustc and cargo, 1.70 or newer. Built and tested with 1.85.0
|
||||||
|
If you do not have Rust, either install your distribution's rust/cargo package or use rustup (https://rustup.rs). Rust is needed only to BUILD; the result is a static library that is linked into ngspice
|
||||||
|
|
||||||
|
BUILD AND INSTALL NGSPICE
|
||||||
|
-------------------------
|
||||||
|
Much like before only with "--enable-ngparse" see below:
|
||||||
|
|
||||||
|
cd ngspice
|
||||||
|
./autogen.sh
|
||||||
|
mkdir -p release && cd release
|
||||||
|
../configure --enable-ngparse \
|
||||||
|
--with-x ....... and so on
|
||||||
|
|
||||||
|
|
||||||
|
CHECK IT WORKS
|
||||||
|
--------------
|
||||||
|
|
||||||
|
For a real check, run a deck both ways and compare.
|
||||||
|
|
||||||
|
ngspice -b --no-ngparse circuit.net > old.log 2>&1
|
||||||
|
ngspice -b circuit.net > new.log 2>&1
|
||||||
|
diff old.log new.log
|
||||||
|
|
||||||
|
The results should be identical
|
||||||
|
|
||||||
|
|
||||||
|
ngparse reports any parameter it could not resolve, rather than falling back to a default:
|
||||||
|
|
||||||
|
ngparse: WARNING: n parameter(s) could not be resolved and were dropped; the affected device/model falls back to its DEFAULT value:
|
||||||
|
|
||||||
|
A dropped parameter does not stop the simulation -- it converges to a wrong answer instead. None of the PDK decks this was developed against produce any.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
ngparse Licensing
|
||||||
|
|
||||||
|
Copyright (c) 2026 Justin Fisher
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
ngparse is licensed under the `Modified BSD' license -- the same license
|
||||||
|
ngspice adopts for its own source code, so that the two may be combined and
|
||||||
|
redistributed together without friction. ngparse links into ngspice as a
|
||||||
|
static library, and the ngspice glue that calls it (see changes.txt) is
|
||||||
|
likewise Modified BSD.
|
||||||
|
|
||||||
|
This license applies to all of ngparse: parser/, include/ and the
|
||||||
|
build files.
|
||||||
|
|
||||||
|
**************************** ngparse license **********************************
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
******************************* dependencies **********************************
|
||||||
|
|
||||||
|
ngparse has no third-party dependencies. The parser crate declares none, and
|
||||||
|
the built static library links only against the Rust standard library and the
|
||||||
|
platform C runtime (-lpthread -ldl -lm).
|
||||||
|
|
||||||
|
******************************* ngspice *************************************
|
||||||
|
|
||||||
|
ngspice itself is not covered by this file. ngspice is a mixture of licenses
|
||||||
|
-- Modified BSD for the bulk of it, with exceptions (its KLU and tclspice parts
|
||||||
|
are LGPLv2, src/osdi is MPLv2.0, src/xspice is largely public domain, and so
|
||||||
|
on). See ngspice's own COPYING. The files ngparse adds to or modifies in the
|
||||||
|
ngspice tree are listed in changes.txt; all of them sit in parts of ngspice that
|
||||||
|
are Modified BSD.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "ngparse"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
description = "Fast SPICE/HSPICE deck preprocessor and parser for ngspice model loading"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ngparse"
|
||||||
|
# rlib: the CLI and unit tests. staticlib: the ngspice C glue links this.
|
||||||
|
crate-type = ["rlib", "staticlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
//! Run configuration — the core count and the compatibility dialect.
|
||||||
|
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
/// Which input dialect ngparse should emulate when it emits the flat deck.
|
||||||
|
///
|
||||||
|
/// ngparse replaces ngspice's expansion front end, so the PSpice conversions that
|
||||||
|
/// ngspice's own `pspice_compat` (inpcompat.c) applies — `if`→`ternary_fcn`,
|
||||||
|
/// `VSWITCH`→`sw`, `VALUE={TABLE(..)}`→native `TABLE`, `pwr`/`pwrs`/`stp`/`int`
|
||||||
|
/// → native — never run on our output: that pass is triggered per `.include`d
|
||||||
|
/// file, and we have already inlined every include. In [`Compat::Pspice`] we do
|
||||||
|
/// those conversions ourselves so a `ngbehavior=ps` run matches the reference.
|
||||||
|
///
|
||||||
|
/// The mode is not discoverable from the deck (it lives in `ngbehavior`, set in
|
||||||
|
/// spinit), so the C glue reads it via `cp_getvar("ngbehavior", ..)` and passes
|
||||||
|
/// it across the FFI. [`Compat::Default`] is ngparse's long-standing behavior and
|
||||||
|
/// covers the HSPICE/standard decks.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum Compat {
|
||||||
|
/// Standard / HSPICE-style — ngparse's original behavior. No PSpice rewrites.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// PSpice (`ngbehavior=ps`): apply the `pspice_compat` conversions on emit.
|
||||||
|
Pspice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Compat {
|
||||||
|
/// Map ngspice's `ngbehavior` string to a mode. Only an exact/leading `ps`
|
||||||
|
/// selects PSpice; everything else (hs, spe, unset) is [`Compat::Default`],
|
||||||
|
/// matching how ngspice itself keys `newcompat.ps`.
|
||||||
|
pub fn from_ngbehavior(s: &str) -> Compat {
|
||||||
|
if s.trim().to_ascii_lowercase().starts_with("ps") {
|
||||||
|
Compat::Pspice
|
||||||
|
} else {
|
||||||
|
Compat::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From the FFI integer: 1 = PSpice, anything else = Default.
|
||||||
|
pub fn from_ffi(v: i32) -> Compat {
|
||||||
|
if v == 1 {
|
||||||
|
Compat::Pspice
|
||||||
|
} else {
|
||||||
|
Compat::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How ngparse should run. Carried by [`crate::Expander`] and
|
||||||
|
/// [`crate::subckt::SubcktExpander`] so the parallel seams have somewhere to read
|
||||||
|
/// their budget from without an API change later.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Config {
|
||||||
|
/// Worker cores for subckt expansion. Defaults to 1; more fans the
|
||||||
|
/// independent top-level cards across threads (see
|
||||||
|
/// `SubcktExpander::expand_parallel`), byte-identical to single-core.
|
||||||
|
///
|
||||||
|
/// Single-core is the default because on today's decks expansion is small:
|
||||||
|
/// the whole foundry_b expand is ~0.21s of a ~2.0s load, the rest being
|
||||||
|
/// ngspice's own `INPpas1/2/3` and BSIM-CMG/OSDI model setup, which run after
|
||||||
|
/// us and which parser cores cannot speed up. Multi-core is built and tested
|
||||||
|
/// for the decks not seen yet — a library an order of magnitude larger could
|
||||||
|
/// put real time into expansion, where more cores pay off.
|
||||||
|
pub cores: NonZeroUsize,
|
||||||
|
|
||||||
|
/// Input dialect to emulate on emit — see [`Compat`]. Defaults to
|
||||||
|
/// [`Compat::Default`]; the C glue raises it to [`Compat::Pspice`] when
|
||||||
|
/// `ngbehavior=ps`.
|
||||||
|
pub compat: Compat,
|
||||||
|
|
||||||
|
/// Remove dangling passives (two-terminal R/C whose far node is referenced
|
||||||
|
/// nowhere else) from the expanded deck. This is the "reduce EARLY, remove
|
||||||
|
/// the device entirely" scheme ngspice's own during-setup attempt (commit
|
||||||
|
/// aac195, since reverted) could not deliver: done here, `.probe`/`.save`
|
||||||
|
/// and AC see only surviving devices and the matrix shrinks. OFF by
|
||||||
|
/// default — the emitted deck stays byte-identical unless requested via
|
||||||
|
/// `--topo-reduce` (CLI) or `NGPARSE_TOPO_REDUCE=1` (integrated build).
|
||||||
|
pub topo_reduce: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
cores: NonZeroUsize::new(1).unwrap(),
|
||||||
|
compat: Compat::Default,
|
||||||
|
topo_reduce: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Sequential — the default.
|
||||||
|
pub fn single() -> Self {
|
||||||
|
Config::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request `cores` worker threads for subckt expansion. 1 (the default) runs
|
||||||
|
/// inline; more fans the independent top-level cards across that many threads.
|
||||||
|
pub fn with_cores(cores: NonZeroUsize) -> Self {
|
||||||
|
Config {
|
||||||
|
cores,
|
||||||
|
..Config::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the compatibility dialect, keeping other settings.
|
||||||
|
pub fn with_compat(mut self, compat: Compat) -> Self {
|
||||||
|
self.compat = compat;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable/disable dangling-passive removal, keeping other settings.
|
||||||
|
pub fn with_topo_reduce(mut self, on: bool) -> Self {
|
||||||
|
self.topo_reduce = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True in PSpice dialect ([`Compat::Pspice`]).
|
||||||
|
pub fn is_pspice(&self) -> bool {
|
||||||
|
self.compat == Compat::Pspice
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of worker cores this run will actually use for expansion.
|
||||||
|
///
|
||||||
|
/// Subckt expansion ([`crate::subckt`]) fans the independent top-level cards
|
||||||
|
/// across this many threads (see `SubcktExpander::expand_parallel`); the
|
||||||
|
/// default is 1. The `.lib`/`.inc` walk ([`crate::preprocess`]) stays
|
||||||
|
/// sequential — a file must be read and indexed before we learn what it pulls
|
||||||
|
/// in, so the reference chain is discovered as it is walked.
|
||||||
|
pub fn effective_cores(&self) -> usize {
|
||||||
|
self.cores.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_is_single_core() {
|
||||||
|
let c = Config::default();
|
||||||
|
assert_eq!(c.cores.get(), 1);
|
||||||
|
assert_eq!(c.effective_cores(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A multi-core request is honored: effective_cores reflects it.
|
||||||
|
#[test]
|
||||||
|
fn extra_cores_are_honored() {
|
||||||
|
let c = Config::with_cores(NonZeroUsize::new(4).unwrap());
|
||||||
|
assert_eq!(c.cores.get(), 4);
|
||||||
|
assert_eq!(c.effective_cores(), 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,901 @@
|
||||||
|
//! SPICE/HSPICE numeric expression engine: number parsing, a Pratt expression
|
||||||
|
//! parser, and an evaluator. This is the computational core of the numparam
|
||||||
|
//! replacement — `.param` values and inline `{...}` expressions are parsed here
|
||||||
|
//! into an AST and evaluated against an [`Env`] (which supplies parameter values
|
||||||
|
//! and user-defined functions).
|
||||||
|
//!
|
||||||
|
//! Values are `f64` (SPICE numbers are doubles). String-valued constructs
|
||||||
|
//! (`str(...)`, table string keys) are not modeled yet.
|
||||||
|
//!
|
||||||
|
//! Grammar (loosest → tightest binding):
|
||||||
|
//! ternary ?: (right assoc)
|
||||||
|
//! logical || &&
|
||||||
|
//! equality == != < > <= >=
|
||||||
|
//! additive + -
|
||||||
|
//! multiplicative * / %
|
||||||
|
//! power ** ^ (right assoc)
|
||||||
|
//! unary - + !
|
||||||
|
//! atom number | ident | ident(args) | ( expr )
|
||||||
|
//!
|
||||||
|
//! Comparisons/booleans yield 1.0 (true) / 0.0 (false), matching ngspice.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Expression AST.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Expr {
|
||||||
|
Num(f64),
|
||||||
|
Var(String),
|
||||||
|
Unary(UnOp, Box<Expr>),
|
||||||
|
Binary(BinOp, Box<Expr>, Box<Expr>),
|
||||||
|
Ternary(Box<Expr>, Box<Expr>, Box<Expr>),
|
||||||
|
Call(String, Vec<Expr>),
|
||||||
|
/// A string literal. SPICE expressions are numeric, but HSPICE's
|
||||||
|
/// `table_param(str("file"), ...)` takes a filename — the one place a string
|
||||||
|
/// reaches the evaluator. It never participates in arithmetic: `eval` rejects
|
||||||
|
/// it, and only `table_param` reads it (via `str_arg`).
|
||||||
|
Str(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum UnOp {
|
||||||
|
Neg,
|
||||||
|
Pos,
|
||||||
|
Not,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BinOp {
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Rem,
|
||||||
|
Pow,
|
||||||
|
Eq,
|
||||||
|
Ne,
|
||||||
|
Lt,
|
||||||
|
Gt,
|
||||||
|
Le,
|
||||||
|
Ge,
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum EvalError {
|
||||||
|
UnknownVar(String),
|
||||||
|
UnknownFunc(String),
|
||||||
|
Arity { func: String, got: usize },
|
||||||
|
Parse(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EvalError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
EvalError::UnknownVar(n) => write!(f, "unknown parameter `{n}`"),
|
||||||
|
EvalError::UnknownFunc(n) => write!(f, "unknown function `{n}`"),
|
||||||
|
EvalError::Arity { func, got } => {
|
||||||
|
write!(f, "function `{func}` called with {got} args")
|
||||||
|
}
|
||||||
|
EvalError::Parse(m) => write!(f, "expression parse error: {m}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for EvalError {}
|
||||||
|
|
||||||
|
/// Environment supplying parameter values and user functions to the evaluator.
|
||||||
|
///
|
||||||
|
/// Methods take `&self`; implementations use interior mutability (RefCell) for
|
||||||
|
/// memoization so hierarchical scopes with parent chains compose without `&mut`
|
||||||
|
/// aliasing headaches during recursive evaluation.
|
||||||
|
pub trait Env {
|
||||||
|
/// Resolve a bare identifier to a value (parameter lookup, possibly memoized).
|
||||||
|
fn var(&self, name: &str) -> Result<f64, EvalError>;
|
||||||
|
/// Call a user-defined function. Return `Ok(None)` to signal "not a user
|
||||||
|
/// function" so the evaluator can fall back to built-ins.
|
||||||
|
fn call_user(&self, name: &str, args: &[f64]) -> Result<Option<f64>, EvalError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Number parsing (SPICE engineering suffixes)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Parse a SPICE number at the start of `s`, returning (value, bytes_consumed).
|
||||||
|
///
|
||||||
|
/// Handles a leading float (with optional `e`/`E` exponent) followed by an
|
||||||
|
/// optional engineering suffix. `meg`/`MEG` (1e6) is checked before `m` (1e-3).
|
||||||
|
/// Any trailing alphabetic characters after the suffix are part of the token but
|
||||||
|
/// ignored for the value (e.g. `1kohm` -> 1000, `10uF` -> 1e-5), matching SPICE.
|
||||||
|
pub fn parse_number(s: &str) -> Option<(f64, usize)> {
|
||||||
|
let b = s.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
// optional sign
|
||||||
|
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let digits_start = i;
|
||||||
|
while i < b.len() && b[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i < b.len() && b[i] == b'.' {
|
||||||
|
i += 1;
|
||||||
|
while i < b.len() && b[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// need at least one digit in the mantissa
|
||||||
|
if s[digits_start..i].bytes().filter(|c| c.is_ascii_digit()).count() == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// exponent
|
||||||
|
if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
|
||||||
|
let mut j = i + 1;
|
||||||
|
if j < b.len() && (b[j] == b'+' || b[j] == b'-') {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
let exp_digits = {
|
||||||
|
let start = j;
|
||||||
|
while j < b.len() && b[j].is_ascii_digit() {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
j > start
|
||||||
|
};
|
||||||
|
if exp_digits {
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mant_str = &s[..i];
|
||||||
|
let mantissa: f64 = mant_str.parse().ok()?;
|
||||||
|
|
||||||
|
// Engineering suffix. For the power-of-10 suffixes, fold the exponent into the
|
||||||
|
// mantissa's decimal string and re-parse (`14n` -> `"14e-9"`) so the result is
|
||||||
|
// the correctly-rounded f64 that a direct decimal (`0.014e-6`) would produce.
|
||||||
|
// Doing `mantissa * 1e-9` instead loses a ULP and breaks the exact geometry
|
||||||
|
// equality tests PDK models rely on (e.g. `l == 0.014e-6`).
|
||||||
|
let rest = &s[i..];
|
||||||
|
let lower = rest.to_ascii_lowercase();
|
||||||
|
// The micro sign as a suffix: ngspice accepts `µ`/`μ` for `u` (1e-6), and PDK
|
||||||
|
// and hand-written decks use it (`2µ` = 2uA). Both are 2-byte UTF-8 chars, so
|
||||||
|
// the ASCII byte match below would miss them and stop the number at the digit,
|
||||||
|
// turning `2µ` into a bare 2 -- 1e6x wrong. Handle them first, by char.
|
||||||
|
let (pow10, suf_len): (Option<i32>, usize) = if rest.starts_with('\u{00B5}') {
|
||||||
|
(Some(-6), '\u{00B5}'.len_utf8())
|
||||||
|
} else if rest.starts_with('\u{03BC}') {
|
||||||
|
(Some(-6), '\u{03BC}'.len_utf8())
|
||||||
|
} else if lower.starts_with("meg") {
|
||||||
|
(Some(6), 3)
|
||||||
|
} else if lower.starts_with("mil") {
|
||||||
|
(None, 3) // 25.4e-6, not a power of ten
|
||||||
|
} else {
|
||||||
|
match lower.bytes().next() {
|
||||||
|
Some(b't') => (Some(12), 1),
|
||||||
|
Some(b'g') => (Some(9), 1),
|
||||||
|
Some(b'k') => (Some(3), 1),
|
||||||
|
Some(b'm') => (Some(-3), 1),
|
||||||
|
Some(b'u') => (Some(-6), 1),
|
||||||
|
Some(b'n') => (Some(-9), 1),
|
||||||
|
Some(b'p') => (Some(-12), 1),
|
||||||
|
Some(b'f') => (Some(-15), 1),
|
||||||
|
Some(b'a') => (Some(-18), 1),
|
||||||
|
_ => (Some(0), 0),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let value = match (suf_len, pow10) {
|
||||||
|
(0, _) => mantissa,
|
||||||
|
(_, None) => mantissa * 25.4e-6, // mil
|
||||||
|
(_, Some(0)) => mantissa,
|
||||||
|
(_, Some(p)) => {
|
||||||
|
// if the mantissa already carries an exponent, fall back to a multiply
|
||||||
|
if mant_str.contains(['e', 'E']) {
|
||||||
|
mantissa * 10f64.powi(p)
|
||||||
|
} else {
|
||||||
|
format!("{mant_str}e{p}")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| mantissa * 10f64.powi(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
i += suf_len;
|
||||||
|
// consume trailing alphanumerics of the unit (ignored), e.g. "ohm" in "1kohm"
|
||||||
|
let rb = s.as_bytes();
|
||||||
|
while i < rb.len() && (rb[i].is_ascii_alphanumeric() || rb[i] == b'_') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((value, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lexer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
enum Tok {
|
||||||
|
Num(f64),
|
||||||
|
Ident(String),
|
||||||
|
Str(String),
|
||||||
|
Op(&'static str),
|
||||||
|
LParen,
|
||||||
|
RParen,
|
||||||
|
Comma,
|
||||||
|
Question,
|
||||||
|
Colon,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex(input: &str) -> Result<Vec<Tok>, EvalError> {
|
||||||
|
let mut toks = Vec::new();
|
||||||
|
let bytes = input.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut depth = 0usize; // paren nesting depth
|
||||||
|
// Depth of an open `v(`/`i(` argument list. Inside it, arguments are NODE
|
||||||
|
// NAMES, not expressions, so `+`/`-` are part of the name.
|
||||||
|
let mut vi_paren: Option<usize> = None;
|
||||||
|
while i < bytes.len() {
|
||||||
|
let c = bytes[i];
|
||||||
|
if c.is_ascii_whitespace() {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Inside a v()/i() argument list the args are node/source names, not
|
||||||
|
// arithmetic: `v(vin+,vin-)` names two differential nodes and the trailing
|
||||||
|
// `+`/`-` belong to the name (opamp macromodels are full of these). Lex
|
||||||
|
// each argument as one raw token so such names parse instead of blowing up
|
||||||
|
// the whole expression (which would then be kept verbatim, unsubstituted
|
||||||
|
// and unrenamed -- e.g. an undefined `gain` reaching ngspice).
|
||||||
|
if vi_paren == Some(depth) {
|
||||||
|
match c {
|
||||||
|
b')' => {
|
||||||
|
vi_paren = None;
|
||||||
|
depth = depth.saturating_sub(1);
|
||||||
|
toks.push(Tok::RParen);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
b',' => {
|
||||||
|
toks.push(Tok::Comma);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let start = i;
|
||||||
|
while i < bytes.len() {
|
||||||
|
let d = bytes[i];
|
||||||
|
if d.is_ascii_whitespace() || d == b',' || d == b')' || d == b'(' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
toks.push(Tok::Ident(input[start..i].to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// `'...'` delimits an EXPRESSION in SPICE, not a string, so the quotes are
|
||||||
|
// transparent to the lexer: `agauss('1-mc_sw',1,3)` is agauss(1-mc_sw,1,3).
|
||||||
|
// Nested quoting like this is common in foundry statistical blocks (foundry_c's
|
||||||
|
// fet_dist.inc), and rejecting it made the whole `.param` unparsable, so
|
||||||
|
// `aclv_nest_ags` never resolved and every dependent parameter was dropped
|
||||||
|
// across all 26 foundry_c decks. String LITERALS use `"` (see Tok::Str) — no
|
||||||
|
// deck in the corpus writes `str('...')`.
|
||||||
|
if c == b'\'' {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// `{...}` is the OTHER SPICE expression delimiter, so a `{` nested inside an
|
||||||
|
// already-braced value is transparent too: `{TABLE(v(a,b),2,{64e-6-iee},...)}`
|
||||||
|
// carries an inner braced sub-expression, and rejecting it made the whole
|
||||||
|
// behavioral source unparsable (kept verbatim with nested braces, which
|
||||||
|
// ngspice then reports as a "mal formed E source"). Treated exactly like the
|
||||||
|
// `'...'` case above -- the outer span is stripped by subst_exprs before we
|
||||||
|
// get here, so any brace we see now is a nested sub-expression.
|
||||||
|
if c == b'{' || c == b'}' {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// number (digit, or leading '.' followed by digit). A leading sign is
|
||||||
|
// handled as a unary operator by the parser, not the number lexer, so
|
||||||
|
// that `a-1` lexes as `a`,`-`,`1`.
|
||||||
|
if c.is_ascii_digit() || (c == b'.' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit())
|
||||||
|
{
|
||||||
|
if let Some((v, used)) = parse_number(&input[i..]) {
|
||||||
|
toks.push(Tok::Num(v));
|
||||||
|
i += used;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// identifier
|
||||||
|
if c.is_ascii_alphabetic() || c == b'_' {
|
||||||
|
let start = i;
|
||||||
|
while i < bytes.len()
|
||||||
|
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' )
|
||||||
|
{
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
toks.push(Tok::Ident(input[start..i].to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// multi-char operators
|
||||||
|
let two = if i + 1 < bytes.len() { &input[i..i + 2] } else { "" };
|
||||||
|
match two {
|
||||||
|
"**" => { toks.push(Tok::Op("**")); i += 2; continue; }
|
||||||
|
"==" => { toks.push(Tok::Op("==")); i += 2; continue; }
|
||||||
|
"!=" => { toks.push(Tok::Op("!=")); i += 2; continue; }
|
||||||
|
"<=" => { toks.push(Tok::Op("<=")); i += 2; continue; }
|
||||||
|
">=" => { toks.push(Tok::Op(">=")); i += 2; continue; }
|
||||||
|
"&&" => { toks.push(Tok::Op("&&")); i += 2; continue; }
|
||||||
|
"||" => { toks.push(Tok::Op("||")); i += 2; continue; }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match c {
|
||||||
|
b'(' => {
|
||||||
|
depth += 1;
|
||||||
|
// A `(` immediately preceded by a `v`/`i` identifier opens a
|
||||||
|
// node-argument list (voltage/current probe), lexed name-wise above.
|
||||||
|
if let Some(Tok::Ident(name)) = toks.last() {
|
||||||
|
if name.eq_ignore_ascii_case("v") || name.eq_ignore_ascii_case("i") {
|
||||||
|
vi_paren = Some(depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toks.push(Tok::LParen);
|
||||||
|
}
|
||||||
|
b')' => {
|
||||||
|
depth = depth.saturating_sub(1);
|
||||||
|
toks.push(Tok::RParen);
|
||||||
|
}
|
||||||
|
b',' => toks.push(Tok::Comma),
|
||||||
|
b'?' => toks.push(Tok::Question),
|
||||||
|
b':' => toks.push(Tok::Colon),
|
||||||
|
b'+' => toks.push(Tok::Op("+")),
|
||||||
|
b'-' => toks.push(Tok::Op("-")),
|
||||||
|
b'*' => toks.push(Tok::Op("*")),
|
||||||
|
b'/' => toks.push(Tok::Op("/")),
|
||||||
|
b'%' => toks.push(Tok::Op("%")),
|
||||||
|
b'^' => toks.push(Tok::Op("^")),
|
||||||
|
b'<' => toks.push(Tok::Op("<")),
|
||||||
|
b'>' => toks.push(Tok::Op(">")),
|
||||||
|
b'!' => toks.push(Tok::Op("!")),
|
||||||
|
// SPICE behavioral expressions use single `|`/`&` as logical or/and
|
||||||
|
// (PSpice `IF((V(a)>0 | V(b)>0),1,0)`); treat them as the boolean
|
||||||
|
// operators, same as `||`/`&&` (matched above when doubled).
|
||||||
|
b'|' => toks.push(Tok::Op("||")),
|
||||||
|
b'&' => toks.push(Tok::Op("&&")),
|
||||||
|
b'"' => {
|
||||||
|
// string literal: `str("./x.table")`. No escapes — SPICE paths
|
||||||
|
// never contain quotes, and neither HSPICE nor ngspice's reader
|
||||||
|
// define an escape here.
|
||||||
|
let start = i + 1;
|
||||||
|
let mut j = start;
|
||||||
|
while j < bytes.len() && bytes[j] != b'"' {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j >= bytes.len() {
|
||||||
|
return Err(EvalError::Parse("unterminated string literal".into()));
|
||||||
|
}
|
||||||
|
toks.push(Tok::Str(input[start..j].to_string()));
|
||||||
|
i = j; // the trailing `i += 1` below steps past the closing quote
|
||||||
|
}
|
||||||
|
_ => return Err(EvalError::Parse(format!("unexpected char {:?}", c as char))),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Ok(toks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parser (Pratt)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Parser {
|
||||||
|
toks: Vec<Tok>,
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
fn peek(&self) -> Option<&Tok> {
|
||||||
|
self.toks.get(self.pos)
|
||||||
|
}
|
||||||
|
fn next(&mut self) -> Option<Tok> {
|
||||||
|
let t = self.toks.get(self.pos).cloned();
|
||||||
|
if t.is_some() {
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
t
|
||||||
|
}
|
||||||
|
fn eat(&mut self, t: &Tok) -> Result<(), EvalError> {
|
||||||
|
if self.peek() == Some(t) {
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(EvalError::Parse(format!("expected {t:?}, found {:?}", self.peek())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// left binding power for binary operators; higher binds tighter.
|
||||||
|
fn bin_bp(op: &str) -> Option<(u8, BinOp, bool)> {
|
||||||
|
// (bp, op, right_assoc)
|
||||||
|
Some(match op {
|
||||||
|
"||" => (1, BinOp::Or, false),
|
||||||
|
"&&" => (2, BinOp::And, false),
|
||||||
|
"==" => (3, BinOp::Eq, false),
|
||||||
|
"!=" => (3, BinOp::Ne, false),
|
||||||
|
"<" => (4, BinOp::Lt, false),
|
||||||
|
">" => (4, BinOp::Gt, false),
|
||||||
|
"<=" => (4, BinOp::Le, false),
|
||||||
|
">=" => (4, BinOp::Ge, false),
|
||||||
|
"+" => (5, BinOp::Add, false),
|
||||||
|
"-" => (5, BinOp::Sub, false),
|
||||||
|
"*" => (6, BinOp::Mul, false),
|
||||||
|
"/" => (6, BinOp::Div, false),
|
||||||
|
"%" => (6, BinOp::Rem, false),
|
||||||
|
"**" | "^" => (8, BinOp::Pow, true),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_expr(&mut self, min_bp: u8) -> Result<Expr, EvalError> {
|
||||||
|
// Prefix operators bind looser than `**`/`^` (bp 8) but tighter than the
|
||||||
|
// multiplicative operators (bp 6), so `-2**2` == `-(2**2)` and
|
||||||
|
// `-2*3` == `(-2)*3`, matching HSPICE.
|
||||||
|
const PREFIX_BP: u8 = 7;
|
||||||
|
let mut lhs = match self.peek() {
|
||||||
|
Some(Tok::Op("-")) => { self.pos += 1; Expr::Unary(UnOp::Neg, Box::new(self.parse_expr(PREFIX_BP)?)) }
|
||||||
|
Some(Tok::Op("+")) => { self.pos += 1; Expr::Unary(UnOp::Pos, Box::new(self.parse_expr(PREFIX_BP)?)) }
|
||||||
|
Some(Tok::Op("!")) => { self.pos += 1; Expr::Unary(UnOp::Not, Box::new(self.parse_expr(PREFIX_BP)?)) }
|
||||||
|
_ => self.parse_atom()?,
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let op = match self.peek() {
|
||||||
|
Some(Tok::Op(o)) => *o,
|
||||||
|
_ => break,
|
||||||
|
};
|
||||||
|
let Some((bp, binop, right)) = Self::bin_bp(op) else { break };
|
||||||
|
if bp < min_bp {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.pos += 1;
|
||||||
|
let next_min = if right { bp } else { bp + 1 };
|
||||||
|
let rhs = self.parse_expr(next_min)?;
|
||||||
|
lhs = Expr::Binary(binop, Box::new(lhs), Box::new(rhs));
|
||||||
|
}
|
||||||
|
// ternary has the lowest precedence; bind it once lhs is complete.
|
||||||
|
if min_bp == 0 {
|
||||||
|
if let Some(Tok::Question) = self.peek() {
|
||||||
|
self.pos += 1;
|
||||||
|
let then_e = self.parse_expr(0)?;
|
||||||
|
self.eat(&Tok::Colon)?;
|
||||||
|
let else_e = self.parse_expr(0)?;
|
||||||
|
lhs = Expr::Ternary(Box::new(lhs), Box::new(then_e), Box::new(else_e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(lhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_atom(&mut self) -> Result<Expr, EvalError> {
|
||||||
|
match self.next() {
|
||||||
|
Some(Tok::Num(v)) => Ok(Expr::Num(v)),
|
||||||
|
Some(Tok::LParen) => {
|
||||||
|
let e = self.parse_expr(0)?;
|
||||||
|
self.eat(&Tok::RParen)?;
|
||||||
|
Ok(e)
|
||||||
|
}
|
||||||
|
Some(Tok::Ident(name)) => {
|
||||||
|
if self.peek() == Some(&Tok::LParen) {
|
||||||
|
self.pos += 1;
|
||||||
|
let mut args = Vec::new();
|
||||||
|
if self.peek() != Some(&Tok::RParen) {
|
||||||
|
loop {
|
||||||
|
args.push(self.parse_expr(0)?);
|
||||||
|
match self.peek() {
|
||||||
|
Some(Tok::Comma) => { self.pos += 1; }
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.eat(&Tok::RParen)?;
|
||||||
|
Ok(Expr::Call(name, args))
|
||||||
|
} else {
|
||||||
|
Ok(Expr::Var(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Tok::Str(v)) => Ok(Expr::Str(v)),
|
||||||
|
other => Err(EvalError::Parse(format!("unexpected token {other:?}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an expression string into an AST.
|
||||||
|
pub fn parse(input: &str) -> Result<Expr, EvalError> {
|
||||||
|
let toks = lex(input)?;
|
||||||
|
let mut p = Parser { toks, pos: 0 };
|
||||||
|
let e = p.parse_expr(0)?;
|
||||||
|
if p.pos != p.toks.len() {
|
||||||
|
return Err(EvalError::Parse(format!(
|
||||||
|
"trailing tokens from {:?}",
|
||||||
|
&p.toks[p.pos..]
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Evaluation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn as_bool(v: f64) -> bool {
|
||||||
|
v != 0.0
|
||||||
|
}
|
||||||
|
fn from_bool(b: bool) -> f64 {
|
||||||
|
if b { 1.0 } else { 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a built-in function. Returns `None` if `name` is not a built-in.
|
||||||
|
/// Extract a string argument: either a bare literal or `str("...")`, which is how
|
||||||
|
/// HSPICE decks spell a filename (`table_param(str("./x.table"), ...)`).
|
||||||
|
/// HSPICE's `str()` is a to-string coercion; on a literal it is the identity, so
|
||||||
|
/// unwrapping it here is exact — and it is the only context a string can appear in.
|
||||||
|
pub fn str_arg(e: &Expr) -> Option<&str> {
|
||||||
|
match e {
|
||||||
|
Expr::Str(s) => Some(s),
|
||||||
|
Expr::Call(f, a) if f.eq_ignore_ascii_case("str") && a.len() == 1 => str_arg(&a[0]),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a `table_param(file, N_int, ints.., N_real, reals.., col)` call from
|
||||||
|
/// its argument ASTs. `evala` evaluates a numeric argument.
|
||||||
|
///
|
||||||
|
/// Returns `None` if this is not a well-formed `table_param` call, so the caller
|
||||||
|
/// can fall through to the ordinary builtin path.
|
||||||
|
pub fn eval_table_param<F>(
|
||||||
|
name: &str,
|
||||||
|
args: &[Expr],
|
||||||
|
mut evala: F,
|
||||||
|
) -> Option<Result<f64, EvalError>>
|
||||||
|
where
|
||||||
|
F: FnMut(&Expr) -> Result<f64, EvalError>,
|
||||||
|
{
|
||||||
|
if !name.eq_ignore_ascii_case("table_param") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bad = |m: &str| Some(Err(EvalError::Parse(format!("table_param: {m}"))));
|
||||||
|
let Some(path) = args.first().and_then(str_arg) else {
|
||||||
|
return bad("first argument must be a file name");
|
||||||
|
};
|
||||||
|
|
||||||
|
// file, N_int, <N_int values>, N_real, <N_real values>, output_col
|
||||||
|
let mut i = 1;
|
||||||
|
// `count(i)` reads a key-count argument: a non-negative integer.
|
||||||
|
macro_rules! count {
|
||||||
|
() => {{
|
||||||
|
let Some(a) = args.get(i) else {
|
||||||
|
return bad("missing key count");
|
||||||
|
};
|
||||||
|
let v = match evala(a) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return Some(Err(e)),
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
if v < 0.0 || v.fract() != 0.0 {
|
||||||
|
return bad("key count must be a non-negative integer");
|
||||||
|
}
|
||||||
|
v as usize
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
let n_int = count!();
|
||||||
|
let mut ints = Vec::with_capacity(n_int);
|
||||||
|
for _ in 0..n_int {
|
||||||
|
match args.get(i).map(&mut evala) {
|
||||||
|
Some(Ok(v)) => ints.push(v),
|
||||||
|
Some(Err(e)) => return Some(Err(e)),
|
||||||
|
None => return bad("too few integer keys"),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let n_real = count!();
|
||||||
|
let mut reals = Vec::with_capacity(n_real);
|
||||||
|
for _ in 0..n_real {
|
||||||
|
match args.get(i).map(&mut evala) {
|
||||||
|
Some(Ok(v)) => reals.push(v),
|
||||||
|
Some(Err(e)) => return Some(Err(e)),
|
||||||
|
None => return bad("too few real keys"),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let col = match args.get(i).map(&mut evala) {
|
||||||
|
Some(Ok(v)) => v as i64,
|
||||||
|
Some(Err(e)) => return Some(Err(e)),
|
||||||
|
None => return bad("missing output column"),
|
||||||
|
};
|
||||||
|
if i + 1 != args.len() {
|
||||||
|
return bad("too many arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
match crate::table::lookup(path, &ints, &reals, col) {
|
||||||
|
Some(v) => Some(Ok(v)),
|
||||||
|
None => Some(Err(EvalError::Parse(format!(
|
||||||
|
"table_param: lookup failed in {path:?}"
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_builtin(name: &str, a: &[f64]) -> Option<Result<f64, EvalError>> {
|
||||||
|
let n = name.to_ascii_lowercase();
|
||||||
|
let one = |f: fn(f64) -> f64| -> Option<Result<f64, EvalError>> {
|
||||||
|
if a.len() == 1 { Some(Ok(f(a[0]))) }
|
||||||
|
else { Some(Err(EvalError::Arity { func: n.clone(), got: a.len() })) }
|
||||||
|
};
|
||||||
|
match n.as_str() {
|
||||||
|
"sqrt" => one(f64::sqrt),
|
||||||
|
"exp" => one(f64::exp),
|
||||||
|
"ln" | "log" => one(f64::ln),
|
||||||
|
"log10" => one(f64::log10),
|
||||||
|
"abs" => one(f64::abs),
|
||||||
|
"sin" => one(f64::sin),
|
||||||
|
"cos" => one(f64::cos),
|
||||||
|
"tan" => one(f64::tan),
|
||||||
|
"asin" => one(f64::asin),
|
||||||
|
"acos" => one(f64::acos),
|
||||||
|
"atan" => one(f64::atan),
|
||||||
|
"sinh" => one(f64::sinh),
|
||||||
|
"cosh" => one(f64::cosh),
|
||||||
|
"tanh" => one(f64::tanh),
|
||||||
|
"asinh" => one(f64::asinh),
|
||||||
|
"acosh" => one(f64::acosh),
|
||||||
|
"atanh" => one(f64::atanh),
|
||||||
|
// Step/ramp and compare-to-zero helpers, matching ngspice
|
||||||
|
// `spicelib/parser/ptfuncs.c` exactly (PTustep/PTustep2/PTuramp/PTeq0...).
|
||||||
|
"u" => one(|x| if x < 0.0 { 0.0 } else if x > 0.0 { 1.0 } else { 0.5 }),
|
||||||
|
"u2" => one(|x| if x <= 0.0 { 0.0 } else if x <= 1.0 { x } else { 1.0 }),
|
||||||
|
"uramp" => one(|x| if x < 0.0 { 0.0 } else { x }),
|
||||||
|
"eq0" => one(|x| if x == 0.0 { 1.0 } else { 0.0 }),
|
||||||
|
"ne0" => one(|x| if x != 0.0 { 1.0 } else { 0.0 }),
|
||||||
|
"gt0" => one(|x| if x > 0.0 { 1.0 } else { 0.0 }),
|
||||||
|
"lt0" => one(|x| if x < 0.0 { 1.0 } else { 0.0 }),
|
||||||
|
"ge0" => one(|x| if x >= 0.0 { 1.0 } else { 0.0 }),
|
||||||
|
"le0" => one(|x| if x <= 0.0 { 1.0 } else { 0.0 }),
|
||||||
|
"int" => one(f64::trunc),
|
||||||
|
// ngspice PTnint uses nearbyint(): round half-integers to the nearest EVEN
|
||||||
|
// integer (banker's rounding), NOT away-from-zero like f64::round().
|
||||||
|
// e.g. nint(2.5)=2, nint(0.5)=0, nint(-0.5)=0, nint(-2.5)=-2.
|
||||||
|
"nint" => one(f64::round_ties_even),
|
||||||
|
"ceil" => one(f64::ceil),
|
||||||
|
"floor" => one(f64::floor),
|
||||||
|
"sgn" | "sign" => one(|x| if x > 0.0 { 1.0 } else if x < 0.0 { -1.0 } else { 0.0 }),
|
||||||
|
"pow" | "pwr" => {
|
||||||
|
if a.len() == 2 { Some(Ok(a[0].powf(a[1]))) }
|
||||||
|
else { Some(Err(EvalError::Arity { func: n, got: a.len() })) }
|
||||||
|
}
|
||||||
|
"min" => {
|
||||||
|
if a.len() == 2 { Some(Ok(a[0].min(a[1]))) }
|
||||||
|
else { Some(Err(EvalError::Arity { func: n, got: a.len() })) }
|
||||||
|
}
|
||||||
|
"max" => {
|
||||||
|
if a.len() == 2 { Some(Ok(a[0].max(a[1]))) }
|
||||||
|
else { Some(Err(EvalError::Arity { func: n, got: a.len() })) }
|
||||||
|
}
|
||||||
|
// Statistical draws: in a non-Monte-Carlo run these resolve to the
|
||||||
|
// nominal (first argument). agauss(nom,var,sigma), aunif(nom,var), etc.
|
||||||
|
//
|
||||||
|
// `limit(nominal, abs_variation)` belongs to the same family — ngspice
|
||||||
|
// lists it alongside the others (`inp.c:979` iterates
|
||||||
|
// {agauss, gauss, aunif, unif, limit}) and implements it as
|
||||||
|
// nominal + (drand() > 0 ? abs_variation : -abs_variation)
|
||||||
|
// with `drand()` uniform on [-1,+1), i.e. a true coin flip about
|
||||||
|
// `nominal`. So its mean is the first argument, like the rest. Unseeded
|
||||||
|
// and therefore non-reproducible run-to-run, which is why we return the
|
||||||
|
// nominal rather than chase a bit-match (the accepted agauss policy).
|
||||||
|
//
|
||||||
|
// foundry_c's PDK needs this: `limit(-0.5, 0.5)` appears inside every
|
||||||
|
// `prdsw_<dev>` chain, and without it the whole expression failed to fold
|
||||||
|
// and the parameter was DROPPED (silently defaulting) on all 26 decks.
|
||||||
|
"agauss" | "gauss" | "aunif" | "unif" | "limit" => {
|
||||||
|
if a.is_empty() { Some(Err(EvalError::Arity { func: n, got: 0 })) }
|
||||||
|
// 3-arg `limit(x,lo,hi)` is PSpice's clamp (HSPICE's MC `limit` takes
|
||||||
|
// 2 args) — the arity alone disambiguates the dialects.
|
||||||
|
else if n == "limit" && a.len() == 3 {
|
||||||
|
let (lo, hi) = if a[1] < a[2] { (a[1], a[2]) } else { (a[2], a[1]) };
|
||||||
|
Some(Ok(a[0].max(lo).min(hi)))
|
||||||
|
}
|
||||||
|
else { Some(Ok(a[0])) }
|
||||||
|
}
|
||||||
|
// `if(cond, a, b)` functional form of the ternary.
|
||||||
|
"if" => {
|
||||||
|
if a.len() == 3 { Some(Ok(if as_bool(a[0]) { a[1] } else { a[2] })) }
|
||||||
|
else { Some(Err(EvalError::Arity { func: n, got: a.len() })) }
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate an expression against an environment.
|
||||||
|
pub fn eval(e: &Expr, env: &dyn Env) -> Result<f64, EvalError> {
|
||||||
|
match e {
|
||||||
|
Expr::Num(v) => Ok(*v),
|
||||||
|
Expr::Var(name) => env.var(name),
|
||||||
|
Expr::Unary(op, x) => {
|
||||||
|
let v = eval(x, env)?;
|
||||||
|
Ok(match op {
|
||||||
|
UnOp::Neg => -v,
|
||||||
|
UnOp::Pos => v,
|
||||||
|
UnOp::Not => from_bool(!as_bool(v)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Expr::Binary(op, l, r) => {
|
||||||
|
let a = eval(l, env)?;
|
||||||
|
let b = eval(r, env)?;
|
||||||
|
Ok(match op {
|
||||||
|
BinOp::Add => a + b,
|
||||||
|
BinOp::Sub => a - b,
|
||||||
|
BinOp::Mul => a * b,
|
||||||
|
BinOp::Div => a / b,
|
||||||
|
BinOp::Rem => a % b,
|
||||||
|
BinOp::Pow => a.powf(b),
|
||||||
|
BinOp::Eq => from_bool(a == b),
|
||||||
|
BinOp::Ne => from_bool(a != b),
|
||||||
|
BinOp::Lt => from_bool(a < b),
|
||||||
|
BinOp::Gt => from_bool(a > b),
|
||||||
|
BinOp::Le => from_bool(a <= b),
|
||||||
|
BinOp::Ge => from_bool(a >= b),
|
||||||
|
BinOp::And => from_bool(as_bool(a) && as_bool(b)),
|
||||||
|
BinOp::Or => from_bool(as_bool(a) || as_bool(b)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Expr::Ternary(c, t, f) => {
|
||||||
|
if as_bool(eval(c, env)?) { eval(t, env) } else { eval(f, env) }
|
||||||
|
}
|
||||||
|
Expr::Str(_) => Err(EvalError::Parse(
|
||||||
|
"string literal used where a number is required".into(),
|
||||||
|
)),
|
||||||
|
Expr::Call(name, args) => {
|
||||||
|
// table_param needs its args unevaluated: the first is a string.
|
||||||
|
if let Some(r) = eval_table_param(name, args, |a| eval(a, env)) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
let mut vals = Vec::with_capacity(args.len());
|
||||||
|
for a in args {
|
||||||
|
vals.push(eval(a, env)?);
|
||||||
|
}
|
||||||
|
// user functions take precedence, then built-ins
|
||||||
|
if let Some(v) = env.call_user(name, &vals)? {
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
match eval_builtin(name, &vals) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => Err(EvalError::UnknownFunc(name.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// HSPICE spells a table filename `str("...")`; the parser must accept the
|
||||||
|
/// string literal, and `str_arg` must see through the `str()` wrapper.
|
||||||
|
#[test]
|
||||||
|
fn parses_string_literals_and_str_wrapper() {
|
||||||
|
let e = parse(r#"str("./RF_COMPONENTS/egnfet_SHE.table")"#).unwrap();
|
||||||
|
assert_eq!(str_arg(&e), Some("./RF_COMPONENTS/egnfet_SHE.table"));
|
||||||
|
let e = parse(r#""bare.table""#).unwrap();
|
||||||
|
assert_eq!(str_arg(&e), Some("bare.table"));
|
||||||
|
// a string is not a number
|
||||||
|
assert!(eval(&parse(r#""x""#).unwrap(), &MapEnv(HashMap::new())).is_err());
|
||||||
|
assert!(parse(r#"str("unterminated"#).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full foundry_b call shape must parse: file, N_int, ints, N_real, reals, col.
|
||||||
|
#[test]
|
||||||
|
fn parses_table_param_call() {
|
||||||
|
let e = parse(
|
||||||
|
r#"table_param(str("./x.table"),2, xnf_clamp, nfin_clamp, 1, l_clamp, 1)"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
match &e {
|
||||||
|
Expr::Call(n, a) => {
|
||||||
|
assert!(n.eq_ignore_ascii_case("table_param"));
|
||||||
|
assert_eq!(a.len(), 7);
|
||||||
|
assert_eq!(str_arg(&a[0]), Some("./x.table"));
|
||||||
|
}
|
||||||
|
other => panic!("expected a call, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A failed lookup must be an ERROR, never a silent default — a wrong rth0
|
||||||
|
/// would quietly change self-heating and converge to the wrong answer.
|
||||||
|
#[test]
|
||||||
|
fn failed_lookup_is_an_error_not_a_default() {
|
||||||
|
let e = parse(r#"table_param(str("/nonexistent/x.table"), 1, 1, 0, 1)"#).unwrap();
|
||||||
|
assert!(eval(&e, &MapEnv(HashMap::new())).is_err());
|
||||||
|
}
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
struct MapEnv(HashMap<String, f64>);
|
||||||
|
impl Env for MapEnv {
|
||||||
|
fn var(&self, name: &str) -> Result<f64, EvalError> {
|
||||||
|
self.0
|
||||||
|
.get(&name.to_ascii_lowercase())
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| EvalError::UnknownVar(name.to_string()))
|
||||||
|
}
|
||||||
|
fn call_user(&self, _n: &str, _a: &[f64]) -> Result<Option<f64>, EvalError> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ev(s: &str, vars: &[(&str, f64)]) -> f64 {
|
||||||
|
let map = vars.iter().map(|(k, v)| (k.to_string(), *v)).collect();
|
||||||
|
let env = MapEnv(map);
|
||||||
|
eval(&parse(s).unwrap(), &env).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers_with_suffixes() {
|
||||||
|
assert_eq!(parse_number("1k").unwrap().0, 1000.0);
|
||||||
|
// micro sign: `µ` (U+00B5) and Greek `μ` (U+03BC) both mean u = 1e-6.
|
||||||
|
assert_eq!(parse_number("2\u{00B5}").unwrap().0, 2e-6);
|
||||||
|
assert_eq!(parse_number("2\u{03BC}").unwrap().0, 2e-6);
|
||||||
|
assert_eq!(parse_number("2.5e-08").unwrap().0, 2.5e-8);
|
||||||
|
assert_eq!(parse_number("550n").unwrap().0, 550e-9);
|
||||||
|
assert_eq!(parse_number("16u").unwrap().0, 16e-6);
|
||||||
|
assert_eq!(parse_number("1meg").unwrap().0, 1e6);
|
||||||
|
assert_eq!(parse_number("1mil").unwrap().0, 25.4e-6);
|
||||||
|
assert_eq!(parse_number("2p").unwrap().0, 2e-12);
|
||||||
|
assert_eq!(parse_number("1kohm").unwrap().0, 1000.0); // trailing unit ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn precedence_and_power() {
|
||||||
|
assert_eq!(ev("2+3*4", &[]), 14.0);
|
||||||
|
assert_eq!(ev("(2+3)*4", &[]), 20.0);
|
||||||
|
assert_eq!(ev("2**3**2", &[]), 512.0); // right assoc
|
||||||
|
assert_eq!(ev("-2**2", &[]), -4.0); // unary binds looser than ** -> -(2**2)
|
||||||
|
assert_eq!(ev("-2*3", &[]), -6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparisons_and_ternary() {
|
||||||
|
assert_eq!(ev("3==3", &[]), 1.0);
|
||||||
|
assert_eq!(ev("m==m1", &[("m", 5.0), ("m1", 5.0)]), 1.0);
|
||||||
|
assert_eq!(ev("a>b ? 10 : 20", &[("a", 1.0), ("b", 2.0)]), 20.0);
|
||||||
|
assert_eq!(ev("5*(x==m2)", &[("x", 2.0), ("m2", 2.0)]), 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtins_and_params() {
|
||||||
|
assert_eq!(ev("max(3,7)", &[]), 7.0);
|
||||||
|
assert_eq!(ev("sqrt(16)", &[]), 4.0);
|
||||||
|
assert_eq!(ev("abs(0-5)", &[]), 5.0);
|
||||||
|
assert_eq!(ev("agauss(1.5, 0.2, 3)", &[]), 1.5); // nominal
|
||||||
|
// `limit(nominal, abs_variation)` is the same family: ngspice returns
|
||||||
|
// nominal +/- abs_variation on an unseeded coin flip, so the nominal is
|
||||||
|
// the first argument. foundry_c's prdsw_* chains use limit(-0.5, 0.5).
|
||||||
|
assert_eq!(ev("limit(-0.5, 0.5)", &[]), -0.5);
|
||||||
|
assert_eq!(ev("limit(2.0, 0.25)", &[]), 2.0);
|
||||||
|
assert_eq!(ev("0*(corner_sigma/3)", &[("corner_sigma", 3.0)]), 0.0);
|
||||||
|
assert_eq!(ev("if(1>0, 2, 3)", &[]), 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inside v()/i(), a `+`/`-` suffix is part of the node name (differential
|
||||||
|
/// pins like VIN+/VIN-), not an operator. Such names must parse -- otherwise
|
||||||
|
/// the whole behavioral expression is kept verbatim, unsubstituted (an opamp
|
||||||
|
/// macromodel's `gain` param then reaches ngspice undefined).
|
||||||
|
#[test]
|
||||||
|
fn node_names_with_sign() {
|
||||||
|
// node names with +/- parse as single Var arguments to v()/i()
|
||||||
|
match parse("v(vin+,vin-)").unwrap() {
|
||||||
|
Expr::Call(f, args) => {
|
||||||
|
assert_eq!(f, "v");
|
||||||
|
assert!(matches!(&args[0], Expr::Var(n) if n == "vin+"));
|
||||||
|
assert!(matches!(&args[1], Expr::Var(n) if n == "vin-"));
|
||||||
|
}
|
||||||
|
other => panic!("expected v() call, got {other:?}"),
|
||||||
|
}
|
||||||
|
// the outer expression still parses (was previously a hard parse error)
|
||||||
|
assert!(parse("limit(g*v(vin+,vin-),v(vp-,vin-),v(vp+,vin-))").is_ok());
|
||||||
|
// arithmetic OUTSIDE a v()/i() call is unaffected: `a-1` is still `a` `-` `1`
|
||||||
|
assert_eq!(ev("a-1", &[("a", 5.0)]), 4.0);
|
||||||
|
assert_eq!(ev("2*(x-y)", &[("x", 3.0), ("y", 1.0)]), 4.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
//! C ABI for the ngspice glue.
|
||||||
|
//!
|
||||||
|
//! The contract is deliberately narrow: hand ngspice the same thing its own
|
||||||
|
//! frontend would have produced by the time it reaches `if_inpdeck` — a flat,
|
||||||
|
//! fully-resolved list of card texts, title first — and let it get on with
|
||||||
|
//! `INPpas1/2/3`. Everything downstream of expansion (`eval_agauss`,
|
||||||
|
//! `ENHtranslate_poly`, `inp_dodeck`) stays ngspice's job.
|
||||||
|
//!
|
||||||
|
//! Ownership: every pointer handed out belongs to the [`NgpDeck`] it came from and
|
||||||
|
//! stays valid until [`ngparse_deck_free`]. Nothing is copied out; the C side must
|
||||||
|
//! not free or mutate any of it.
|
||||||
|
//!
|
||||||
|
//! ```c
|
||||||
|
//! NgpDeck *d = ngparse_expand_file("tb_driver.net", 1, 0);
|
||||||
|
//! if (!d) { fprintf(stderr, "%s\n", ngparse_last_error()); return 1; }
|
||||||
|
//! for (size_t i = 0; i < ngparse_deck_len(d); i++)
|
||||||
|
//! puts(ngparse_deck_card(d, i)); /* card 0 is the title */
|
||||||
|
//! ngparse_deck_free(d);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Panics never cross the boundary (that would be UB): every entry point traps
|
||||||
|
//! them and reports through [`ngparse_last_error`].
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ffi::{c_char, c_int, CStr, CString};
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::subckt::SubcktExpander;
|
||||||
|
use crate::Expander;
|
||||||
|
|
||||||
|
/// An expanded deck: the card texts, title first, plus the drops the expansion
|
||||||
|
/// could not resolve. Opaque to C.
|
||||||
|
pub struct NgpDeck {
|
||||||
|
/// NUL-terminated card texts, kept alive for the deck's lifetime.
|
||||||
|
cards: Vec<CString>,
|
||||||
|
/// Parameters that could not be resolved (see `Expanded::drops`).
|
||||||
|
drops: Vec<CString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error(msg: impl Into<Vec<u8>>) {
|
||||||
|
let c = CString::new(msg).unwrap_or_else(|_| CString::new("error").unwrap());
|
||||||
|
LAST_ERROR.with(|e| *e.borrow_mut() = Some(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `f`, converting a panic into a `NULL`/error return. A panic unwinding into
|
||||||
|
/// C is undefined behavior, so this must wrap every entry point.
|
||||||
|
fn guard<T>(default: T, f: impl FnOnce() -> T + std::panic::UnwindSafe) -> T {
|
||||||
|
match std::panic::catch_unwind(f) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
set_error("ngparse: internal panic (this is a bug; please report the deck)");
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last error on this thread, or `NULL` if none. Owned by ngparse; valid until
|
||||||
|
/// the next failing call on this thread.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// The returned pointer must not be freed or retained across further ngparse calls.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn ngparse_last_error() -> *const c_char {
|
||||||
|
LAST_ERROR.with(|e| match &*e.borrow() {
|
||||||
|
Some(c) => c.as_ptr(),
|
||||||
|
None => std::ptr::null(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand `path` into a flat, resolved card list.
|
||||||
|
///
|
||||||
|
/// `cores` is the worker-core budget; **pass 1**. Values above 1 are accepted for
|
||||||
|
/// forward compatibility but are not yet honored — the parser is sequential (see
|
||||||
|
/// `Config::effective_cores`). 0 is treated as 1.
|
||||||
|
///
|
||||||
|
/// `compat` selects the input dialect: `1` = PSpice (`ngbehavior=ps`), anything
|
||||||
|
/// else = default/HSPICE. The glue reads ngspice's `ngbehavior` via `cp_getvar`
|
||||||
|
/// and passes it here so PSpice decks get the same conversions the reference
|
||||||
|
/// applies (see [`crate::config::Compat`]).
|
||||||
|
///
|
||||||
|
/// Returns `NULL` on failure, with the reason in [`ngparse_last_error`]. The result
|
||||||
|
/// must be released with [`ngparse_deck_free`].
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `path` must be a valid NUL-terminated C string.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn ngparse_expand_file(
|
||||||
|
path: *const c_char,
|
||||||
|
cores: c_int,
|
||||||
|
compat: c_int,
|
||||||
|
) -> *mut NgpDeck {
|
||||||
|
guard(std::ptr::null_mut(), || {
|
||||||
|
if path.is_null() {
|
||||||
|
set_error("ngparse_expand_file: path is NULL");
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
let path = match unsafe { CStr::from_ptr(path) }.to_str() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
set_error("ngparse_expand_file: path is not valid UTF-8");
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cfg = Config::with_cores(
|
||||||
|
NonZeroUsize::new(cores.max(1) as usize).unwrap_or(NonZeroUsize::MIN),
|
||||||
|
)
|
||||||
|
.with_compat(crate::config::Compat::from_ffi(compat))
|
||||||
|
// Dangling-passive removal for the integrated build, without an ABI
|
||||||
|
// change: NGPARSE_TOPO_REDUCE=1 in the environment turns it on.
|
||||||
|
.with_topo_reduce(
|
||||||
|
std::env::var("NGPARSE_TOPO_REDUCE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut ex = Expander::with_config(cfg);
|
||||||
|
let flat = match ex.expand_file(Path::new(path)) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
set_error(format!("ngparse: {path}: {e}"));
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let expanded = SubcktExpander::with_config(&flat, cfg).expand();
|
||||||
|
|
||||||
|
// Card 0 is the TITLE. SPICE consumes the first card of a deck as the
|
||||||
|
// title and starts the netlist at card 1 — `if_inpdeck` walks straight
|
||||||
|
// into INPpas1 from card 0 with no title skip of its own, so the title has
|
||||||
|
// to be here or the first real card is silently eaten.
|
||||||
|
let mut cards = Vec::with_capacity(expanded.cards.len() + 1);
|
||||||
|
let title = ex.title();
|
||||||
|
let title = if title.trim().is_empty() { "*" } else { title };
|
||||||
|
let mut push = |s: &str| {
|
||||||
|
// A NUL cannot appear in a SPICE deck; if one somehow does, cut there
|
||||||
|
// rather than fail the whole load.
|
||||||
|
cards.push(CString::new(s).unwrap_or_else(|e| {
|
||||||
|
let n = e.nul_position();
|
||||||
|
CString::new(&e.into_vec()[..n]).unwrap()
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
push(title);
|
||||||
|
for c in &expanded.cards {
|
||||||
|
push(c);
|
||||||
|
}
|
||||||
|
let drops = expanded
|
||||||
|
.drops
|
||||||
|
.iter()
|
||||||
|
.filter_map(|d| CString::new(d.as_str()).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Box::into_raw(Box::new(NgpDeck { cards, drops }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of cards, including the title at index 0.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `d` must be a live deck from [`ngparse_expand_file`], or NULL.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn ngparse_deck_len(d: *const NgpDeck) -> usize {
|
||||||
|
if d.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
unsafe { &*d }.cards.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Card `i` as a NUL-terminated string, or `NULL` if out of range. Index 0 is the
|
||||||
|
/// title. Borrowed from the deck — do not free.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `d` must be a live deck from [`ngparse_expand_file`], or NULL.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn ngparse_deck_card(d: *const NgpDeck, i: usize) -> *const c_char {
|
||||||
|
if d.is_null() {
|
||||||
|
return std::ptr::null();
|
||||||
|
}
|
||||||
|
match unsafe { &*d }.cards.get(i) {
|
||||||
|
Some(c) => c.as_ptr(),
|
||||||
|
None => std::ptr::null(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of parameters that could not be resolved.
|
||||||
|
///
|
||||||
|
/// A drop is never harmless: it means a device or model silently fell back to a
|
||||||
|
/// DEFAULT, which yields a wrong-but-converging answer. The caller should surface
|
||||||
|
/// these (and may refuse the deck, as the CLI's `--strict` does).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `d` must be a live deck from [`ngparse_expand_file`], or NULL.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn ngparse_deck_drop_count(d: *const NgpDeck) -> usize {
|
||||||
|
if d.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
unsafe { &*d }.drops.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop `i` as a human-readable message, or `NULL` if out of range. Borrowed.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `d` must be a live deck from [`ngparse_expand_file`], or NULL.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn ngparse_deck_drop(d: *const NgpDeck, i: usize) -> *const c_char {
|
||||||
|
if d.is_null() {
|
||||||
|
return std::ptr::null();
|
||||||
|
}
|
||||||
|
match unsafe { &*d }.drops.get(i) {
|
||||||
|
Some(c) => c.as_ptr(),
|
||||||
|
None => std::ptr::null(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release a deck. Safe to call with NULL. All pointers previously handed out for
|
||||||
|
/// this deck become dangling.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `d` must be a deck from [`ngparse_expand_file`] that has not already been
|
||||||
|
/// freed, or NULL.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn ngparse_deck_free(d: *mut NgpDeck) {
|
||||||
|
if d.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(unsafe { Box::from_raw(d) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ngparse's version string, for banners and bug reports. Static; never freed.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn ngparse_version() -> *const c_char {
|
||||||
|
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn deck(body: &str, tag: &str) -> String {
|
||||||
|
let p = std::env::temp_dir().join(format!("ngparse_ffi_{}_{tag}.cir", std::process::id()));
|
||||||
|
let mut f = std::fs::File::create(&p).unwrap();
|
||||||
|
f.write_all(body.as_bytes()).unwrap();
|
||||||
|
p.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card(d: *const NgpDeck, i: usize) -> String {
|
||||||
|
let p = unsafe { ngparse_deck_card(d, i) };
|
||||||
|
assert!(!p.is_null(), "card {i} missing");
|
||||||
|
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expands_a_deck_title_first() {
|
||||||
|
let p = deck("* my title\n.param r=2k\nr1 n1 0 {r}\nv1 n1 0 1\n.end\n", "ok");
|
||||||
|
let cp = CString::new(p).unwrap();
|
||||||
|
let d = unsafe { ngparse_expand_file(cp.as_ptr(), 1, 0) };
|
||||||
|
assert!(!d.is_null(), "expand failed");
|
||||||
|
// index 0 is the title; SPICE eats it and starts the netlist at 1
|
||||||
|
assert_eq!(card(d, 0), "* my title");
|
||||||
|
let all: Vec<String> = (0..unsafe { ngparse_deck_len(d) }).map(|i| card(d, i)).collect();
|
||||||
|
// top-level `.param` cards are kept deliberately, so `.control` blocks can
|
||||||
|
// still reference them; the device value is resolved regardless
|
||||||
|
assert!(all.iter().any(|c| c.starts_with(".param")), "{all:?}");
|
||||||
|
assert!(
|
||||||
|
all.iter().any(|c| c.starts_with("r1 n1 0 2")),
|
||||||
|
"resolved r1 not found: {all:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(unsafe { ngparse_deck_drop_count(d) }, 0);
|
||||||
|
assert!(unsafe { ngparse_deck_card(d, 999) }.is_null());
|
||||||
|
unsafe { ngparse_deck_free(d) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A deck whose first line is a real card: SPICE still consumes it as the
|
||||||
|
/// title, so ngparse must emit one and not lose the card.
|
||||||
|
#[test]
|
||||||
|
fn bare_title_is_not_a_card() {
|
||||||
|
let p = deck("check scoping\nr1 n1 0 1k\nv1 n1 0 1\n.end\n", "bare");
|
||||||
|
let cp = CString::new(p).unwrap();
|
||||||
|
let d = unsafe { ngparse_expand_file(cp.as_ptr(), 1, 0) };
|
||||||
|
assert!(!d.is_null());
|
||||||
|
assert_eq!(card(d, 0), "check scoping");
|
||||||
|
assert!(card(d, 1).starts_with("r1"));
|
||||||
|
unsafe { ngparse_deck_free(d) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_drops() {
|
||||||
|
// An EXPRESSION referencing an undefined param drops (a bare identifier is
|
||||||
|
// instead kept verbatim -- see is_bare_word). `1+nosuch` can't resolve.
|
||||||
|
let p = deck("* t\n.model dm d (is='1.0+nosuch')\nd1 n1 0 dm\nv1 n1 0 1\n.end\n", "drop");
|
||||||
|
let cp = CString::new(p).unwrap();
|
||||||
|
let d = unsafe { ngparse_expand_file(cp.as_ptr(), 1, 0) };
|
||||||
|
assert!(!d.is_null());
|
||||||
|
assert!(unsafe { ngparse_deck_drop_count(d) } > 0, "drop not reported");
|
||||||
|
assert!(!unsafe { ngparse_deck_drop(d, 0) }.is_null());
|
||||||
|
assert!(unsafe { ngparse_deck_drop(d, 999) }.is_null());
|
||||||
|
unsafe { ngparse_deck_free(d) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn errors_are_reported_not_crashed() {
|
||||||
|
let cp = CString::new("/nonexistent/deck.net").unwrap();
|
||||||
|
let d = unsafe { ngparse_expand_file(cp.as_ptr(), 1, 0) };
|
||||||
|
assert!(d.is_null(), "expected failure");
|
||||||
|
let e = ngparse_last_error();
|
||||||
|
assert!(!e.is_null(), "no error message");
|
||||||
|
let msg = unsafe { CStr::from_ptr(e) }.to_string_lossy().into_owned();
|
||||||
|
assert!(msg.contains("nonexistent"), "unhelpful message: {msg}");
|
||||||
|
|
||||||
|
// NULL path must not crash
|
||||||
|
assert!(unsafe { ngparse_expand_file(std::ptr::null(), 1, 0) }.is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn null_and_free_are_safe() {
|
||||||
|
unsafe {
|
||||||
|
assert_eq!(ngparse_deck_len(std::ptr::null()), 0);
|
||||||
|
assert!(ngparse_deck_card(std::ptr::null(), 0).is_null());
|
||||||
|
assert_eq!(ngparse_deck_drop_count(std::ptr::null()), 0);
|
||||||
|
ngparse_deck_free(std::ptr::null_mut()); // no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// cores > 1 is accepted (forward-compatible) and must not change the result.
|
||||||
|
#[test]
|
||||||
|
fn cores_argument_is_accepted() {
|
||||||
|
let p = deck("* t\nr1 n1 0 1k\nv1 n1 0 1\n.end\n", "cores");
|
||||||
|
let cp = CString::new(p).unwrap();
|
||||||
|
for c in [0, 1, 4] {
|
||||||
|
let d = unsafe { ngparse_expand_file(cp.as_ptr(), c, 0) };
|
||||||
|
assert!(!d.is_null(), "cores={c} failed");
|
||||||
|
assert_eq!(card(d, 1), "r1 n1 0 1.000000000000000e3");
|
||||||
|
unsafe { ngparse_deck_free(d) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
//! ngparse — a fast SPICE/HSPICE deck preprocessor and parser for ngspice.
|
||||||
|
//!
|
||||||
|
//! Goal: replace ngspice's O(n^2) text-expansion frontend (`.lib` extraction +
|
||||||
|
//! `numparam` substitution + `.subckt` expansion) with a single-pass, optionally
|
||||||
|
//! parallel parser, and hand ngspice a fully-resolved card stream. See the project
|
||||||
|
//! README and `docs/` for architecture.
|
||||||
|
//!
|
||||||
|
//! This crate is developed standalone-first: it can emit the flattened, resolved
|
||||||
|
//! deck as text so its output can be validated against ngspice's own expanded-deck
|
||||||
|
//! dump before any C glue is wired up.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod expr;
|
||||||
|
pub mod ffi;
|
||||||
|
pub mod params;
|
||||||
|
pub mod preprocess;
|
||||||
|
pub mod reader;
|
||||||
|
pub mod subckt;
|
||||||
|
pub mod table;
|
||||||
|
|
||||||
|
pub use config::{Compat, Config};
|
||||||
|
pub use preprocess::{ExpandError, Expander};
|
||||||
|
pub use reader::{logical_lines, LogicalLine};
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
//! `ngparse` CLI — standalone driver for developing and validating the parser
|
||||||
|
//! before it is linked into ngspice.
|
||||||
|
//!
|
||||||
|
//! Subcommands:
|
||||||
|
//! ngparse lines <deck> dump logical lines (continuations joined, comments stripped)
|
||||||
|
//! ngparse flatten <deck> expand all .inc/.lib sections into a flat card list
|
||||||
|
//!
|
||||||
|
//! `flatten` output is what gets diffed against ngspice's expanded-deck dump.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
fn usage(prog: &str) -> ExitCode {
|
||||||
|
eprintln!("usage:");
|
||||||
|
eprintln!(" {prog} lines <deck> dump logical lines");
|
||||||
|
eprintln!(" {prog} flatten <deck> expand .inc/.lib into a flat card list");
|
||||||
|
eprintln!(" {prog} resolve <deck> flatten + evaluate params -> numeric deck");
|
||||||
|
eprintln!("");
|
||||||
|
eprintln!("flags:");
|
||||||
|
eprintln!(" --strict fail if any parameter could not be resolved");
|
||||||
|
eprintln!(" --topo-reduce remove dangling passives (two-terminal R/C on a");
|
||||||
|
eprintln!(" node referenced nowhere else) from the deck");
|
||||||
|
eprintln!(" --cores <n> worker cores (default 1; >1 not yet implemented)");
|
||||||
|
eprintln!("env:");
|
||||||
|
eprintln!(" NGPARSE_DEBUG_DROP=1 list every dropped parameter");
|
||||||
|
ExitCode::from(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The title to emit as line 1 of an expanded deck.
|
||||||
|
///
|
||||||
|
/// Passed through verbatim so a round-trip through ngspice reproduces the original
|
||||||
|
/// deck's title exactly. An empty title (unreadable/empty entry file) still has to
|
||||||
|
/// occupy line 1 — something will be consumed as the title regardless, so it had
|
||||||
|
/// better be a placeholder and not the first real card.
|
||||||
|
fn title_line(title: &str) -> String {
|
||||||
|
if title.trim().is_empty() {
|
||||||
|
"*".to_string()
|
||||||
|
} else {
|
||||||
|
title.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `--cores <n>` (or `--cores=<n>`) from anywhere in argv.
|
||||||
|
///
|
||||||
|
/// Defaults to 1. A request for more is ACCEPTED but not yet honored, and says so
|
||||||
|
/// loudly — never let a user believe they got 4 cores when they got 1. See
|
||||||
|
/// `Config::effective_cores` for why the parser is sequential: it is 0.21s of a
|
||||||
|
/// 2.0s load, the rest being ngspice's own model ingest.
|
||||||
|
fn parse_cores(argv: &[String]) -> Result<ngparse::Config, String> {
|
||||||
|
let mut val: Option<&str> = None;
|
||||||
|
for (i, a) in argv.iter().enumerate() {
|
||||||
|
if let Some(v) = a.strip_prefix("--cores=") {
|
||||||
|
val = Some(v);
|
||||||
|
} else if a == "--cores" {
|
||||||
|
val = Some(argv.get(i + 1).map(String::as_str).unwrap_or(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(v) = val else {
|
||||||
|
return Ok(ngparse::Config::single());
|
||||||
|
};
|
||||||
|
let n: usize = v
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("--cores: expected a positive integer, got {v:?}"))?;
|
||||||
|
let n = NonZeroUsize::new(n).ok_or_else(|| "--cores: must be >= 1".to_string())?;
|
||||||
|
Ok(ngparse::Config::with_cores(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
// flags may appear anywhere after the subcommand
|
||||||
|
let strict = argv.iter().any(|a| a == "--strict");
|
||||||
|
// `--pspice` mirrors ngspice's `ngbehavior=ps`: apply the PSpice conversions
|
||||||
|
// (if->ternary_fcn, VSWITCH->sw, VALUE={TABLE(..)}->native TABLE, pwr/pwrs/
|
||||||
|
// stp/int) on emit. The glue sets this from ngbehavior; the flag lets the CLI
|
||||||
|
// reproduce a ps-mode run.
|
||||||
|
let compat = if argv.iter().any(|a| a == "--pspice") {
|
||||||
|
ngparse::config::Compat::Pspice
|
||||||
|
} else {
|
||||||
|
ngparse::config::Compat::Default
|
||||||
|
};
|
||||||
|
// `--topo-reduce` removes dangling passives from the expanded deck (see
|
||||||
|
// Config::topo_reduce). Off by default.
|
||||||
|
let topo = argv.iter().any(|a| a == "--topo-reduce");
|
||||||
|
let cfg = match parse_cores(&argv) {
|
||||||
|
Ok(c) => c.with_compat(compat).with_topo_reduce(topo),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ngparse: {e}");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// `--cores <n>` puts a bare number in argv; drop it so it is not read as a path.
|
||||||
|
let args: Vec<String> = {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut skip = false;
|
||||||
|
for a in &argv {
|
||||||
|
if skip {
|
||||||
|
skip = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if a == "--cores" {
|
||||||
|
skip = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !a.starts_with("--") {
|
||||||
|
out.push(a.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
if args.len() < 3 {
|
||||||
|
return usage(&argv[0]);
|
||||||
|
}
|
||||||
|
let (cmd, path) = (args[1].as_str(), args[2].as_str());
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
"lines" => {
|
||||||
|
let src = match std::fs::read_to_string(path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ngparse: cannot read {path}: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let lines = ngparse::logical_lines(&src, Arc::from(path));
|
||||||
|
for l in &lines {
|
||||||
|
println!("{}", l.text);
|
||||||
|
}
|
||||||
|
eprintln!("ngparse: {} logical lines", lines.len());
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
"flatten" => {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let mut ex = ngparse::Expander::new();
|
||||||
|
match ex.expand_file(Path::new(path)) {
|
||||||
|
Ok(lines) => {
|
||||||
|
for l in &lines {
|
||||||
|
println!("{}", l.text);
|
||||||
|
}
|
||||||
|
let dt = t0.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"ngparse: flattened to {} cards in {:.3}s",
|
||||||
|
lines.len(),
|
||||||
|
dt.as_secs_f64()
|
||||||
|
);
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ngparse: flatten failed: {e}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getp" => {
|
||||||
|
// getp <deck> <param> — flatten, collect, resolve one param (debug)
|
||||||
|
let name = args.get(3).map(String::as_str).unwrap_or("");
|
||||||
|
let mut ex = ngparse::Expander::new();
|
||||||
|
let flat = match ex.expand_file(Path::new(path)) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => { eprintln!("flatten failed: {e}"); return ExitCode::FAILURE; }
|
||||||
|
};
|
||||||
|
let mut table = ngparse::params::ParamTable::new();
|
||||||
|
table.collect(&flat);
|
||||||
|
eprintln!("collected {} params", table.param_count());
|
||||||
|
match table.eval_str(name) {
|
||||||
|
Ok(v) => println!("{name} = {v}"),
|
||||||
|
Err(e) => println!("{name} : ERROR {e}"),
|
||||||
|
}
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
"expand" => {
|
||||||
|
// full pipeline: flatten -> subckt-expand + param-resolve -> flat numeric deck
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let mut ex = ngparse::Expander::with_config(cfg);
|
||||||
|
let flat = match ex.expand_file(Path::new(path)) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => { eprintln!("ngparse: flatten failed: {e}"); return ExitCode::FAILURE; }
|
||||||
|
};
|
||||||
|
let t_flat = t0.elapsed();
|
||||||
|
let se = ngparse::subckt::SubcktExpander::with_config(&flat, cfg);
|
||||||
|
let r = se.expand();
|
||||||
|
let dt = t0.elapsed();
|
||||||
|
// Line 1 must be the title: whoever reads this deck back — ngspice via
|
||||||
|
// `source`, or the C glue via if_inpdeck — consumes it and starts the
|
||||||
|
// netlist at line 2. Without it the first real card is silently eaten.
|
||||||
|
println!("{}", title_line(ex.title()));
|
||||||
|
for c in &r.cards {
|
||||||
|
println!("{c}");
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"ngparse: expand done in {:.3}s (flatten {:.3}s) -> {} cards",
|
||||||
|
dt.as_secs_f64(),
|
||||||
|
t_flat.as_secs_f64(),
|
||||||
|
r.cards.len()
|
||||||
|
);
|
||||||
|
// A dropped parameter is never silent: it means a device/model quietly
|
||||||
|
// falls back to a DEFAULT, which yields a wrong-but-converging answer
|
||||||
|
// (this exact failure mode gave foundry_a a dead transistor via u0=0, and
|
||||||
|
// bxpressn-1 a malformed B source via a dropped `v=`).
|
||||||
|
if !r.drops.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"ngparse: WARNING: {} parameter(s) could not be resolved and were DROPPED;",
|
||||||
|
r.drops.len()
|
||||||
|
);
|
||||||
|
eprintln!(" the affected device/model silently falls back to its DEFAULT value.");
|
||||||
|
let show = 10.min(r.drops.len());
|
||||||
|
for d in r.drops.iter().take(show) {
|
||||||
|
eprintln!(" {d}");
|
||||||
|
}
|
||||||
|
if r.drops.len() > show {
|
||||||
|
eprintln!(
|
||||||
|
" ... and {} more (NGPARSE_DEBUG_DROP=1 to see every one)",
|
||||||
|
r.drops.len() - show
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if strict {
|
||||||
|
eprintln!("ngparse: --strict: refusing to emit a deck with dropped parameters");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
eprintln!(" (use --strict to make this an error)");
|
||||||
|
}
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
"resolve" => {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let mut ex = ngparse::Expander::new();
|
||||||
|
let flat = match ex.expand_file(Path::new(path)) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ngparse: flatten failed: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let t_flat = t0.elapsed();
|
||||||
|
|
||||||
|
let mut table = ngparse::params::ParamTable::new();
|
||||||
|
table.collect(&flat);
|
||||||
|
let t_collect = t0.elapsed();
|
||||||
|
|
||||||
|
let mut stats = ngparse::params::SubstStats::default();
|
||||||
|
let mut nparam = 0usize;
|
||||||
|
println!("{}", title_line(ex.title())); // line 1 is the title — see `expand`
|
||||||
|
for l in &flat {
|
||||||
|
let t = l.text.trim_start();
|
||||||
|
if t.len() >= 6 && t[..6].eq_ignore_ascii_case(".param") {
|
||||||
|
nparam += 1;
|
||||||
|
continue; // .param lines are dropped from the resolved netlist
|
||||||
|
}
|
||||||
|
let out = ngparse::params::substitute_line(&table, &l.text, "0", &mut stats);
|
||||||
|
println!("{out}");
|
||||||
|
}
|
||||||
|
let dt = t0.elapsed();
|
||||||
|
eprintln!(
|
||||||
|
"ngparse: resolve done in {:.3}s (flatten {:.3}s, collect {:.3}s)",
|
||||||
|
dt.as_secs_f64(),
|
||||||
|
t_flat.as_secs_f64(),
|
||||||
|
(t_collect - t_flat).as_secs_f64()
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"ngparse: {nparam} .param cards, {} inline exprs ({} failed to evaluate)",
|
||||||
|
stats.exprs_total, stats.exprs_failed
|
||||||
|
);
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
_ => usage(&args[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,544 @@
|
||||||
|
//! Parameter collection, resolution, and `{...}`/`'...'` substitution — the
|
||||||
|
//! numparam replacement. Given the flattened card stream, this:
|
||||||
|
//! 1. collects every `.param` definition (multi-assignment lines and
|
||||||
|
//! `name(args)=expr` function definitions),
|
||||||
|
//! 2. resolves parameter values lazily with memoization + cycle detection
|
||||||
|
//! (so forward references and inter-parameter dependencies just work),
|
||||||
|
//! 3. substitutes inline `{expr}` / `'expr'` on device/model lines with the
|
||||||
|
//! evaluated numeric value,
|
||||||
|
//! and emits a fully-numeric card stream plus resolution statistics.
|
||||||
|
//!
|
||||||
|
//! Values are `f64`. Anything that fails to evaluate is reported and (in numeric
|
||||||
|
//! output mode) replaced with a placeholder so downstream ngspice sees no
|
||||||
|
//! leftover `{}` — this is what lets us measure the model-ingest floor before the
|
||||||
|
//! evaluator is fully complete.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::expr::{eval, parse, Env, EvalError, Expr};
|
||||||
|
use crate::reader::LogicalLine;
|
||||||
|
|
||||||
|
/// One `.param` assignment parsed from a card.
|
||||||
|
pub(crate) struct Assign {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) args: Option<Vec<String>>,
|
||||||
|
pub(crate) rhs: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Case-insensitive key.
|
||||||
|
pub(crate) fn key(s: &str) -> String {
|
||||||
|
s.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the text after `.func`: `name(args) [=] body`. Returns
|
||||||
|
/// `(name, arg names, body)` with the body's surrounding `'…'`/`{…}` stripped.
|
||||||
|
/// `.func` bodies are always a single function, so unlike `.param` there is no
|
||||||
|
/// multi-assignment to handle.
|
||||||
|
pub(crate) fn parse_func_def(rest: &str) -> Option<(String, Vec<String>, String)> {
|
||||||
|
let s = rest.trim();
|
||||||
|
let open = s.find('(')?;
|
||||||
|
let name = s[..open].trim().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Match the args paren (they never nest in a func header).
|
||||||
|
let close = s[open..].find(')')? + open;
|
||||||
|
let argnames: Vec<String> = s[open + 1..close]
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|a| !a.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect();
|
||||||
|
// Body: skip an optional `=`, then strip one layer of quotes/braces.
|
||||||
|
let mut body = s[close + 1..].trim();
|
||||||
|
if let Some(rest) = body.strip_prefix('=') {
|
||||||
|
body = rest.trim();
|
||||||
|
}
|
||||||
|
let body = body
|
||||||
|
.strip_prefix('\'')
|
||||||
|
.and_then(|b| b.strip_suffix('\''))
|
||||||
|
.or_else(|| body.strip_prefix('{').and_then(|b| b.strip_suffix('}')))
|
||||||
|
.unwrap_or(body)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if body.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((name, argnames, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the text following `.param` into assignments. Handles:
|
||||||
|
/// * multiple `name = value` pairs on one line
|
||||||
|
/// * whitespace around `=`
|
||||||
|
/// * values quoted with `'...'`, braced `{...}`, or a bare token (with
|
||||||
|
/// paren-balancing so `max(a, b)` survives even unquoted)
|
||||||
|
/// * `name(a,b) = ...` function-definition headers
|
||||||
|
pub(crate) fn parse_assignments(rest: &str) -> Vec<Assign> {
|
||||||
|
let b = rest.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let skip_ws = |b: &[u8], mut i: usize| {
|
||||||
|
while i < b.len() && (b[i] as char).is_whitespace() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i
|
||||||
|
};
|
||||||
|
|
||||||
|
while i < b.len() {
|
||||||
|
i = skip_ws(b, i);
|
||||||
|
if i >= b.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// name
|
||||||
|
let name_start = i;
|
||||||
|
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i == name_start {
|
||||||
|
// not an identifier start; skip a char to avoid infinite loop
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = rest[name_start..i].to_string();
|
||||||
|
|
||||||
|
// optional (args)
|
||||||
|
let mut args = None;
|
||||||
|
let j = skip_ws(b, i);
|
||||||
|
if j < b.len() && b[j] == b'(' {
|
||||||
|
let mut depth = 0;
|
||||||
|
let mut k = j;
|
||||||
|
while k < b.len() {
|
||||||
|
match b[k] {
|
||||||
|
b'(' => depth += 1,
|
||||||
|
b')' => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
k += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
let arglist = &rest[j + 1..k - 1];
|
||||||
|
args = Some(
|
||||||
|
arglist
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
i = k;
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect '='
|
||||||
|
i = skip_ws(b, i);
|
||||||
|
if i >= b.len() || b[i] != b'=' {
|
||||||
|
// malformed; bail on this assignment
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1; // consume '='
|
||||||
|
i = skip_ws(b, i);
|
||||||
|
if i >= b.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// value
|
||||||
|
let rhs = match b[i] {
|
||||||
|
b'\'' | b'{' => {
|
||||||
|
let close = if b[i] == b'\'' { b'\'' } else { b'}' };
|
||||||
|
let start = i + 1;
|
||||||
|
let mut k = start;
|
||||||
|
let mut depth = 1;
|
||||||
|
while k < b.len() {
|
||||||
|
if b[i] == b'{' && b[k] == b'{' {
|
||||||
|
depth += 1;
|
||||||
|
} else if b[k] == close {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
let v = rest[start..k.min(b.len())].to_string();
|
||||||
|
i = (k + 1).min(b.len());
|
||||||
|
v
|
||||||
|
}
|
||||||
|
b'[' => {
|
||||||
|
// Bracketed vector value (XSPICE array parameters):
|
||||||
|
// `cntl_array = [-2 -1 1 2]`. The whole `[...]` group, spaces
|
||||||
|
// included, is ONE value token; cutting it at the first space
|
||||||
|
// truncated the model body and every remaining parameter with
|
||||||
|
// it ("Too few values for parameter 'cntl_array'").
|
||||||
|
let start = i;
|
||||||
|
let mut depth = 0;
|
||||||
|
while i < b.len() {
|
||||||
|
match b[i] {
|
||||||
|
b'[' => depth += 1,
|
||||||
|
b']' => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
rest[start..i].to_string()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Bare token, paren-balanced. A `,` ends it just like whitespace:
|
||||||
|
// ngspice's INPgetTok (inpgtok.c) treats `=`, `(`, `)` and `,` as
|
||||||
|
// separators — skipped before a token, terminating after one — so
|
||||||
|
// `.model dm d (a=500.0, b=-500.0)` has TWO parameters, not a value
|
||||||
|
// of `"500.0,"`. Depth-guarded, so an unquoted `max(a, b)` keeps its
|
||||||
|
// internal commas.
|
||||||
|
let start = i;
|
||||||
|
let mut depth = 0;
|
||||||
|
while i < b.len() {
|
||||||
|
let c = b[i];
|
||||||
|
if c == b'(' {
|
||||||
|
depth += 1;
|
||||||
|
} else if c == b')' {
|
||||||
|
depth -= 1;
|
||||||
|
} else if ((c as char).is_whitespace() || c == b',') && depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
rest[start..i].to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
out.push(Assign { name, args, rhs });
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resolved/lazy parameter + function table.
|
||||||
|
pub struct ParamTable {
|
||||||
|
raw: HashMap<String, String>,
|
||||||
|
funcs: HashMap<String, (Vec<String>, Rc<Expr>)>,
|
||||||
|
parsed: RefCell<HashMap<String, Rc<Expr>>>,
|
||||||
|
values: RefCell<HashMap<String, f64>>,
|
||||||
|
resolving: RefCell<HashSet<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParamTable {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ParamTable {
|
||||||
|
raw: HashMap::new(),
|
||||||
|
funcs: HashMap::new(),
|
||||||
|
parsed: RefCell::new(HashMap::new()),
|
||||||
|
values: RefCell::new(HashMap::new()),
|
||||||
|
resolving: RefCell::new(HashSet::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all `.param` and `.func` definitions from the flattened deck.
|
||||||
|
pub fn collect(&mut self, lines: &[LogicalLine]) {
|
||||||
|
for l in lines {
|
||||||
|
let t = l.text.trim_start();
|
||||||
|
if t.len() >= 6 && t[..6].eq_ignore_ascii_case(".param") {
|
||||||
|
let rest = &t[6..];
|
||||||
|
for a in parse_assignments(rest) {
|
||||||
|
let k = key(&a.name);
|
||||||
|
match a.args {
|
||||||
|
Some(argnames) => {
|
||||||
|
if let Ok(body) = parse(&a.rhs) {
|
||||||
|
self.funcs.insert(k, (argnames, Rc::new(body)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// last definition wins (matches ngspice override order)
|
||||||
|
self.raw.insert(k, a.rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if t.len() >= 5 && t[..5].eq_ignore_ascii_case(".func") {
|
||||||
|
// `.func name(args) [=] body` -- ngspice's dedicated spelling for
|
||||||
|
// a user function, equivalent to `.param name(args) = body`, which
|
||||||
|
// it in fact rewrites .func into. The `=` is optional and the body
|
||||||
|
// may be 'quoted', {braced} or bare. Collected the same way, so a
|
||||||
|
// B-source `v='bar2(17.0)'` can inline bar2 instead of dropping it.
|
||||||
|
if let Some((name, argnames, body)) = parse_func_def(&t[5..]) {
|
||||||
|
if let Ok(b) = parse(&body) {
|
||||||
|
self.funcs.insert(key(&name), (argnames, Rc::new(b)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a parameter to its value (memoized; detects cycles).
|
||||||
|
fn resolve(&self, name: &str) -> Result<f64, EvalError> {
|
||||||
|
let k = key(name);
|
||||||
|
if let Some(v) = self.values.borrow().get(&k) {
|
||||||
|
return Ok(*v);
|
||||||
|
}
|
||||||
|
if self.resolving.borrow().contains(&k) {
|
||||||
|
return Err(EvalError::Parse(format!("cyclic parameter `{k}`")));
|
||||||
|
}
|
||||||
|
let raw = self
|
||||||
|
.raw
|
||||||
|
.get(&k)
|
||||||
|
.ok_or_else(|| EvalError::UnknownVar(name.to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// parse (cache). Clone the Rc out of the borrow before any borrow_mut.
|
||||||
|
let cached = self.parsed.borrow().get(&k).cloned();
|
||||||
|
let expr: Rc<Expr> = match cached {
|
||||||
|
Some(e) => e,
|
||||||
|
None => {
|
||||||
|
let e = Rc::new(parse(&raw)?);
|
||||||
|
self.parsed.borrow_mut().insert(k.clone(), Rc::clone(&e));
|
||||||
|
e
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.resolving.borrow_mut().insert(k.clone());
|
||||||
|
let scope = Scope {
|
||||||
|
table: self,
|
||||||
|
locals: HashMap::new(),
|
||||||
|
};
|
||||||
|
let res = eval(&expr, &scope);
|
||||||
|
self.resolving.borrow_mut().remove(&k);
|
||||||
|
|
||||||
|
let v = res?;
|
||||||
|
self.values.borrow_mut().insert(k, v);
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of scalar parameters collected (for diagnostics).
|
||||||
|
pub fn param_count(&self) -> usize {
|
||||||
|
self.raw.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate an inline expression string against this table.
|
||||||
|
pub fn eval_str(&self, s: &str) -> Result<f64, EvalError> {
|
||||||
|
let e = parse(s)?;
|
||||||
|
let scope = Scope {
|
||||||
|
table: self,
|
||||||
|
locals: HashMap::new(),
|
||||||
|
};
|
||||||
|
eval(&e, &scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ParamTable {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ParamTable` is itself an environment (the global scope).
|
||||||
|
impl Env for ParamTable {
|
||||||
|
fn var(&self, name: &str) -> Result<f64, EvalError> {
|
||||||
|
self.resolve(name)
|
||||||
|
}
|
||||||
|
fn call_user(&self, name: &str, args: &[f64]) -> Result<Option<f64>, EvalError> {
|
||||||
|
let k = key(name);
|
||||||
|
let Some((argnames, body)) = self.funcs.get(&k) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if argnames.len() != args.len() {
|
||||||
|
return Err(EvalError::Arity {
|
||||||
|
func: name.to_string(),
|
||||||
|
got: args.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut locals = HashMap::new();
|
||||||
|
for (n, v) in argnames.iter().zip(args) {
|
||||||
|
locals.insert(key(n), *v);
|
||||||
|
}
|
||||||
|
let inner = Scope {
|
||||||
|
table: self,
|
||||||
|
locals,
|
||||||
|
};
|
||||||
|
Ok(Some(eval(body, &inner)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An evaluation scope: the global table plus function-argument locals.
|
||||||
|
struct Scope<'a> {
|
||||||
|
table: &'a ParamTable,
|
||||||
|
locals: HashMap<String, f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Env for Scope<'_> {
|
||||||
|
fn var(&self, name: &str) -> Result<f64, EvalError> {
|
||||||
|
let k = key(name);
|
||||||
|
if let Some(v) = self.locals.get(&k) {
|
||||||
|
return Ok(*v);
|
||||||
|
}
|
||||||
|
self.table.resolve(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_user(&self, name: &str, args: &[f64]) -> Result<Option<f64>, EvalError> {
|
||||||
|
let k = key(name);
|
||||||
|
let Some((argnames, body)) = self.table.funcs.get(&k) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if argnames.len() != args.len() {
|
||||||
|
return Err(EvalError::Arity {
|
||||||
|
func: name.to_string(),
|
||||||
|
got: args.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut locals = HashMap::new();
|
||||||
|
for (n, v) in argnames.iter().zip(args) {
|
||||||
|
locals.insert(key(n), *v);
|
||||||
|
}
|
||||||
|
let body = Rc::clone(body);
|
||||||
|
let inner = Scope {
|
||||||
|
table: self.table,
|
||||||
|
locals,
|
||||||
|
};
|
||||||
|
Ok(Some(eval(&body, &inner)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistics from a substitution pass.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct SubstStats {
|
||||||
|
pub exprs_total: usize,
|
||||||
|
pub exprs_failed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a resolved value the way ngspice prints numeric params.
|
||||||
|
pub(crate) fn fmt_num(v: f64) -> String {
|
||||||
|
format!("{v:.15e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Substitute every `{expr}` (and single-quoted `'expr'`) in a device/model line
|
||||||
|
/// with its evaluated value against `env`. On failure the original delimited
|
||||||
|
/// expression is kept intact (so behavioral `v()`/`i()` refs and not-yet-resolved
|
||||||
|
/// params survive for a later stage or for ngspice).
|
||||||
|
pub fn substitute_line(
|
||||||
|
env: &dyn Env,
|
||||||
|
line: &str,
|
||||||
|
placeholder: &str,
|
||||||
|
stats: &mut SubstStats,
|
||||||
|
) -> String {
|
||||||
|
let table = env;
|
||||||
|
let b = line.as_bytes();
|
||||||
|
let mut out = String::with_capacity(line.len());
|
||||||
|
let mut i = 0;
|
||||||
|
while i < b.len() {
|
||||||
|
let c = b[i];
|
||||||
|
if c == b'{' || c == b'\'' {
|
||||||
|
let close = if c == b'{' { b'}' } else { b'\'' };
|
||||||
|
let start = i + 1;
|
||||||
|
let mut k = start;
|
||||||
|
let mut depth = 1;
|
||||||
|
while k < b.len() {
|
||||||
|
if c == b'{' && b[k] == b'{' {
|
||||||
|
depth += 1;
|
||||||
|
} else if b[k] == close {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
if k <= b.len() {
|
||||||
|
let inner = &line[start..k.min(b.len())];
|
||||||
|
stats.exprs_total += 1;
|
||||||
|
match parse(inner).and_then(|e| eval(&e, table)) {
|
||||||
|
Ok(v) => out.push_str(&fmt_num(v)),
|
||||||
|
Err(_) => {
|
||||||
|
// Keep the original delimited expression so ngspice can
|
||||||
|
// resolve it (e.g. behavioral `v()`/`i()` refs, or
|
||||||
|
// subckt-local params resolved during expansion). Zeroing
|
||||||
|
// these would corrupt behavioral sources. `placeholder` is
|
||||||
|
// reserved for a future strict/numeric-only mode.
|
||||||
|
let _ = placeholder;
|
||||||
|
stats.exprs_failed += 1;
|
||||||
|
out.push(c as char);
|
||||||
|
out.push_str(inner);
|
||||||
|
out.push(close as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = (k + 1).min(b.len());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(c as char);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn func_def_parses_all_body_forms() {
|
||||||
|
// .func name(args) 'body' | {body} | = body | bare
|
||||||
|
assert_eq!(parse_func_def("bar2(p) 'v(p)+p'"),
|
||||||
|
Some(("bar2".into(), vec!["p".into()], "v(p)+p".into())));
|
||||||
|
assert_eq!(parse_func_def("foo0() '1013.0'"),
|
||||||
|
Some(("foo0".into(), vec![], "1013.0".into())));
|
||||||
|
assert_eq!(parse_func_def("baz1(n,vp) 'n+i(vp)+vp'"),
|
||||||
|
Some(("baz1".into(), vec!["n".into(),"vp".into()], "n+i(vp)+vp".into())));
|
||||||
|
assert_eq!(parse_func_def("g(x) = {x*2}"),
|
||||||
|
Some(("g".into(), vec!["x".into()], "x*2".into())));
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn lines(src: &str) -> Vec<LogicalLine> {
|
||||||
|
crate::reader::logical_lines(src, Arc::from("t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_value_param_line() {
|
||||||
|
let a = parse_assignments(" a=0 b = 1 c=nonfet_mm d = 'a+b' ");
|
||||||
|
assert_eq!(a.len(), 4);
|
||||||
|
assert_eq!(a[0].name, "a");
|
||||||
|
assert_eq!(a[2].rhs, "nonfet_mm");
|
||||||
|
assert_eq!(a[3].rhs, "a+b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_dependencies_and_forward_refs() {
|
||||||
|
let mut t = ParamTable::new();
|
||||||
|
t.collect(&lines(".param x={2*y} y=10\n.param z=nonfet_mm nonfet_mm=1\n"));
|
||||||
|
assert_eq!(t.resolve("x").unwrap(), 20.0);
|
||||||
|
assert_eq!(t.resolve("z").unwrap(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_function_definition_and_call() {
|
||||||
|
let mut t = ParamTable::new();
|
||||||
|
t.collect(&lines(
|
||||||
|
".param sel(m)='ka*(m==1)+kb*(m==2)'\n.param ka=10 kb=20\n",
|
||||||
|
));
|
||||||
|
assert_eq!(t.eval_str("sel(1)").unwrap(), 10.0);
|
||||||
|
assert_eq!(t.eval_str("sel(2)").unwrap(), 20.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn substitutes_inline_exprs() {
|
||||||
|
let mut t = ParamTable::new();
|
||||||
|
t.collect(&lines(".param fclk=150e6\n"));
|
||||||
|
let mut s = SubstStats::default();
|
||||||
|
let out = substitute_line(&t, "L1 a b {(2.7e-9)*(150e6/fclk)}", "0", &mut s);
|
||||||
|
assert!(out.starts_with("L1 a b 2.7"), "got {out}");
|
||||||
|
assert_eq!(s.exprs_failed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_is_reported_not_hung() {
|
||||||
|
let mut t = ParamTable::new();
|
||||||
|
t.collect(&lines(".param p=q q=p\n"));
|
||||||
|
assert!(t.resolve("p").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,629 @@
|
||||||
|
//! Section/include expansion — the replacement for ngspice `inpcom.c`'s
|
||||||
|
//! `.lib`/`.inc` handling, which is the O(n^2) hotspot on large PDK decks.
|
||||||
|
//!
|
||||||
|
//! Semantics mirrored from `src/frontend/inpcom.c` (`read_a_lib`,
|
||||||
|
//! `find_section_definition`, `expand_section_ref`, `expand_section_references`),
|
||||||
|
//! targeting **hs (HSPICE) compatibility** — the mode the foundry_b 14nm deck uses:
|
||||||
|
//!
|
||||||
|
//! * `.inc <file>` / `.include <file>` — textually include the whole file
|
||||||
|
//! (path resolved relative to the *referencing* file's directory).
|
||||||
|
//! * `.lib <file> <section>` — reference: splice the body of the `.lib <section>`
|
||||||
|
//! definition found in <file>, from just after the definition line up to (but
|
||||||
|
//! excluding) the first matching `.endl`. Nested `.lib <file> <section>`
|
||||||
|
//! references inside the body are expanded recursively. `.endl` nesting is NOT
|
||||||
|
//! counted — the first `.endl` ends the section (matches ngspice).
|
||||||
|
//! * `.lib <section>` (one token) — a section *definition* boundary. Indexed;
|
||||||
|
//! never emitted on its own.
|
||||||
|
//!
|
||||||
|
//! Each source file is read and indexed exactly once, then cached (keyed by
|
||||||
|
//! canonical path) — the same reuse ngspice's global `libraries[]` gives, but with
|
||||||
|
//! an O(1) section-name index instead of a linear scan per reference.
|
||||||
|
//!
|
||||||
|
//! Deliberately NOT handled yet (absent from the foundry_b tree — see project notes):
|
||||||
|
//! `.alter` block skipping, `.title`, `.hdl`, `.biaschk`, `.del`, `$ENV`/`$var`
|
||||||
|
//! path expansion, old-style one-token `.lib <file>` includes (lt/ps modes).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::reader::{logical_lines, LogicalLine};
|
||||||
|
|
||||||
|
/// Errors surfaced while expanding a deck.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ExpandError {
|
||||||
|
Read { path: PathBuf, err: std::io::Error },
|
||||||
|
Resolve { token: String, base: PathBuf },
|
||||||
|
Section { file: PathBuf, name: String },
|
||||||
|
MissingEndl { file: PathBuf, name: String },
|
||||||
|
Recursion { path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ExpandError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ExpandError::Read { path, err } => {
|
||||||
|
write!(f, "cannot read {}: {err}", path.display())
|
||||||
|
}
|
||||||
|
ExpandError::Resolve { token, base } => write!(
|
||||||
|
f,
|
||||||
|
"cannot resolve include/library path {token:?} relative to {}",
|
||||||
|
base.display()
|
||||||
|
),
|
||||||
|
ExpandError::Section { file, name } => write!(
|
||||||
|
f,
|
||||||
|
"library file {}: section .lib {name} not found",
|
||||||
|
file.display()
|
||||||
|
),
|
||||||
|
ExpandError::MissingEndl { file, name } => write!(
|
||||||
|
f,
|
||||||
|
"library file {}: section .lib {name} has no matching .endl",
|
||||||
|
file.display()
|
||||||
|
),
|
||||||
|
ExpandError::Recursion { path } => {
|
||||||
|
write!(f, "include/library recursion detected at {}", path.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for ExpandError {}
|
||||||
|
|
||||||
|
/// A source file loaded once: its logical lines plus a name->index map of the
|
||||||
|
/// `.lib <section>` definitions it contains.
|
||||||
|
struct LoadedFile {
|
||||||
|
lines: Vec<LogicalLine>,
|
||||||
|
/// lowercased section name -> index in `lines` of its `.lib <name>` line.
|
||||||
|
sections: HashMap<String, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quote-aware token splitter: whitespace-separated, with `'`/`"` grouping and
|
||||||
|
/// stripped. SPICE only uses quotes to hold paths together, never for escaping.
|
||||||
|
fn split_tokens(s: &str) -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
let mut quote: Option<char> = None;
|
||||||
|
let mut has = false;
|
||||||
|
for ch in s.chars() {
|
||||||
|
match quote {
|
||||||
|
Some(q) => {
|
||||||
|
if ch == q {
|
||||||
|
quote = None;
|
||||||
|
} else {
|
||||||
|
cur.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if ch == '\'' || ch == '"' {
|
||||||
|
quote = Some(ch);
|
||||||
|
has = true;
|
||||||
|
} else if ch.is_whitespace() {
|
||||||
|
if has {
|
||||||
|
out.push(std::mem::take(&mut cur));
|
||||||
|
has = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cur.push(ch);
|
||||||
|
has = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
out.push(cur);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lowercased first token of a line, for dot-command dispatch.
|
||||||
|
fn keyword(line: &str) -> String {
|
||||||
|
line.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_kw(line: &str, kw: &str) -> bool {
|
||||||
|
let lstart = line.trim_start();
|
||||||
|
lstart.len() >= kw.len()
|
||||||
|
&& lstart.as_bytes()[..kw.len()].eq_ignore_ascii_case(kw.as_bytes())
|
||||||
|
&& matches!(
|
||||||
|
lstart.as_bytes().get(kw.len()),
|
||||||
|
None | Some(b' ') | Some(b'\t')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite HSPICE spellings into the canonical SPICE ones, mirroring ngspice's
|
||||||
|
/// `inpcom.c::inp_fix_macro_param_func_paren_io` (line 2952):
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// .macro name ... -> .subckt name ...
|
||||||
|
/// .eom [name] -> .ends [name]
|
||||||
|
/// .subckt name (a b c) -> .subckt name a b c
|
||||||
|
/// x1 (a b c) sub -> x1 a b c sub
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Done once, centrally, so no later stage has to know these aliases exist.
|
||||||
|
///
|
||||||
|
/// Without it a `.macro` body is emitted verbatim and its `x` instances are never
|
||||||
|
/// expanded, and parenthesized ports parse as the tokens `(a` and `c)` — which
|
||||||
|
/// silently leaves the subckt's devices wired to its INTERNAL node names instead
|
||||||
|
/// of the caller's. foundry_c's PDK needs both: `.macro`/`.eom` x5, `.subckt name (…)`
|
||||||
|
/// x20 and `x… (…)` x31 in one deck.
|
||||||
|
fn normalize_hspice(l: &mut LogicalLine) {
|
||||||
|
let t = l.text.trim_start();
|
||||||
|
// `.macro`/`.eom` -> `.subckt`/`.ends`, keeping the rest of the line verbatim.
|
||||||
|
for (from, to) in [(".macro", ".subckt"), (".eom", ".ends")] {
|
||||||
|
if is_kw(t, from) {
|
||||||
|
l.text = format!("{to}{}", &t[from.len()..]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the parentheses around a port/connection list. ngspice blanks the
|
||||||
|
// first `(` and its matching `)` on `.subckt` and `x` cards only.
|
||||||
|
let t = l.text.trim_start();
|
||||||
|
let is_sub = is_kw(t, ".subckt");
|
||||||
|
let is_x = t.as_bytes().first().is_some_and(|c| c.eq_ignore_ascii_case(&b'x'));
|
||||||
|
if !(is_sub || is_x) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip the leading keyword, plus the subckt's name.
|
||||||
|
let mut rest = t.split_at(t.find(char::is_whitespace).unwrap_or(t.len())).1;
|
||||||
|
if is_sub {
|
||||||
|
let s = rest.trim_start();
|
||||||
|
rest = s.split_at(s.find(char::is_whitespace).unwrap_or(s.len())).1;
|
||||||
|
}
|
||||||
|
let head_len = l.text.len() - rest.len();
|
||||||
|
let Some(open) = rest.find('(') else { return };
|
||||||
|
// Only a port list — never touch a `(` that belongs to an expression or a
|
||||||
|
// value, which always follows a `=` or other text on these cards.
|
||||||
|
if !rest[..open].trim().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(close) = rest.find(')') else { return };
|
||||||
|
if close < open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut body = rest.to_string();
|
||||||
|
body.replace_range(open..open + 1, " ");
|
||||||
|
body.replace_range(close..close + 1, " ");
|
||||||
|
l.text = format!("{}{}", &l.text[..head_len], body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve relative `table_param(str("./x.table"), ...)` paths against the
|
||||||
|
/// directory of the file the line came from, rewriting them in place to absolute.
|
||||||
|
///
|
||||||
|
/// HSPICE resolves such a path against the *referencing* file's directory, and
|
||||||
|
/// ngspice's `table_param_lookup` takes a `dir_hint` for exactly this ("the
|
||||||
|
/// directory of the file that originated this call — typically the .lib file
|
||||||
|
/// containing the table_param() invocation"). foundry_b's `.param rth0_n` lives in
|
||||||
|
/// `<pdk>/fets_hp.lib` and asks for `./RF_COMPONENTS/egnfet_SHE.table`, which
|
||||||
|
/// exists only relative to the PDK dir — never relative to the simulation cwd.
|
||||||
|
///
|
||||||
|
/// Doing it here, during expansion, is what keeps the later stages simple: only
|
||||||
|
/// `LogicalLine` knows its source file, and once the path is absolute the lookup
|
||||||
|
/// is a pure function that `ParamTable` and `SubcktExpander` can both evaluate
|
||||||
|
/// without carrying provenance.
|
||||||
|
///
|
||||||
|
/// A path that does not resolve to an existing file is left untouched, so the
|
||||||
|
/// error names what the deck actually wrote.
|
||||||
|
fn rewrite_table_paths(l: &mut LogicalLine) {
|
||||||
|
if !l.text.to_ascii_lowercase().contains("table_param") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(dir) = Path::new(l.file.as_ref()).parent().map(Path::to_path_buf) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut out = String::with_capacity(l.text.len());
|
||||||
|
let mut rest = l.text.as_str();
|
||||||
|
while let Some(open) = rest.find('"') {
|
||||||
|
let after = &rest[open + 1..];
|
||||||
|
let Some(close) = after.find('"') else { break };
|
||||||
|
let (path, tail) = (&after[..close], &after[close + 1..]);
|
||||||
|
out.push_str(&rest[..=open]);
|
||||||
|
let abs = dir.join(path);
|
||||||
|
match (Path::new(path).is_absolute(), abs.canonicalize()) {
|
||||||
|
(false, Ok(p)) => out.push_str(&p.to_string_lossy()),
|
||||||
|
_ => out.push_str(path),
|
||||||
|
}
|
||||||
|
out.push('"');
|
||||||
|
rest = tail;
|
||||||
|
}
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push_str(rest);
|
||||||
|
l.text = out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The deck expander. Owns the file cache for one expansion run.
|
||||||
|
pub struct Expander {
|
||||||
|
cache: HashMap<PathBuf, Arc<LoadedFile>>,
|
||||||
|
/// Paths currently on the expansion stack — cheap cycle guard.
|
||||||
|
active: Vec<PathBuf>,
|
||||||
|
/// The top deck's title line (see [`Expander::title`]).
|
||||||
|
title: String,
|
||||||
|
/// Run configuration. The `.lib` walk below is the first parallel seam — see
|
||||||
|
/// `Config::effective_cores` for why it is sequential today.
|
||||||
|
cfg: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expander {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Expander::with_config(Config::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An expander running under `cfg`.
|
||||||
|
pub fn with_config(cfg: Config) -> Self {
|
||||||
|
Expander {
|
||||||
|
cache: HashMap::new(),
|
||||||
|
active: Vec::new(),
|
||||||
|
title: String::new(),
|
||||||
|
cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This expander's run configuration.
|
||||||
|
pub fn config(&self) -> Config {
|
||||||
|
self.cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The top deck's title: its first physical line, whatever it contains.
|
||||||
|
///
|
||||||
|
/// SPICE unconditionally consumes line 1 of the top deck as the title — it is
|
||||||
|
/// never a card, even when it looks exactly like one. Verified against the
|
||||||
|
/// reference: a deck whose first line is `v1 n1 0 1` reports `v(n1) = 0`
|
||||||
|
/// (no such source exists); prepend a comment line and it reports `v(n1) = 1`.
|
||||||
|
///
|
||||||
|
/// The same applies to a netlist pulled in by `source` from a `.control` block
|
||||||
|
/// — the PDK harness path — but NOT to `.include`/`.lib` files, whose first
|
||||||
|
/// line stays an ordinary card.
|
||||||
|
///
|
||||||
|
/// Emitting this back as line 1 is what makes `expand` output re-readable:
|
||||||
|
/// ngspice will eat the title again, leaving the cards intact. It equally
|
||||||
|
/// satisfies the C glue, where `if_inpdeck` walks straight into `INPpas1` from
|
||||||
|
/// card #1 with no title skip of its own (`inp.c` does the skipping, at 1146).
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand a top-level deck file into a flat list of logical lines with all
|
||||||
|
/// `.inc`/`.lib` references resolved. `.param`/`{}`/`.subckt` are left intact
|
||||||
|
/// for later stages.
|
||||||
|
pub fn expand_file(&mut self, entry: &Path) -> Result<Vec<LogicalLine>, ExpandError> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let file = self.load(entry)?;
|
||||||
|
let dir = entry.parent().unwrap_or(Path::new(".")).to_path_buf();
|
||||||
|
|
||||||
|
// Split off the title before walking: line 1 of the TOP deck is never a
|
||||||
|
// card (see `title()`). A comment-style title (`* foo`) has already been
|
||||||
|
// dropped by the reader; a bare-text one (`check scoping of ...`, which is
|
||||||
|
// what tests/regression/subckt-processing/model-scope-5.cir uses) is still
|
||||||
|
// in `lines` and would otherwise be emitted as a bogus card — there, a
|
||||||
|
// capacitor, since it happens to start with `c`.
|
||||||
|
self.title = self.first_physical_line(entry);
|
||||||
|
let body: Vec<LogicalLine> = file
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.filter(|l| l.line_no != 1)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
self.walk(&body, &dir, &mut out)?;
|
||||||
|
for l in &mut out {
|
||||||
|
normalize_hspice(l);
|
||||||
|
rewrite_table_paths(l);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The entry deck's first physical line, verbatim (minus the trailing newline).
|
||||||
|
/// Read from source rather than taken from the logical lines because the reader
|
||||||
|
/// has already stripped comments, and a title is usually a comment.
|
||||||
|
fn first_physical_line(&self, entry: &Path) -> String {
|
||||||
|
let Ok(bytes) = std::fs::read(entry) else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
String::from_utf8_lossy(&bytes)
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim_end()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a path token relative to `base_dir`, canonicalizing it.
|
||||||
|
fn resolve(&self, token: &str, base_dir: &Path) -> Result<PathBuf, ExpandError> {
|
||||||
|
let raw = Path::new(token);
|
||||||
|
let joined = if raw.is_absolute() {
|
||||||
|
raw.to_path_buf()
|
||||||
|
} else {
|
||||||
|
base_dir.join(raw)
|
||||||
|
};
|
||||||
|
joined.canonicalize().map_err(|_| ExpandError::Resolve {
|
||||||
|
token: token.to_string(),
|
||||||
|
base: base_dir.to_path_buf(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load + index a file (cached by canonical path).
|
||||||
|
fn load(&mut self, path: &Path) -> Result<Arc<LoadedFile>, ExpandError> {
|
||||||
|
let key = path
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap_or_else(|_| path.to_path_buf());
|
||||||
|
if let Some(f) = self.cache.get(&key) {
|
||||||
|
return Ok(Arc::clone(f));
|
||||||
|
}
|
||||||
|
// Read as bytes and decode lossily: PDK model files are frequently
|
||||||
|
// Latin-1 / not strictly UTF-8 (special chars in comments, etc.), and
|
||||||
|
// `read_to_string` would reject them. SPICE syntax is ASCII, so any
|
||||||
|
// replacement of stray non-UTF-8 bytes only affects comment text.
|
||||||
|
let bytes = std::fs::read(&key).map_err(|err| ExpandError::Read {
|
||||||
|
path: key.clone(),
|
||||||
|
err,
|
||||||
|
})?;
|
||||||
|
let src = String::from_utf8_lossy(&bytes);
|
||||||
|
let lines = logical_lines(&src, Arc::from(key.to_string_lossy().as_ref()));
|
||||||
|
|
||||||
|
// Index `.lib <name>` definitions (exactly one token after `.lib`).
|
||||||
|
let mut sections = HashMap::new();
|
||||||
|
for (i, l) in lines.iter().enumerate() {
|
||||||
|
if is_kw(&l.text, ".lib") {
|
||||||
|
let toks = split_tokens(&l.text);
|
||||||
|
if toks.len() == 2 {
|
||||||
|
sections
|
||||||
|
.entry(toks[1].to_ascii_lowercase())
|
||||||
|
.or_insert(i); // first definition wins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let loaded = Arc::new(LoadedFile { lines, sections });
|
||||||
|
self.cache.insert(key, Arc::clone(&loaded));
|
||||||
|
Ok(loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk a body of lines at "deck level" (top file or an `.inc`'d file),
|
||||||
|
/// resolving includes and section references into `out`.
|
||||||
|
fn walk(
|
||||||
|
&mut self,
|
||||||
|
lines: &[LogicalLine],
|
||||||
|
base_dir: &Path,
|
||||||
|
out: &mut Vec<LogicalLine>,
|
||||||
|
) -> Result<(), ExpandError> {
|
||||||
|
for l in lines {
|
||||||
|
let kw = keyword(&l.text);
|
||||||
|
match kw.as_str() {
|
||||||
|
".inc" | ".include" => {
|
||||||
|
let toks = split_tokens(&l.text);
|
||||||
|
if let Some(file) = toks.get(1) {
|
||||||
|
let resolved = self.resolve(file, base_dir)?;
|
||||||
|
self.include(&resolved, out)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
".lib" => {
|
||||||
|
let toks = split_tokens(&l.text);
|
||||||
|
if toks.len() >= 3 {
|
||||||
|
// reference: .lib <file> <section>
|
||||||
|
let resolved = self.resolve(&toks[1], base_dir)?;
|
||||||
|
self.expand_section(&resolved, &toks[2], out)?;
|
||||||
|
}
|
||||||
|
// one-token `.lib <name>` at deck level (hs): definition
|
||||||
|
// boundary — drop it (nothing to emit).
|
||||||
|
}
|
||||||
|
".endl" => { /* stray at deck level — drop */ }
|
||||||
|
_ => out.push(l.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Include a whole file at deck level (`.inc`).
|
||||||
|
fn include(&mut self, path: &Path, out: &mut Vec<LogicalLine>) -> Result<(), ExpandError> {
|
||||||
|
if self.active.iter().any(|p| p == path) {
|
||||||
|
return Err(ExpandError::Recursion {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let file = self.load(path)?;
|
||||||
|
let dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
|
||||||
|
self.active.push(path.to_path_buf());
|
||||||
|
let r = self.walk(&file.lines, &dir, out);
|
||||||
|
self.active.pop();
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand a `.lib <file> <section>` reference: splice the section body from
|
||||||
|
/// just after `.lib <section>` up to the first `.endl`, recursing on nested
|
||||||
|
/// references.
|
||||||
|
fn expand_section(
|
||||||
|
&mut self,
|
||||||
|
file: &Path,
|
||||||
|
section: &str,
|
||||||
|
out: &mut Vec<LogicalLine>,
|
||||||
|
) -> Result<(), ExpandError> {
|
||||||
|
let loaded = self.load(file)?;
|
||||||
|
let start = *loaded
|
||||||
|
.sections
|
||||||
|
.get(§ion.to_ascii_lowercase())
|
||||||
|
.ok_or_else(|| ExpandError::Section {
|
||||||
|
file: file.to_path_buf(),
|
||||||
|
name: section.to_string(),
|
||||||
|
})?;
|
||||||
|
let dir = file.parent().unwrap_or(Path::new(".")).to_path_buf();
|
||||||
|
|
||||||
|
let mut i = start + 1;
|
||||||
|
let mut saw_endl = false;
|
||||||
|
while i < loaded.lines.len() {
|
||||||
|
let l = &loaded.lines[i];
|
||||||
|
let kw = keyword(&l.text);
|
||||||
|
match kw.as_str() {
|
||||||
|
".endl" => {
|
||||||
|
saw_endl = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
".inc" | ".include" => {
|
||||||
|
let toks = split_tokens(&l.text);
|
||||||
|
if let Some(f) = toks.get(1) {
|
||||||
|
let resolved = self.resolve(f, &dir)?;
|
||||||
|
self.include(&resolved, out)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
".lib" => {
|
||||||
|
let toks = split_tokens(&l.text);
|
||||||
|
if toks.len() >= 3 {
|
||||||
|
let resolved = self.resolve(&toks[1], &dir)?;
|
||||||
|
self.expand_section(&resolved, &toks[2], out)?;
|
||||||
|
} else {
|
||||||
|
// nested one-token `.lib <name>` definition inside a body:
|
||||||
|
// ngspice leaves it literal. Preserve to match.
|
||||||
|
out.push(l.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => out.push(l.clone()),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if !saw_endl {
|
||||||
|
return Err(ExpandError::MissingEndl {
|
||||||
|
file: file.to_path_buf(),
|
||||||
|
name: section.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Expander {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn write(dir: &Path, name: &str, body: &str) -> PathBuf {
|
||||||
|
let p = dir.join(name);
|
||||||
|
let mut f = std::fs::File::create(&p).unwrap();
|
||||||
|
f.write_all(body.as_bytes()).unwrap();
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scratch directory of its own per test — tests share a process, so a
|
||||||
|
/// single fixed name would let them clobber each other's files.
|
||||||
|
fn tmpdir(tag: &str) -> PathBuf {
|
||||||
|
let d = std::env::temp_dir().join(format!("ngparse_pp_{}_{tag}", std::process::id()));
|
||||||
|
let _ = std::fs::create_dir_all(&d);
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokens_strip_quotes() {
|
||||||
|
assert_eq!(split_tokens(".lib './a b.lib' TT"), vec![".lib", "./a b.lib", "TT"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SPICE consumes line 1 of the TOP deck as the title, whatever it holds.
|
||||||
|
/// Verified against the reference: a deck whose line 1 is `v1 n1 0 1` reports
|
||||||
|
/// `v(n1)=0` (the source does not exist); add a title line and it reports 1.
|
||||||
|
#[test]
|
||||||
|
fn title_line_is_never_a_card() {
|
||||||
|
let dir = tmpdir("title_card");
|
||||||
|
// a bare-text title that would otherwise parse as a device (leading `c`)
|
||||||
|
let top = write(
|
||||||
|
&dir,
|
||||||
|
"top.net",
|
||||||
|
"check scoping of nested .model definitions\nR1 1 0 1k\n.end\n",
|
||||||
|
);
|
||||||
|
let mut ex = Expander::new();
|
||||||
|
let out = ex.expand_file(&top).unwrap();
|
||||||
|
let texts: Vec<&str> = out.iter().map(|l| l.text.as_str()).collect();
|
||||||
|
assert_eq!(ex.title(), "check scoping of nested .model definitions");
|
||||||
|
assert_eq!(texts, vec!["R1 1 0 1k", ".end"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A comment title is stripped by the reader, but must still be recoverable so
|
||||||
|
/// `expand` can re-emit it as line 1.
|
||||||
|
#[test]
|
||||||
|
fn comment_title_is_captured() {
|
||||||
|
let dir = tmpdir("title_comment");
|
||||||
|
let top = write(&dir, "top.net", "* my title\nR1 1 0 1k\n.end\n");
|
||||||
|
let mut ex = Expander::new();
|
||||||
|
let out = ex.expand_file(&top).unwrap();
|
||||||
|
assert_eq!(ex.title(), "* my title");
|
||||||
|
assert_eq!(out[0].text, "R1 1 0 1k");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only the TOP deck has a title. An `.include`d file's first line stays an
|
||||||
|
/// ordinary card — confirmed against the reference, where a resistor on line 1
|
||||||
|
/// of an included file is present and measurable.
|
||||||
|
#[test]
|
||||||
|
fn included_file_keeps_its_first_line() {
|
||||||
|
let dir = tmpdir("title_inc");
|
||||||
|
write(&dir, "sub.inc", "R_from_inc 1 0 2k\nC_sub 1 0 1p\n");
|
||||||
|
let top = write(&dir, "top.net", "* title\n.inc './sub.inc'\n.end\n");
|
||||||
|
let mut ex = Expander::new();
|
||||||
|
let out = ex.expand_file(&top).unwrap();
|
||||||
|
let texts: Vec<&str> = out.iter().map(|l| l.text.as_str()).collect();
|
||||||
|
assert_eq!(texts, vec!["R_from_inc 1 0 2k", "C_sub 1 0 1p", ".end"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expands_sections_includes_and_nesting() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("ngparse_pp_{}", std::process::id()));
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
|
||||||
|
// models.lib: section TT includes a sub-file and references a nested section.
|
||||||
|
write(
|
||||||
|
&dir,
|
||||||
|
"models.lib",
|
||||||
|
"\
|
||||||
|
.lib TT
|
||||||
|
.inc './sub.inc'
|
||||||
|
.lib './corner.lib' TT_core
|
||||||
|
R_end 1 0 1
|
||||||
|
.endl TT
|
||||||
|
",
|
||||||
|
);
|
||||||
|
write(&dir, "sub.inc", "* sub\nC_sub 1 0 1p\n");
|
||||||
|
write(
|
||||||
|
&dir,
|
||||||
|
"corner.lib",
|
||||||
|
"\
|
||||||
|
.lib TT_core
|
||||||
|
.param vth=0.3
|
||||||
|
.endl TT_core
|
||||||
|
.lib OTHER
|
||||||
|
.param unused=1
|
||||||
|
.endl OTHER
|
||||||
|
",
|
||||||
|
);
|
||||||
|
// Top deck references models.lib section TT. Line 1 is the title — SPICE
|
||||||
|
// consumes it unconditionally, so a real deck never starts with a card.
|
||||||
|
let top = write(
|
||||||
|
&dir,
|
||||||
|
"top.net",
|
||||||
|
"* top title\nV1 1 0 1\n.lib './models.lib' TT\n.end\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut ex = Expander::new();
|
||||||
|
let out = ex.expand_file(&top).unwrap();
|
||||||
|
let texts: Vec<&str> = out.iter().map(|l| l.text.as_str()).collect();
|
||||||
|
assert_eq!(ex.title(), "* top title");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
texts,
|
||||||
|
vec![
|
||||||
|
"V1 1 0 1",
|
||||||
|
"C_sub 1 0 1p", // from .inc
|
||||||
|
".param vth=0.3", // from nested .lib corner.lib TT_core
|
||||||
|
"R_end 1 0 1", // rest of TT body
|
||||||
|
".end",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// OTHER section must not leak in.
|
||||||
|
assert!(!texts.iter().any(|t| t.contains("unused")));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
//! Logical-line reader: turns raw deck text into logical lines with provenance.
|
||||||
|
//!
|
||||||
|
//! Responsibilities (mirrors the earliest stage of ngspice `inpcom.c`):
|
||||||
|
//! * strip full-line comments (`*` as the first non-blank character)
|
||||||
|
//! * strip inline `$` comments (HSPICE style)
|
||||||
|
//! * join continuation lines (a line whose first non-blank char is `+`)
|
||||||
|
//! * drop blank lines
|
||||||
|
//! * keep source provenance (file + physical line number) for diagnostics
|
||||||
|
//! and for the eventual `struct card.linesource` / `linenum`.
|
||||||
|
//!
|
||||||
|
//! SPICE is case-insensitive for keywords; we preserve original case in the text
|
||||||
|
//! and lower-case only where semantics require it (done by later stages).
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// One logical line of a deck: continuations joined, comments stripped.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LogicalLine {
|
||||||
|
/// The joined, comment-stripped text (no trailing newline).
|
||||||
|
pub text: String,
|
||||||
|
/// Source file this line originated from.
|
||||||
|
pub file: Arc<str>,
|
||||||
|
/// 1-based physical line number of the first physical line of this logical line.
|
||||||
|
pub line_no: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the byte offset at which an inline comment begins, if any.
|
||||||
|
///
|
||||||
|
/// Mirrors ngspice `inpcom.c::inp_stripcomments_line` (hs/HSPICE mode, which is
|
||||||
|
/// what the PDK decks select). `cs` is true inside a `.control ... .endc` section.
|
||||||
|
///
|
||||||
|
/// * quoted strings (`"..."` / `'...'`, with `\` escapes) are skipped over, so a
|
||||||
|
/// `$`/`;` inside them is never a comment (e.g. `echo "v = $&x"`),
|
||||||
|
/// * `;` and `//` always start a comment,
|
||||||
|
/// * OUTSIDE `.control`, `$` starts a comment regardless of the preceding
|
||||||
|
/// character — foundry decks write `...=10u$ comment` with no separator,
|
||||||
|
/// * INSIDE `.control`, `$` is a comment ONLY when followed by a space, so
|
||||||
|
/// ngspice variable substitutions (`$&tests`, `$n_test`) survive.
|
||||||
|
fn inline_comment_start(s: &str, cs: bool) -> Option<usize> {
|
||||||
|
let b = s.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < b.len() {
|
||||||
|
let c = b[i];
|
||||||
|
if c == b'"' || c == b'\'' {
|
||||||
|
// skip the quoted string
|
||||||
|
let q = c;
|
||||||
|
i += 1;
|
||||||
|
while i < b.len() && !(b[i] == q && b[i - 1] != b'\\') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i += 1; // step past the closing quote (or off the end)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c == b';' {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
if c == b'/' && i + 1 < b.len() && b[i + 1] == b'/' {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
if c == b'$' {
|
||||||
|
if !cs {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
if i + 1 < b.len() && b[i + 1] == b' ' {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this physical line a full-line comment? `*` is the SPICE comment marker;
|
||||||
|
/// ngspice also converts a leading `#` into one (inpcom.c::inp_stripcomments_line).
|
||||||
|
fn is_full_comment(s: &str) -> bool {
|
||||||
|
matches!(s.trim_start().as_bytes().first(), Some(b'*') | Some(b'#'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lowercased first token, for `.control`/`.endc` tracking.
|
||||||
|
fn first_kw(s: &str) -> String {
|
||||||
|
s.split_whitespace().next().unwrap_or("").to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply HSPICE/shell-style `\\` end-of-line continuation, which happens *before*
|
||||||
|
/// `+` stitching. Mirrors ngspice `inpcom.c::chk_for_line_continuation`: a line
|
||||||
|
/// whose last two non-blank chars are `\\` (and which does not start with `*`/`$`)
|
||||||
|
/// continues on the next physical line. ngspice implements this by blanking the
|
||||||
|
/// `\\` and prepending `+` to the following line, turning it into an ordinary
|
||||||
|
/// continuation — so we do exactly that and let `+` stitching finish the job.
|
||||||
|
///
|
||||||
|
/// Line numbering is preserved: each output line still corresponds 1:1 to an input
|
||||||
|
/// physical line (we never add or remove lines here, only rewrite their content).
|
||||||
|
fn splice_shell_continuations(src: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(src.len() + 16);
|
||||||
|
let mut prev_continues = false;
|
||||||
|
for raw in src.lines() {
|
||||||
|
// A line following a `\\` continuation gets a leading `+` (ngspice does
|
||||||
|
// this unconditionally, before checking the line itself).
|
||||||
|
let line: Cow<str> = if prev_continues {
|
||||||
|
Cow::Owned(format!("+{raw}"))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(raw)
|
||||||
|
};
|
||||||
|
prev_continues = false;
|
||||||
|
|
||||||
|
let trimmed_end = line.trim_end();
|
||||||
|
let first = trimmed_end.trim_start().as_bytes().first().copied();
|
||||||
|
let emit: &str = if first != Some(b'*')
|
||||||
|
&& first != Some(b'$')
|
||||||
|
&& trimmed_end.ends_with("\\\\")
|
||||||
|
{
|
||||||
|
prev_continues = true;
|
||||||
|
&trimmed_end[..trimmed_end.len() - 2] // drop the trailing `\\`
|
||||||
|
} else {
|
||||||
|
&line
|
||||||
|
};
|
||||||
|
out.push_str(emit);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split raw deck text into logical lines.
|
||||||
|
///
|
||||||
|
/// `file` is the provenance label attached to every produced line.
|
||||||
|
pub fn logical_lines(src: &str, file: Arc<str>) -> Vec<LogicalLine> {
|
||||||
|
let mut out: Vec<LogicalLine> = Vec::new();
|
||||||
|
|
||||||
|
let spliced = splice_shell_continuations(src);
|
||||||
|
// Comment rules differ inside a `.control ... .endc` section (ngspice passes
|
||||||
|
// `found_control` as the `cs` flag to inp_stripcomments_line), so track it.
|
||||||
|
let mut in_control = false;
|
||||||
|
for (idx, raw) in spliced.lines().enumerate() {
|
||||||
|
let line_no = idx + 1;
|
||||||
|
|
||||||
|
// Full-line comments never contribute text and never break continuation
|
||||||
|
// joining (a `+` line after a comment still continues the last real line).
|
||||||
|
if is_full_comment(raw) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match first_kw(raw).as_str() {
|
||||||
|
".control" => in_control = true,
|
||||||
|
".endc" => in_control = false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip inline comment (`$`/`;`/`//`), honoring quotes and .control rules.
|
||||||
|
let content = match inline_comment_start(raw, in_control) {
|
||||||
|
Some(pos) => &raw[..pos],
|
||||||
|
None => raw,
|
||||||
|
};
|
||||||
|
|
||||||
|
let trimmed = content.trim_end();
|
||||||
|
if trimmed.trim_start().is_empty() {
|
||||||
|
continue; // blank after stripping
|
||||||
|
}
|
||||||
|
|
||||||
|
let lstripped = trimmed.trim_start();
|
||||||
|
if let Some(rest) = lstripped.strip_prefix('+') {
|
||||||
|
// Continuation: append to the previous logical line, replacing the
|
||||||
|
// leading `+` with a single space (ngspice behavior).
|
||||||
|
if let Some(last) = out.last_mut() {
|
||||||
|
last.text.push(' ');
|
||||||
|
last.text.push_str(rest.trim_start());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// No previous line to continue: treat the remainder as its own line.
|
||||||
|
out.push(LogicalLine {
|
||||||
|
text: rest.trim_start().to_string(),
|
||||||
|
file: Arc::clone(&file),
|
||||||
|
line_no,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Emit fully-trimmed: leading whitespace is insignificant in SPICE, and
|
||||||
|
// downstream code identifies a card by its FIRST character (device-type
|
||||||
|
// letter, `.` for dot-cards). Indented body lines (common in nested
|
||||||
|
// subckts) would otherwise present an empty instance name and be
|
||||||
|
// misparsed — e.g. ` x31 41a 41b sub3` would not be seen as an X call.
|
||||||
|
out.push(LogicalLine {
|
||||||
|
text: lstripped.to_string(),
|
||||||
|
file: Arc::clone(&file),
|
||||||
|
line_no,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn ll(src: &str) -> Vec<LogicalLine> {
|
||||||
|
logical_lines(src, Arc::from("test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn joins_continuations() {
|
||||||
|
let out = ll("R1 a b 1k\n.param\n+ x = 1\n+ y = 2\n");
|
||||||
|
assert_eq!(out.len(), 2);
|
||||||
|
assert_eq!(out[0].text, "R1 a b 1k");
|
||||||
|
assert_eq!(out[1].text, ".param x = 1 y = 2");
|
||||||
|
assert_eq!(out[1].line_no, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_full_and_inline_comments() {
|
||||||
|
let out = ll("* a comment\nR1 a b 1k $ inline note\n");
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].text, "R1 a b 1k");
|
||||||
|
assert_eq!(out[0].line_no, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comment_between_line_and_continuation() {
|
||||||
|
// A comment line between a line and its `+` continuation must not break joining.
|
||||||
|
let out = ll("V1 n 0 1\n* note\n+ ac 1\n");
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].text, "V1 n 0 1 ac 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dollar_comment_hs_mode() {
|
||||||
|
// hs/HSPICE mode: `$` ends the line regardless of the preceding char —
|
||||||
|
// foundry decks write `l=10u$ comment` with no separator.
|
||||||
|
let out = ll("X1 a c $ tail\n");
|
||||||
|
assert_eq!(out[0].text, "X1 a c");
|
||||||
|
let out = ll("M1 d g s b nch l=10u$ no separator\n");
|
||||||
|
assert_eq!(out[0].text, "M1 d g s b nch l=10u");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semicolon_and_slash_comments() {
|
||||||
|
assert_eq!(ll("R1 a b 1k ; trailing\n")[0].text, "R1 a b 1k");
|
||||||
|
assert_eq!(ll("R1 a b 1k // trailing\n")[0].text, "R1 a b 1k");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_line_is_a_comment() {
|
||||||
|
assert!(ll("# a comment\nR1 a b 1k\n").len() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quotes_protect_comment_chars() {
|
||||||
|
// a `$` inside a quoted string is not a comment (ngspice skips strings)
|
||||||
|
let out = ll("echo \"value = $&x\" $ real comment\n");
|
||||||
|
assert_eq!(out[0].text, "echo \"value = $&x\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_block_keeps_dollar_substitutions() {
|
||||||
|
// Inside `.control`, `$` is only a comment when followed by a space, so
|
||||||
|
// ngspice variable substitutions survive.
|
||||||
|
let out = ll(".control\nforeach n $&tests $ note\nset x = $n_test\n.endc\n");
|
||||||
|
let texts: Vec<&str> = out.iter().map(|l| l.text.as_str()).collect();
|
||||||
|
assert!(texts.contains(&"foreach n $&tests"), "got {texts:?}");
|
||||||
|
assert!(texts.contains(&"set x = $n_test"), "got {texts:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skips_blank_lines() {
|
||||||
|
let out = ll("\n \nR1 a b 1k\n\n");
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn joins_backslash_continuation() {
|
||||||
|
// HSPICE quoted-expression continuation via trailing `\\`.
|
||||||
|
let out = ll(".param f(x)='a*(x==m1)+\\\\\n b*(x==m2)+\\\\\n c*(x==m3)'\n");
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].text, ".param f(x)='a*(x==m1)+ b*(x==m2)+ c*(x==m3)'");
|
||||||
|
assert!(!out[0].text.contains('\\'));
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,298 @@
|
||||||
|
//! HSPICE `table_param()` — table-file parameter lookup.
|
||||||
|
//!
|
||||||
|
//! Foundry PDKs use it for self-heating thermal resistance and RF parasitics;
|
||||||
|
//! foundry_b's TT corner alone calls it ~1300 times (1,289 in the flattened
|
||||||
|
//! `tb_driver` deck).
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! table_param(file, N_int, int_1..int_N, N_real, real_1..real_M, output_col)
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Integer keys match exactly; real keys are multilinearly interpolated;
|
||||||
|
//! `output_col` is a 1-based index into the *value* columns (those after the
|
||||||
|
//! keys). The file is `#`-header-first:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! #nf nfin l rth0 <- header names the columns: 3 keys, 1 value
|
||||||
|
//! 1 1 1e-07 0.0105 <- data rows
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! This mirrors ngspice's own implementation
|
||||||
|
//! (`src/frontend/numparam/table_param.c`, added in commit 7131b97e0), which is
|
||||||
|
//! compiled into the reference binary — so its results are directly diffable.
|
||||||
|
//! The lookup is a pure function of `(path, int keys, real keys, column)`;
|
||||||
|
//! relative paths are resolved to absolute earlier, during deck expansion, where
|
||||||
|
//! the defining file is known (see `preprocess::rewrite_table_paths`).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
/// A parsed table file: `n_cols`-wide rows of `f64`.
|
||||||
|
struct Table {
|
||||||
|
n_cols: usize,
|
||||||
|
rows: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
fn n_rows(&self) -> usize {
|
||||||
|
self.rows.len() / self.n_cols
|
||||||
|
}
|
||||||
|
fn at(&self, row: usize, col: usize) -> f64 {
|
||||||
|
self.rows[row * self.n_cols + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process-wide cache: foundry decks reference the same table file hundreds of
|
||||||
|
/// times, and ngspice caches for the process lifetime for the same reason.
|
||||||
|
fn cache() -> &'static Mutex<HashMap<String, Option<&'static Table>>> {
|
||||||
|
static C: OnceLock<Mutex<HashMap<String, Option<&'static Table>>>> = OnceLock::new();
|
||||||
|
C.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load and parse a table file. The first non-blank line must be the `#` header;
|
||||||
|
/// its token count (minus the `#`) fixes the column count. `*`/`#` lines and
|
||||||
|
/// blanks are skipped; a row with the wrong arity is skipped.
|
||||||
|
fn load(path: &str) -> Option<Table> {
|
||||||
|
let text = std::fs::read(path).ok()?;
|
||||||
|
let text = String::from_utf8_lossy(&text);
|
||||||
|
let mut lines = text.lines();
|
||||||
|
|
||||||
|
let n_cols = loop {
|
||||||
|
let l = lines.next()?;
|
||||||
|
let t = l.trim_start();
|
||||||
|
if t.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ngspice insists on a header and errors out otherwise.
|
||||||
|
let hdr = t.strip_prefix('#')?;
|
||||||
|
break hdr.split_whitespace().count();
|
||||||
|
};
|
||||||
|
if n_cols == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for l in lines {
|
||||||
|
let t = l.trim_start();
|
||||||
|
if t.is_empty() || t.starts_with('*') || t.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let vals: Vec<f64> = t
|
||||||
|
.split_whitespace()
|
||||||
|
.filter_map(|tok| tok.parse::<f64>().ok())
|
||||||
|
.collect();
|
||||||
|
if vals.len() == n_cols {
|
||||||
|
rows.extend(vals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rows.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Table { n_cols, rows })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(path: &str) -> Option<&'static Table> {
|
||||||
|
let mut c = cache().lock().ok()?;
|
||||||
|
if let Some(hit) = c.get(path) {
|
||||||
|
return *hit;
|
||||||
|
}
|
||||||
|
// Leaked deliberately: the cache lives for the process, exactly as ngspice's
|
||||||
|
// does, and this hands out plain `&'static` refs without a lock on every read.
|
||||||
|
let loaded = load(path).map(|t| &*Box::leak(Box::new(t)));
|
||||||
|
c.insert(path.to_string(), loaded);
|
||||||
|
loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do this row's integer keys match? ngspice compares with a 0.5 tolerance —
|
||||||
|
/// these are integers stored as doubles.
|
||||||
|
fn int_keys_match(t: &Table, row: usize, ints: &[f64]) -> bool {
|
||||||
|
ints.iter()
|
||||||
|
.enumerate()
|
||||||
|
.all(|(i, v)| (t.at(row, i) - v).abs() <= 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a `table_param()` lookup. Returns `None` on any failure (missing
|
||||||
|
/// file, no matching rows, column out of range) — the caller then leaves the
|
||||||
|
/// expression symbolic and reports a drop rather than inventing a value.
|
||||||
|
pub fn lookup(path: &str, ints: &[f64], reals: &[f64], output_col: i64) -> Option<f64> {
|
||||||
|
let t = get(path)?;
|
||||||
|
let n_keys = ints.len() + reals.len();
|
||||||
|
|
||||||
|
// output_col is 1-based within the VALUE columns, which start after the keys.
|
||||||
|
if output_col < 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let data_col = n_keys + (output_col as usize - 1);
|
||||||
|
if data_col >= t.n_cols {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per real-key dimension, find the values bracketing the target among rows
|
||||||
|
// whose integer keys match, and the weight between them. Out-of-range targets
|
||||||
|
// clamp to the nearest endpoint — no extrapolation.
|
||||||
|
let mut v_lo = vec![0.0; reals.len()];
|
||||||
|
let mut v_hi = vec![0.0; reals.len()];
|
||||||
|
let mut w = vec![0.0; reals.len()];
|
||||||
|
let mut found_any = false;
|
||||||
|
|
||||||
|
for (d, &target) in reals.iter().enumerate() {
|
||||||
|
let (mut best_lo, mut best_hi) = (f64::NEG_INFINITY, f64::INFINITY);
|
||||||
|
let (mut have_lo, mut have_hi) = (false, false);
|
||||||
|
for r in 0..t.n_rows() {
|
||||||
|
if !int_keys_match(t, r, ints) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
found_any = true;
|
||||||
|
let v = t.at(r, ints.len() + d);
|
||||||
|
if v <= target && v > best_lo {
|
||||||
|
best_lo = v;
|
||||||
|
have_lo = true;
|
||||||
|
}
|
||||||
|
if v >= target && v < best_hi {
|
||||||
|
best_hi = v;
|
||||||
|
have_hi = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !have_lo && !have_hi {
|
||||||
|
return None; // no rows matched the integer keys
|
||||||
|
}
|
||||||
|
if !have_lo {
|
||||||
|
best_lo = best_hi;
|
||||||
|
}
|
||||||
|
if !have_hi {
|
||||||
|
best_hi = best_lo;
|
||||||
|
}
|
||||||
|
v_lo[d] = best_lo;
|
||||||
|
v_hi[d] = best_hi;
|
||||||
|
w[d] = if best_hi == best_lo {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
((target - best_lo) / (best_hi - best_lo)).clamp(0.0, 1.0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no real keys the integer keys alone select the row, so confirm a match.
|
||||||
|
if reals.is_empty() {
|
||||||
|
let row = (0..t.n_rows()).find(|&r| int_keys_match(t, r, ints))?;
|
||||||
|
return Some(t.at(row, data_col));
|
||||||
|
}
|
||||||
|
if !found_any {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum the 2^n_real corners of the bracketing hyper-rectangle.
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut wsum = 0.0;
|
||||||
|
for c in 0..(1usize << reals.len()) {
|
||||||
|
let mut weight = 1.0;
|
||||||
|
let mut rk = vec![0.0; reals.len()];
|
||||||
|
for d in 0..reals.len() {
|
||||||
|
if c & (1 << d) != 0 {
|
||||||
|
rk[d] = v_hi[d];
|
||||||
|
weight *= w[d];
|
||||||
|
} else {
|
||||||
|
rk[d] = v_lo[d];
|
||||||
|
weight *= 1.0 - w[d];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if weight == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for r in 0..t.n_rows() {
|
||||||
|
if !int_keys_match(t, r, ints) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hit = rk.iter().enumerate().all(|(d, &want)| {
|
||||||
|
(t.at(r, ints.len() + d) - want).abs() <= 1e-12 * (want.abs() + 1e-30)
|
||||||
|
});
|
||||||
|
if hit {
|
||||||
|
sum += weight * t.at(r, data_col);
|
||||||
|
wsum += weight;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wsum == 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn tbl(body: &str, tag: &str) -> String {
|
||||||
|
let p = std::env::temp_dir().join(format!("ngparse_tbl_{}_{tag}", std::process::id()));
|
||||||
|
let mut f = std::fs::File::create(&p).unwrap();
|
||||||
|
f.write_all(body.as_bytes()).unwrap();
|
||||||
|
p.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shape of foundry_b's egnfet_SHE.table: 2 int keys (nf, nfin), 1 real key (l),
|
||||||
|
/// 1 value column (rth0).
|
||||||
|
const SHE: &str = "\
|
||||||
|
#nf nfin l rth0
|
||||||
|
1 1 1e-07 0.0105
|
||||||
|
1 1 1.5e-07 0.0108
|
||||||
|
1 1 2e-07 0.0083
|
||||||
|
1 2 1e-07 0.0146
|
||||||
|
1 2 2e-07 0.0200
|
||||||
|
2 1 1e-07 0.0500
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_hit_on_a_grid_point() {
|
||||||
|
let p = tbl(SHE, "exact");
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[1e-7], 1), Some(0.0105));
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[2e-7], 1), Some(0.0083));
|
||||||
|
// integer keys select the row set
|
||||||
|
assert_eq!(lookup(&p, &[2.0, 1.0], &[1e-7], 1), Some(0.05));
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 2.0], &[1e-7], 1), Some(0.0146));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interpolates_real_key() {
|
||||||
|
let p = tbl(SHE, "interp");
|
||||||
|
// midway between 1e-7 (0.0105) and 1.5e-7 (0.0108)
|
||||||
|
let v = lookup(&p, &[1.0, 1.0], &[1.25e-7], 1).unwrap();
|
||||||
|
assert!((v - 0.01065).abs() < 1e-12, "got {v}");
|
||||||
|
// 1e-7 (0.0146) .. 2e-7 (0.0200), quarter of the way
|
||||||
|
let v = lookup(&p, &[1.0, 2.0], &[1.25e-7], 1).unwrap();
|
||||||
|
assert!((v - 0.01595).abs() < 1e-12, "got {v}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Out-of-range clamps to the nearest endpoint — ngspice never extrapolates.
|
||||||
|
#[test]
|
||||||
|
fn clamps_instead_of_extrapolating() {
|
||||||
|
let p = tbl(SHE, "clamp");
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[1e-9], 1), Some(0.0105)); // below min
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[1.0], 1), Some(0.0083)); // above max
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_bad_lookups() {
|
||||||
|
let p = tbl(SHE, "bad");
|
||||||
|
assert_eq!(lookup(&p, &[9.0, 9.0], &[1e-7], 1), None); // no such int keys
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[1e-7], 2), None); // only 1 value column
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[1e-7], 0), None); // 1-based
|
||||||
|
assert_eq!(lookup("/nonexistent/x.table", &[1.0], &[1.0], 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn header_is_required() {
|
||||||
|
let p = tbl("1 1 1e-07 0.0105\n", "nohdr");
|
||||||
|
assert_eq!(lookup(&p, &[1.0, 1.0], &[1e-7], 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiple value columns: output_col indexes them 1-based, after the keys.
|
||||||
|
#[test]
|
||||||
|
fn selects_output_column() {
|
||||||
|
let p = tbl("#k a b c\n1 10 20 30\n2 40 50 60\n", "cols");
|
||||||
|
assert_eq!(lookup(&p, &[1.0], &[], 1), Some(10.0));
|
||||||
|
assert_eq!(lookup(&p, &[1.0], &[], 2), Some(20.0));
|
||||||
|
assert_eq!(lookup(&p, &[1.0], &[], 3), Some(30.0));
|
||||||
|
assert_eq!(lookup(&p, &[2.0], &[], 3), Some(60.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -144,6 +144,21 @@ if NDEV_WANTED
|
||||||
ngspice_LDADD += spicelib/devices/ndev/libndev.la
|
ngspice_LDADD += spicelib/devices/ndev/libndev.la
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if NGPARSE_WANTED
|
||||||
|
# The ngparse Rust staticlib. Must come after libfte.la, which references it.
|
||||||
|
ngspice_LDADD += @NGPARSE_LIBS@
|
||||||
|
# Build (or refresh) libngparse.a as part of `make`, from the ngspice top
|
||||||
|
# directory -- there is no separate cargo step. It is a BUILT_SOURCES entry so
|
||||||
|
# it is made before anything links against it; the phony prerequisite makes
|
||||||
|
# cargo run on every build (cargo is incremental, so it is a fast no-op when
|
||||||
|
# nothing changed), which also stops a stale .a from being silently linked.
|
||||||
|
BUILT_SOURCES = @NGPARSE_LIB@
|
||||||
|
@NGPARSE_LIB@: ngparse-cargo-build
|
||||||
|
ngparse-cargo-build:
|
||||||
|
cd @NGPARSE_DIR@ && @CARGO@ build --release
|
||||||
|
.PHONY: ngparse-cargo-build
|
||||||
|
endif
|
||||||
|
|
||||||
if NUMDEV_WANTED
|
if NUMDEV_WANTED
|
||||||
ngspice_LDADD += \
|
ngspice_LDADD += \
|
||||||
spicelib/devices/nbjt/libnbjt.la \
|
spicelib/devices/nbjt/libnbjt.la \
|
||||||
|
|
@ -514,6 +529,11 @@ libngspice_la_LIBADD = \
|
||||||
libngspice_la_LIBADD += \
|
libngspice_la_LIBADD += \
|
||||||
frontend/plotting/libplotting.la
|
frontend/plotting/libplotting.la
|
||||||
|
|
||||||
|
if NGPARSE_WANTED
|
||||||
|
# The shared library pulls in libfte.la too, so it needs ngparse as well.
|
||||||
|
libngspice_la_LIBADD += @NGPARSE_LIBS@
|
||||||
|
endif
|
||||||
|
|
||||||
if XSPICE_WANTED
|
if XSPICE_WANTED
|
||||||
libngspice_la_LIBADD += \
|
libngspice_la_LIBADD += \
|
||||||
xspice/cm/libcmxsp.la \
|
xspice/cm/libcmxsp.la \
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ libfte_la_SOURCES = \
|
||||||
inpcompat.c \
|
inpcompat.c \
|
||||||
inpcompat.h \
|
inpcompat.h \
|
||||||
inpc_probe.c \
|
inpc_probe.c \
|
||||||
|
ngparse_glue.c \
|
||||||
interp.c \
|
interp.c \
|
||||||
interp.h \
|
interp.h \
|
||||||
inventory.c \
|
inventory.c \
|
||||||
|
|
@ -208,7 +209,7 @@ libfte_la_SOURCES = \
|
||||||
# testcommands_LDADD = libfte.a plotting/libplotting.a ../misc/libmisc.a
|
# testcommands_LDADD = libfte.a plotting/libplotting.a ../misc/libmisc.a
|
||||||
|
|
||||||
|
|
||||||
AM_CPPFLAGS = @AM_CPPFLAGS@ -I$(top_srcdir)/src/include @X_CFLAGS@
|
AM_CPPFLAGS = @AM_CPPFLAGS@ -I$(top_srcdir)/src/include @X_CFLAGS@ @NGPARSE_CFLAGS@
|
||||||
AM_CFLAGS = $(STATIC)
|
AM_CFLAGS = $(STATIC)
|
||||||
AM_YFLAGS = -d
|
AM_YFLAGS = -d
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ Author: 1985 Wayne A. Christopher
|
||||||
#include "ngspice/cktdefs.h"
|
#include "ngspice/cktdefs.h"
|
||||||
#include "ngspice/cpdefs.h"
|
#include "ngspice/cpdefs.h"
|
||||||
#include "ngspice/inpdefs.h"
|
#include "ngspice/inpdefs.h"
|
||||||
|
#include "ngspice/ngparse_glue.h"
|
||||||
#include "ngspice/ftedefs.h"
|
#include "ngspice/ftedefs.h"
|
||||||
#include "ngspice/dvec.h"
|
#include "ngspice/dvec.h"
|
||||||
#include "ngspice/fteinp.h"
|
#include "ngspice/fteinp.h"
|
||||||
|
|
@ -529,7 +530,55 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile)
|
||||||
inp_source() called with fp: load circuit netlist from file, */
|
inp_source() called with fp: load circuit netlist from file, */
|
||||||
/* called with *fp == NULL and intfile: we want to load circuit from circarray */
|
/* called with *fp == NULL and intfile: we want to load circuit from circarray */
|
||||||
if (fp || intfile) {
|
if (fp || intfile) {
|
||||||
deck = inp_readall(fp, dir_name, filename, comfile, intfile, &expr_w_temper);
|
/* ngparse (opt-in, NGSPICE_NGPARSE=1): expand .lib/.inc, the parameters
|
||||||
|
* and the subcircuits up front, then read the RESULT below. Everything
|
||||||
|
* inp_readall() does besides section extraction still runs over it --
|
||||||
|
* the compatibility passes, and inp_subcktexpand()'s numparam over the
|
||||||
|
* expressions ngparse deliberately leaves symbolic (temper, v(), ...).
|
||||||
|
* .lib/.inc expansion simply finds nothing left to expand.
|
||||||
|
* See ngspice/ngparse_glue.h. */
|
||||||
|
char *ngp_deck = NULL;
|
||||||
|
FILE *ngp_fp = NULL;
|
||||||
|
/* Feed ngparse the file ngspice actually opened (resolved via sourcepath/
|
||||||
|
* inputdir), not the possibly-relative `filename`: a *ng_script deck that
|
||||||
|
* `source`s a netlist from a `set sourcepath` directory hands us a bare
|
||||||
|
* name that does not exist in the cwd. */
|
||||||
|
char *ngp_path = ngparse_glue_realpath(fp, filename);
|
||||||
|
if (ngparse_glue_enabled(comfile, intfile, ngp_path)) {
|
||||||
|
ngp_deck = ngparse_glue_expand(ngp_path);
|
||||||
|
if (!ngp_deck) {
|
||||||
|
tfree(ngp_path);
|
||||||
|
/* ngparse already said why. Close fp as the !deck path below
|
||||||
|
* does -- inp_spsource owns it -- and report failure. */
|
||||||
|
if (!intfile && fp)
|
||||||
|
fclose(fp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ngp_fp = fopen(ngp_deck, "r");
|
||||||
|
if (!ngp_fp) {
|
||||||
|
fprintf(cp_err, "Error: ngparse: cannot reopen %s\n", ngp_deck);
|
||||||
|
remove(ngp_deck);
|
||||||
|
tfree(ngp_deck);
|
||||||
|
tfree(ngp_path);
|
||||||
|
if (!intfile && fp)
|
||||||
|
fclose(fp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
/* `fp` is deliberately left open and untouched: the code below still
|
||||||
|
* uses it to set `inputdir` (relative paths resolve against it) and
|
||||||
|
* closes it on the normal path. We only divert what inp_readall
|
||||||
|
* READS, to the expanded deck. */
|
||||||
|
}
|
||||||
|
tfree(ngp_path);
|
||||||
|
|
||||||
|
deck = inp_readall(ngp_fp ? ngp_fp : fp, dir_name, filename,
|
||||||
|
comfile, ngp_fp ? FALSE : intfile, &expr_w_temper);
|
||||||
|
|
||||||
|
if (ngp_fp) {
|
||||||
|
fclose(ngp_fp);
|
||||||
|
remove(ngp_deck);
|
||||||
|
tfree(ngp_deck);
|
||||||
|
}
|
||||||
|
|
||||||
/* files starting with *ng_script are user supplied command files.
|
/* files starting with *ng_script are user supplied command files.
|
||||||
* Walk past any leading blank cards (see same logic in
|
* Walk past any leading blank cards (see same logic in
|
||||||
|
|
@ -731,9 +780,21 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile)
|
||||||
if (!ciprefix(".control", dd->line) && !ciprefix(".endc", dd->line)) {
|
if (!ciprefix(".control", dd->line) && !ciprefix(".endc", dd->line)) {
|
||||||
if (dd->line[0] == '*')
|
if (dd->line[0] == '*')
|
||||||
cp_evloop(dd->line + 2);
|
cp_evloop(dd->line + 2);
|
||||||
/* option line stored but not processed */
|
/* option line stored for the next circuit load ... */
|
||||||
else if (ciprefix("option", dd->line))
|
else if (ciprefix("option", dd->line)) {
|
||||||
com_options = inp_getoptsc(dd->line, com_options);
|
com_options = inp_getoptsc(dd->line, com_options);
|
||||||
|
/* ... and ALSO applied immediately when a circuit is
|
||||||
|
* already loaded. Without this, option lines placed
|
||||||
|
* AFTER the `source` command in a script are silently
|
||||||
|
* dropped: the com_options list is only merged into a
|
||||||
|
* circuit at load time (see the line_nconc merge in
|
||||||
|
* inp_dodeck), so post-source options never reached
|
||||||
|
* the task. com_option handles name/value parsing
|
||||||
|
* and routes through if_option to the default task,
|
||||||
|
* which analysis commands inherit from. */
|
||||||
|
if (ft_curckt && ft_curckt->ci_ckt)
|
||||||
|
cp_evloop(dd->line);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
cp_evloop(dd->line);
|
cp_evloop(dd->line);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ struct function_env
|
||||||
const char *accept;
|
const char *accept;
|
||||||
} *functions;
|
} *functions;
|
||||||
/* Hash on `name` for O(1) lookup in find_function. Foundry PDKs
|
/* Hash on `name` for O(1) lookup in find_function. Foundry PDKs
|
||||||
* register hundreds-to-thousands of .funcs (Samsung 14LPU's TT
|
* register hundreds-to-thousands of .funcs (foundry_b 14LPU's TT
|
||||||
* corner has ~1300), and find_function is called once per `(` in
|
* corner has ~1300), and find_function is called once per `(` in
|
||||||
* every line during macro expansion — the linear scan was billions
|
* every line during macro expansion — the linear scan was billions
|
||||||
* of strcmp on real decks. */
|
* of strcmp on real decks. */
|
||||||
|
|
@ -3725,8 +3725,8 @@ static void inp_stripcomments_line(char *s, bool cs, bool inc)
|
||||||
/* outside of .control section, and not in PS mode */
|
/* outside of .control section, and not in PS mode */
|
||||||
else if (!cs && (c == '$') && !newcompat.ps) {
|
else if (!cs && (c == '$') && !newcompat.ps) {
|
||||||
/* HSPICE treats '$' as an end-of-line comment regardless of
|
/* HSPICE treats '$' as an end-of-line comment regardless of
|
||||||
* the preceding character — foundry decks (Samsung 14LPU,
|
* the preceding character — foundry decks (foundry_b 14LPU,
|
||||||
* TSMC, GF) routinely write `...)'$ comment` or `...=10u$ ...`
|
* foundry_a, GF) routinely write `...)'$ comment` or `...=10u$ ...`
|
||||||
* with no separator. In ngbehavior=hs / hsa, accept that.
|
* with no separator. In ngbehavior=hs / hsa, accept that.
|
||||||
*
|
*
|
||||||
* Outside HS mode keep the original conservative rule (only
|
* Outside HS mode keep the original conservative rule (only
|
||||||
|
|
@ -4000,7 +4000,7 @@ static void inp_fix_for_numparam(
|
||||||
* conversion that would otherwise hand the path to
|
* conversion that would otherwise hand the path to
|
||||||
* numparam as an expression and trigger
|
* numparam as an expression and trigger
|
||||||
* Number format error: "../path...} <section>"
|
* Number format error: "../path...} <section>"
|
||||||
* Observed on GF55 bcd55 sample_netlist/isoednfet_5p0_lr.sp
|
* Observed on foundry_c bcd55 sample_netlist/isoednfet_5p0_lr.sp
|
||||||
* which uses `.del lib '../models/design_wrapper.lib'` in
|
* which uses `.del lib '../models/design_wrapper.lib'` in
|
||||||
* .alter blocks. Matches the existing `.lib` skip above. */
|
* .alter blocks. Matches the existing `.lib` skip above. */
|
||||||
if (ciprefix(".del", c->line) ||
|
if (ciprefix(".del", c->line) ||
|
||||||
|
|
@ -5644,7 +5644,7 @@ static void inp_sort_params(struct card *param_cards,
|
||||||
* scanning every other param's expression for occurrences of each
|
* scanning every other param's expression for occurrences of each
|
||||||
* param's name
|
* param's name
|
||||||
* Both passes are now O(N) + O(total expression chars), enabling real
|
* Both passes are now O(N) + O(total expression chars), enabling real
|
||||||
* PDK use (Samsung 14LPU's TT corner had N~2700 per subckt, called
|
* PDK use (foundry_b 14LPU's TT corner had N~2700 per subckt, called
|
||||||
* ~500 times, totalling many billions of ops in the old code).
|
* ~500 times, totalling many billions of ops in the old code).
|
||||||
*
|
*
|
||||||
* Encoding: hash stores `(void *)(intptr_t)(i + 1)` so that the NULL
|
* Encoding: hash stores `(void *)(intptr_t)(i + 1)` so that the NULL
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
/*
|
||||||
|
* ngparse glue. See ngspice/ngparse_glue.h for the design.
|
||||||
|
*/
|
||||||
|
#include "ngspice/ngspice.h"
|
||||||
|
#include "ngspice/cpdefs.h"
|
||||||
|
#include "ngspice/ftedefs.h"
|
||||||
|
#include "ngspice/ngparse_glue.h"
|
||||||
|
|
||||||
|
#ifdef USE_NGPARSE
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "../misc/mktemp.h"
|
||||||
|
#include "ngparse.h" /* the ngparse Rust ABI */
|
||||||
|
|
||||||
|
/* Worker cores for ngparse. One core unless ngparse_cores asks for
|
||||||
|
* more -- there is never a reason to set it to 1, that being the default.
|
||||||
|
*
|
||||||
|
* Single-threaded is the measured right answer, not an omission: expansion is
|
||||||
|
* ~10% of deck-load time, the rest being INPpas1/2/3 and model setup here in
|
||||||
|
* ngspice, which this does not touch. The knob exists so that if that ever
|
||||||
|
* changes, parallelism lands without a surface change; ngparse accepts a larger
|
||||||
|
* value and warns that it is not honored yet. */
|
||||||
|
static int ngparse_glue_cores(void)
|
||||||
|
{
|
||||||
|
const char *s = getenv("ngparse_cores");
|
||||||
|
if (!s || !*s)
|
||||||
|
return 1;
|
||||||
|
long n = strtol(s, NULL, 10);
|
||||||
|
if (n < 1) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"ngparse: ngparse_cores=%s is not a positive integer; using 1\n",
|
||||||
|
s);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return (int) n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compatibility dialect for ngparse, read from ngspice's `ngbehavior` variable
|
||||||
|
* (set in spinit / .spiceinit). ngparse has taken over expansion, so ngspice's
|
||||||
|
* own pspice_compat pass -- which is what rewrites if()/VSWITCH/TABLE and injects
|
||||||
|
* the PSpice funcs -- never runs on our already-flat deck. We therefore tell
|
||||||
|
* ngparse the mode so it can apply those conversions itself and a `ngbehavior=ps`
|
||||||
|
* run matches the reference. 1 = PSpice; 0 = default/HSPICE. */
|
||||||
|
static int ngparse_glue_compat(void)
|
||||||
|
{
|
||||||
|
char behaviour[128];
|
||||||
|
if (cp_getvar("ngbehavior", CP_STRING, behaviour, sizeof(behaviour))) {
|
||||||
|
/* leading "ps" selects PSpice, matching how ngspice keys newcompat.ps */
|
||||||
|
if (behaviour[0] == 'p' && behaviour[1] == 's')
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ON by default in a build configured --enable-ngparse: enabling it at build
|
||||||
|
* time is already the deliberate choice, so there is nothing to opt into again
|
||||||
|
* on every run. `--no-ngparse` turns it off for a run -- an escape hatch if
|
||||||
|
* some deck ever trips ngparse up, without needing a rebuild. */
|
||||||
|
static bool ngparse_requested = TRUE;
|
||||||
|
|
||||||
|
void ngparse_glue_request(bool on)
|
||||||
|
{
|
||||||
|
ngparse_requested = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Does `filename` start with *ng_script -- i.e. is it a command file rather than
|
||||||
|
* a netlist?
|
||||||
|
*
|
||||||
|
* inp_spsource's `comfile` argument cannot answer this for us: it detects
|
||||||
|
* *ng_script by probing the deck AFTER inp_readall() has read it, which is well
|
||||||
|
* past the point where we must decide. So run the same test directly on the
|
||||||
|
* file, with the same "blank card" rule as that probe.
|
||||||
|
*
|
||||||
|
* Getting this wrong is not subtle: ngparse expands the .control script as if it
|
||||||
|
* were a netlist, producing an empty circuit ("Circuit: *" / "incomplete or
|
||||||
|
* empty netlist") before the script's own `source` line ever runs. */
|
||||||
|
static bool file_is_ng_script(const char *filename)
|
||||||
|
{
|
||||||
|
char buf[BSIZE_SP + 1];
|
||||||
|
bool is_script = FALSE;
|
||||||
|
FILE *f = fopen(filename, "r");
|
||||||
|
|
||||||
|
if (!f)
|
||||||
|
return FALSE; /* let the normal path report the open failure */
|
||||||
|
while (fgets(buf, sizeof buf, f)) {
|
||||||
|
if (buf[0] == '\0' || buf[0] == '\n' ||
|
||||||
|
(buf[0] == '\r' && buf[1] == '\n'))
|
||||||
|
continue; /* leading blank card */
|
||||||
|
is_script = ciprefix("*ng_script", buf) ? TRUE : FALSE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return is_script;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ngparse_glue_enabled(bool comfile, bool intfile, const char *filename)
|
||||||
|
{
|
||||||
|
if (!ngparse_requested)
|
||||||
|
return FALSE;
|
||||||
|
/* Command files (*ng_script) are .control scripts, not netlists, and an
|
||||||
|
* internal/array deck has no file for ngparse to read. */
|
||||||
|
if (comfile || intfile || !filename)
|
||||||
|
return FALSE;
|
||||||
|
if (file_is_ng_script(filename))
|
||||||
|
return FALSE;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resolve the real filesystem path of the file ngspice already opened: `fp` was
|
||||||
|
* located via `sourcepath`/inputdir, whereas `filename` may be a bare relative
|
||||||
|
* name that does NOT exist in the process cwd -- which is exactly the case when a
|
||||||
|
* *ng_script control deck does `source foo.net` with `set sourcepath = (dir)`.
|
||||||
|
* ngparse must read the file ngspice found, not re-open the relative name, so we
|
||||||
|
* read the open descriptor's path from /proc. Falls back to a copy of `filename`
|
||||||
|
* if that is unavailable (an internal deck with no fp, or a non-Linux host); the
|
||||||
|
* caller then behaves exactly as before. Returned string is owned by the caller. */
|
||||||
|
char *ngparse_glue_realpath(FILE *fp, const char *filename)
|
||||||
|
{
|
||||||
|
/* A NULL filename is the caller opting out (internal/array deck, or a
|
||||||
|
* multi-file `source` concatenated into a temp): keep it NULL so the glue
|
||||||
|
* skips, exactly as before. Only resolve a real, named file. */
|
||||||
|
if (!filename)
|
||||||
|
return NULL;
|
||||||
|
if (fp) {
|
||||||
|
char proc[64];
|
||||||
|
char buf[PATH_MAX];
|
||||||
|
snprintf(proc, sizeof proc, "/proc/self/fd/%d", fileno(fp));
|
||||||
|
ssize_t n = readlink(proc, buf, sizeof buf - 1);
|
||||||
|
if (n > 0) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
/* Trust it ONLY if it names a real, readable file. In batch mode
|
||||||
|
* ngspice slurps the top-level deck into an UNLINKED temp, whose
|
||||||
|
* descriptor reads back as "/tmp/#NNN (deleted)" -- not openable, and
|
||||||
|
* handing it to ngparse would break the *ng_script check and the
|
||||||
|
* expand. Fall back to `filename` (the pre-resolution behavior) then;
|
||||||
|
* the real win is the `source foo.net` case, where fp IS the file
|
||||||
|
* ngspice found via sourcepath and access() succeeds. */
|
||||||
|
if (access(buf, R_OK) == 0)
|
||||||
|
return copy(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy(filename ? filename : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
char *ngparse_glue_expand(const char *filename)
|
||||||
|
{
|
||||||
|
NgpDeck *deck;
|
||||||
|
size_t i, n, drops;
|
||||||
|
char *tmp_path;
|
||||||
|
FILE *out;
|
||||||
|
|
||||||
|
deck = ngparse_expand_file(filename, ngparse_glue_cores(), ngparse_glue_compat());
|
||||||
|
if (!deck) {
|
||||||
|
const char *err = ngparse_last_error();
|
||||||
|
fprintf(stderr, "ngparse: %s\n", err ? err : "expansion failed");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
n = ngparse_deck_len(deck);
|
||||||
|
if (n == 0) {
|
||||||
|
fprintf(stderr, "ngparse: %s expanded to an empty deck\n", filename);
|
||||||
|
ngparse_deck_free(deck);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A dropped parameter is never harmless: the device or model silently falls
|
||||||
|
* back to its DEFAULT, which converges to a wrong answer rather than
|
||||||
|
* erroring. Report every one -- do not let the user find out from a bad
|
||||||
|
* waveform. */
|
||||||
|
drops = ngparse_deck_drop_count(deck);
|
||||||
|
if (drops > 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"ngparse: WARNING: %zu parameter(s) could not be resolved and were dropped;\n"
|
||||||
|
" the affected device/model falls back to its DEFAULT value:\n",
|
||||||
|
drops);
|
||||||
|
for (i = 0; i < drops && i < 10; i++)
|
||||||
|
fprintf(stderr, " %s\n", ngparse_deck_drop(deck, i));
|
||||||
|
if (drops > 10)
|
||||||
|
fprintf(stderr, " ... and %zu more\n", drops - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* smktemp() is ngspice's portable temp-name helper (see com_xgraph.c). */
|
||||||
|
tmp_path = smktemp("ngp");
|
||||||
|
out = fopen(tmp_path, "w");
|
||||||
|
if (!out) {
|
||||||
|
fprintf(stderr, "ngparse: cannot write the temporary deck %s: %s\n",
|
||||||
|
tmp_path, strerror(errno));
|
||||||
|
tfree(tmp_path);
|
||||||
|
ngparse_deck_free(deck);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card 0 is the TITLE, and is written first: whatever reads this deck back
|
||||||
|
* consumes line 1 as the title and starts the netlist at line 2. Dropping
|
||||||
|
* it would silently eat the first real card. */
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
const char *card = ngparse_deck_card(deck, i);
|
||||||
|
if (fprintf(out, "%s\n", card ? card : "") < 0) {
|
||||||
|
fprintf(stderr, "ngparse: writing the temporary deck failed: %s\n",
|
||||||
|
strerror(errno));
|
||||||
|
fclose(out);
|
||||||
|
remove(tmp_path);
|
||||||
|
tfree(tmp_path);
|
||||||
|
ngparse_deck_free(deck);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fclose(out) != 0) {
|
||||||
|
fprintf(stderr, "ngparse: closing the temporary deck failed: %s\n",
|
||||||
|
strerror(errno));
|
||||||
|
remove(tmp_path);
|
||||||
|
tfree(tmp_path);
|
||||||
|
ngparse_deck_free(deck);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ft_ngdebug)
|
||||||
|
fprintf(stdout, "ngparse: %s -> %s (%zu cards, %zu drops)\n",
|
||||||
|
filename, tmp_path, n, drops);
|
||||||
|
|
||||||
|
ngparse_deck_free(deck);
|
||||||
|
return tmp_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* !USE_NGPARSE */
|
||||||
|
|
||||||
|
void ngparse_glue_request(bool on)
|
||||||
|
{
|
||||||
|
/* --no-ngparse (on == FALSE) asks for the old parser, which is all this
|
||||||
|
* build has, so say nothing. Only an explicit --ngparse is worth a word. */
|
||||||
|
if (on)
|
||||||
|
fprintf(stderr,
|
||||||
|
"ngspice: --ngparse: this build has no ngparse support "
|
||||||
|
"(configure with --enable-ngparse); parsing normally.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ngparse_glue_enabled(bool comfile, bool intfile, const char *filename)
|
||||||
|
{
|
||||||
|
NG_IGNORE(comfile);
|
||||||
|
NG_IGNORE(intfile);
|
||||||
|
NG_IGNORE(filename);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *ngparse_glue_expand(const char *filename)
|
||||||
|
{
|
||||||
|
NG_IGNORE(filename);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *ngparse_glue_realpath(FILE *fp, const char *filename)
|
||||||
|
{
|
||||||
|
NG_IGNORE(fp);
|
||||||
|
NG_IGNORE(filename);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* USE_NGPARSE */
|
||||||
|
|
@ -749,7 +749,7 @@ nupa_eval(struct card *card)
|
||||||
} else if (c == 'B') { /* substitute braces line */
|
} else if (c == 'B') { /* substitute braces line */
|
||||||
/* nupa_substitute() may reallocate line buffer. */
|
/* nupa_substitute() may reallocate line buffer. */
|
||||||
|
|
||||||
/* HSPICE-style OSDI model cards (Samsung 14LPU et al.) embed
|
/* HSPICE-style OSDI model cards (foundry_b 14LPU et al.) embed
|
||||||
* `{...}` expressions on the right-hand side of `.model`
|
* `{...}` expressions on the right-hand side of `.model`
|
||||||
* params that reference per-instance geometry symbols (`l`,
|
* params that reference per-instance geometry symbols (`l`,
|
||||||
* `w`, `nf`, `m`, `xnf`). These cannot be evaluated at
|
* `w`, `nf`, `m`, `xnf`). These cannot be evaluated at
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ static Table *load_table_file(const char *path) {
|
||||||
* a malloc'd absolute path the caller must free, or a copy of the
|
* a malloc'd absolute path the caller must free, or a copy of the
|
||||||
* input if no search is needed.
|
* input if no search is needed.
|
||||||
*
|
*
|
||||||
* Foundry PDKs (Samsung 14LPU) reference tables by paths relative to
|
* Foundry PDKs (foundry_b 14LPU) reference tables by paths relative to
|
||||||
* the .lib file that uses them — `./RF_COMPONENTS/foo.table` is
|
* the .lib file that uses them — `./RF_COMPONENTS/foo.table` is
|
||||||
* relative to the directory of fets_rf.lib (or wherever the call
|
* relative to the directory of fets_rf.lib (or wherever the call
|
||||||
* originated), NOT to the user's cwd or ngspice's sourcepath.
|
* originated), NOT to the user's cwd or ngspice's sourcepath.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* HSPICE table_param() implementation.
|
* HSPICE table_param() implementation.
|
||||||
*
|
*
|
||||||
* Foundry PDKs (Samsung 14LPU, TSMC, GF) use HSPICE's table_param()
|
* Foundry PDKs (foundry_b 14LPU, foundry_a, GF) use HSPICE's table_param()
|
||||||
* extensively for table-file-based parameter lookup — self-heating
|
* extensively for table-file-based parameter lookup — self-heating
|
||||||
* thermal resistance, RF parasitics, etc. Samsung's TT corner alone
|
* thermal resistance, RF parasitics, etc. foundry_b's TT corner alone
|
||||||
* references it ~1300 times.
|
* references it ~1300 times.
|
||||||
*
|
*
|
||||||
* Syntax:
|
* Syntax:
|
||||||
|
|
|
||||||
|
|
@ -1210,7 +1210,7 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror)
|
||||||
* `vec`, `min`, `max`, `pow`, `table_param`). Only treat
|
* `vec`, `min`, `max`, `pow`, `table_param`). Only treat
|
||||||
* it as a function call if it's followed by `(` (after
|
* it as a function call if it's followed by `(` (after
|
||||||
* optional whitespace). Otherwise fall back to treating
|
* optional whitespace). Otherwise fall back to treating
|
||||||
* it as a parameter name — foundry decks (GF55 bcd55
|
* it as a parameter name — foundry decks (foundry_c bcd55
|
||||||
* diode_rr.inc) use `var` as a subckt parameter, and
|
* diode_rr.inc) use `var` as a subckt parameter, and
|
||||||
* shadowing it with the built-in function broke
|
* shadowing it with the built-in function broke
|
||||||
* `vrb='var'` and similar chains. */
|
* `vrb='var'` and similar chains. */
|
||||||
|
|
@ -1260,7 +1260,7 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror)
|
||||||
/* Symmetric to the `c == '-'` case above: a unary `+` directly
|
/* Symmetric to the `c == '-'` case above: a unary `+` directly
|
||||||
* following a binary operator (e.g. `0.67*+2e-8`) is a no-op
|
* following a binary operator (e.g. `0.67*+2e-8`) is a no-op
|
||||||
* sign — drop it and re-read the next token. Foundry decks
|
* sign — drop it and re-read the next token. Foundry decks
|
||||||
* (GF55 bcd55 fixed_corner_bcdlite.inc) use this idiom for
|
* (foundry_c bcd55 fixed_corner_bcdlite.inc) use this idiom for
|
||||||
* explicit-sign literals: `(sw5)*(0.67*+2e-8)`. Without this,
|
* explicit-sign literals: `(sw5)*(0.67*+2e-8)`. Without this,
|
||||||
* the unary-plus form failed with "Misplaced operator" while
|
* the unary-plus form failed with "Misplaced operator" while
|
||||||
* the unary-minus form parsed fine. */
|
* the unary-minus form parsed fine. */
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ inp_subcktexpand(struct card *deck) {
|
||||||
* name (typically the 5th token, after the 4 node terminals) names
|
* name (typically the 5th token, after the 4 node terminals) names
|
||||||
* the type. ngspice's subckt expander only handles the .subckt
|
* the type. ngspice's subckt expander only handles the .subckt
|
||||||
* case; if no .subckt matches, we'd error here. But foundry PDKs
|
* case; if no .subckt matches, we'd error here. But foundry PDKs
|
||||||
* (Samsung 14LPU, TSMC, GF) routinely embed VA-module diagnostic
|
* (foundry_b 14LPU, foundry_a, GF) routinely embed VA-module diagnostic
|
||||||
* instances like `xesd_monitor d g s b esd_nfet_monitor ...`
|
* instances like `xesd_monitor d g s b esd_nfet_monitor ...`
|
||||||
* inside their FET subckts, expecting HSPICE-style dispatch.
|
* inside their FET subckts, expecting HSPICE-style dispatch.
|
||||||
*
|
*
|
||||||
|
|
@ -605,7 +605,7 @@ get_model_bins(char *curr_line, float *fwmin, float *fwmax,
|
||||||
parameter. HSPICE applies a subcircuit's `scale` parameter as the
|
parameter. HSPICE applies a subcircuit's `scale` parameter as the
|
||||||
element scale factor for the MOSFETs inside it, multiplying their
|
element scale factor for the MOSFETs inside it, multiplying their
|
||||||
W/L before the model bins and simulates. Foundry MOS macro subckts
|
W/L before the model bins and simulates. Foundry MOS macro subckts
|
||||||
rely on this (e.g. TSMC `nch_mac ... scale='scale_mos'`). Detected
|
rely on this (e.g. foundry_a `nch_mac ... scale='scale_mos'`). Detected
|
||||||
by a word-boundary "scale" immediately followed (after optional
|
by a word-boundary "scale" immediately followed (after optional
|
||||||
whitespace) by '=', so names like `l_scale=` / `noscale=` don't
|
whitespace) by '=', so names like `l_scale=` / `noscale=` don't
|
||||||
match. */
|
match. */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* ngparse glue — use the ngparse Rust parser for deck expansion.
|
||||||
|
*
|
||||||
|
* ngparse replaces the slow part of the frontend: .lib/.inc section extraction,
|
||||||
|
* numparam substitution and .subckt expansion. On the foundry_b 14LPU PDK deck
|
||||||
|
* that takes model load from 15m27s to ~2s.
|
||||||
|
*
|
||||||
|
* The glue is deliberately shallow. ngparse expands the top deck into a flat,
|
||||||
|
* resolved netlist; inp_readall() is then run over THAT, so every compatibility
|
||||||
|
* pass it performs (inp_compat, inp_bsource_compat, inp_dot_if,
|
||||||
|
* inp_temper_compat, inp_meas_control, inp_add_series_resistor, renumbering)
|
||||||
|
* still happens, and inp_subcktexpand() still drives numparam over whatever
|
||||||
|
* expressions ngparse deliberately left symbolic (anything depending on
|
||||||
|
* `temper`, v(), i(), time, ...). .lib/.inc expansion simply finds nothing left
|
||||||
|
* to do. Nothing downstream of expansion changes.
|
||||||
|
*
|
||||||
|
* Opt-in, and OFF by default:
|
||||||
|
*
|
||||||
|
* ngspice deck.cir parses as it always has
|
||||||
|
* ngspice --ngparse deck.cir uses ngparse (single core)
|
||||||
|
* ngparse_cores=4 ngspice --ngparse deck.cir asks for 4 cores
|
||||||
|
*
|
||||||
|
* ngparse_cores asks for more than one core; there is no reason to set
|
||||||
|
* it to 1, which is the default. It is a forward-compatibility hook: expansion
|
||||||
|
* is single-threaded today and says so if asked for more.
|
||||||
|
*/
|
||||||
|
#ifndef NGPARSE_GLUE_H
|
||||||
|
#define NGPARSE_GLUE_H
|
||||||
|
|
||||||
|
#include <stdio.h> /* FILE (ngparse_glue_realpath) */
|
||||||
|
|
||||||
|
#include "ngspice/bool.h"
|
||||||
|
|
||||||
|
/* Turn ngparse on/off. Called by main.c for `--ngparse`; off by default. */
|
||||||
|
void ngparse_glue_request(bool on);
|
||||||
|
|
||||||
|
/* TRUE if ngparse was requested AND can handle this source. Command files
|
||||||
|
* (*ng_script) are .control scripts, not netlists, and an internal/array deck
|
||||||
|
* has no file to read, so both take the normal path. */
|
||||||
|
bool ngparse_glue_enabled(bool comfile, bool intfile, const char *filename);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Expand `filename` with ngparse into a temporary netlist.
|
||||||
|
*
|
||||||
|
* Returns a malloc'd path the caller must unlink() and tfree(), or NULL on
|
||||||
|
* failure (reported to stderr). The caller opens it and passes it to
|
||||||
|
* inp_readall() in place of the original file.
|
||||||
|
*
|
||||||
|
* A temporary file rather than an in-memory hand-off because it reproduces
|
||||||
|
* exactly the `source <expanded deck>` path that ngparse is validated against;
|
||||||
|
* the write/read costs a few ms against a ~2s load.
|
||||||
|
*/
|
||||||
|
char *ngparse_glue_expand(const char *filename);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resolve the real path of the file ngspice already opened (`fp`, located via
|
||||||
|
* sourcepath/inputdir) so ngparse reads THAT rather than re-opening the possibly
|
||||||
|
* relative `filename` against the process cwd. Returns a malloc'd path the caller
|
||||||
|
* must tfree(); falls back to a copy of `filename` when the fp path is
|
||||||
|
* unavailable. Pass the result to ngparse_glue_enabled()/ngparse_glue_expand().
|
||||||
|
*/
|
||||||
|
char *ngparse_glue_realpath(FILE *fp, const char *filename);
|
||||||
|
|
||||||
|
#endif /* NGPARSE_GLUE_H */
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* OSDI deferred-evaluation side table.
|
* OSDI deferred-evaluation side table.
|
||||||
*
|
*
|
||||||
* HSPICE-style PDK model cards (Samsung 14LPU et al.) embed expressions
|
* HSPICE-style PDK model cards (foundry_b 14LPU et al.) embed expressions
|
||||||
* like
|
* like
|
||||||
* cgbn={((l<=1e-07)*(1e-012)+(l>1e-07)*(...))}
|
* cgbn={((l<=1e-07)*(1e-012)+(l>1e-07)*(...))}
|
||||||
* directly on the right-hand side of `.model` parameters. The expression
|
* directly on the right-hand side of `.model` parameters. The expression
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
* `snap_*` capture the subckt-instance scope visible at register time.
|
* `snap_*` capture the subckt-instance scope visible at register time.
|
||||||
* HSPICE-style PDKs put `.model` cards INSIDE a `.subckt` body and let
|
* HSPICE-style PDKs put `.model` cards INSIDE a `.subckt` body and let
|
||||||
* the model's expressions reference subckt-scope `.params` (e.g.
|
* the model's expressions reference subckt-scope `.params` (e.g.
|
||||||
* Samsung 14LPU's `vsat1=...*(1+vsat_nfet/...)*velsat_mult` where
|
* foundry_b 14LPU's `vsat1=...*(1+vsat_nfet/...)*velsat_mult` where
|
||||||
* `vsat_nfet`/`velsat_mult`/`xl_nfet` are passed to the subckt via
|
* `vsat_nfet`/`velsat_mult`/`xl_nfet` are passed to the subckt via
|
||||||
* its `params:` list). By register time the subckt scope has been
|
* its `params:` list). By register time the subckt scope has been
|
||||||
* resolved per-instance (subckt expansion produced model names like
|
* resolved per-instance (subckt expansion produced model names like
|
||||||
|
|
@ -90,7 +90,7 @@ void osdi_defer_clear(void);
|
||||||
* INPgetModBin once the model line's lmin/lmax tokens have been
|
* INPgetModBin once the model line's lmin/lmax tokens have been
|
||||||
* parsed, so that OSDIsetup's pre-eval pass can pick a default
|
* parsed, so that OSDIsetup's pre-eval pass can pick a default
|
||||||
* L within the bin's range (midpoint). Without this, default
|
* L within the bin's range (midpoint). Without this, default
|
||||||
* L=30nm makes Samsung-PDK expressions like
|
* L=30nm makes foundry_b-PDK expressions like
|
||||||
* `vsat1='(l==14n)*X + (l==16n)*Y'` evaluate to 0 and BSIM-CMG
|
* `vsat1='(l==14n)*X + (l==16n)*Y'` evaluate to 0 and BSIM-CMG
|
||||||
* rejects "vsat1 = 0". Matched against the runtime model name
|
* rejects "vsat1 = 0". Matched against the runtime model name
|
||||||
* (after subckt-path prefix is stripped). */
|
* (after subckt-path prefix is stripped). */
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ int SMPcAddCol(SMPmatrix *Matrix, int Accum_Col, int Addend_Col);
|
||||||
int SMPzeroRow(SMPmatrix *Matrix, int Row);
|
int SMPzeroRow(SMPmatrix *Matrix, int Row);
|
||||||
void SMPconstMult(SMPmatrix *, double);
|
void SMPconstMult(SMPmatrix *, double);
|
||||||
void SMPmultiply(SMPmatrix *, double *, double *, double *, double *);
|
void SMPmultiply(SMPmatrix *, double *, double *, double *, double *);
|
||||||
|
void SMPmultiplyAbs(SMPmatrix *, double *, double *, double *, double *);
|
||||||
|
|
||||||
#ifdef CIDER
|
#ifdef CIDER
|
||||||
void SMPcSolveForCIDER (SMPmatrix *, double [], double [], double [], double []) ;
|
void SMPcSolveForCIDER (SMPmatrix *, double [], double [], double [], double []) ;
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,7 @@ extern void spConstMult(MatrixPtr, double);
|
||||||
extern void spDeterminant ( MatrixPtr, int*, spREAL*, spREAL* );
|
extern void spDeterminant ( MatrixPtr, int*, spREAL*, spREAL* );
|
||||||
extern int spFileVector( MatrixPtr, char * , spREAL*, spREAL*);
|
extern int spFileVector( MatrixPtr, char * , spREAL*, spREAL*);
|
||||||
extern void spMultiply( MatrixPtr, spREAL*, spREAL*, spREAL*, spREAL* );
|
extern void spMultiply( MatrixPtr, spREAL*, spREAL*, spREAL*, spREAL* );
|
||||||
|
extern void spMultiplyAbs( MatrixPtr, spREAL*, spREAL*, spREAL*, spREAL* );
|
||||||
extern void spMultTransposed(MatrixPtr,spREAL*,spREAL*,spREAL*,spREAL*);
|
extern void spMultTransposed(MatrixPtr,spREAL*,spREAL*,spREAL*,spREAL*);
|
||||||
extern void spSolve( MatrixPtr, spREAL*, spREAL*, spREAL*, spREAL* );
|
extern void spSolve( MatrixPtr, spREAL*, spREAL*, spREAL*, spREAL* );
|
||||||
extern void spSolveTransposed(MatrixPtr,spREAL*,spREAL*,spREAL*,spREAL*);
|
extern void spSolveTransposed(MatrixPtr,spREAL*,spREAL*,spREAL*,spREAL*);
|
||||||
|
|
|
||||||
23
src/main.c
23
src/main.c
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ngspice/ngspice.h"
|
#include "ngspice/ngspice.h"
|
||||||
|
#include "ngspice/ngparse_glue.h"
|
||||||
#include "ngspice/const.h"
|
#include "ngspice/const.h"
|
||||||
#include "ngspice/dstring.h"
|
#include "ngspice/dstring.h"
|
||||||
|
|
||||||
|
|
@ -751,6 +752,11 @@ show_help(void)
|
||||||
" -p, --pipe run in I/O pipe mode\n"
|
" -p, --pipe run in I/O pipe mode\n"
|
||||||
" -r, --rawfile=FILE set the rawfile output\n"
|
" -r, --rawfile=FILE set the rawfile output\n"
|
||||||
" --soa-log=FILE set the outputfile for SOA warnings\n"
|
" --soa-log=FILE set the outputfile for SOA warnings\n"
|
||||||
|
#ifdef USE_NGPARSE
|
||||||
|
/* This build has the ngparse expander, and uses it by default. */
|
||||||
|
" --no-ngparse parse decks with ngspice's own parser\n"
|
||||||
|
" instead of ngparse\n"
|
||||||
|
#endif
|
||||||
" -s, --server run spice as a server process\n"
|
" -s, --server run spice as a server process\n"
|
||||||
" -t, --term=TERM set the terminal type\n"
|
" -t, --term=TERM set the terminal type\n"
|
||||||
" -h, --help display this help and exit\n"
|
" -h, --help display this help and exit\n"
|
||||||
|
|
@ -948,7 +954,7 @@ int main(int argc, char **argv)
|
||||||
|
|
||||||
/* --- Process command line options --- */
|
/* --- Process command line options --- */
|
||||||
for (;;) {
|
for (;;) {
|
||||||
enum { soa_log = 1001, };
|
enum { soa_log = 1001, ngparse_opt = 1002, no_ngparse_opt = 1003, };
|
||||||
|
|
||||||
static struct option long_options[] = {
|
static struct option long_options[] = {
|
||||||
{"define", required_argument, NULL, 'D'},
|
{"define", required_argument, NULL, 'D'},
|
||||||
|
|
@ -968,6 +974,8 @@ int main(int argc, char **argv)
|
||||||
{"server", no_argument, NULL, 's'},
|
{"server", no_argument, NULL, 's'},
|
||||||
{"terminal", required_argument, NULL, 't'},
|
{"terminal", required_argument, NULL, 't'},
|
||||||
{"soa-log", required_argument, NULL, soa_log},
|
{"soa-log", required_argument, NULL, soa_log},
|
||||||
|
{"ngparse", no_argument, NULL, ngparse_opt},
|
||||||
|
{"no-ngparse", no_argument, NULL, no_ngparse_opt},
|
||||||
{NULL, 0, NULL, 0}
|
{NULL, 0, NULL, 0}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1105,6 +1113,19 @@ int main(int argc, char **argv)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ngparse_opt:
|
||||||
|
/* Accepted and redundant in an --enable-ngparse build, where the
|
||||||
|
* expander is already on; kept so it can be written explicitly, and
|
||||||
|
* so it can say something useful in a build without ngparse. */
|
||||||
|
ngparse_glue_request(TRUE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case no_ngparse_opt:
|
||||||
|
/* Fall back to ngspice's own parser for this run -- the escape
|
||||||
|
* hatch if a deck ever trips ngparse up, no rebuild needed. */
|
||||||
|
ngparse_glue_request(FALSE);
|
||||||
|
break;
|
||||||
|
|
||||||
case '?':
|
case '?':
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2030,3 +2030,25 @@ SMPmultiply (SMPmatrix *Matrix, double *RHS, double *Solution, double *iRHS, dou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SMPmultiplyAbs()
|
||||||
|
* Real-only: RHS_n = sum_j G_nj*x_j, AbsRHS_n = sum_j |G_nj*x_j|,
|
||||||
|
* AbsRow_n = sum_j |G_nj|.
|
||||||
|
* Sparse-mode only — the axis-4 residual check in NIiter skips KLU mode,
|
||||||
|
* so the KLU branch just zeroes the outputs.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
SMPmultiplyAbs (SMPmatrix *Matrix, double *RHS, double *AbsRHS, double *AbsRow,
|
||||||
|
double *Solution)
|
||||||
|
{
|
||||||
|
if (Matrix->CKTkluMODE)
|
||||||
|
{
|
||||||
|
size_t i, n = (size_t) Matrix->SMPkluMatrix->KLUmatrixN ;
|
||||||
|
for (i = 0 ; i <= n ; i++)
|
||||||
|
RHS [i] = AbsRHS [i] = AbsRow [i] = 0.0 ;
|
||||||
|
NG_IGNORE (Solution) ;
|
||||||
|
} else {
|
||||||
|
spMultiplyAbs (Matrix->SPmatrix, RHS, AbsRHS, AbsRow, Solution) ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,71 @@ NIiter(CKTcircuit *ckt, int maxIter)
|
||||||
return (E_ITERLIM);
|
return (E_ITERLIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Axis 4 — row-relative KCL residual convergence check.
|
||||||
|
* f = G*x - b is computed here, in the load->factor window
|
||||||
|
* where the matrix still holds the device Jacobian, and each
|
||||||
|
* row is compared against its own current scale
|
||||||
|
* s_n = sum_j |G_nj*x_j| + |b_n| (plus the gmin-stepping
|
||||||
|
* diagonal term for rows that receive it, so continuation
|
||||||
|
* systems are measured against what is actually solved):
|
||||||
|
*
|
||||||
|
* |f_n| <= 10*reltol * s_n + abstol
|
||||||
|
*
|
||||||
|
* The row-relative form is what makes this enforceable where
|
||||||
|
* the earlier absolute-norm attempt was not: lenient SPICE3
|
||||||
|
* |dx|-only accepts that real PDK operating points depend on
|
||||||
|
* carry roundoff-scale RELATIVE residuals and pass untouched,
|
||||||
|
* while a false solution (e.g. a multi-million-fin OSDI
|
||||||
|
* device whose hundreds-of-siemens row satisfies |dx|<vntol
|
||||||
|
* long before KCL holds — dynamic gmin stepping was observed
|
||||||
|
* "completing" 1.6 V off the rail at high temperature) shows
|
||||||
|
* an O(1) relative residual and is sent back for further
|
||||||
|
* Newton iterations instead of being accepted. Threshold
|
||||||
|
* 10*reltol: KCL must close to within an order of magnitude
|
||||||
|
* of the voltage-convergence precision — dimensionless, no
|
||||||
|
* voltage-range or technology assumptions. NIconvTest gates
|
||||||
|
* on CKTresidConverged (niconv.c). Sparse-matrix path only;
|
||||||
|
* `.option noresidcheck` opts out. */
|
||||||
|
ckt->CKTresidConverged = 1;
|
||||||
|
#ifdef KLU
|
||||||
|
if (!ckt->CKTresidCheckDisabled && !ckt->CKTmatrix->CKTkluMODE)
|
||||||
|
#else
|
||||||
|
if (!ckt->CKTresidCheckDisabled)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
int rn, rsize = SMPmatSize(ckt->CKTmatrix);
|
||||||
|
double *rf = TMALLOC(double, (size_t) rsize + 1);
|
||||||
|
double *rs = TMALLOC(double, (size_t) rsize + 1);
|
||||||
|
double *rg = TMALLOC(double, (size_t) rsize + 1);
|
||||||
|
double rtol = 10.0 * ckt->CKTreltol;
|
||||||
|
double rvtol = 10.0 * ckt->CKTvoltTol;
|
||||||
|
char *rskip = ckt->CKTmatrix->gmin_skip;
|
||||||
|
SMPmultiplyAbs(ckt->CKTmatrix, rf, rs, rg, ckt->CKTrhsOld);
|
||||||
|
for (rn = 1; rn <= rsize; rn++) {
|
||||||
|
double gterm = (rskip && rskip[rn]) ? 0.0 :
|
||||||
|
ckt->CKTdiagGmin * ckt->CKTrhsOld[rn];
|
||||||
|
double fr = rf[rn] + gterm - ckt->CKTrhs[rn];
|
||||||
|
/* Tolerance = what a solution within per-unknown
|
||||||
|
* tolerance could produce on this row: the relative
|
||||||
|
* part scales with the row's own current magnitude,
|
||||||
|
* the row-norm part (|G|_1 * vntol) is the attainable
|
||||||
|
* precision of a stiff row — an LU solve cannot push
|
||||||
|
* a row residual below rownorm * solution-roundoff,
|
||||||
|
* and demanding it burns iterations at breakpoint
|
||||||
|
* corners where row values pass through zero. */
|
||||||
|
double tol_ = rtol * (rs[rn] + fabs(gterm) +
|
||||||
|
fabs(ckt->CKTrhs[rn]))
|
||||||
|
+ rvtol * rg[rn] + ckt->CKTabstol;
|
||||||
|
if (fabs(fr) > tol_) {
|
||||||
|
ckt->CKTresidConverged = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FREE(rf);
|
||||||
|
FREE(rs);
|
||||||
|
FREE(rg);
|
||||||
|
}
|
||||||
|
|
||||||
/* printf("after loading, before solving\n"); */
|
/* printf("after loading, before solving\n"); */
|
||||||
/* CKTdump(ckt); */
|
/* CKTdump(ckt); */
|
||||||
|
|
||||||
|
|
@ -322,18 +387,9 @@ NIiter(CKTcircuit *ckt, int maxIter)
|
||||||
memcpy(OldCKTstate0, ckt->CKTstate0,
|
memcpy(OldCKTstate0, ckt->CKTstate0,
|
||||||
(size_t) ckt->CKTnumStates * sizeof(double));
|
(size_t) ckt->CKTnumStates * sizeof(double));
|
||||||
|
|
||||||
/* Axis 4 placeholder — the |f|-magnitude (residual-norm) half
|
/* Axis 4: CKTresidConverged was computed in the load->factor
|
||||||
* of dual-norm convergence stays disabled. A correct true-KCL
|
* window above (row-relative KCL residual); by this point the
|
||||||
* residual check (f = G*x - b via SMPmultiply in the pre-factor
|
* matrix is factored and no longer holds the Jacobian. */
|
||||||
* window) was implemented and verified CORRECT, but enforcing it
|
|
||||||
* by default over-rejects real PDK operating points that rely on
|
|
||||||
* ngspice's lenient SPICE3 |dx|-only convergence (TSMC22 OP
|
|
||||||
* diverged, Samsung slowed badly) while a clean circuit (0.9V
|
|
||||||
* inverter) was byte-identical. So treat residual as always-
|
|
||||||
* passed; NIconvTest gates on |dx| only. The CKTresidConverged
|
|
||||||
* / CKTresidCheckDisabled fields + niconv.c gate + `.option
|
|
||||||
* noresidcheck` plumbing remain for a future opt-in form. */
|
|
||||||
ckt->CKTresidConverged = 1;
|
|
||||||
|
|
||||||
startTime = SPfrontEnd->IFseconds();
|
startTime = SPfrontEnd->IFseconds();
|
||||||
SMPsolve(ckt->CKTmatrix, ckt->CKTrhs, ckt->CKTrhsSpare);
|
SMPsolve(ckt->CKTmatrix, ckt->CKTrhs, ckt->CKTrhsSpare);
|
||||||
|
|
@ -403,7 +459,7 @@ NIiter(CKTcircuit *ckt, int maxIter)
|
||||||
* Stagnation = current max|Δv| not dropping by at
|
* Stagnation = current max|Δv| not dropping by at
|
||||||
* least 30% from prev iteration. Unconditional
|
* least 30% from prev iteration. Unconditional
|
||||||
* halving past iter 3 was preventing convergence
|
* halving past iter 3 was preventing convergence
|
||||||
* on the TSMC22 ULP driver_lv_2v5_tb VSN node: the
|
* on the foundry_a ULP driver_lv_2v5_tb VSN node: the
|
||||||
* inductor-coupled supply (L1 = 2.7 nH) naturally
|
* inductor-coupled supply (L1 = 2.7 nH) naturally
|
||||||
* needs ~100 mV swings to track each switching
|
* needs ~100 mV swings to track each switching
|
||||||
* transition, but dv_max would collapse to 8 mV by
|
* transition, but dv_max would collapse to 8 mV by
|
||||||
|
|
|
||||||
|
|
@ -625,3 +625,15 @@ SMPmultiply(SMPmatrix *Matrix, double *RHS, double *Solution, double *iRHS, doub
|
||||||
{
|
{
|
||||||
spMultiply(Matrix->SPmatrix, RHS, Solution, iRHS, iSolution);
|
spMultiply(Matrix->SPmatrix, RHS, Solution, iRHS, iSolution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SMPmultiplyAbs()
|
||||||
|
* Real-only: RHS_n = sum_j G_nj*x_j, AbsRHS_n = sum_j |G_nj*x_j|,
|
||||||
|
* AbsRow_n = sum_j |G_nj|.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
SMPmultiplyAbs(SMPmatrix *Matrix, double *RHS, double *AbsRHS, double *AbsRow,
|
||||||
|
double *Solution)
|
||||||
|
{
|
||||||
|
spMultiplyAbs(Matrix->SPmatrix, RHS, AbsRHS, AbsRow, Solution);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -611,6 +611,70 @@ spMultiply(MatrixPtr Matrix, RealVector RHS, RealVector Solution,
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MATRIX MULTIPLICATION WITH ROW MAGNITUDES
|
||||||
|
*
|
||||||
|
* Real-only companion to spMultiply: computes, in a single traversal,
|
||||||
|
* the product RHS_n = sum_j G_nj * x_j, the row magnitude
|
||||||
|
* AbsRHS_n = sum_j |G_nj * x_j|, and the row 1-norm
|
||||||
|
* AbsRow_n = sum_j |G_nj|. The magnitudes give each row's natural
|
||||||
|
* current scale and its attainable precision (row norm times the
|
||||||
|
* per-unknown solution tolerance), so a caller can form a
|
||||||
|
* dimensionless residual test — the basis of the axis-4 KCL residual
|
||||||
|
* convergence check. Assumes the matrix is real and not factored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
void
|
||||||
|
spMultiplyAbs(MatrixPtr Matrix, RealVector RHS, RealVector AbsRHS,
|
||||||
|
RealVector AbsRow, RealVector Solution)
|
||||||
|
{
|
||||||
|
ElementPtr pElement;
|
||||||
|
RealVector Vector;
|
||||||
|
RealNumber Sum, AbsSum, RowSum, Term;
|
||||||
|
int I, *pExtOrder;
|
||||||
|
|
||||||
|
/* Begin `spMultiplyAbs'. */
|
||||||
|
/* Note: Matrix->Complex may still carry its creation-time value before
|
||||||
|
* the first factor calls spSetReal; each element's Real field is the
|
||||||
|
* stamped real value either way, so only Factored matters here. */
|
||||||
|
assert( IS_SPARSE( Matrix ) && !Matrix->Factored );
|
||||||
|
if (!Matrix->RowsLinked)
|
||||||
|
spcLinkRows(Matrix);
|
||||||
|
if (!Matrix->InternalVectorsAllocated)
|
||||||
|
spcCreateInternalVectors( Matrix );
|
||||||
|
|
||||||
|
/* Initialize Intermediate vector with reordered Solution vector. */
|
||||||
|
Vector = Matrix->Intermediate;
|
||||||
|
pExtOrder = &Matrix->IntToExtColMap[Matrix->Size];
|
||||||
|
for (I = Matrix->Size; I > 0; I--)
|
||||||
|
Vector[I] = Solution[*(pExtOrder--)];
|
||||||
|
|
||||||
|
pExtOrder = &Matrix->IntToExtRowMap[Matrix->Size];
|
||||||
|
for (I = Matrix->Size; I > 0; I--)
|
||||||
|
{
|
||||||
|
pElement = Matrix->FirstInRow[I];
|
||||||
|
Sum = 0.0;
|
||||||
|
AbsSum = 0.0;
|
||||||
|
RowSum = 0.0;
|
||||||
|
|
||||||
|
while (pElement != NULL)
|
||||||
|
{
|
||||||
|
Term = pElement->Real * Vector[pElement->Col];
|
||||||
|
Sum += Term;
|
||||||
|
AbsSum += ABS(Term);
|
||||||
|
RowSum += ABS(pElement->Real);
|
||||||
|
pElement = pElement->NextInRow;
|
||||||
|
}
|
||||||
|
RHS[*pExtOrder] = Sum;
|
||||||
|
AbsRHS[*pExtOrder] = AbsSum;
|
||||||
|
AbsRow[*pExtOrder--] = RowSum;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
#endif /* MULTIPLICATION */
|
#endif /* MULTIPLICATION */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ static int is_ident_cont(int c) {
|
||||||
*
|
*
|
||||||
* Reserved set:
|
* Reserved set:
|
||||||
* l, w, nf, m, xnf — standard HSPICE instance params
|
* l, w, nf, m, xnf — standard HSPICE instance params
|
||||||
* l_calc — Samsung/foundry "computed length" (= l + p_la
|
* l_calc — foundry_b/foundry "computed length" (= l + p_la
|
||||||
* in the foundry subckt; defaults to l alone) */
|
* in the foundry subckt; defaults to l alone) */
|
||||||
static bool expr_refs_instance_geom(const char *expr) {
|
static bool expr_refs_instance_geom(const char *expr) {
|
||||||
static const char *const RES[] = {
|
static const char *const RES[] = {
|
||||||
|
|
|
||||||
|
|
@ -338,12 +338,12 @@ int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt,
|
||||||
* ranges and would otherwise reject the `0` placeholders that
|
* ranges and would otherwise reject the `0` placeholders that
|
||||||
* osdi_defer_preprocess_line writes onto deferred-param slots.
|
* osdi_defer_preprocess_line writes onto deferred-param slots.
|
||||||
*
|
*
|
||||||
* Per-model default L: BSIM-CMG/Samsung 14LPU expressions look like
|
* Per-model default L: BSIM-CMG/foundry_b 14LPU expressions look like
|
||||||
* vsat1 = '((l==0.014e-6)*X + (l==0.016e-6)*Y) * ...'
|
* vsat1 = '((l==0.014e-6)*X + (l==0.016e-6)*Y) * ...'
|
||||||
* For these to evaluate to a NON-ZERO valid value at pre-eval time
|
* For these to evaluate to a NON-ZERO valid value at pre-eval time
|
||||||
* (so setup_model accepts), the chosen L must satisfy at least one
|
* (so setup_model accepts), the chosen L must satisfy at least one
|
||||||
* of the `(l==...)` checks. Read the model's lmin/lmax (already
|
* of the `(l==...)` checks. Read the model's lmin/lmax (already
|
||||||
* populated by the .model parse) and pick the midpoint. Samsung's
|
* populated by the .model parse) and pick the midpoint. foundry_b's
|
||||||
* 14LPU nfet.0 has lmin=10nm, lmax=18nm → midpoint=14nm, exactly
|
* 14LPU nfet.0 has lmin=10nm, lmax=18nm → midpoint=14nm, exactly
|
||||||
* the L the expression looks for. For models without lmin/lmax,
|
* the L the expression looks for. For models without lmin/lmax,
|
||||||
* fall back to 30nm (the original constant default). */
|
* fall back to 30nm (the original constant default). */
|
||||||
|
|
@ -528,7 +528,7 @@ extern int OSDItemp(GENmodel *inModel, CKTcircuit *ckt) {
|
||||||
* setup_instance so the model sees the actual computed values.
|
* setup_instance so the model sees the actual computed values.
|
||||||
* Skipped entirely (no map lookup, no syscalls) when this
|
* Skipped entirely (no map lookup, no syscalls) when this
|
||||||
* model has no deferred entries — the common case for non-
|
* model has no deferred entries — the common case for non-
|
||||||
* HSPICE-style OSDI PDKs like TSMC22 BSIM-BULK. */
|
* HSPICE-style OSDI PDKs like foundry_a BSIM-BULK. */
|
||||||
if (has_deferred) {
|
if (has_deferred) {
|
||||||
defer_apply_ctx ctx = {
|
defer_apply_ctx ctx = {
|
||||||
.descr = descr,
|
.descr = descr,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) {
|
||||||
* gradually.
|
* gradually.
|
||||||
*
|
*
|
||||||
* Was 2.0× originally, tightened to 1.5× for pinb/net_7 oscillation
|
* Was 2.0× originally, tightened to 1.5× for pinb/net_7 oscillation
|
||||||
* (commit 143a0805f) and now to 1.2× for TSMC22 ULP driver_lv_2v5_tb
|
* (commit 143a0805f) and now to 1.2× for foundry_a ULP driver_lv_2v5_tb
|
||||||
* to address residual pinb failure at t ≈ 1.369 µs. At 1.5×, dt
|
* to address residual pinb failure at t ≈ 1.369 µs. At 1.5×, dt
|
||||||
* could grow from ~10 ps post-breakpoint to ~5 ns (the user's max
|
* could grow from ~10 ps post-breakpoint to ~5 ns (the user's max
|
||||||
* step) in ~15 accepted steps; once Newton had to land a 0.5 V swing
|
* step) in ~15 accepted steps; once Newton had to land a 0.5 V swing
|
||||||
|
|
|
||||||
|
|
@ -695,7 +695,7 @@ resume:
|
||||||
* non-uniform x-spacings produces a nonsensical slope at
|
* non-uniform x-spacings produces a nonsensical slope at
|
||||||
* the extrapolation end of the fit window, and Newton then
|
* the extrapolation end of the fit window, and Newton then
|
||||||
* has to undo a 0.5-1 V starting offset on the affected
|
* has to undo a 0.5-1 V starting offset on the affected
|
||||||
* nodes within itl4 iterations. Observed on TSMC22 ULP
|
* nodes within itl4 iterations. Observed on foundry_a ULP
|
||||||
* driver_lv_2v5_tb: VSN (an L1-coupled supply driving
|
* driver_lv_2v5_tb: VSN (an L1-coupled supply driving
|
||||||
* 500-finger BSIM-BULK drivers) catches a ~-0.7 V predictor
|
* 500-finger BSIM-BULK drivers) catches a ~-0.7 V predictor
|
||||||
* over-shoot and Newton can't undo it inside Stage A's
|
* over-shoot and Newton can't undo it inside Stage A's
|
||||||
|
|
@ -762,6 +762,22 @@ resume:
|
||||||
/* If no convergence in Central solver step */
|
/* If no convergence in Central solver step */
|
||||||
if(converged != 0) {
|
if(converged != 0) {
|
||||||
|
|
||||||
|
/* A nonconverged attempt returns with CKTstate0 still holding
|
||||||
|
* the last failed Newton iterate's device states (limiter
|
||||||
|
* voltage history, charge states, OSDI LimitState slots) —
|
||||||
|
* NIiter does not restore them. The retry then evaluates
|
||||||
|
* devices against that garbage reference, lands further from
|
||||||
|
* the solution, and fails worse; the contamination compounds
|
||||||
|
* geometrically across retries at a single timepoint
|
||||||
|
* (observed: v6#branch −32 A → −58 → −310 → 3e10 A over ~25
|
||||||
|
* retries) until the state explodes and dt collapses to
|
||||||
|
* delmin. Re-prime the working state from the last ACCEPTED
|
||||||
|
* point so every retry starts from physical values, exactly
|
||||||
|
* like a first attempt does. */
|
||||||
|
if (ckt->CKTstate0 && ckt->CKTstate1)
|
||||||
|
memcpy(ckt->CKTstate0, ckt->CKTstate1,
|
||||||
|
(size_t) ckt->CKTnumStates * sizeof(double));
|
||||||
|
|
||||||
#ifndef SHARED_MODULE
|
#ifndef SHARED_MODULE
|
||||||
ckt->CKTtime = ckt->CKTtime -ckt->CKTdelta;
|
ckt->CKTtime = ckt->CKTtime -ckt->CKTdelta;
|
||||||
ckt->CKTstat->STATrejected ++;
|
ckt->CKTstat->STATrejected ++;
|
||||||
|
|
|
||||||
|
|
@ -672,7 +672,7 @@ DCtrCurv(CKTcircuit *ckt, int restart)
|
||||||
* On the LAST accepted iteration (the one whose NEXT
|
* On the LAST accepted iteration (the one whose NEXT
|
||||||
* would trigger the stop check), snap cur_val to stop
|
* would trigger the stop check), snap cur_val to stop
|
||||||
* exactly so the saved row's X-value is bit-exact.
|
* exactly so the saved row's X-value is bit-exact.
|
||||||
* GF55 bcd55 isoednfet's `.measure when v(n2)=3.3`
|
* foundry_c bcd55 isoednfet's `.measure when v(n2)=3.3`
|
||||||
* relies on this. */
|
* relies on this. */
|
||||||
job->TRCVstepCount[i]++;
|
job->TRCVstepCount[i]++;
|
||||||
double cur_val = job->TRCVvStart[i] +
|
double cur_val = job->TRCVvStart[i] +
|
||||||
|
|
|
||||||
|
|
@ -1991,6 +1991,12 @@ line755: /* standard entry if HSMHVevaluate is bypassed */
|
||||||
/* Preset vectors and matrix for dynamic part */
|
/* Preset vectors and matrix for dynamic part */
|
||||||
|
|
||||||
cq_d = cq_dP = cq_g = cq_gP = cq_s = cq_sP = cq_bP = cq_b = cq_db = cq_sb = cq_t = cq_qi = cq_qb = 0.0 ;
|
cq_d = cq_dP = cq_g = cq_gP = cq_s = cq_sP = cq_bP = cq_b = cq_db = cq_sb = cq_t = cq_qi = cq_qb = 0.0 ;
|
||||||
|
/* Reset the external-charge displacement currents per instance too:
|
||||||
|
* they are function-scope locals only ever added to below, so without
|
||||||
|
* this each instance inherits the accumulated cq_gE/cq_bE/cq_sE of
|
||||||
|
* all previously processed instances of the same model (same defect
|
||||||
|
* as in hisimhv2/hsmhv2ld.c). */
|
||||||
|
cq_dE = cq_gE = cq_sE = cq_bE = 0.0 ;
|
||||||
for (i = 0; i < XDIM ; i++) {
|
for (i = 0; i < XDIM ; i++) {
|
||||||
ydyn_d[i] = ydyn_dP[i] = ydyn_g[i] = ydyn_gP[i] = ydyn_s[i] = ydyn_sP[i] = ydyn_bP[i] = ydyn_b[i]
|
ydyn_d[i] = ydyn_dP[i] = ydyn_g[i] = ydyn_gP[i] = ydyn_s[i] = ydyn_sP[i] = ydyn_bP[i] = ydyn_b[i]
|
||||||
= ydyn_db[i] = ydyn_sb[i] = ydyn_t[i] = 0.0;
|
= ydyn_db[i] = ydyn_sb[i] = ydyn_t[i] = 0.0;
|
||||||
|
|
|
||||||
|
|
@ -651,7 +651,7 @@ int HSMHV2load(
|
||||||
|
|
||||||
/* NaN-recovery (sanity damping).
|
/* NaN-recovery (sanity damping).
|
||||||
*
|
*
|
||||||
* Symptom: on big PDKs (Samsung 14LPU 3.3 V LDMOS as `ld3nfet`)
|
* Symptom: on big PDKs (foundry_b 14LPU 3.3 V LDMOS as `ld3nfet`)
|
||||||
* the initial DC operating-point Newton iteration occasionally
|
* the initial DC operating-point Newton iteration occasionally
|
||||||
* lands on bias values that make the HiSIM_HV physics produce
|
* lands on bias values that make the HiSIM_HV physics produce
|
||||||
* NaN currents/conductances. Those NaN entries are written
|
* NaN currents/conductances. Those NaN entries are written
|
||||||
|
|
@ -2320,6 +2320,18 @@ line755: /* standard entry if HSMHV2evaluate is bypassed */
|
||||||
/* Preset vectors and matrix for dynamic part */
|
/* Preset vectors and matrix for dynamic part */
|
||||||
|
|
||||||
cq_d = cq_dP = cq_g = cq_gP = cq_s = cq_sP = cq_bP = cq_b = cq_db = cq_sb = cq_t = cq_qi = cq_qb = 0.0 ;
|
cq_d = cq_dP = cq_g = cq_gP = cq_s = cq_sP = cq_bP = cq_b = cq_db = cq_sb = cq_t = cq_qi = cq_qb = 0.0 ;
|
||||||
|
/* The external-charge displacement currents must be reset per instance
|
||||||
|
* as well: they are function-scope locals that are only ever added to
|
||||||
|
* below, so without this reset each instance inherits the accumulated
|
||||||
|
* cq_gE/cq_bE/cq_sE of all previously processed instances of the same
|
||||||
|
* model -- a leaked gate/bulk current with no matrix counterpart that
|
||||||
|
* grows with the instance count and (at the huge ag0 of the small
|
||||||
|
* opening transient timesteps) walks parallel power devices away from
|
||||||
|
* the operating point until the model's internal solvers blow up.
|
||||||
|
* (cq_dE is re-read from the state vector further below, but is
|
||||||
|
* included here for symmetry and for the !ChargeComputationNeeded
|
||||||
|
* path.) */
|
||||||
|
cq_dE = cq_gE = cq_sE = cq_bE = 0.0 ;
|
||||||
for (i = 0; i < XDIM ; i++) {
|
for (i = 0; i < XDIM ; i++) {
|
||||||
ydyn_d[i] = ydyn_dP[i] = ydyn_g[i] = ydyn_gP[i] = ydyn_s[i] = ydyn_sP[i] = ydyn_bP[i] = ydyn_b[i]
|
ydyn_d[i] = ydyn_dP[i] = ydyn_g[i] = ydyn_gP[i] = ydyn_s[i] = ydyn_sP[i] = ydyn_bP[i] = ydyn_b[i]
|
||||||
= ydyn_db[i] = ydyn_sb[i] = ydyn_t[i] = 0.0;
|
= ydyn_db[i] = ydyn_sb[i] = ydyn_t[i] = 0.0;
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ INPdevParse(char **line, CKTcircuit *ckt, int dev, GENinstance *fast,
|
||||||
goto quit;
|
goto quit;
|
||||||
}
|
}
|
||||||
/* OSDI models may receive extra instance parameters from PDK
|
/* OSDI models may receive extra instance parameters from PDK
|
||||||
* subcircuits (e.g. 'total' from TSMC nch_mac) that the model
|
* subcircuits (e.g. 'total' from foundry_a nch_mac) that the model
|
||||||
* does not define. Skip the parameter and its value rather than
|
* does not define. Skip the parameter and its value rather than
|
||||||
* aborting; this matches HSPICE/Spectre behaviour. */
|
* aborting; this matches HSPICE/Spectre behaviour. */
|
||||||
if (device->registry_entry != NULL) {
|
if (device->registry_entry != NULL) {
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
|
||||||
|
|
||||||
*model = NULL;
|
*model = NULL;
|
||||||
|
|
||||||
/* Read L (required) and W (optional). FinFET PDKs (Samsung 14LPU
|
/* Read L (required) and W (optional). FinFET PDKs (foundry_b 14LPU
|
||||||
* et al.) have no W on the instance line — only `l=` and
|
* et al.) have no W on the instance line — only `l=` and
|
||||||
* `nfin=` — and their `.model` cards bin on `lmin/lmax/nfinmin/
|
* `nfin=` — and their `.model` cards bin on `lmin/lmax/nfinmin/
|
||||||
* nfinmax`, not on W. Previously this function bailed out as
|
* nfinmax`, not on W. Previously this function bailed out as
|
||||||
|
|
@ -348,8 +348,8 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
|
||||||
/* OSDI bin selection: compare per-finger W (w / nf, already applied
|
/* OSDI bin selection: compare per-finger W (w / nf, already applied
|
||||||
* above) against the bin's wmin/wmax. Do NOT divide by m — `m` is a
|
* above) against the bin's wmin/wmax. Do NOT divide by m — `m` is a
|
||||||
* parallel-instance multiplier (the whole device is replicated m
|
* parallel-instance multiplier (the whole device is replicated m
|
||||||
* times), so the per-finger geometry is unchanged by it. TSMC and
|
* times), so the per-finger geometry is unchanged by it. foundry_a and
|
||||||
* GlobalFoundries BSIM-BULK decks author their wmin/wmax tables
|
* foundry_c BSIM-BULK decks author their wmin/wmax tables
|
||||||
* against the HSPICE/Spectre convention of per-finger W only, so an
|
* against the HSPICE/Spectre convention of per-finger W only, so an
|
||||||
* additional /m here would push the effective W below every bin's
|
* additional /m here would push the effective W below every bin's
|
||||||
* lower bound for any moderately-large multi-instance device. */
|
* lower bound for any moderately-large multi-instance device. */
|
||||||
|
|
@ -399,9 +399,9 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compare the per-finger W (already w/nf above) and L against
|
/* Compare the per-finger W (already w/nf above) and L against
|
||||||
* the bin's wmin/wmax and lmin/lmax. Foundry .lib files (TSMC,
|
* the bin's wmin/wmax and lmin/lmax. Foundry .lib files (foundry_a,
|
||||||
* GlobalFoundries, Samsung) author bin boundaries in POST-SHRINK
|
* foundry_c, foundry_b) author bin boundaries in POST-SHRINK
|
||||||
* (EFFECTIVE) dimensions: e.g. TSMC 22nm ULP nch.1 wmax=2.5651µm
|
* (EFFECTIVE) dimensions: e.g. foundry_a 22nm ULP nch.1 wmax=2.5651µm
|
||||||
* = drawn 3µm × shrink 0.855. The shrink is applied upstream in
|
* = drawn 3µm × shrink 0.855. The shrink is applied upstream in
|
||||||
* subckt expansion (the subcircuit's `scale` parameter multiplies
|
* subckt expansion (the subcircuit's `scale` parameter multiplies
|
||||||
* the device W/L) or by the model's own dimension expressions, so
|
* the device W/L) or by the model's own dimension expressions, so
|
||||||
|
|
@ -425,7 +425,7 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
|
||||||
*model = modtmp;
|
*model = modtmp;
|
||||||
/* Stash the bin's (lmin, lmax) for OSDIsetup's deferred-eval
|
/* Stash the bin's (lmin, lmax) for OSDIsetup's deferred-eval
|
||||||
* pre-pass, which needs a representative L within the bin's
|
* pre-pass, which needs a representative L within the bin's
|
||||||
* range to make Samsung-PDK expressions like
|
* range to make foundry_b-PDK expressions like
|
||||||
* `(l==14n)*X + (l==16n)*Y` evaluate to non-zero. */
|
* `(l==14n)*X + (l==16n)*Y` evaluate to non-zero. */
|
||||||
if (is_osdi)
|
if (is_osdi)
|
||||||
osdi_defer_record_bin_range(modtmp->INPmodName, lmin, lmax);
|
osdi_defer_record_bin_range(modtmp->INPmodName, lmin, lmax);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue