@vessel-dsp/compiler 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +4 -2
  2. package/dist/bbd-clock.d.ts +42 -0
  3. package/dist/bbd-clock.d.ts.map +1 -1
  4. package/dist/bbd-clock.js +383 -1
  5. package/dist/bbd-clock.js.map +1 -1
  6. package/dist/compile.d.ts.map +1 -1
  7. package/dist/compile.js +316 -34
  8. package/dist/compile.js.map +1 -1
  9. package/dist/device-laws.d.ts.map +1 -1
  10. package/dist/device-laws.js +581 -98
  11. package/dist/device-laws.js.map +1 -1
  12. package/dist/fixtures/circuits.d.ts +23 -2
  13. package/dist/fixtures/circuits.d.ts.map +1 -1
  14. package/dist/fixtures/circuits.js +205 -2
  15. package/dist/fixtures/circuits.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/lower.d.ts.map +1 -1
  21. package/dist/lower.js +13 -0
  22. package/dist/lower.js.map +1 -1
  23. package/dist/netlist.d.ts +30 -0
  24. package/dist/netlist.d.ts.map +1 -1
  25. package/dist/netlist.js +502 -8
  26. package/dist/netlist.js.map +1 -1
  27. package/dist/part-catalog.d.ts +42 -18
  28. package/dist/part-catalog.d.ts.map +1 -1
  29. package/dist/part-catalog.js +2518 -576
  30. package/dist/part-catalog.js.map +1 -1
  31. package/dist/partition.d.ts.map +1 -1
  32. package/dist/partition.js +6 -0
  33. package/dist/partition.js.map +1 -1
  34. package/dist/registry-data/bjt.d.ts +3 -0
  35. package/dist/registry-data/bjt.d.ts.map +1 -0
  36. package/dist/registry-data/bjt.js +1350 -0
  37. package/dist/registry-data/bjt.js.map +1 -0
  38. package/dist/registry-data/charge-pump.d.ts +3 -0
  39. package/dist/registry-data/charge-pump.d.ts.map +1 -0
  40. package/dist/registry-data/charge-pump.js +204 -0
  41. package/dist/registry-data/charge-pump.js.map +1 -0
  42. package/dist/registry-data/diode.d.ts +3 -0
  43. package/dist/registry-data/diode.d.ts.map +1 -0
  44. package/dist/registry-data/diode.js +786 -0
  45. package/dist/registry-data/diode.js.map +1 -0
  46. package/dist/registry-data/index.d.ts +21 -0
  47. package/dist/registry-data/index.d.ts.map +1 -0
  48. package/dist/registry-data/index.js +103 -0
  49. package/dist/registry-data/index.js.map +1 -0
  50. package/dist/registry-data/jfet.d.ts +3 -0
  51. package/dist/registry-data/jfet.d.ts.map +1 -0
  52. package/dist/registry-data/jfet.js +605 -0
  53. package/dist/registry-data/jfet.js.map +1 -0
  54. package/dist/registry-data/loader.d.ts +137 -0
  55. package/dist/registry-data/loader.d.ts.map +1 -0
  56. package/dist/registry-data/loader.js +350 -0
  57. package/dist/registry-data/loader.js.map +1 -0
  58. package/dist/registry-data/mosfet.d.ts +3 -0
  59. package/dist/registry-data/mosfet.d.ts.map +1 -0
  60. package/dist/registry-data/mosfet.js +63 -0
  61. package/dist/registry-data/mosfet.js.map +1 -0
  62. package/dist/registry-data/opamp.d.ts +37 -0
  63. package/dist/registry-data/opamp.d.ts.map +1 -0
  64. package/dist/registry-data/opamp.js +669 -0
  65. package/dist/registry-data/opamp.js.map +1 -0
  66. package/dist/registry-data/regulator.d.ts +3 -0
  67. package/dist/registry-data/regulator.d.ts.map +1 -0
  68. package/dist/registry-data/regulator.js +189 -0
  69. package/dist/registry-data/regulator.js.map +1 -0
  70. package/dist/registry-data/schema.d.ts +60 -0
  71. package/dist/registry-data/schema.d.ts.map +1 -0
  72. package/dist/registry-data/schema.js +151 -0
  73. package/dist/registry-data/schema.js.map +1 -0
  74. package/dist/registry-data/ujt.d.ts +3 -0
  75. package/dist/registry-data/ujt.d.ts.map +1 -0
  76. package/dist/registry-data/ujt.js +48 -0
  77. package/dist/registry-data/ujt.js.map +1 -0
  78. package/dist/registry-data/zener.d.ts +3 -0
  79. package/dist/registry-data/zener.d.ts.map +1 -0
  80. package/dist/registry-data/zener.js +547 -0
  81. package/dist/registry-data/zener.js.map +1 -0
  82. package/dist/registry.d.ts +93 -6
  83. package/dist/registry.d.ts.map +1 -1
  84. package/dist/registry.js +35 -7
  85. package/dist/registry.js.map +1 -1
  86. package/dist/types.d.ts +242 -19
  87. package/dist/types.d.ts.map +1 -1
  88. package/package.json +1 -1
package/dist/netlist.js CHANGED
@@ -18,7 +18,8 @@
18
18
  import { componentDevices, deviceTerminalRoles, parseInterchangeYamlWithTopology, } from "@vessel-dsp/core";
19
19
  import { load as loadYaml } from "js-yaml";
20
20
  import { StageRefusal } from "./errors.js";
21
- import { foldToken } from "./registry.js";
21
+ import { emptyRegistry, foldToken, registryEntryFor } from "./registry.js";
22
+ import { classParamNames, requiredClassParams, } from "./registry-data/index.js";
22
23
  import { findBistableLatches } from "./latch-seed.js";
23
24
  import { speakerOnePort } from "./speaker-load.js";
24
25
  import { GROUND, } from "./types.js";
@@ -632,6 +633,7 @@ export function readNetlist(source, options) {
632
633
  ? declaredNodesFromConnectivity(document, parsed.connectivity)
633
634
  : new Map();
634
635
  const nodeOf = resolveNodes(document, declaredNodes);
636
+ const registry = options?.registry ?? emptyRegistry;
635
637
  const warnings = [];
636
638
  /**
637
639
  * **A switch throw or winding tap alone on its node cannot route anything.**
@@ -931,7 +933,11 @@ export function readNetlist(source, options) {
931
933
  packageDeviceId: declared.id,
932
934
  kind: deviceKind,
933
935
  nodes: owned.map((entry) => entry.node),
934
- parameters: parametersFor(component, deviceKind),
936
+ ...applyDeviceOverride(component, deviceKind, parametersFor(component, deviceKind), {
937
+ warnings,
938
+ registry,
939
+ deviceId: `${component.id}.${declared.id}`,
940
+ }),
935
941
  // The control belongs to the package, so only the first device carries it.
936
942
  // A ganged part states the same control on both and the owner map already
937
943
  // keeps the first.
@@ -946,7 +952,11 @@ export function readNetlist(source, options) {
946
952
  id: String(component.id),
947
953
  kind,
948
954
  nodes,
949
- parameters: parametersFor(component, kind),
955
+ ...applyDeviceOverride(component, kind, parametersFor(component, kind), {
956
+ warnings,
957
+ registry,
958
+ deviceId: String(component.id),
959
+ }),
950
960
  control,
951
961
  identity: identityOf(effectiveTerminals, effectiveTerminals.map((terminal) => declaredRole(terminal))),
952
962
  isLed: componentKind === "led" ? true : undefined,
@@ -1098,7 +1108,7 @@ export function readNetlist(source, options) {
1098
1108
  device: owner.id,
1099
1109
  declared: componentTaper.unreadable,
1100
1110
  detail: `Control ${id}: ${owner.id} declares taper "${componentTaper.unreadable}", which ` +
1101
- "this stage cannot turn into one of its four laws, so the control renders " +
1111
+ "this stage cannot turn into one of its laws, so the control renders " +
1102
1112
  "linear. An audio pot rendered linear is audible across the whole sweep.",
1103
1113
  });
1104
1114
  }
@@ -2078,6 +2088,72 @@ export function typedFrequencyHz(component, key) {
2078
2088
  }
2079
2089
  return hertz;
2080
2090
  }
2091
+ /**
2092
+ * The bounds of a declared diode `EmissionCoefficient` (the SPICE `N`, the ideality factor): 1, the
2093
+ * Shockley limit nothing physical sits below, to 10, beyond which the part is not one junction. A
2094
+ * value outside them is refused by name, never clamped.
2095
+ */
2096
+ export const EMISSION_COEFFICIENT_MIN = 1;
2097
+ export const EMISSION_COEFFICIENT_MAX = 10;
2098
+ /**
2099
+ * A diode's declared `EmissionCoefficient`, or `null` when the component states none.
2100
+ *
2101
+ * **The whole value is read, and a value that is not an emission coefficient is refused rather than
2102
+ * skipped or clamped** -- the `typedFrequencyHz` pattern for a dimensionless number. Legal forms,
2103
+ * each checked end to end: a bare number; a string that is exactly a number (`"1.9"`, `"1.9e0"`,
2104
+ * nothing after it: not `"1.9 typ"`, not `"1.9n"`); and a structured `{value, unit}` whose `value`
2105
+ * wins over `raw` and whose unit must be empty. The property is read under this one spelling only
2106
+ * (the corpus has one); nothing here reads a part number, a name or a description.
2107
+ *
2108
+ * A property that is present and is not a number in `EMISSION_COEFFICIENT_MIN` to
2109
+ * `EMISSION_COEFFICIENT_MAX` is a packet error, so this throws and names the component and the
2110
+ * offending value. It states something, and what it states is not an ideality factor.
2111
+ */
2112
+ export function typedEmissionCoefficient(component) {
2113
+ const key = "EmissionCoefficient";
2114
+ const property = component.properties[key];
2115
+ if (property === undefined || property === null) {
2116
+ return null;
2117
+ }
2118
+ const refuse = (why) => {
2119
+ throw new NetlistError(`${component.id}: ${key} ${JSON.stringify(property)} ${why}; a declared emission coefficient must be a number from ${EMISSION_COEFFICIENT_MIN} to ${EMISSION_COEFFICIENT_MAX}, bare, as a number-only string, or as a {value, unit: ""}`, String(component.id));
2120
+ };
2121
+ let n;
2122
+ if (typeof property === "number") {
2123
+ n = property;
2124
+ }
2125
+ else if (typeof property === "string") {
2126
+ const text = property
2127
+ .trim()
2128
+ .replace(/^['"]+|['"]+$/gu, "")
2129
+ .trim();
2130
+ if (text === "") {
2131
+ return null;
2132
+ }
2133
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u.test(text)) {
2134
+ return refuse("is not a number");
2135
+ }
2136
+ n = Number(text);
2137
+ }
2138
+ else if (typeof property === "object" && !Array.isArray(property)) {
2139
+ const { value, unit } = property;
2140
+ if (typeof value !== "number") {
2141
+ return refuse("has no numeric value");
2142
+ }
2143
+ const token = typeof unit === "string" ? unit.replaceAll("'", "").trim() : "";
2144
+ if (token !== "") {
2145
+ return refuse(`has unit "${token}", which is not dimensionless`);
2146
+ }
2147
+ n = value;
2148
+ }
2149
+ else {
2150
+ return refuse("is not a number, string or structured quantity");
2151
+ }
2152
+ if (!Number.isFinite(n) || n < EMISSION_COEFFICIENT_MIN || n > EMISSION_COEFFICIENT_MAX) {
2153
+ return refuse(`is not a finite number from ${EMISSION_COEFFICIENT_MIN} to ${EMISSION_COEFFICIENT_MAX}`);
2154
+ }
2155
+ return n;
2156
+ }
2081
2157
  /**
2082
2158
  * Parse a quantity to SI base units. A value that cannot be parsed is an error, not a
2083
2159
  * default -- a silently defaulted component is a silently wrong circuit.
@@ -2154,6 +2230,410 @@ export function parseQuantity(raw) {
2154
2230
  }
2155
2231
  return value * multiplier;
2156
2232
  }
2233
+ /**
2234
+ * Stage 2: the explicit device-parameter override block.
2235
+ *
2236
+ * A `.vdsp` NAMES the part; the registry holds its parameters. A component may carry one
2237
+ * `DeviceOverride` object in its `properties` stating measured values for this device
2238
+ * (`kind: "measured"`, allowed on any part) or the full parameter set for a part the
2239
+ * registry does not have (`kind: "custom"`). Parameter names are the part's OWN class-schema
2240
+ * names (`CLASS_PARAM_SCHEMAS` for its class); values are plain SI numbers, one per parameter.
2241
+ *
2242
+ * Placement is a nested structured property -- not a new component field -- so no
2243
+ * `circuit-interchange` schema version, no `Component` type, no parser/serializer and no
2244
+ * core change was needed: nested objects round-trip through `parsePropertyValue` /
2245
+ * `propertiesBlock` untouched. An override changes only law numbers that already flow into
2246
+ * program blocks the way today's legacy typed keys do, so `Program`, the stamps and the
2247
+ * consoles are untouched too.
2248
+ *
2249
+ * Precedence, decided under the brief: override > legacy typed key > registry > class default.
2250
+ * Legacy reading is untouched (REMOVAL is stage 4, not this task); while it is still read,
2251
+ * every device carrying a still-read legacy key gets the additive `legacy-device-parameter`
2252
+ * warning, and every applied override gets the additive `registry-override-applied` notice
2253
+ * (emitted in `compile.ts`, where the registry is in scope).
2254
+ *
2255
+ * Every refusal names the component, the parameter and the rule (`NetlistError` carries the
2256
+ * component id, and `compile` turns it into an `unsupported` result at stage `netlist`).
2257
+ */
2258
+ const DEVICE_OVERRIDE_KEY = "DeviceOverride";
2259
+ /**
2260
+ * The schema class an override block on this device is checked against, or `null` when the
2261
+ * device kind takes no registry-class parameters and an override block is an error.
2262
+ *
2263
+ * `diode` device kind covers three classes with the same five names: zener when the component
2264
+ * declares itself one (`Type: Zener`, the only two corpus spellings, or the canonical
2265
+ * `Circuit.ZenerDiode` source type), led when the component kind is `led`, else diode. Only
2266
+ * the *required set* differs between them (a zener additionally requires `breakdownVolts`,
2267
+ * because without it the law stamps an ordinary diode).
2268
+ */
2269
+ function overrideTargetClass(component, deviceKind) {
2270
+ switch (deviceKind) {
2271
+ case "bjt":
2272
+ // A unijunction transistor is `kind: bjt` with the source's own stated
2273
+ // type, and its override names the UJT class parameters, not the BJT's:
2274
+ // stamping IS/BF onto a relaxation element would model a different device.
2275
+ if (component.sourceTypeName === "Circuit.UnijunctionTransistor") {
2276
+ return "ujt";
2277
+ }
2278
+ return "bjt";
2279
+ case "jfet":
2280
+ return "jfet";
2281
+ case "mosfet":
2282
+ return "mosfet";
2283
+ case "diode": {
2284
+ const declaredType = propertyString(component, "Type");
2285
+ if (declaredType === "Zener" ||
2286
+ declaredType === "zener" ||
2287
+ component.sourceTypeName === "Circuit.ZenerDiode") {
2288
+ return "zener";
2289
+ }
2290
+ if (String(component.kind) === "led") {
2291
+ return "led";
2292
+ }
2293
+ return "diode";
2294
+ }
2295
+ case "opamp":
2296
+ return "opamp";
2297
+ default:
2298
+ return null;
2299
+ }
2300
+ }
2301
+ /**
2302
+ * Legacy typed device-parameter keys this stage still reads, per device kind. Only these warn:
2303
+ * keys the compiler never reads (`SR`, `Rout`, `Rin`, `Vsat`, a diode's `IS`/`n`, ...) change
2304
+ * nothing when deleted and belong to the migration table, not to a compile warning.
2305
+ */
2306
+ const LEGACY_DEVICE_PARAM_KEYS = {
2307
+ bjt: ["IS", "BF", "BR", "LeakageCurrent"],
2308
+ jfet: [
2309
+ "Vt0",
2310
+ "VTO",
2311
+ "ThresholdVoltage",
2312
+ "thresholdVolts",
2313
+ "Beta",
2314
+ "BETA",
2315
+ "Transconductance",
2316
+ "transconductance",
2317
+ ],
2318
+ mosfet: [
2319
+ "Vt0",
2320
+ "VTO",
2321
+ "ThresholdVoltage",
2322
+ "thresholdVolts",
2323
+ "Beta",
2324
+ "BETA",
2325
+ "Transconductance",
2326
+ "transconductance",
2327
+ ],
2328
+ diode: [
2329
+ "ReverseBreakdownVoltage",
2330
+ "BreakdownVoltage",
2331
+ "ZenerVoltage",
2332
+ "SaturationCurrent",
2333
+ "SeriesResistance",
2334
+ "OhmicResistance",
2335
+ "RS",
2336
+ "Rs",
2337
+ "EmissionCoefficient",
2338
+ ],
2339
+ opamp: ["Aol", "GBP"],
2340
+ };
2341
+ /**
2342
+ * The engine field a legacy key feeds, for the superseded computation. A legacy key is
2343
+ * superseded when the override supplies the same engine field -- compared on fields, not on
2344
+ * spellings, so the compiler holds no legacy-name to schema-name translation table (that
2345
+ * table lives only in the step-3 migration script).
2346
+ */
2347
+ const LEGACY_KEY_ENGINE_FIELD = {
2348
+ IS: "saturationCurrent",
2349
+ BF: "beta",
2350
+ BR: "reverseBeta",
2351
+ LeakageCurrent: "leakageAmps",
2352
+ Vt0: "thresholdVolts",
2353
+ VTO: "thresholdVolts",
2354
+ ThresholdVoltage: "thresholdVolts",
2355
+ thresholdVolts: "thresholdVolts",
2356
+ Beta: "transconductance",
2357
+ BETA: "transconductance",
2358
+ Transconductance: "transconductance",
2359
+ transconductance: "transconductance",
2360
+ ReverseBreakdownVoltage: "breakdownVolts",
2361
+ BreakdownVoltage: "breakdownVolts",
2362
+ ZenerVoltage: "breakdownVolts",
2363
+ SaturationCurrent: "saturationCurrent",
2364
+ SeriesResistance: "seriesResistance",
2365
+ OhmicResistance: "seriesResistance",
2366
+ RS: "seriesResistance",
2367
+ Rs: "seriesResistance",
2368
+ EmissionCoefficient: "emissionCoefficient",
2369
+ Aol: "openLoopGain",
2370
+ GBP: "gainBandwidthHz",
2371
+ };
2372
+ /**
2373
+ * The engine field an override schema name writes. Every schema name already IS the engine
2374
+ * field except the BJT's gain: the document-typed legacy key is `BF`, the engine field is
2375
+ * `beta` (read by `device-laws.ts` and the provisional-value suppression), and the schema
2376
+ * name is `forwardBeta`.
2377
+ */
2378
+ function overrideEngineField(target, name) {
2379
+ if (target === "bjt" && name === "forwardBeta") {
2380
+ return "beta";
2381
+ }
2382
+ return name;
2383
+ }
2384
+ /**
2385
+ * The class parameters an override block may actually set, by target class: the subset of
2386
+ * the class schema that the engine reads from the document.
2387
+ *
2388
+ * A schema name outside this set is refused even though it is a real class parameter, because
2389
+ * accepting it would be a silent drop: `device-laws.ts` reads a JFET's gate terms
2390
+ * (`gateSaturationCurrent`, `gateOnsetVolts`, `gateScaleVolts`), its
2391
+ * `channelLengthModulation` and `subthresholdVolts`, and an op-amp's output-swing pair ONLY
2392
+ * from the registry entry, never from `device.parameters`. An override value for one of those
2393
+ * would merge, stamp nothing, and change nothing -- the wrong-answer-that-renders this file
2394
+ * exists to prevent. Those numbers come from registry entries (stage 3 fills them), never
2395
+ * from a per-device override; polarity and channel stay structural for the same reason.
2396
+ */
2397
+ const OVERRIDABLE_CLASS_PARAMS = {
2398
+ bjt: [
2399
+ "saturationCurrent",
2400
+ "forwardBeta",
2401
+ "reverseBeta",
2402
+ "thermalVoltage",
2403
+ "leakageAmps",
2404
+ ],
2405
+ jfet: ["thresholdVolts", "transconductance"],
2406
+ mosfet: ["thresholdVolts", "transconductance"],
2407
+ diode: [
2408
+ "saturationCurrent",
2409
+ "emissionCoefficient",
2410
+ "thermalVoltage",
2411
+ "breakdownVolts",
2412
+ "seriesResistance",
2413
+ ],
2414
+ zener: [
2415
+ "saturationCurrent",
2416
+ "emissionCoefficient",
2417
+ "thermalVoltage",
2418
+ "breakdownVolts",
2419
+ "seriesResistance",
2420
+ ],
2421
+ led: [
2422
+ "saturationCurrent",
2423
+ "emissionCoefficient",
2424
+ "thermalVoltage",
2425
+ "breakdownVolts",
2426
+ "seriesResistance",
2427
+ ],
2428
+ opamp: ["openLoopGain", "gainBandwidthHz"],
2429
+ ujt: ["intrinsicStandoffRatio", "interbaseResistanceOhms"],
2430
+ };
2431
+ /** A non-empty authored string, or `null` when the value is not one. */
2432
+ function nonEmptyText(value) {
2433
+ if (typeof value !== "string" || value.trim().length === 0) {
2434
+ return null;
2435
+ }
2436
+ return value.trim();
2437
+ }
2438
+ /**
2439
+ * Check one override value: finite, and positive (or bounded) where the parameter is
2440
+ * physically required to be. Names the component, the parameter and the rule.
2441
+ */
2442
+ function checkOverrideValue(componentId, target, name, value) {
2443
+ const refuse = (rule) => {
2444
+ throw new NetlistError(`component ${componentId}: DeviceOverride parameter "${name}" ${JSON.stringify(value)} ${rule}`, componentId);
2445
+ };
2446
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2447
+ refuse("is not a finite SI number");
2448
+ }
2449
+ const n = value;
2450
+ switch (name) {
2451
+ case "emissionCoefficient":
2452
+ if (n < EMISSION_COEFFICIENT_MIN || n > EMISSION_COEFFICIENT_MAX) {
2453
+ refuse(`is not an ideality factor from ${EMISSION_COEFFICIENT_MIN} to ${EMISSION_COEFFICIENT_MAX}`);
2454
+ }
2455
+ return n;
2456
+ case "leakageAmps":
2457
+ case "breakdownVolts":
2458
+ case "seriesResistance":
2459
+ case "channelLengthModulation":
2460
+ case "subthresholdVolts":
2461
+ case "gateSaturationCurrent":
2462
+ if (n < 0) {
2463
+ refuse(`is negative, and a negative ${name} is not physical`);
2464
+ }
2465
+ return n;
2466
+ case "thresholdVolts":
2467
+ case "gateOnsetVolts":
2468
+ // Sign carries the physics here (depletion negative, enhancement positive, and
2469
+ // p-channel JFETs pinch off positive), so any finite value -- including zero --
2470
+ // parses, exactly as the legacy `Vt0` reader accepts any quantity.
2471
+ return n;
2472
+ case "intrinsicStandoffRatio":
2473
+ // The tap fraction of the interbase bar: strictly inside (0, 1), since eta
2474
+ // is a divider share. 0 shorts RB1, 1 opens RB2, and either inverts the tap.
2475
+ if (!(n > 0) || !(n < 1)) {
2476
+ refuse(`is not a standoff ratio strictly between 0 and 1`);
2477
+ }
2478
+ return n;
2479
+ default:
2480
+ // saturationCurrent, forwardBeta, reverseBeta, thermalVoltage, transconductance,
2481
+ // gateScaleVolts, openLoopGain, gainBandwidthHz, volts, sourceOhms,
2482
+ // chargePumpLossVolts: zero or negative is not a value for any of them.
2483
+ if (!(n > 0)) {
2484
+ refuse(`is not a positive finite number, which ${name} must be`);
2485
+ }
2486
+ return n;
2487
+ }
2488
+ }
2489
+ /**
2490
+ * Parse and validate the `DeviceOverride` block on this component, or return null when it
2491
+ * declares none. Throws `NetlistError` (naming component, parameter and rule) on every
2492
+ * malformed block: unknown kind, missing/malformed provenance, malformed parameter list,
2493
+ * unknown name for the class, duplicate parameter, non-physical value, `custom` on a known
2494
+ * part, or `custom` missing a required parameter. Override on a device kind that takes no
2495
+ * registry-class parameters is an error, so a block on a resistor or an IC can never sit
2496
+ * silently unread.
2497
+ */
2498
+ function readDeviceOverride(component, deviceKind, registry) {
2499
+ const raw = component.properties[DEVICE_OVERRIDE_KEY];
2500
+ if (raw === undefined || raw === null) {
2501
+ return null;
2502
+ }
2503
+ const componentId = String(component.id);
2504
+ const target = overrideTargetClass(component, deviceKind);
2505
+ if (target === null) {
2506
+ throw new NetlistError(`component ${componentId}: DeviceOverride is not supported for ${deviceKind} devices (bjt, jfet, mosfet, diode, opamp and ujt only)`, componentId);
2507
+ }
2508
+ if (typeof raw !== "object" || Array.isArray(raw)) {
2509
+ throw new NetlistError(`component ${componentId}: DeviceOverride must be an object with kind, provenance and parameters, not ${JSON.stringify(raw)}`, componentId);
2510
+ }
2511
+ const block = raw;
2512
+ const kind = block.kind;
2513
+ if (kind !== "measured" && kind !== "custom") {
2514
+ throw new NetlistError(`component ${componentId}: DeviceOverride.kind ${JSON.stringify(kind)} is not "measured" or "custom"`, componentId);
2515
+ }
2516
+ const source = nonEmptyText(block.source);
2517
+ if (source === null) {
2518
+ throw new NetlistError(`component ${componentId}: DeviceOverride.source is missing (what was measured, or which document the numbers come from)`, componentId);
2519
+ }
2520
+ const method = nonEmptyText(block.method);
2521
+ if (method === null) {
2522
+ throw new NetlistError(`component ${componentId}: DeviceOverride.method is missing (instrument/procedure for measured, why the registry lacks the part for custom)`, componentId);
2523
+ }
2524
+ const dateRaw = nonEmptyText(block.date);
2525
+ if (dateRaw === null ||
2526
+ !/^\d{4}-\d{2}-\d{2}$/u.test(dateRaw) ||
2527
+ Number.isNaN(Date.parse(`${dateRaw}T00:00:00Z`))) {
2528
+ throw new NetlistError(`component ${componentId}: DeviceOverride.date ${JSON.stringify(block.date)} is not an ISO date (YYYY-MM-DD)`, componentId);
2529
+ }
2530
+ const by = nonEmptyText(block.by);
2531
+ if (by === null) {
2532
+ throw new NetlistError(`component ${componentId}: DeviceOverride.by is missing (a named person or organisation)`, componentId);
2533
+ }
2534
+ const conditions = block.conditions;
2535
+ if (conditions !== undefined && typeof conditions !== "string") {
2536
+ throw new NetlistError(`component ${componentId}: DeviceOverride.conditions must be free text, not ${JSON.stringify(conditions)}`, componentId);
2537
+ }
2538
+ const list = block.parameters;
2539
+ if (!Array.isArray(list) || list.length === 0) {
2540
+ throw new NetlistError(`component ${componentId}: DeviceOverride.parameters must be a non-empty list of {name, value}`, componentId);
2541
+ }
2542
+ const allowed = new Set(classParamNames(target));
2543
+ const overridable = new Set(OVERRIDABLE_CLASS_PARAMS[target]);
2544
+ const seen = new Set();
2545
+ const parameters = [];
2546
+ for (const [index, entry] of list.entries()) {
2547
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
2548
+ throw new NetlistError(`component ${componentId}: DeviceOverride.parameters[${index}] must be {name, value}, not ${JSON.stringify(entry)}`, componentId);
2549
+ }
2550
+ const record = entry;
2551
+ const name = record.name;
2552
+ if (typeof name !== "string" || !allowed.has(name)) {
2553
+ throw new NetlistError(`component ${componentId}: DeviceOverride parameter ${JSON.stringify(name)} is not a ${target} parameter (accepted: ${[...allowed].join(", ")})`, componentId);
2554
+ }
2555
+ if (!overridable.has(name)) {
2556
+ throw new NetlistError(`component ${componentId}: DeviceOverride parameter "${name}" is a ${target} parameter but the engine reads it only from the registry entry, never from the document, so an override cannot set it (overridable: ${[...overridable].join(", ")})`, componentId);
2557
+ }
2558
+ if (seen.has(name)) {
2559
+ throw new NetlistError(`component ${componentId}: DeviceOverride parameter "${name}" is stated twice (one value per parameter)`, componentId);
2560
+ }
2561
+ seen.add(name);
2562
+ const value = checkOverrideValue(componentId, target, name, record.value);
2563
+ parameters.push({ name, value });
2564
+ }
2565
+ if (kind === "custom") {
2566
+ const partNumber = propertyString(component, "PartNumber");
2567
+ if (partNumber !== null &&
2568
+ registryEntryFor(registry, { partId: partNumber, evidence: "exact-part" }) !== null) {
2569
+ throw new NetlistError(`component ${componentId}: DeviceOverride kind "custom" on part "${partNumber}", which the registry already has (use kind "measured", or remove the block)`, componentId);
2570
+ }
2571
+ const missing = requiredClassParams(target).filter((name) => !seen.has(name));
2572
+ if (missing.length > 0) {
2573
+ throw new NetlistError(`component ${componentId}: DeviceOverride kind "custom" for class ${target} is missing required parameter(s): ${missing.join(", ")}`, componentId);
2574
+ }
2575
+ }
2576
+ const provenance = {
2577
+ kind,
2578
+ source,
2579
+ method,
2580
+ date: dateRaw,
2581
+ by,
2582
+ ...(conditions === undefined ? {} : { conditions }),
2583
+ };
2584
+ return { override: { provenance, parameters }, target };
2585
+ }
2586
+ /**
2587
+ * Merge an explicit override over this device's legacy typed parameters (override wins),
2588
+ * and emit the additive `legacy-device-parameter` warning for every legacy key still read.
2589
+ * No override block: legacy parameters pass through bit-identically and only the warning
2590
+ * is new.
2591
+ */
2592
+ function applyDeviceOverride(component, deviceKind, legacy, context) {
2593
+ const read = readDeviceOverride(component, deviceKind, context.registry);
2594
+ const legacyKeys = (LEGACY_DEVICE_PARAM_KEYS[deviceKind] ?? []).filter((key) => component.properties[key] !== undefined);
2595
+ if (read === null) {
2596
+ if (legacyKeys.length > 0) {
2597
+ const warning = {
2598
+ code: "legacy-device-parameter",
2599
+ device: context.deviceId,
2600
+ keys: legacyKeys,
2601
+ overridden: [],
2602
+ detail: `${context.deviceId} still types device parameters under legacy keys (${legacyKeys.join(", ")}), ` +
2603
+ `which the compiler still reads and the stage-4 removal will stop reading: ` +
2604
+ `migrate them to a DeviceOverride block (measured with provenance, or custom ` +
2605
+ `for a part the registry lacks).`,
2606
+ };
2607
+ context.warnings.push(warning);
2608
+ }
2609
+ return { parameters: legacy, deviceOverride: undefined };
2610
+ }
2611
+ const { override, target } = read;
2612
+ const values = { ...legacy };
2613
+ const overrideFields = new Set(override.parameters.map((parameter) => overrideEngineField(target, parameter.name)));
2614
+ for (const parameter of override.parameters) {
2615
+ values[overrideEngineField(target, parameter.name)] =
2616
+ parameter.value;
2617
+ }
2618
+ const overridden = legacyKeys.filter((key) => {
2619
+ const field = LEGACY_KEY_ENGINE_FIELD[key];
2620
+ return field !== undefined && overrideFields.has(field);
2621
+ });
2622
+ const warning = {
2623
+ code: "legacy-device-parameter",
2624
+ device: context.deviceId,
2625
+ keys: legacyKeys,
2626
+ overridden,
2627
+ detail: overridden.length > 0
2628
+ ? `${context.deviceId} applies a ${override.provenance.kind} DeviceOverride that supersedes legacy keys (${overridden.join(", ")}); the override values win. ` +
2629
+ `Remaining legacy keys (${legacyKeys.filter((key) => !overridden.includes(key)).join(", ") || "none"}) still read as before.`
2630
+ : `${context.deviceId} applies a ${override.provenance.kind} DeviceOverride alongside legacy keys (${legacyKeys.join(", ") || "none"}); no legacy key feeds an overridden parameter, so all legacy values still read.`,
2631
+ };
2632
+ if (legacyKeys.length > 0) {
2633
+ context.warnings.push(warning);
2634
+ }
2635
+ return { parameters: values, deviceOverride: override };
2636
+ }
2157
2637
  function parametersFor(component, kind) {
2158
2638
  // A supply rail asserts a potential at its node. Most declare one -- 84 of the
2159
2639
  // corpus's 90 -- and the six that do not are named support shells with nothing to
@@ -2466,6 +2946,12 @@ function parametersFor(component, kind) {
2466
2946
  optionalQuantity(component, "OhmicResistance") ??
2467
2947
  optionalQuantity(component, "RS") ??
2468
2948
  optionalQuantity(component, "Rs");
2949
+ // `EmissionCoefficient` -- the junction's ideality, the slope of its knee. One packet declares
2950
+ // it (`boss-sd-1`, 1.9 on three clipping diodes) and until this read it was discarded: the law
2951
+ // stamped 1.752 whatever the source said, and nothing said so, so a knee that a packet states
2952
+ // was a knee the engine never had. Typed and refused when unreadable, like `GBP`; absent stays
2953
+ // absent, so `deviceLaw` keeps ownership of what an undeclared coefficient means.
2954
+ const emission = typedEmissionCoefficient(component);
2469
2955
  return {
2470
2956
  ...(breakdown !== null && breakdown > 0
2471
2957
  ? { breakdownVolts: breakdown }
@@ -2476,6 +2962,7 @@ function parametersFor(component, kind) {
2476
2962
  ...(seriesResistance !== null && seriesResistance > 0
2477
2963
  ? { seriesResistance }
2478
2964
  : {}),
2965
+ ...(emission === null ? {} : { emissionCoefficient: emission }),
2479
2966
  };
2480
2967
  }
2481
2968
  // A bipolar transistor's device type and SPICE parameters, which this stage used to
@@ -3394,6 +3881,11 @@ function selectedOption(component, stated) {
3394
3881
  * Deliberately absent: `W`, `W20`, `G`, `D`, `L`, `BH`, `Stepped`, `Trim`,
3395
3882
  * `Boss-G-taper` and the other manufacturer-specific curves. We do not know their
3396
3883
  * shapes, and guessing one would be inventing a device law from a letter.
3884
+ *
3885
+ * `s-curve` is the one S-shaped law present, and it is the *law's name*, not Alpha's letter: a
3886
+ * pot marked `W` or `W20` stays absent, because makers disagree on W and a letter selects a
3887
+ * curve only with its maker. A document that has established its pot's S-curve is Alpha W(4B)
3888
+ * says so by writing `s-curve`.
3397
3889
  */
3398
3890
  const taperSpellings = new Map([
3399
3891
  ["linear", "linear"],
@@ -3411,6 +3903,9 @@ const taperSpellings = new Map([
3411
3903
  ["negativelog", "reverse-logarithmic"],
3412
3904
  ["c", "reverse-logarithmic"],
3413
3905
  ["reverselinear", "reverse-linear"],
3906
+ // Alpha W(4B). `foldTaper` strips `-`, `_` and whitespace, so this one key is `s-curve`,
3907
+ // `S-Curve`, `S Curve` and `scurve`; a hyphenated key would be unreachable.
3908
+ ["scurve", "s-curve"],
3414
3909
  ]);
3415
3910
  /**
3416
3911
  * Values whose meaning is "the source does not state a taper".
@@ -3428,12 +3923,11 @@ const taperUnknownSpellings = new Set([
3428
3923
  /**
3429
3924
  * Tapers `@vessel-dsp/core`'s format supports and this runtime has no curve for.
3430
3925
  *
3431
- * `PotentiometerTaper` carries seven values; `TaperKind` -- the executable law -- carries four.
3432
- * A document declaring one of these three has stated a real taper that the render cannot produce,
3926
+ * `PotentiometerTaper` carries seven values; `TaperKind` -- the executable law -- carries five.
3927
+ * A document declaring one of these two has stated a real taper that the render cannot produce,
3433
3928
  * which is a different failure from an unreadable marking and is worth saying differently.
3434
3929
  */
3435
3930
  const taperWithoutLaw = new Set([
3436
- "scurve",
3437
3931
  "stepped",
3438
3932
  "custom",
3439
3933
  ]);
@@ -3462,7 +3956,7 @@ function taperKind(value) {
3462
3956
  * have read;
3463
3957
  * - the value states anything else -> linear, and **name it**, whether it is a part marking whose
3464
3958
  * curve is not derivable here (`W20`, `Boss-G-taper`) or a taper the format supports and this
3465
- * runtime has no law for (`s-curve`, `stepped`).
3959
+ * runtime has no law for (`stepped`, `custom`).
3466
3960
  */
3467
3961
  function declaredTaper(component) {
3468
3962
  // `Sweep` is what a variable-resistor calls its taper. Both are closed vocabularies