/* osdi_rescodegen.c  --  general behavioral-resistor -> C-OSDI code generator.
 *
 * Takes a behavioral 2-terminal current expression  I = f(v(n1,n2), temper)
 * (as produced by the R b_transformation, inpcom.c), reuses ngspice's OWN
 * expression parser (INPgetTree) and symbolic differentiator (PTdifferentiate,
 * exposed through INPparseTree.derivs[]) to obtain I and dI/dv, and emits a
 * standalone C source implementing the OSDI v0.3 device ABI. That C is then
 * compiled with the system gcc into a .osdi and loaded via load_osdi() -- the
 * productized, OpenVAF-free path.
 *
 * Milestone 1: the emitter core + a hidden `osdigen` command for testing.
 * Constants are emitted as literals (one model per exact expression); the
 * structural-dedup / per-instance-parameter step comes next.
 */

#include "ngspice/ngspice.h"
#include "ngspice/cpdefs.h"
#include "ngspice/ftedefs.h"
#include "ngspice/inpdefs.h"
#include "ngspice/inpptree.h"
#include "ngspice/ifsim.h"
#include "ngspice/cktdefs.h"
#include "ngspice/devdefs.h"           /* SPICEdev (for dev.h) */
#include "com_dl.h"
#include "ngspice/hash.h"               /* node-degree census for dangling caps */
#include "../spicelib/devices/dev.h"   /* load_osdi */

/* ---- constant-leaf -> instance-param map (M2) ----
 * Collected from the ORIGINAL tree in DFS order. Each distinct PT_CONSTANT
 * *node* (by pointer) gets a param slot p0..pk. The differentiator SHARES the
 * original subtrees, so those same const nodes appearing in derivs map to the
 * SAME slot; constants that PTdifferentiate *creates* (e.g. the 2 in a power
 * rule) are not in the map and emit as literals -- they are structural, equal
 * for every instance sharing the signature. */
typedef struct { INPparseNode **node; double *val; int n, cap; } ConstMap;

/* A "constant leaf" is a PT_CONSTANT or a unary-minus wrapping a constant
 * (`-0.5` parses as uminus(const)). Canonicalizing the latter into a single
 * signed constant param removes sign from the STRUCTURE, so laws that differ
 * only in coefficient signs dedup to one model. */
static int const_leaf(INPparseNode *pn, double *v)
{
    if (!pn) return 0;
    if (pn->type == PT_CONSTANT) { if (v) *v = pn->constant; return 1; }
    if (pn->type == PT_FUNCTION && pn->funcnum == PTF_UMINUS &&
        pn->left && pn->left->type == PT_CONSTANT) { if (v) *v = -pn->left->constant; return 1; }
    return 0;
}
static int cm_find(ConstMap *m, INPparseNode *p)
{
    for (int i = 0; i < m->n; i++) if (m->node[i] == p) return i;
    return -1;
}
static int cm_add(ConstMap *m, INPparseNode *p, double val)
{
    int i = cm_find(m, p);
    if (i >= 0) return i;
    if (m->n == m->cap) { m->cap = m->cap ? m->cap * 2 : 16;
        m->node = TREALLOC(INPparseNode *, m->node, m->cap);
        m->val  = TREALLOC(double, m->val, m->cap); }
    m->node[m->n] = p; m->val[m->n] = val;
    return m->n++;
}
static void collect_consts(INPparseNode *pn, ConstMap *m)
{
    double v;
    if (!pn) return;
    if (const_leaf(pn, &v)) { cm_add(m, pn, v); return; }   /* leaf: don't recurse */
    collect_consts(pn->left, m);
    collect_consts(pn->right, m);
}

/* ---- emission context ---- */
typedef struct {
    IFparseTree *pt;     /* for numVars / varTypes / vars */
    ConstMap *cmap;      /* constant-node -> param slot */
    int idx_p, idx_n;    /* valueIndex of terminals n1, n2 (-1 if unused) */
    int ok;              /* cleared on an un-emittable node */
    const char *err;     /* reason, when ok==0 */
} EmitCtx;

/* forward */
static char *enode(INPparseNode *pn, EmitCtx *c);

/* structural signature: same string  <=>  same C model, params aside.
 * constants -> "#", terminal vars -> P/N, other node var -> X, temper -> T. */
static char *ssig(INPparseNode *pn, EmitCtx *c)
{
    if (!pn) return copy("_");
    if (const_leaf(pn, 0)) return copy("#");   /* const or uminus(const) */
    switch (pn->type) {
    case PT_CONSTANT: return copy("#");
    case PT_VAR:
        if (pn->valueIndex == c->idx_p) return copy("P");
        if (pn->valueIndex == c->idx_n) return copy("N");
        return copy("X");
    case PT_TEMPERATURE: return copy("T");
    case PT_TIME: return copy("t"); case PT_FREQUENCY: return copy("h");
    case PT_PLUS: case PT_MINUS: case PT_TIMES: case PT_DIVIDE: case PT_POWER: {
        char *l = ssig(pn->left, c), *r = ssig(pn->right, c);
        char *s = tprintf("(%d %s %s)", pn->type, l, r); tfree(l); tfree(r); return s; }
    case PT_FUNCTION: {
        char *a = ssig(pn->left, c);
        char *s = tprintf("f%d(%s)", pn->funcnum, a); tfree(a); return s; }
    case PT_COMMA: { char *l = ssig(pn->left, c), *r = ssig(pn->right, c);
        char *s = tprintf(",%s %s", l, r); tfree(l); tfree(r); return s; }
    case PT_TERN: { char *l = ssig(pn->left, c), *r = ssig(pn->right, c);
        char *s = tprintf("?%s %s", l, r); tfree(l); tfree(r); return s; }
    default: return copy("?"); }
}

static const char *unary_libm(int fn)
{
    switch (fn) {
    case PTF_ACOS:  return "acos";   case PTF_ACOSH: return "acosh";
    case PTF_ASIN:  return "asin";   case PTF_ASINH: return "asinh";
    case PTF_ATAN:  return "atan";   case PTF_ATANH: return "atanh";
    case PTF_COS:   return "cos";    case PTF_COSH:  return "cosh";
    case PTF_EXP:   return "exp";    case PTF_LOG:   return "log";
    case PTF_LOG10: return "log10";  case PTF_SIN:   return "sin";
    case PTF_SINH:  return "sinh";   case PTF_SQRT:  return "sqrt";
    case PTF_TAN:   return "tan";    case PTF_TANH:  return "tanh";
    case PTF_ABS:   return "fabs";   case PTF_CEIL:  return "ceil";
    case PTF_FLOOR: return "floor";
    default: return NULL;
    }
}

/* emit a PT_FUNCTION node */
static char *efunc(INPparseNode *pn, EmitCtx *c)
{
    const char *nm = unary_libm(pn->funcnum);
    if (nm) {
        char *a = enode(pn->left, c);
        char *s = tprintf("%s(%s)", nm, a);
        tfree(a);
        return s;
    }
    switch (pn->funcnum) {
    case PTF_UMINUS: { char *a = enode(pn->left, c);
        char *s = tprintf("(-(%s))", a); tfree(a); return s; }
    case PTF_SGN:    { char *a = enode(pn->left, c);
        char *s = tprintf("osdi_sgn(%s)", a); tfree(a); return s; }
    case PTF_USTEP:  { char *a = enode(pn->left, c);
        char *s = tprintf("osdi_ustep(%s)", a); tfree(a); return s; }
    case PTF_USTEP2: { char *a = enode(pn->left, c);
        char *s = tprintf("osdi_ustep2(%s)", a); tfree(a); return s; }
    case PTF_URAMP:  { char *a = enode(pn->left, c);
        char *s = tprintf("osdi_uramp(%s)", a); tfree(a); return s; }
    case PTF_NINT:   { char *a = enode(pn->left, c);
        char *s = tprintf("rint(%s)", a); tfree(a); return s; }
    case PTF_EQ0: case PTF_NE0: case PTF_GT0:
    case PTF_LT0: case PTF_GE0: case PTF_LE0: {
        const char *op = (pn->funcnum==PTF_EQ0)?"==":(pn->funcnum==PTF_NE0)?"!=":
                         (pn->funcnum==PTF_GT0)?">" :(pn->funcnum==PTF_LT0)?"<" :
                         (pn->funcnum==PTF_GE0)?">=":"<=";
        char *a = enode(pn->left, c);
        char *s = tprintf("((%s %s 0.0)?1.0:0.0)", a, op); tfree(a); return s; }
    /* 2-arg: argument is a PT_COMMA (left,right) */
    case PTF_POW: case PTF_PWR: case PTF_MIN: case PTF_MAX: {
        INPparseNode *cm = pn->left;
        if (!cm || cm->type != PT_COMMA) { c->ok = 0; c->err = "bad 2-arg function"; return copy("0.0"); }
        char *a = enode(cm->left, c), *b = enode(cm->right, c);
        const char *fn = (pn->funcnum==PTF_POW)?"pow":(pn->funcnum==PTF_PWR)?"osdi_pwr":
                         (pn->funcnum==PTF_MIN)?"fmin":"fmax";
        char *s = tprintf("%s(%s, %s)", fn, a, b); tfree(a); tfree(b); return s; }
    case PTF_PWL: case PTF_PWL_DERIVATIVE: case PTF_DDT:
        c->ok = 0; c->err = "pwl/ddt not statically emittable"; return copy("0.0");
    default:
        c->ok = 0; c->err = "unsupported function"; return copy("0.0");
    }
}

/* recursively emit a parse node as a C double-expression string (malloc'd) */
static char *enode(INPparseNode *pn, EmitCtx *c)
{
    if (!pn) { c->ok = 0; c->err = "null node"; return copy("0.0"); }
    { double cv;
      if (const_leaf(pn, &cv)) {                  /* const or uminus(const) */
        int k = c->cmap ? cm_find(c->cmap, pn) : -1;
        if (k >= 0) return tprintf("p%d", k);     /* original -> instance param */
        return tprintf("(%.17g)", cv);            /* diff-introduced -> literal */
      } }
    switch (pn->type) {
    case PT_CONSTANT: return tprintf("(%.17g)", pn->constant);
    case PT_VAR:
        if (pn->valueIndex == c->idx_p) return copy("vp");
        if (pn->valueIndex == c->idx_n) return copy("vn");
        c->ok = 0; c->err = "references v() of a non-terminal node"; return copy("0.0");
    case PT_TEMPERATURE: return copy("Tc");   /* Celsius, from setup_instance */
    case PT_TIME: case PT_FREQUENCY:
        c->ok = 0; c->err = "time/hertz not static for a resistor"; return copy("0.0");
    case PT_PLUS:  case PT_MINUS: case PT_TIMES: case PT_DIVIDE: {
        char op = (pn->type==PT_PLUS)?'+':(pn->type==PT_MINUS)?'-':(pn->type==PT_TIMES)?'*':'/';
        char *l = enode(pn->left, c), *r = enode(pn->right, c);
        char *s = tprintf("(%s %c %s)", l, op, r); tfree(l); tfree(r); return s; }
    case PT_POWER: {
        char *l = enode(pn->left, c), *r = enode(pn->right, c);
        char *s = tprintf("osdi_power(%s, %s)", l, r); tfree(l); tfree(r); return s; }
    case PT_TERN: {
        /* left = condition; right = PT_COMMA(then, else) */
        if (!pn->right || pn->right->type != PT_COMMA) { c->ok=0; c->err="bad ternary"; return copy("0.0"); }
        char *cd = enode(pn->left, c), *a = enode(pn->right->left, c), *b = enode(pn->right->right, c);
        char *s = tprintf("(((%s)!=0.0)?(%s):(%s))", cd, a, b);
        tfree(cd); tfree(a); tfree(b); return s; }
    case PT_FUNCTION: return efunc(pn, c);
    default:
        c->ok = 0; c->err = "unsupported node type"; return copy("0.0");
    }
}

/* helper C prelude emitted into every generated model */
static const char *PRELUDE =
"#include <math.h>\n#include <stddef.h>\n#include <stdint.h>\n#include <stdbool.h>\n"
"#include \"osdi.h\"\n"
"static double osdi_sgn(double x){return (x>0)-(x<0);}\n"
"static double osdi_ustep(double x){return x>0.0?1.0:0.0;}\n"
"static double osdi_ustep2(double x){return x<0.0?0.0:(x>1.0?1.0:x*x*(3.0-2.0*x));}\n"
"static double osdi_uramp(double x){return x>0.0?x:0.0;}\n"
"static double osdi_pwr(double a,double b){return osdi_sgn(a)*pow(fabs(a),b);}\n"
"static double osdi_power(double a,double b){return pow(fabs(a),b);}\n"
/* bypass: reuse the last operating point when both terminal voltages barely
 * changed (paras.vals[7]=reltol, [8]=vntol -- see get_simparams in osdiload.c) */
"static int osdi_bypass(const OsdiSimInfo*si,double vp,double vn,double vpp,double vnp){\n"
"  if(!(si->flags & ENABLE_BYPASS)) return 0;\n"
"  double rt=si->paras.vals[7], vt=si->paras.vals[8];\n"
"  double tp=vt+rt*(fabs(vp)>fabs(vpp)?fabs(vp):fabs(vpp));\n"
"  double tn=vt+rt*(fabs(vn)>fabs(vnp)?fabs(vn):fabs(vnp));\n"
"  return (fabs(vp-vpp)<tp)&&(fabs(vn-vnp)<tn);}\n";

/* Generate a full OSDI-v0.3 C model for I=f(v(n1,n2),temper) with `np`
 * per-instance constant params p0..p{np-1} (plus mfactor m).
 * Returns malloc'd C source; caller frees. */
static char *gen_model_c(const char *model, int np,
                         const char *cI, const char *cdp, const char *cdn)
{
    /* build the per-model fragments that depend on np */
    char *plocals = copy("");   /* double p0=i->p[0], ... */
    char *pnames  = copy("");   /* static char*pm_pN[]={"pN"}; */
    char *parr    = copy("");   /* OsdiParamOpvar rows */
    char *pacc    = copy("");   /* access() cases */
    for (int k = 0; k < np; k++) {
        char *t;
        t = tprintf("%s  double p%d=i->p[%d];\n", plocals, k, k); tfree(plocals); plocals = t;
        t = tprintf("%sstatic char*pm_p%d[]={\"p%d\"};\n", pnames, k, k); tfree(pnames); pnames = t;
        t = tprintf("%s ,{pm_p%d,0,\"c\",\"\",(PARA_KIND_INST|PARA_TY_REAL),0}", parr, k); tfree(parr); parr = t;
        t = tprintf("%s if(id==%d)return &i->p[%d];\n", pacc, k + 1, k); tfree(pacc); pacc = t;
    }
    int nparam = np + 1;   /* + mfactor */

    char *src = tprintf(
"%s"
"#define NN 2\n#define NJ 4\n#define NP %d\n"
"typedef struct { uint32_t map[NN]; double *jp[NJ]; double m, Tc; double p[NP>0?NP:1]; double resid[NN]; double jac[NJ]; double vpp,vnp; int bpv; } Inst;\n"
"typedef struct { double dummy; } Mod;\n"
"static void *acc(void *i_,void *m_,uint32_t id,uint32_t f){(void)m_;(void)f;Inst*i=i_;\n"
"  if(id==0)return &i->m;\n%s  return NULL;}\n"
"static void smod(void*h,void*m,OsdiSimParas*s,OsdiInitInfo*r){(void)h;(void)m;(void)s;r->flags=0;r->num_errors=0;r->errors=0;}\n"
"static void sinst(void*h,void*i_,void*m_,double T,uint32_t nt,OsdiSimParas*s,OsdiInitInfo*r){(void)h;(void)m_;(void)nt;(void)s;Inst*i=i_;if(i->m<=0.0)i->m=1.0;i->Tc=T-273.15;i->bpv=0;r->flags=0;r->num_errors=0;r->errors=0;}\n"
"static uint32_t ev(void*h,void*i_,const void*m_,const OsdiSimInfo*si){(void)h;(void)m_;Inst*i=i_;\n"
"  double vp=si->prev_solve[i->map[0]];\n"
"  double vn=si->prev_solve[i->map[1]];\n"
"  if(i->bpv && osdi_bypass(si,vp,vn,i->vpp,i->vnp)) return 0;\n"   /* reuse last op point */
"  double Tc=i->Tc; (void)Tc;\n"
"%s"
"  double I=%s;\n  double dIp=%s;\n  double dIn=%s;\n"
"  i->resid[0]=i->m*I; i->resid[1]=-i->m*I;\n"
"  i->jac[0]=i->m*dIp; i->jac[1]=i->m*dIn; i->jac[2]=-i->m*dIp; i->jac[3]=-i->m*dIn;\n"
"  i->vpp=vp; i->vnp=vn; i->bpv=1;\n"                              /* store linearization point */
"  return 0;}\n"
"static void ljr(void*i_,void*m_){(void)m_;Inst*i=i_;for(int k=0;k<NJ;k++)*(i->jp[k])+=i->jac[k];}\n"
"static void ljt(void*i_,void*m_,double a){(void)a;ljr(i_,m_);}\n"
"static void ljc(void*i_,void*m_,double a){(void)i_;(void)m_;(void)a;}\n"
/* RHS companion uses the STORED linearization voltages so bypass is exact */
"static void lrdc(void*i_,void*m_,double*d,double*ps){(void)m_;(void)ps;Inst*i=i_;uint32_t a=i->map[0],b=i->map[1];double vp=i->vpp,vn=i->vnp;\n"
"  d[a]+=i->jac[0]*vp+i->jac[1]*vn-i->resid[0]; d[b]+=i->jac[2]*vp+i->jac[3]*vn-i->resid[1];}\n"
"static void lrtr(void*i_,void*m_,double*d,double*ps,double a){(void)a;lrdc(i_,m_,d,ps);}\n"
"static void nz(void*i,void*m,double f,double*n){(void)i;(void)m;(void)f;(void)n;}\n"
"static void st2(void*i,void*m,double*d){(void)i;(void)m;(void)d;}\n"
"static char*pm_m[]={\"m\"};\n%s"
"static OsdiParamOpvar params[%d]={{pm_m,0,\"mfactor\",\"\",(PARA_KIND_INST|PARA_TY_REAL),0}%s};\n"
"static OsdiNode nodes[NN]={{\"p\",\"V\",\"A\",offsetof(Inst,resid),UINT32_MAX,UINT32_MAX,UINT32_MAX,false},\n"
"  {\"n\",\"V\",\"A\",offsetof(Inst,resid)+sizeof(double),UINT32_MAX,UINT32_MAX,UINT32_MAX,false}};\n"
"static OsdiJacobianEntry jed[NJ]={{{0,0},UINT32_MAX,JACOBIAN_ENTRY_RESIST},{{0,1},UINT32_MAX,JACOBIAN_ENTRY_RESIST},\n"
"  {{1,0},UINT32_MAX,JACOBIAN_ENTRY_RESIST},{{1,1},UINT32_MAX,JACOBIAN_ENTRY_RESIST}};\n"
"uint32_t OSDI_VERSION_MAJOR=0; uint32_t OSDI_VERSION_MINOR=3; uint32_t OSDI_NUM_DESCRIPTORS=1;\n"
"void (*osdi_log)(void*,char*,uint32_t)=0;\n"
"OsdiDescriptor OSDI_DESCRIPTORS[1]={{ .name=\"%s\",\n"
"  .num_nodes=NN,.num_terminals=NN,.nodes=nodes,.num_jacobian_entries=NJ,.jacobian_entries=jed,\n"
"  .num_collapsible=0,.collapsible=0,.collapsed_offset=0,.noise_sources=0,.num_noise_src=0,\n"
"  .num_params=%d,.num_instance_params=%d,.num_opvars=0,.param_opvar=params,\n"
"  .node_mapping_offset=offsetof(Inst,map),.jacobian_ptr_resist_offset=offsetof(Inst,jp),\n"
"  .num_states=0,.state_idx_off=0,.bound_step_offset=UINT32_MAX,\n"
"  .instance_size=sizeof(Inst),.model_size=sizeof(Mod),\n"
"  .access=acc,.setup_model=smod,.setup_instance=sinst,.eval=ev,.load_noise=nz,\n"
"  .load_residual_resist=st2,.load_residual_react=st2,.load_limit_rhs_resist=st2,.load_limit_rhs_react=st2,\n"
"  .load_spice_rhs_dc=lrdc,.load_spice_rhs_tran=lrtr,\n"
"  .load_jacobian_resist=ljr,.load_jacobian_react=ljc,.load_jacobian_tran=ljt }};\n",
        PRELUDE, np, pacc, plocals, cI, cdp, cdn,
        pnames, nparam, parr, model, nparam, nparam);
    tfree(plocals); tfree(pnames); tfree(parr); tfree(pacc);
    return src;
}

/* Generate an OSDI-v0.3 CAPACITOR (charge) device: the node CHARGE is Q=q(v),
 * emitted into the reactive residual slots; ngspice integrates it charge-
 * conservingly (NIintegrate) for dQ/dt -- no behavioral differentiator.
 * cQ = q(v), cdp = dQ/dvp, cdn = dQ/dvn (from PTdifferentiate). `m` scales charge. */
static char *gen_cap_model_c(const char *model, int np,
                             const char *cQ, const char *cdp, const char *cdn)
{
    char *plocals = copy(""), *pnames = copy(""), *parr = copy(""), *pacc = copy("");
    for (int k = 0; k < np; k++) {
        char *t;
        t = tprintf("%s  double p%d=i->p[%d];\n", plocals, k, k); tfree(plocals); plocals = t;
        t = tprintf("%sstatic char*pm_p%d[]={\"p%d\"};\n", pnames, k, k); tfree(pnames); pnames = t;
        t = tprintf("%s ,{pm_p%d,0,\"c\",\"\",(PARA_KIND_INST|PARA_TY_REAL),0}", parr, k); tfree(parr); parr = t;
        t = tprintf("%s if(id==%d)return &i->p[%d];\n", pacc, k + 1, k); tfree(pacc); pacc = t;
    }
    int nparam = np + 1;

    char *src = tprintf(
"%s"
"#define NN 2\n#define NJ 4\n#define NP %d\n"
/* NOTE: a pure-reactive (charge) device must NOT bypass -- freezing the charge
 * across timesteps corrupts dQ/dt integration (native caps don't bypass either). */
"typedef struct { uint32_t map[NN]; double *jp[NJ]; double *jpr[NJ]; double m, Tc;"
" double p[NP>0?NP:1]; double q[NN]; double jac[NJ]; } Inst;\n"
"typedef struct { double dummy; } Mod;\n"
"static void *acc(void *i_,void *m_,uint32_t id,uint32_t f){(void)m_;(void)f;Inst*i=i_;\n"
"  if(id==0)return &i->m;\n%s  return NULL;}\n"
"static void smod(void*h,void*m,OsdiSimParas*s,OsdiInitInfo*r){(void)h;(void)m;(void)s;r->flags=0;r->num_errors=0;r->errors=0;}\n"
"static void sinst(void*h,void*i_,void*m_,double T,uint32_t nt,OsdiSimParas*s,OsdiInitInfo*r){(void)h;(void)m_;(void)nt;(void)s;Inst*i=i_;if(i->m<=0.0)i->m=1.0;i->Tc=T-273.15;r->flags=0;r->num_errors=0;r->errors=0;}\n"
"static uint32_t ev(void*h,void*i_,const void*m_,const OsdiSimInfo*si){(void)h;(void)m_;(void)si;Inst*i=i_;\n"
"  double vp=si->prev_solve[i->map[0]];\n"
"  double vn=si->prev_solve[i->map[1]];\n"
"  double Tc=i->Tc; (void)Tc;\n"
"%s"
"  double Q=%s;\n  double dQp=%s;\n  double dQn=%s;\n"
"  i->q[0]=i->m*Q; i->q[1]=-i->m*Q;\n"
"  i->jac[0]=i->m*dQp; i->jac[1]=i->m*dQn; i->jac[2]=-i->m*dQp; i->jac[3]=-i->m*dQn;\n"
"  return 0;}\n"
/* tran: add alpha*dQ/dv into the real matrix; AC: into the react (imag) matrix */
"static void ljt(void*i_,void*m_,double a){(void)m_;Inst*i=i_;for(int k=0;k<NJ;k++)*(i->jp[k])+=a*i->jac[k];}\n"
"static void ljc(void*i_,void*m_,double a){(void)m_;Inst*i=i_;for(int k=0;k<NJ;k++)*(i->jpr[k])+=a*i->jac[k];}\n"
"static void ljr(void*i_,void*m_){(void)i_;(void)m_;}\n"   /* no resistive part */
"static void lrdc(void*i_,void*m_,double*d,double*ps){(void)i_;(void)m_;(void)d;(void)ps;}\n" /* cap open in DC */
"static void lrtr(void*i_,void*m_,double*d,double*ps,double a){(void)m_;Inst*i=i_;uint32_t p=i->map[0],n=i->map[1];double vp=ps[p],vn=ps[n];\n"
"  d[p]+=a*(i->jac[0]*vp+i->jac[1]*vn); d[n]+=a*(i->jac[2]*vp+i->jac[3]*vn);}\n"
"static void nz(void*i,void*m,double f,double*n){(void)i;(void)m;(void)f;(void)n;}\n"
"static void st2(void*i,void*m,double*d){(void)i;(void)m;(void)d;}\n"
"static char*pm_m[]={\"m\"};\n%s"
"static OsdiParamOpvar params[%d]={{pm_m,0,\"mfactor\",\"\",(PARA_KIND_INST|PARA_TY_REAL),0}%s};\n"
/* both terminals carry a node CHARGE (react residual); no resistive residual */
"static OsdiNode nodes[NN]={{\"p\",\"V\",\"C\",UINT32_MAX,offsetof(Inst,q),UINT32_MAX,UINT32_MAX,false},\n"
"  {\"n\",\"V\",\"C\",UINT32_MAX,offsetof(Inst,q)+sizeof(double),UINT32_MAX,UINT32_MAX,false}};\n"
/* reactive jacobian entries; react_ptr_off gives the per-entry AC (imag) pointer slot */
"static OsdiJacobianEntry jed[NJ]={{{0,0},offsetof(Inst,jpr),JACOBIAN_ENTRY_REACT},\n"
"  {{0,1},offsetof(Inst,jpr)+sizeof(double*),JACOBIAN_ENTRY_REACT},\n"
"  {{1,0},offsetof(Inst,jpr)+2*sizeof(double*),JACOBIAN_ENTRY_REACT},\n"
"  {{1,1},offsetof(Inst,jpr)+3*sizeof(double*),JACOBIAN_ENTRY_REACT}};\n"
"uint32_t OSDI_VERSION_MAJOR=0; uint32_t OSDI_VERSION_MINOR=3; uint32_t OSDI_NUM_DESCRIPTORS=1;\n"
"void (*osdi_log)(void*,char*,uint32_t)=0;\n"
"OsdiDescriptor OSDI_DESCRIPTORS[1]={{ .name=\"%s\",\n"
"  .num_nodes=NN,.num_terminals=NN,.nodes=nodes,.num_jacobian_entries=NJ,.jacobian_entries=jed,\n"
"  .num_collapsible=0,.collapsible=0,.collapsed_offset=0,.noise_sources=0,.num_noise_src=0,\n"
"  .num_params=%d,.num_instance_params=%d,.num_opvars=0,.param_opvar=params,\n"
"  .node_mapping_offset=offsetof(Inst,map),.jacobian_ptr_resist_offset=offsetof(Inst,jp),\n"
"  .num_states=0,.state_idx_off=0,.bound_step_offset=UINT32_MAX,\n"
"  .instance_size=sizeof(Inst),.model_size=sizeof(Mod),\n"
"  .access=acc,.setup_model=smod,.setup_instance=sinst,.eval=ev,.load_noise=nz,\n"
"  .load_residual_resist=st2,.load_residual_react=st2,.load_limit_rhs_resist=st2,.load_limit_rhs_react=st2,\n"
"  .load_spice_rhs_dc=lrdc,.load_spice_rhs_tran=lrtr,\n"
"  .load_jacobian_resist=ljr,.load_jacobian_react=ljc,.load_jacobian_tran=ljt }};\n",
        PRELUDE, np, pacc, plocals, cQ, cdp, cdn,
        pnames, nparam, parr, model, nparam, nparam);
    tfree(plocals); tfree(pnames); tfree(parr); tfree(pacc);
    return src;
}

/* GENERAL capacitance-form charge device: given an ARBITRARY capacitance
 * c(v,temper) (cC) and its derivative dc/dv (cdC), compute the charge
 * Q(V)=integral_0^V c(u)du by Gauss-Legendre QUADRATURE inside eval, and the
 * capacitance dQ/dV as the exact analytic derivative of that quadrature (so the
 * Jacobian is consistent and Newton converges). Handles any differentiable c
 * (tanh/exp/sqrt/temper/...), charge-conserving; exact for polynomials up to the
 * quadrature degree. This is the capacitor analog of the resistor's "any I=f(v)". */
static char *gen_cap_quad_model_c(const char *model, int np,
                                  const char *cC, const char *cdC)
{
    char *plocf = copy(""), *pnames = copy(""), *parr = copy(""), *pacc = copy("");
    for (int k = 0; k < np; k++) {
        char *t;
        t = tprintf("%s  double p%d=p[%d];\n", plocf, k, k); tfree(plocf); plocf = t;   /* for the c/dc helpers */
        t = tprintf("%sstatic char*pm_p%d[]={\"p%d\"};\n", pnames, k, k); tfree(pnames); pnames = t;
        t = tprintf("%s ,{pm_p%d,0,\"c\",\"\",(PARA_KIND_INST|PARA_TY_REAL),0}", parr, k); tfree(parr); parr = t;
        t = tprintf("%s if(id==%d)return &i->p[%d];\n", pacc, k + 1, k); tfree(pacc); pacc = t;
    }
    int nparam = np + 1;

    char *src = tprintf(
"%s"
"#define NN 2\n#define NJ 4\n#define NP %d\n#define NG 8\n"
"static const double GLx[NG]={-0.9602898564975363,-0.7966664774136267,-0.5255324099163290,-0.1834346424956498,"
"0.1834346424956498,0.5255324099163290,0.7966664774136267,0.9602898564975363};\n"
"static const double GLw[NG]={0.1012285362903763,0.2223810344533745,0.3137066458778873,0.3626837833783620,"
"0.3626837833783620,0.3137066458778873,0.2223810344533745,0.1012285362903763};\n"
/* pure-reactive: no bypass (see gen_cap_model_c note) */
"typedef struct { uint32_t map[NN]; double *jp[NJ]; double *jpr[NJ]; double m, Tc;"
" double p[NP>0?NP:1]; double q[NN]; double jac[NJ]; } Inst;\n"
"typedef struct { double dummy; } Mod;\n"
/* capacitance c(v,T) and dc/dv, as functions of the terminal voltages */
"static double capc(double vp,double vn,double Tc,const double*p){(void)Tc;\n%s  return %s;}\n"
"static double capdc(double vp,double vn,double Tc,const double*p){(void)Tc;\n%s  return %s;}\n"
"static void *acc(void *i_,void *m_,uint32_t id,uint32_t f){(void)m_;(void)f;Inst*i=i_;\n"
"  if(id==0)return &i->m;\n%s  return NULL;}\n"
"static void smod(void*h,void*m,OsdiSimParas*s,OsdiInitInfo*r){(void)h;(void)m;(void)s;r->flags=0;r->num_errors=0;r->errors=0;}\n"
"static void sinst(void*h,void*i_,void*m_,double T,uint32_t nt,OsdiSimParas*s,OsdiInitInfo*r){(void)h;(void)m_;(void)nt;(void)s;Inst*i=i_;if(i->m<=0.0)i->m=1.0;i->Tc=T-273.15;r->flags=0;r->num_errors=0;r->errors=0;}\n"
"static uint32_t ev(void*h,void*i_,const void*m_,const OsdiSimInfo*si){(void)h;(void)m_;(void)si;Inst*i=i_;\n"
"  double vp=si->prev_solve[i->map[0]];\n  double vn=si->prev_solve[i->map[1]];\n"
"  double V=vp-vn, Tc=i->Tc; double sumC=0.0,sumDC=0.0;\n"
"  for(int g=0;g<NG;g++){ double s=0.5*V*(1.0+GLx[g]);\n"
"    double c=capc(s,0.0,Tc,i->p), dc=capdc(s,0.0,Tc,i->p);\n"
"    sumC+=GLw[g]*c; sumDC+=GLw[g]*(1.0+GLx[g])*dc; }\n"
"  double Q=0.5*V*sumC;\n"                         /* integral_0^V c du */
"  double dQ=0.5*sumC+0.25*V*sumDC;\n"             /* exact d/dV of the quadrature */
"  i->q[0]=i->m*Q; i->q[1]=-i->m*Q;\n"
"  i->jac[0]=i->m*dQ; i->jac[1]=-i->m*dQ; i->jac[2]=-i->m*dQ; i->jac[3]=i->m*dQ;\n"
"  return 0;}\n"
"static void ljt(void*i_,void*m_,double a){(void)m_;Inst*i=i_;for(int k=0;k<NJ;k++)*(i->jp[k])+=a*i->jac[k];}\n"
"static void ljc(void*i_,void*m_,double a){(void)m_;Inst*i=i_;for(int k=0;k<NJ;k++)*(i->jpr[k])+=a*i->jac[k];}\n"
"static void ljr(void*i_,void*m_){(void)i_;(void)m_;}\n"
"static void lrdc(void*i_,void*m_,double*d,double*ps){(void)i_;(void)m_;(void)d;(void)ps;}\n"
"static void lrtr(void*i_,void*m_,double*d,double*ps,double a){(void)m_;Inst*i=i_;uint32_t p=i->map[0],n=i->map[1];double vp=ps[p],vn=ps[n];\n"
"  d[p]+=a*(i->jac[0]*vp+i->jac[1]*vn); d[n]+=a*(i->jac[2]*vp+i->jac[3]*vn);}\n"
"static void nz(void*i,void*m,double f,double*n){(void)i;(void)m;(void)f;(void)n;}\n"
"static void st2(void*i,void*m,double*d){(void)i;(void)m;(void)d;}\n"
"static char*pm_m[]={\"m\"};\n%s"
"static OsdiParamOpvar params[%d]={{pm_m,0,\"mfactor\",\"\",(PARA_KIND_INST|PARA_TY_REAL),0}%s};\n"
"static OsdiNode nodes[NN]={{\"p\",\"V\",\"C\",UINT32_MAX,offsetof(Inst,q),UINT32_MAX,UINT32_MAX,false},\n"
"  {\"n\",\"V\",\"C\",UINT32_MAX,offsetof(Inst,q)+sizeof(double),UINT32_MAX,UINT32_MAX,false}};\n"
"static OsdiJacobianEntry jed[NJ]={{{0,0},offsetof(Inst,jpr),JACOBIAN_ENTRY_REACT},\n"
"  {{0,1},offsetof(Inst,jpr)+sizeof(double*),JACOBIAN_ENTRY_REACT},\n"
"  {{1,0},offsetof(Inst,jpr)+2*sizeof(double*),JACOBIAN_ENTRY_REACT},\n"
"  {{1,1},offsetof(Inst,jpr)+3*sizeof(double*),JACOBIAN_ENTRY_REACT}};\n"
"uint32_t OSDI_VERSION_MAJOR=0; uint32_t OSDI_VERSION_MINOR=3; uint32_t OSDI_NUM_DESCRIPTORS=1;\n"
"void (*osdi_log)(void*,char*,uint32_t)=0;\n"
"OsdiDescriptor OSDI_DESCRIPTORS[1]={{ .name=\"%s\",\n"
"  .num_nodes=NN,.num_terminals=NN,.nodes=nodes,.num_jacobian_entries=NJ,.jacobian_entries=jed,\n"
"  .num_collapsible=0,.collapsible=0,.collapsed_offset=0,.noise_sources=0,.num_noise_src=0,\n"
"  .num_params=%d,.num_instance_params=%d,.num_opvars=0,.param_opvar=params,\n"
"  .node_mapping_offset=offsetof(Inst,map),.jacobian_ptr_resist_offset=offsetof(Inst,jp),\n"
"  .num_states=0,.state_idx_off=0,.bound_step_offset=UINT32_MAX,\n"
"  .instance_size=sizeof(Inst),.model_size=sizeof(Mod),\n"
"  .access=acc,.setup_model=smod,.setup_instance=sinst,.eval=ev,.load_noise=nz,\n"
"  .load_residual_resist=st2,.load_residual_react=st2,.load_limit_rhs_resist=st2,.load_limit_rhs_react=st2,\n"
"  .load_spice_rhs_dc=lrdc,.load_spice_rhs_tran=lrtr,\n"
"  .load_jacobian_resist=ljr,.load_jacobian_react=ljc,.load_jacobian_tran=ljt }};\n",
        PRELUDE, np, plocf, cC, plocf, cdC, pacc,
        pnames, nparam, parr, model, nparam, nparam);
    tfree(plocf); tfree(pnames); tfree(parr); tfree(pacc);
    return src;
}

/* result of the emitter core */
/* emit_core returns the emitted C expressions for value f(v) and its two
 * terminal derivatives, plus signature + per-instance constant values. The
 * caller wraps them into a resistor (current) or capacitor (charge) OSDI model. */
typedef struct { int rc; char *sig; char *cI, *cdp, *cdn; double *vals; int nvals; } EmitOut;

/* Core: parse `expr`, emit OSDI-C for model `model`. by_order=1 binds the two
 * terminals as the first two IF_NODE vars (pass mode, no ckt names); else match
 * vars[k].nValue->name to n1/n2 (command mode). Returns rc 0 on success with
 * csrc/sig/vals filled (caller frees), nonzero + reason on cp_err on failure. */
static EmitOut emit_core(const char *model, const char *n1, const char *n2,
                         const char *expr, CKTcircuit *ckt, INPtables *tab, int by_order)
{
    EmitOut out = { 1, NULL, NULL, NULL, NULL, NULL, 0 };
    INPparseTree *pt = NULL;
    char *ebuf = copy(expr), *eptr = ebuf;
    INPgetTree(&eptr, &pt, ckt, tab);
    tfree(ebuf);
    if (!pt || !pt->tree) { out.rc = 1; return out; }

    /* qualify: exactly 2 IF_NODE vars, no IF_INSTANCE (i()) vars */
    int nnode = 0, ninst = 0, first = -1, second = -1;
    for (int k = 0; k < pt->p.numVars; k++) {
        if (pt->p.varTypes[k] == IF_NODE) { if (nnode==0) first=k; else if (nnode==1) second=k; nnode++; }
        else if (pt->p.varTypes[k] == IF_INSTANCE) ninst++;
    }
    if (ninst || nnode != 2) { INPfreeTree((IFparseTree *) pt); out.rc = 5; return out; }

    ConstMap cmap = { NULL, NULL, 0, 0 };
    collect_consts(pt->tree, &cmap);
    EmitCtx c = { &pt->p, &cmap, -1, -1, 1, NULL };
    if (by_order) { c.idx_p = first; c.idx_n = second; }
    else for (int k = 0; k < pt->p.numVars; k++) {
        if (pt->p.varTypes[k] != IF_NODE) continue;
        CKTnode *nd = (CKTnode *) pt->p.vars[k].nValue;
        const char *nm = nd ? nd->name : NULL;
        if (nm && cieq((char *)nm, (char *)n1)) c.idx_p = k;
        else if (nm && cieq((char *)nm, (char *)n2)) c.idx_n = k;
    }
    if (c.idx_p < 0 || c.idx_n < 0) {
        tfree(cmap.node); tfree(cmap.val); INPfreeTree((IFparseTree *) pt); out.rc = 2; return out; }

    char *sig = ssig(pt->tree, &c);
    char *cI  = enode(pt->tree, &c);
    char *cdp = c.ok ? enode(pt->derivs[c.idx_p], &c) : copy("0.0");
    char *cdn = c.ok ? enode(pt->derivs[c.idx_n], &c) : copy("0.0");
    if (!c.ok) {
        if (c.err) fprintf(cp_err, "osdi-res: cannot emit (%s)\n", c.err);
        tfree(cI); tfree(cdp); tfree(cdn); tfree(sig);
        tfree(cmap.node); tfree(cmap.val); INPfreeTree((IFparseTree *) pt); out.rc = 3; return out; }

    (void)model;
    out.cI = cI; out.cdp = cdp; out.cdn = cdn;
    out.sig = sig; out.nvals = cmap.n;
    out.vals = TMALLOC(double, cmap.n > 0 ? cmap.n : 1);
    for (int k = 0; k < cmap.n; k++) out.vals[k] = cmap.val[k];
    out.rc = 0;
    tfree(cmap.node); tfree(cmap.val);
    INPfreeTree((IFparseTree *) pt);
    return out;
}
static void emit_free(EmitOut *e)
{ tfree(e->sig); tfree(e->cI); tfree(e->cdp); tfree(e->cdn); tfree(e->vals); }

int osdi_res_emit(const char *model, const char *n1, const char *n2,
                  const char *expr, const char *out_c)
{
    CKTcircuit *ckt = ft_curckt ? ft_curckt->ci_ckt : NULL;
    INPtables *tab  = ft_curckt ? (INPtables *) ft_curckt->ci_symtab : NULL;
    EmitOut e = emit_core(model, n1, n2, expr, ckt, tab, 0);
    if (e.rc) { fprintf(cp_err, "osdigen: failed (rc=%d) for '%s'\n", e.rc, expr); return e.rc; }
    char *csrc = gen_model_c(model, e.nvals, e.cI, e.cdp, e.cdn);
    FILE *f = fopen(out_c, "w");
    if (!f) { fprintf(cp_err, "osdigen: cannot write %s\n", out_c); }
    else { fputs(csrc, f); fclose(f);
        fprintf(cp_out, "osdigen: wrote %s  (model '%s', %d instance params)\n", out_c, model, e.nvals);
        fprintf(cp_out, "  signature: %s\n", e.sig);
        fprintf(cp_out, "  instance line:  N<name> %s %s %s m=1", n1, n2, model);
        for (int k = 0; k < e.nvals; k++) fprintf(cp_out, " p%d=%.17g", k, e.vals[k]);
        fprintf(cp_out, "\n"); }
    tfree(csrc); emit_free(&e);
    return 0;
}

/* command: osdigen <model> <n1> <n2> <current-expression ...> */
void com_osdigen(wordlist *wl)
{
    if (!wl || !wl->wl_next || !wl->wl_next->wl_next || !wl->wl_next->wl_next->wl_next) {
        fprintf(cp_err, "usage: osdigen <model> <n1> <n2> <current-expr ...>\n");
        return;
    }
    char *model = wl->wl_word;
    char *n1 = wl->wl_next->wl_word;
    char *n2 = wl->wl_next->wl_next->wl_word;
    char *expr = wl_flatten(wl->wl_next->wl_next->wl_next);
    char *outc = tprintf("%s.c", model);
    osdi_res_emit(model, n1, n2, expr, outc);
    tfree(expr); tfree(outc);
}

/* command: osdigenc <model> <n1> <n2> <charge-expression ...>
 * emit an OSDI capacitor (charge) device from Q(v(n1,n2),temper). */
void com_osdigenc(wordlist *wl)
{
    if (!wl || !wl->wl_next || !wl->wl_next->wl_next || !wl->wl_next->wl_next->wl_next) {
        fprintf(cp_err, "usage: osdigenc <model> <n1> <n2> <charge-expr ...>\n");
        return;
    }
    char *model = wl->wl_word, *n1 = wl->wl_next->wl_word, *n2 = wl->wl_next->wl_next->wl_word;
    char *expr = wl_flatten(wl->wl_next->wl_next->wl_next);
    CKTcircuit *ckt = ft_curckt ? ft_curckt->ci_ckt : NULL;
    INPtables *tab  = ft_curckt ? (INPtables *) ft_curckt->ci_symtab : NULL;
    EmitOut e = emit_core(model, n1, n2, expr, ckt, tab, 0);
    if (e.rc) { fprintf(cp_err, "osdigenc: failed (rc=%d) for '%s'\n", e.rc, expr); tfree(expr); return; }
    char *csrc = gen_cap_model_c(model, e.nvals, e.cI, e.cdp, e.cdn);   /* cI=Q, cdp/cdn=dQ/dv */
    char *outc = tprintf("%s.c", model);
    FILE *f = fopen(outc, "w");
    if (f) { fputs(csrc, f); fclose(f);
        fprintf(cp_out, "osdigenc: wrote %s  (cap model '%s', %d instance params)\n", outc, model, e.nvals);
        fprintf(cp_out, "  Q     = %s\n  instance:  N<name> %s %s %s m=1", e.cI, n1, n2, model);
        for (int k = 0; k < e.nvals; k++) fprintf(cp_out, " p%d=%.17g", k, e.vals[k]);
        fprintf(cp_out, "\n"); }
    tfree(csrc); tfree(outc); tfree(expr); emit_free(&e);
}

/* ================= M3: automatic inpcom pass ================= */

/* signature -> compiled model cache for this run */
typedef struct { char **sig; char **model; int n, cap; } SigCache;
static SigCache g_cache;

static const char *cache_lookup(const char *sig)
{
    for (int i = 0; i < g_cache.n; i++)
        if (!strcmp(g_cache.sig[i], sig)) return g_cache.model[i];
    return NULL;
}
static void cache_add(const char *sig, const char *model)
{
    if (g_cache.n == g_cache.cap) { g_cache.cap = g_cache.cap ? g_cache.cap*2 : 16;
        g_cache.sig = TREALLOC(char *, g_cache.sig, g_cache.cap);
        g_cache.model = TREALLOC(char *, g_cache.model, g_cache.cap); }
    g_cache.sig[g_cache.n] = copy(sig);
    g_cache.model[g_cache.n] = copy(model);
    g_cache.n++;
}

/* compile csrc -> <model>.osdi (system gcc) and load_osdi it. 0 on success. */
static int compile_and_load(const char *model, const char *csrc)
{
    char *inc = NULL;
    if (!cp_getvar("osdi_res_include", CP_STRING, NULL, 0))
        inc = copy("/users/tmp/ngspice_debug/ng46build/src/osdi");
    else { inc = TMALLOC(char, 512); cp_getvar("osdi_res_include", CP_STRING, inc, 512); }
    /* Write and load the .osdi by ABSOLUTE path.  load_osdi() resolves a
     * relative name through a search path (NGSPICE_OSDI_DIR, the input dir,
     * the cwd, and the executable's directory), so a stale osdires_<model>.osdi
     * left anywhere earlier on that path would shadow the model we just
     * generated -- loading a different law under the same name.  That is
     * silent wrong-physics whenever the stale model happens to take the same
     * number of parameters, so pin the path to the file we actually wrote. */
    char cwdbuf[PATH_MAX];
    if (!getcwd(cwdbuf, sizeof(cwdbuf))) {
        fprintf(cp_err, "osdi-res: getcwd failed\n"); tfree(inc); return 1;
    }
    char *cfile = tprintf("%s/osdires_%s.c", cwdbuf, model);
    char *ofile = tprintf("%s/osdires_%s.osdi", cwdbuf, model);
    FILE *f = fopen(cfile, "w");
    if (!f) { fprintf(cp_err, "osdi-res: cannot write %s\n", cfile); tfree(inc);tfree(cfile);tfree(ofile); return 1; }
    fputs(csrc, f); fclose(f);
    char *cmd = tprintf("gcc -O2 -shared -fPIC -I%s '%s' -o '%s' -lm", inc, cfile, ofile);
    int rc = system(cmd);
    tfree(cmd);
    if (rc != 0) { fprintf(cp_err, "osdi-res: gcc failed for %s (rc=%d)\n", model, rc);
        tfree(inc);tfree(cfile);tfree(ofile); return 2; }
    if (load_osdi(ofile)) { fprintf(cp_err, "osdi-res: load_osdi failed for %s\n", ofile);
        tfree(inc);tfree(cfile);tfree(ofile); return 3; }
    tfree(inc); tfree(cfile); tfree(ofile);
    return 0;
}

/* extract a whitespace-delimited `m=<val>` mfactor from a b-source line
 * (skips the `reciprocm=`/`reciproctc=` substrings). Returns 1.0 if absent. */
static double extract_m(const char *s)
{
    for (const char *p = s; *p; p++)
        if (p[0] == 'm' && p[1] == '=' && (p == s || isspace_c(p[-1]))) {
            char *end; double v = strtod(p + 2, &end);
            if (end != p + 2 && v > 0.0) return v;
        }
    return 1.0;
}

/* splice a new card carrying `line` right after `c` (line is taken over). */
static struct card *ins_after(struct card *c, char *line)
{
    struct card *n = TMALLOC(struct card, 1);
    n->line = line;
    n->linenum = c->linenum; n->linenum_orig = c->linenum_orig;
    n->linesource = c->linesource; n->level = c->level;
    n->actualLine = NULL;
    n->nextcard = c->nextcard;
    c->nextcard = n;
    return n;
}

static int tree_has_temper(INPparseNode *p)
{
    if (!p) return 0;
    if (p->type == PT_TEMPERATURE) return 1;
    return tree_has_temper(p->left) || tree_has_temper(p->right);
}

/* Gaussian elimination with partial pivoting; A row-major n*n (destroyed),
 * b in/out (solution). Returns 1 on success, 0 if singular. */
static int gauss_solve(double *A, double *b, int n)
{
    for (int c = 0; c < n; c++) {
        int piv = c; double mx = fabs(A[c*n+c]);
        for (int r = c+1; r < n; r++) { double v = fabs(A[r*n+c]); if (v > mx) { mx = v; piv = r; } }
        if (mx < 1e-300) return 0;
        if (piv != c) { for (int k = 0; k < n; k++) { double t = A[c*n+k]; A[c*n+k] = A[piv*n+k]; A[piv*n+k] = t; }
                        double t = b[c]; b[c] = b[piv]; b[piv] = t; }
        for (int r = 0; r < n; r++) { if (r == c) continue; double f = A[r*n+c]/A[c*n+c];
            for (int k = c; k < n; k++) A[r*n+k] -= f*A[c*n+k]; b[r] -= f*b[c]; }
    }
    for (int i = 0; i < n; i++) b[i] /= A[i*n+i];
    return 1;
}

/* Integrate a capacitance expression c(v) wrt V=v(n1,n2) into a CHARGE string
 * Q(v) = integral_0^V c.  Only handles temper-free polynomials in V up to degree
 * DMAX (covers vc1/vc2 and any polynomial cap). Returns malloc'd Q string, or
 * NULL if c is not a temper-free polynomial (caller falls back to E/C/B).
 * ngspice has PTdifferentiate but no integrator, so we recover the polynomial
 * coefficients by sampling c(V) and solving a Vandermonde system, verify on
 * off-grid + NEGATIVE points (to reject abs()/kinks), then integrate term by term
 * (V^(k+1) via explicit multiplication so the sign is correct for odd powers). */
static char *cap_charge_from_C(const char *cexpr, const char *n1, const char *n2,
                               CKTcircuit *ckt, INPtables *tab)
{
    const int DMAX = 6, N = DMAX + 1;
    INPparseTree *pt = NULL; char *eb = copy(cexpr), *ep = eb;
    INPgetTree(&ep, &pt, ckt, tab); tfree(eb);
    if (!pt || !pt->tree) return NULL;
    if (tree_has_temper(pt->tree)) { INPfreeTree((IFparseTree *) pt); return NULL; }

    int ip = -1, in = -1, nnode = 0, ninst = 0, nv = pt->p.numVars;
    for (int k = 0; k < nv; k++) {
        if (pt->p.varTypes[k] == IF_NODE) { nnode++;
            CKTnode *nd = (CKTnode *) pt->p.vars[k].nValue; const char *nm = nd ? nd->name : NULL;
            if (nm && cieq((char *)nm, (char *)n1)) ip = k;
            else if (nm && cieq((char *)nm, (char *)n2)) in = k;
        } else if (pt->p.varTypes[k] == IF_INSTANCE) ninst++;
    }
    if (ip < 0 || in < 0 || ninst || nnode != 2) { INPfreeTree((IFparseTree *) pt); return NULL; }

    double *vals = TMALLOC(double, nv), *der = TMALLOC(double, nv);
    double *A = TMALLOC(double, N*N), *a = TMALLOC(double, N);
    for (int i = 0; i < N; i++) {
        for (int k = 0; k < nv; k++) vals[k] = 0.0;
        vals[ip] = (double) i;                       /* V = i (vn=0) */
        double res = 0; pt->p.IFeval(&pt->p, 1e-12, &res, vals, der);
        a[i] = res; double xp = 1.0;
        for (int j = 0; j < N; j++) { A[i*N+j] = xp; xp *= (double) i; }
    }
    char *Q = NULL;
    if (gauss_solve(A, a, N)) {                       /* a[] = polynomial coeffs */
        double tv[3] = { 0.5, -1.3, 2.7 }; int good = 1;
        for (int t = 0; t < 3 && good; t++) {
            for (int k = 0; k < nv; k++) vals[k] = 0.0; vals[ip] = tv[t];
            double res = 0; pt->p.IFeval(&pt->p, 1e-12, &res, vals, der);
            double pe = 0, xp = 1; for (int j = 0; j < N; j++) { pe += a[j]*xp; xp *= tv[t]; }
            if (fabs(pe - res) / (fabs(res) + 1e-30) > 1e-9) good = 0;   /* not this polynomial */
        }
        if (good) {
            int deg = DMAX; while (deg > 0 && fabs(a[deg]) < 1e-28) deg--;
            char *acc = copy("(0.0");
            for (int k = 0; k <= deg; k++) {
                char *vpow = NULL;                     /* v(n1,n2) multiplied (k+1) times */
                for (int r = 0; r <= k; r++) { char *t = vpow ? tprintf("%s*v(%s,%s)", vpow, n1, n2)
                                                             : tprintf("v(%s,%s)", n1, n2);
                                               tfree(vpow); vpow = t; }
                char *t = tprintf("%s+(%.17g)*(%s)", acc, a[k]/(k+1), vpow); tfree(acc); acc = t; tfree(vpow);
            }
            Q = tprintf("%s)", acc); tfree(acc);
        }
    }
    tfree(vals); tfree(der); tfree(A); tfree(a);
    INPfreeTree((IFparseTree *) pt);
    return Q;
}

/* --- node-degree census for dangling-cap detection ---------------------------
 * A cap whose terminal touches a degree-1 (floating) node is electrically
 * dangling: it drives an undetermined node whose voltage the cap's own charge
 * integration then defines, injecting integration noise into the node it shares
 * (e.g. opamp compensation caps left on a dead net). Native CKTtopologyReduce
 * removes these; but once a behavioral cap becomes a B-source the reducer no
 * longer recognizes it, so we must prune them here on the flattened deck.
 * Terminal count per type: MOSFET/BJT = 4 (BJT's model token counts as a
 * harmless fake node), everything else = 2. B-source terminals are exactly the
 * first two tokens -- the i=<expr> (which may mention the terminals via v()) is
 * never scanned, so a truly floating terminal stays at degree 1. */
static void dfree(void *p) { tfree(p); }

static void tally_terms(char *line, NGHASHPTR deg)
{
    if (!line) return;
    char c0 = (char) tolower((unsigned char) line[0]);
    if (c0 == '*' || c0 == '.' || c0 == '+' || c0 == 'x' ||
        c0 == '\0' || isspace_c(c0))
        return;
    int nterm = (c0 == 'm' || c0 == 'q') ? 4 : 2;
    char *cut = line;
    char *name = gettok(&cut); tfree(name);
    for (int t = 0; t < nterm; t++) {
        char *tok = gettok(&cut);
        if (!tok) break;
        for (char *p = tok; *p; p++) *p = (char) tolower((unsigned char) *p);
        int *cnt = (int *) nghash_find(deg, tok);
        if (cnt) { (*cnt)++; tfree(tok); }
        else { cnt = TMALLOC(int, 1); *cnt = 1; nghash_insert(deg, tok, cnt); }
    }
}

static int node_deg(NGHASHPTR deg, const char *n)
{
    char *k = copy(n);
    for (char *p = k; *p; p++) *p = (char) tolower((unsigned char) *p);
    int *cnt = (int *) nghash_find(deg, k);
    tfree(k);
    return cnt ? *cnt : 0;
}

/* The pass: convert qualifying behavioral resistor B-sources to C-OSDI N-devices.
 * Runs after subckt expansion (params numeric).
 * Policy: OSDI is the DEFAULT; the behavioral B-source is the last-resort fallback,
 * used automatically per-resistor when codegen can't handle it (unsupported
 * function, no compiler) and globally when the user forces it with
 * `set osdi_res_bsource`. */
void osdi_res_transform(struct card *deck)
{
    /* explicit user opt-out: keep everything behavioral */
    if (cp_getvar("osdi_res_bsource", CP_BOOL, NULL, 0)) return;

    /* need a C compiler; if absent, warn once and keep all as B-source */
    if (system("gcc --version >/dev/null 2>&1") != 0) {
        fprintf(cp_err, "warning(osdi-res): no C compiler (gcc) found; "
                        "behavioral resistors kept as B-source.\n");
        return;
    }
    int nconv = 0, ncap = 0, nfall = 0, ndrop = 0, seq = 0, capseq = 0;

    /* throwaway circuit + symbol table for INPgetTree's node resolution
       (no real circuit exists yet at this parse stage) */
    CKTcircuit *tmpckt = NULL;
    if (ft_sim->newCircuit(&tmpckt)) return;
    INPtables *tmptab = INPtabInit(1024);

    /* node-degree census over the whole flattened deck (for dangling-cap prune) */
    NGHASHPTR deg = nghash_init(nghash_table_size(4096));
    for (struct card *card = deck; card; card = card->nextcard)
        tally_terms(card->line, deg);

    for (struct card *card = deck; card; card = card->nextcard) {
        char *line = card->line;
        if (!line || (*line != 'b' && *line != 'B')) continue;

        /* tokens: name n1 n2 ... then "i = <expr> [trailing]" */
        char *cut = line;
        char *name = gettok(&cut);
        char *n1 = gettok(&cut);
        char *n2 = gettok(&cut);
        if (!name || !n1 || !n2) { tfree(name);tfree(n1);tfree(n2); continue; }

        /* first '=' after nodes; preceding non-space char must be 'i' (current out) */
        char *eq = strchr(cut, '=');
        char *pc = eq; if (pc) { do { pc--; } while (pc > cut && isspace_c(*pc)); }
        if (!eq || !pc || (*pc != 'i' && *pc != 'I')) { tfree(name);tfree(n1);tfree(n2); continue; }

        char *expr = eq + 1;                 /* expr begins right after '=' */
        size_t nlen = strlen(name);          /* cap tags = name suffix _oscapq / _oscapc */
        int capq = (nlen > 7 && !strcmp(name + nlen - 7, "_oscapq"));  /* charge form */
        int capc = (nlen > 7 && !strcmp(name + nlen - 7, "_oscapc"));  /* capacitance form */

        if (capq || capc) {
            /* Shorted cap (both terminals the same node) is electrically inert:
             * dV across it is identically 0, so Q is constant and the current is 0.
             * These are layout dummy/matching MIM caps. Converting them to the E/C/B
             * behavioral differentiator adds an internal node + a current-controlled
             * B-source with a nonzero Jacobian weight onto that node -- an inert-but-
             * perturbing network that degrades the bypass=1 solve. Drop them. */
            if (cieq(n1, n2)) {
                *(card->line) = '*';
                ndrop++;
                tfree(name); tfree(n1); tfree(n2);
                continue;
            }
            /* Dangling cap: a terminal on a degree-1 (floating) node. The cap
             * drives an undetermined node -> its charge integration injects
             * noise onto the node it shares. Drop it (mirrors CKTtopologyReduce,
             * which can't see the cap once it's a B-source). */
            if (node_deg(deg, n1) == 1 || node_deg(deg, n2) == 1) {
                *(card->line) = '*';
                ndrop++;
                tfree(name); tfree(n1); tfree(n2);
                continue;
            }
            /* Q-form: charge given directly (exact). C-form: integrate c(v)->Q,
             * exactly if c is polynomial, else via the GENERAL quadrature device. */
            int quad = 0;
            char *chg = capq ? copy(expr) : cap_charge_from_C(expr, n1, n2, tmpckt, tmptab);
            if (!chg && capc) { quad = 1; chg = copy(expr); }   /* general: emit on c(v) itself */
            EmitOut e = { 5, NULL, NULL, NULL, NULL, NULL, 0 };
            if (chg) e = emit_core("m", n1, n2, chg, tmpckt, tmptab, 0);

            const char *model = NULL; char *modelbuf = NULL;
            if (chg && !e.rc) {
                char *key = tprintf("%s%s", quad ? "CQ:" : "C:", e.sig);
                model = cache_lookup(key);
                if (!model) {
                    modelbuf = tprintf("osdicap%d", capseq++);
                    char *csrc = quad ? gen_cap_quad_model_c(modelbuf, e.nvals, e.cI, e.cdp)
                                      : gen_cap_model_c(modelbuf, e.nvals, e.cI, e.cdp, e.cdn);
                    if (compile_and_load(modelbuf, csrc)) { tfree(csrc); tfree(modelbuf); modelbuf = NULL; }
                    else { tfree(csrc); cache_add(key, modelbuf); model = cache_lookup(key); }
                }
                tfree(key);
            }
            *(card->line) = '*';
            if (model) {   /* success: N charge-device + .model */
                char *nl = tprintf("n%s %s %s %s m=1", name + 1, n1, n2, model);
                for (int k = 0; k < e.nvals; k++) { char *t = tprintf("%s p%d=%.17g", nl, k, e.vals[k]); tfree(nl); nl = t; }
                struct card *ncard = ins_after(card, nl);
                if (modelbuf) { char *mc = tprintf(".model %s %s()", modelbuf, modelbuf); ins_after(card, mc); }
                card = ncard; ncap++;
            } else if (capq) {   /* fall back: behavioral G/L/B charge network (expr=(m)*(q)) */
                fprintf(cp_err, "warning(osdi-res): cap '%s' -> behavioral network.\n", name); nfall++;
                struct card *c1 = ins_after(card, tprintf("g%s %s %s %s_int1 0 1", name, n1, n2, name));
                struct card *c2 = ins_after(c1, tprintf("l%s %s_int1 0 1", name, name));
                card = ins_after(c2, tprintf("b%s 0 %s_int1 i = %s reciproctc=1", name, name, expr));
            } else {             /* fall back: behavioral E/C/B differentiator (expr=(m)*(c)) */
                fprintf(cp_err, "warning(osdi-res): cap '%s' -> behavioral differentiator (unsupported node).\n", name); nfall++;
                struct card *c1 = ins_after(card, tprintf("e%s %s_int1 0 %s %s 1", name, name, n2, n1));
                struct card *c2 = ins_after(c1, tprintf("c%s %s_int1 0 1", name, name));
                card = ins_after(c2, tprintf("b%s %s %s i = i(e%s) * (%s) reciproctc=1", name, n1, n2, name, expr));
            }
            if (chg) { tfree(chg); emit_free(&e); }
            tfree(modelbuf); tfree(name); tfree(n1); tfree(n2);
            continue;
        }

        EmitOut e = emit_core("m", n1, n2, expr, tmpckt, tmptab, 0);
        if (e.rc) {
            /* rc==5 = not a 2-terminal VCCS (a genuine user B-source) -> silent.
               anything else = resistor-like but not emittable -> warn + fall back. */
            if (e.rc != 5) { fprintf(cp_err,
                "warning(osdi-res): '%s' not emittable to OSDI, keeping B-source.\n", name); nfall++; }
            tfree(name);tfree(n1);tfree(n2); emit_free(&e); continue;
        }

        /* dedup: reuse a model with the same signature, else compile a new one */
        char *rkey = tprintf("R:%s", e.sig);
        const char *model = cache_lookup(rkey);
        char *modelbuf = NULL;
        if (!model) {
            modelbuf = tprintf("osdires%d", seq++);
            char *csrc = gen_model_c(modelbuf, e.nvals, e.cI, e.cdp, e.cdn);
            if (compile_and_load(modelbuf, csrc)) {
                fprintf(cp_err, "warning(osdi-res): '%s' gcc-compile/load failed, "
                                "keeping B-source.\n", name); nfall++;
                tfree(csrc); tfree(modelbuf); tfree(rkey); tfree(name);tfree(n1);tfree(n2);
                emit_free(&e); continue;
            }
            tfree(csrc);
            cache_add(rkey, modelbuf);
            model = cache_lookup(rkey);
        }
        tfree(rkey);

        /* build the N-device line: N<name> n1 n2 <model> m=<mf> p0=.. .. */
        double mf = extract_m(cut);        /* honor an explicit user m= (else 1) */
        char *nl = tprintf("n%s %s %s %s m=%.17g", name+1, n1, n2, model, mf);
        for (int k = 0; k < e.nvals; k++) { char *t = tprintf("%s p%d=%.17g", nl, k, e.vals[k]); tfree(nl); nl = t; }
        *(card->line) = '*';                       /* comment out the b-source */
        struct card *ncard = ins_after(card, nl);  /* N-line right after it */
        if (modelbuf) {                            /* newly compiled: emit its .model, ahead of N */
            char *mcard = tprintf(".model %s %s()", modelbuf, modelbuf);
            ins_after(card, mcard);
        }
        card = ncard;                              /* skip past inserted cards */
        nconv++;

        tfree(modelbuf); tfree(name);tfree(n1);tfree(n2);
        emit_free(&e);
    }
    nghash_free(deg, dfree, dfree);
    if (nconv || ncap || nfall || ndrop)
        fprintf(cp_out, "osdi-res: %d resistor(s) + %d capacitor(s) -> %d C-OSDI model(s); %d kept behavioral; %d shorted/dangling cap(s) dropped\n",
                nconv, ncap, g_cache.n, nfall, ndrop);
}
