PageSourceSearch

https://velxio.dev/assets/ngspice-interactive-worker-Do928hJp.js

js velxio.dev collected 2026-10-06 16:15:11 UTC 28,138 bytes, 1,054 lines download raw bytes

1/**
2 * Interactive ngspice worker — vendored + extended from
3 * `ejkreboot/ngspice-xspice-wasm` (MIT, 2026). The original repo provides
4 * a batch-only client; we vendor the WASM build as-is and add new message
5 * types for mixed-mode (event-driven) coupling between an MCU simulator
6 * and the analog SPICE solver.
7 *
8 * Original batch API (preserved for backward-compat with non-interactive
9 * callers):
10 *   'init', 'run', 'reset'
11 *
12 * New interactive message types (Phase 1a of the mixed-mode simulator
13 * roadmap — see `project/sim-mixedmode/phase-01-mixed-mode-coupling.md`
14 * in the velxio-prod repo):
15 *   'loadNetlist' — send a netlist to ngspice without running yet
16 *                   (so subsequent commands can drive analysis manually)
17 *   'command'     — send a raw ngspice command string and return stdout
18 *                   captured during its execution (`alter`, `tran`,
19 *                   `display`, etc.)
20 *   'readVec'     — read a vector's current value(s) by name
21 *
22 * Not yet implemented (Phase 1b — needs investigation of single-thread
23 * WASM workarounds for ngspice background mode):
24 *   'bgRun', 'bgHalt', 'bgResume' — true continuation across alter calls.
25 *   Current workaround at the client level: chain `tran` invocations
26 *   with shrinking dt, each one alter-ing the source from the previous
27 *   state. See NgSpiceInteractive.ts for the workaround impl.
28 */
29
30const MODEL_FILES = [
31	'analog.cm',
32	'digital.cm',
33	'spice2poly.cm',
34	'table.cm',
35	'tlines.cm',
36	'xtradev.cm',
37	'xtraevt.cm',
38];
39
40// vecvaluesall struct offsets (used by onData callback during simulation)
41const VECVALUESALL_COUNT_OFFSET = 0;
42const VECVALUESALL_VALUES_OFFSET = 8;
43const VECVALUES_NAME_OFFSET = 0;
44const VECVALUES_REAL_OFFSET = 8;
45const VECVALUES_IS_SCALE_OFFSET = 24;
46
47// vector_info struct offsets (used for post-simulation data extraction)
48const VECTOR_INFO_NAME_OFFSET = 0;
49const VECTOR_INFO_TYPE_OFFSET = 4;
50const VECTOR_INFO_FLAGS_OFFSET = 8;
51const VECTOR_INFO_REALDATA_OFFSET = 12;
52const VECTOR_INFO_IMAGDATA_OFFSET = 16;
53const VECTOR_INFO_LENGTH_OFFSET = 20;
54
55// vecinfoall struct offsets (used by onDataInit callback)
56const VECINFOALL_COUNT_OFFSET = 16;
57const VECINFOALL_VECS_OFFSET = 20;
58const VECINFO_NAME_OFFSET = 4;
59
60// ngspice vector type flag for complex data
61const VF_COMPLEX = 0x400;
62
63// Analysis types that produce scalar results (no sweep)
64const SCALAR_ANALYSIS_TYPES = new Set(['op', 'tf', 'sens']);
65
66let moduleConfig = null;
67let moduleReady = null;
68let filesystemReady = false;
69let api = null;
70let callbackPointers = null;
71let ngspiceInitialized = false;
72let currentRun = null;
73
74function postDebug(event, details = {}) {
75	self.postMessage({
76		type: 'debug',
77		requestId: currentRun?.requestId,
78		event,
79		details,
80	});
81}
82
83self.addEventListener('message', async (event) => {
84	const data = event.data || {};
85
86	try {
87		// ── Original batch API (preserved) ──────────────────────────────
88		if (data.type === 'init') {
89			await ensureSession(data.config || {});
90			self.postMessage({ type: 'ready', requestId: data.requestId });
91			return;
92		}
93
94		if (data.type === 'run') {
95			await runSimulation(data.requestId, data.netlist || '');
96			return;
97		}
98
99		if (data.type === 'reset') {
100			resetNgspice();
101			self.postMessage({ type: 'reset-done', requestId: data.requestId });
102			return;
103		}
104
105		// ── New interactive API (Phase 1a) ──────────────────────────────
106		if (data.type === 'loadNetlist') {
107			await handleLoadNetlist(data.requestId, data.netlist || '');
108			return;
109		}
110
111		if (data.type === 'command') {
112			handleCommand(data.requestId, data.command || '');
113			return;
114		}
115
116		if (data.type === 'readVec') {
117			handleReadVec(data.requestId, data.name || '');
118			return;
119		}
120
121		if (data.type === 'listVectors') {
122			handleListVectors(data.requestId);
123			return;
124		}
125
126		throw new Error(`Unknown message type: ${data.type}`);
127	} catch (error) {
128		self.postMessage({
129			type: 'error',
130			requestId: data.requestId,
131			message: error instanceof Error ? error.message : String(error),
132		});
133	}
134});
135
136// ── Interactive handlers (Phase 1a) ─────────────────────────────────
137
138/**
139 * Load a netlist into ngspice without immediately running an analysis.
140 * The netlist may include `.tran`, `.dc`, etc. directives that fire
141 * automatically when ngspice processes them — or it may be purely
142 * structural (components, sources, models) with analysis triggered
143 * later via separate `command('tran ...')` calls.
144 */
145async function handleLoadNetlist(requestId, netlist) {
146	if (!netlist.trim()) {
147		throw new Error('Netlist is empty.');
148	}
149	resetNgspice();
150	await ensureSession(moduleConfig || {});
151	const lines = buildCircuitLines(netlist);
152	const allocations = allocateCStringArray(lines);
153	try {
154		const rc = api.circ(allocations.arrayPointer);
155		if (rc !== 0) {
156			throw new Error(`ngSpice_Circ failed with status ${rc}.`);
157		}
158		self.postMessage({ type: 'loaded', requestId });
159	} finally {
160		freeCStringArray(allocations);
161	}
162}
163
164/**
165 * Send a raw command string to ngspice. Stdout / stderr lines captured
166 * during the command's execution are buffered and returned in the
167 * response, plus also emitted as 'stdout' / 'stderr' events (compatible
168 * with the existing onPrint callback flow used by batch mode).
169 */
170function handleCommand(requestId, command) {
171	if (!command.trim()) {
172		throw new Error('Command is empty.');
173	}
174	const capture = beginCommandCapture(requestId);
175	try {
176		const rc = api.command(command);
177		self.postMessage({
178			type: 'command-result',
179			requestId,
180			rc,
181			stdout: capture.stdout.slice(),
182			stderr: capture.stderr.slice(),
183		});
184	} finally {
185		endCommandCapture(capture);
186	}
187}
188
189/**
190 * Read the current state of a named vector. For DC operating point this
191 * is a single scalar; for `.tran` it's the entire time series of samples
192 * captured so far.
193 */
194function handleReadVec(requestId, vectorName) {
195	const infoPtr = api.getVecInfo(vectorName);
196	if (!infoPtr) {
197		throw new Error(`Vector '${vectorName}' not found. Run an analysis first.`);
198	}
199	const data = readVectorData(infoPtr, /* readImag */ true);
200	const transferables = [data.real.buffer];
201	if (data.imag) transferables.push(data.imag.buffer);
202	self.postMessage(
203		{
204			type: 'vec',
205			requestId,
206			name: vectorName,
207			real: data.real,
208			imag: data.imag,
209			complex: data.complex,
210			unit: data.unit,
211		},
212		transferables,
213	);
214}
215
216/**
217 * Enumerate every vector in the current plot.  Phase 1d #6.
218 * `ngSpice_AllVecs` returns a NULL-terminated char** array; we walk
219 * it until the first 0 pointer and decode each cstring.  Names are
220 * case-preserved — getVecInfo lookups care about case.
221 */
222function handleListVectors(requestId) {
223	const plot = api.curPlot && api.curPlot();
224	const names = [];
225	if (plot) {
226		const arrPtr = api.allVecs(plot);
227		if (arrPtr) {
228			for (let i = 0; i < 4096; i++) {
229				const ptr = HEAPU32[(arrPtr >> 2) + i];
230				if (!ptr) break;
231				names.push(Module.UTF8ToString(ptr));
232			}
233		}
234	}
235	self.postMessage({ type: 'vector-list', requestId, names });
236}
237
238// ── Command-capture machinery (Phase 1a) ────────────────────────────
239//
240// onPrint pushes to currentRun.log.stdout when a batch run is active.
241// For interactive commands we need our own capture buffer so the
242// caller of `command()` gets the lines that command produced.
243
244let activeCommandCapture = null;
245
246function beginCommandCapture(requestId) {
247	activeCommandCapture = {
248		requestId,
249		stdout: [],
250		stderr: [],
251	};
252	return activeCommandCapture;
253}
254
255function endCommandCapture(capture) {
256	if (activeCommandCapture === capture) {
257		activeCommandCapture = null;
258	}
259}
260
261// freeCStringArray and allocateCStringArray are defined further down in
262// the file as part of the existing batch-mode infrastructure — reused
263// here.
264
265async function runSimulation(requestId, netlist) {
266	if (!netlist.trim()) {
267		throw new Error('Netlist is empty.');
268	}
269
270	if (currentRun) {
271		throw new Error('A library simulation is already in progress.');
272	}
273
274	resetNgspice();
275	await ensureSession(moduleConfig || {});
276
277	currentRun = {
278		requestId,
279		finalTime: extractTranFinalTime(netlist),
280		lastProgress: 0,
281		lastCurrentTime: 0,
282		lastEmitAt: 0,
283		timeVectorName: 'time',
284	};
285
286	postDebug('run-start', { finalTime: currentRun.finalTime, netlistLines: netlist.split(/\r?\n/).length });
287
288	self.postMessage({ type: 'status', requestId, message: 'Submitting circuit to shared ngspice…' });
289
290	const circuitLines = buildCircuitLines(netlist);
291	const allocations = allocateCStringArray(circuitLines);
292
293	try {
294		const rc = api.circ(allocations.arrayPointer);
295		if (rc !== 0) {
296			throw new Error(`ngSpice_Circ failed with status ${rc}.`);
297		}
298
299		emitProgress(true);
300
301		const analyses = extractAllAnalyses();
302		const transferables = collectTransferables(analyses);
303
304		self.postMessage(
305			{
306				type: 'done',
307				requestId,
308				exitCode: rc,
309				finalTime: currentRun.finalTime,
310				progress: currentRun.lastProgress,
311				analyses,
312			},
313			transferables,
314		);
315	} finally {
316		freeCStringArray(allocations);
317		currentRun = null;
318	}
319}
320
321// ---------------------------------------------------------------------------
322// Post-simulation data extraction
323// ---------------------------------------------------------------------------
324
325function extractAllAnalyses() {
326	const plotNames = readAllPlotNames();
327	const analyses = [];
328
329	for (const plotName of plotNames) {
330		if (plotName === 'const') {
331			continue;
332		}
333
334		const analysis = extractPlotAnalysis(plotName);
335		if (analysis) {
336			analyses.push(analysis);
337		}
338	}
339
340	postDebug('extract-analyses', { plotCount: plotNames.length, analysisCount: analyses.length });
341	return analyses;
342}
343
344function readAllPlotNames() {
345	const plotsPtr = api.allPlots();
346	if (!plotsPtr) {
347		return [];
348	}
349
350	const names = [];
351	let offset = plotsPtr;
352	while (true) {
353		const strPtr = HEAPU32[offset >> 2];
354		if (!strPtr) {
355			break;
356		}
357		names.push(Module.UTF8ToString(strPtr));
358		offset += 4;
359	}
360	return names;
361}
362
363function readAllVecNames(plotName) {
364	const vecsPtr = api.allVecs(plotName);
365	if (!vecsPtr) {
366		return [];
367	}
368
369	const names = [];
370	let offset = vecsPtr;
371	while (true) {
372		const strPtr = HEAPU32[offset >> 2];
373		if (!strPtr) {
374			break;
375		}
376		names.push(Module.UTF8ToString(strPtr));
377		offset += 4;
378	}
379	return names;
380}
381
382function extractPlotAnalysis(plotName) {
383	const vecNames = readAllVecNames(plotName);
384	if (vecNames.length === 0) {
385		return null;
386	}
387
388	const analysisType = detectAnalysisType(plotName);
389	const isComplex = analysisType === 'ac';
390	const isScalar = SCALAR_ANALYSIS_TYPES.has(analysisType);
391
392	let sweepVec = null;
393	const dataVecs = [];
394
395	for (const vecName of vecNames) {
396		const qualifiedName = `${plotName}.${vecName}`;
397		const infoPtr = api.getVecInfo(qualifiedName);
398		if (!infoPtr) {
399			continue;
400		}
401
402		const vecData = readVectorData(infoPtr, isComplex);
403		if (!vecData) {
404			continue;
405		}
406
407		const typeFlags = HEAP32[(infoPtr + VECTOR_INFO_TYPE_OFFSET) >> 2];
408		// SV_TIME=1, SV_FREQUENCY=2 are scale (independent) vectors
409		const svType = typeFlags & 0xFF;
410		const isScale = svType === 1 || svType === 2;
411
412		if (isScale) {
413			sweepVec = { name: vecName, ...vecData };
414		} else {
415			dataVecs.push({ name: vecName, ...vecData });
416		}
417	}
418
419	if (isScalar) {
420		return buildScalarResult(analysisType, plotName, sweepVec, dataVecs);
421	}
422
423	return buildVectorResult(analysisType, plotName, sweepVec, dataVecs, isComplex);
424}
425
426function readVectorData(infoPtr, readImag) {
427	const length = HEAP32[(infoPtr + VECTOR_INFO_LENGTH_OFFSET) >> 2];
428	if (length <= 0) {
429		return null;
430	}
431
432	const realDataPtr = HEAPU32[(infoPtr + VECTOR_INFO_REALDATA_OFFSET) >> 2];
433	const compDataPtr = HEAPU32[(infoPtr + VECTOR_INFO_IMAGDATA_OFFSET) >> 2];
434
435	// For complex vectors (AC analysis), ngspice stores data as an array of
436	// ngcomplex_t structs ({double real; double imag;}) in the compdata field,
437	// with realdata set to NULL.
438	if (!realDataPtr && compDataPtr && readImag) {
439		const real = new Float64Array(length);
440		const imag = new Float64Array(length);
441		const baseIdx = compDataPtr >> 3; // byte offset to float64 index
442		for (let i = 0; i < length; i++) {
443			real[i] = HEAPF64[baseIdx + i * 2];
444			imag[i] = HEAPF64[baseIdx + i * 2 + 1];
445		}
446		return { real, imag, length };
447	}
448
449	if (!realDataPtr) {
450		return null;
451	}
452
453	const real = new Float64Array(length);
454	real.set(HEAPF64.subarray(realDataPtr >> 3, (realDataPtr >> 3) + length));
455
456	let imag = null;
457	if (readImag && compDataPtr) {
458		// If both realdata and compdata exist, compdata holds ngcomplex_t structs
459		imag = new Float64Array(length);
460		const baseIdx = compDataPtr >> 3;
461		for (let i = 0; i < length; i++) {
462			imag[i] = HEAPF64[baseIdx + i * 2 + 1];
463		}
464	}
465
466	return { real, imag, length };
467}
468
469function buildScalarResult(analysisType, plotName, sweepVec, dataVecs) {
470	const scalars = {};
471
472	if (sweepVec) {
473		for (let i = 0; i < sweepVec.length; i++) {
474			scalars[sweepVec.name] = sweepVec.real[0];
475		}
476	}
477
478	for (const vec of dataVecs) {
479		scalars[vec.name] = vec.real[0];
480	}
481
482	return {
483		type: analysisType,
484		sweep: null,
485		vectors: [],
486		scalars,
487		meta: { plotName },
488	};
489}
490
491function buildVectorResult(analysisType, plotName, sweepVec, dataVecs, isComplex) {
492	const sweep = sweepVec
493		? {
494				name: sweepVec.name,
495				unit: inferUnit(sweepVec.name),
496				values: sweepVec.real,
497			}
498		: null;
499
500	const vectors = dataVecs.map((vec) => ({
501		name: vec.name,
502		unit: inferUnit(vec.name),
503		real: vec.real,
504		imag: isComplex ? vec.imag : null,
505		complex: isComplex,
506	}));
507
508	return {
509		type: analysisType,
510		sweep,
511		vectors,
512		scalars: null,
513		meta: { plotName },
514	};
515}
516
517function detectAnalysisType(plotName) {
518	const match = plotName.match(/^(tran|ac|dc|op|noise|sens|tf|pz)/i);
519	if (match) {
520		return match[1].toLowerCase();
521	}
522	return 'unknown';
523}
524
525function inferUnit(vectorName) {
526	const lower = vectorName.toLowerCase();
527	if (lower === 'time') return 's';
528	if (lower === 'frequency') return 'Hz';
529	if (/^v\(/.test(lower) || /^v_/.test(lower)) return 'V';
530	if (/^i\(/.test(lower) || /^i_/.test(lower)) return 'A';
531	if (/^p\(/.test(lower)) return 'W';
532	return '';
533}
534
535function collectTransferables(analyses) {
536	const buffers = new Set();
537
538	for (const analysis of analyses) {
539		if (analysis.sweep) {
540			buffers.add(analysis.sweep.values.buffer);
541		}
542
543		for (const vec of analysis.vectors) {
544			buffers.add(vec.real.buffer);
545			if (vec.imag) {
546				buffers.add(vec.imag.buffer);
547			}
548		}
549	}
550
551	return [...buffers];
552}
553
554// ---------------------------------------------------------------------------
555// Netlist preprocessing
556// ---------------------------------------------------------------------------
557
558function buildCircuitLines(netlist) {
559	const lines = netlist.replace(/\r/g, '').split('\n');
560	const filtered = [];
561	let inControl = false;
562
563	for (const line of lines) {
564		const trimmed = line.trim().toLowerCase();
565
566		if (/^\.control\b/.test(trimmed)) {
567			inControl = true;
568			filtered.push(line);
569			continue;
570		}
571
572		if (/^\.endc\b/.test(trimmed)) {
573			inControl = false;
574			filtered.push(line);
575			continue;
576		}
577
578		if (inControl) {
579			if (/^save\b/.test(trimmed) || /^wrdt\b/.test(trimmed)) {
580				continue;
581			}
582			filtered.push(line);
583			continue;
584		}
585
586		if (/^\s*\.save\b/i.test(line) || /^\s*\.wrdt\b/i.test(line)) {
587			continue;
588		}
589
590		filtered.push(line);
591	}
592
593	const hasEnd = filtered.some((line) => /^\s*\.end\s*$/i.test(line));
594	if (!hasEnd) {
595		filtered.push('.end');
596	}
597	return filtered;
598}
599
600// ---------------------------------------------------------------------------
601// Session & module setup
602// ---------------------------------------------------------------------------
603
604async function ensureSession(config) {
605	await ensureModule(config);
606
607	if (!filesystemReady) {
608		self.postMessage({ type: 'status', requestId: config.requestId, message: 'Staging library assets…' });
609		await stageFilesystem();
610		filesystemReady = true;
611	}
612
613	if (!callbackPointers) {
614		registerCallbacks();
615	}
616
617	if (!ngspiceInitialized) {
618		initializeNgspice();
619	}
620}
621
622async function ensureModule(config) {
623	if (!moduleReady) {
624		moduleConfig = normalizeConfig(config);
625		moduleReady = new Promise((resolve, reject) => {
626			self.Module = {
627				noInitialRun: true,
628				locateFile: (path) => {
629					if (path.endsWith('.wasm')) {
630						return resolveAssetUrl(moduleConfig.assetBaseUrl, moduleConfig.wasmFile);
631					}
632					return path;
633				},
634				print: (text) => {
635					if (currentRun) {
636						self.postMessage({ type: 'stdout', requestId: currentRun.requestId, line: text });
637					}
638				},
639				printErr: (text) => {
640					if (currentRun && !text.includes('keepRuntimeAlive() is set')) {
641						self.postMessage({ type: 'stderr', requestId: currentRun.requestId, line: text });
642					}
643				},
644				onRuntimeInitialized: () => {
645					bindApi();
646					resolve();
647				},
648			};
649
650			try {
651				importScripts(resolveAssetUrl(moduleConfig.assetBaseUrl, moduleConfig.moduleScript));
652			} catch (error) {
653				reject(error);
654			}
655		});
656	}
657
658	return moduleReady;
659}
660
661function bindApi() {
662	api = {
663		init: Module.cwrap('ngSpice_Init', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number']),
664		command: Module.cwrap('ngSpice_Command', 'number', ['string']),
665		circ: Module.cwrap('ngSpice_Circ', 'number', ['number']),
666		curPlot: Module.cwrap('ngSpice_CurPlot', 'string', []),
667		allPlots: Module.cwrap('ngSpice_AllPlots', 'number', []),
668		allVecs: Module.cwrap('ngSpice_AllVecs', 'number', ['string']),
669		getVecInfo: Module.cwrap('ngGet_Vec_Info', 'number', ['string']),
670		reset: Module.cwrap('ngSpice_Reset', 'number', []),
671		nospiceinit: Module.cwrap('ngSpice_nospiceinit', 'number', []),
672		setInputPath: Module.cwrap('ngCM_Input_Path', 'number', ['string']),
673	};
674}
675
676function registerCallbacks() {
677	callbackPointers = {
678		print: Module.addFunction(onPrint, 'iiii'),
679		status: Module.addFunction(onStatus, 'iiii'),
680		exit: Module.addFunction(onControlledExit, 'iiiiii'),
681		data: Module.addFunction(onData, 'iiiii'),
682		dataInit: Module.addFunction(onDataInit, 'iiii'),
683		bg: Module.addFunction(onBackground, 'iiii'),
684	};
685}
686
687function initializeNgspice() {
688	api.nospiceinit();
689	const rc = api.init(
690		callbackPointers.print,
691		callbackPointers.status,
692		callbackPointers.exit,
693		callbackPointers.data,
694		callbackPointers.dataInit,
695		callbackPointers.bg,
696		0,
697	);
698
699	if (rc !== 0) {
700		throw new Error(`ngSpice_Init failed with status ${rc}.`);
701	}
702
703	api.setInputPath('/');
704	api.command('set xspice_enabled');
705	api.command('source /spinit');
706	ngspiceInitialized = true;
707}
708
709function resetNgspice() {
710	if (!ngspiceInitialized || !api) {
711		return;
712	}
713
714	api.reset();
715	ngspiceInitialized = false;
716}
717
718async function stageFilesystem() {
719	ensurePath('/usr/local/lib/ngspice');
720	ensurePath('/usr/local/share/ngspice/scripts');
721
722	for (const [index, name] of MODEL_FILES.entries()) {
723		self.postMessage({
724			type: 'status',
725			message: `Loading code model ${index + 1}/${MODEL_FILES.length}: ${name}`,
726		});
727		const data = await fetchBinary(name);
728		FS.writeFile(`/usr/local/lib/ngspice/${name}`, new Uint8Array(data));
729	}
730
731	const spinitText = await fetchText('spinit');
732	FS.writeFile('/usr/local/share/ngspice/scripts/spinit', spinitText);
733	FS.writeFile('/spinit', spinitText);
734}
735
736// ---------------------------------------------------------------------------
737// Ngspice callbacks (invoked during simulation)
738// ---------------------------------------------------------------------------
739
740function onPrint(messagePtr) {
741	const text = Module.UTF8ToString(messagePtr);
742	const isStderr = text.startsWith('stderr ');
743	const isStdout = text.startsWith('stdout ');
744	const body = isStderr ? text.slice(7) : isStdout ? text.slice(7) : text;
745	const channel = isStderr ? 'stderr' : 'stdout';
746
747	// Interactive command capture: route stdout/stderr from a command()
748	// call into the per-command buffer AND emit a live event.
749	if (activeCommandCapture) {
750		if (isStderr) {
751			activeCommandCapture.stderr.push(body);
752		} else {
753			activeCommandCapture.stdout.push(body);
754		}
755		self.postMessage({ type: channel, requestId: activeCommandCapture.requestId, line: body });
756		return 0;
757	}
758
759	// Batch run path — unchanged from the original worker.
760	if (currentRun) {
761		self.postMessage({ type: channel, requestId: currentRun.requestId, line: body });
762	}
763	return 0;
764}
765
766function onStatus(messagePtr) {
767	if (currentRun) {
768		const message = Module.UTF8ToString(messagePtr);
769		self.postMessage({ type: 'status', requestId: currentRun.requestId, message });
770	}
771	return 0;
772}
773
774function onControlledExit(status, immediate, fromQuit) {
775	if (currentRun && status !== 0 && !fromQuit) {
776		self.postMessage({
777			type: 'stderr',
778			requestId: currentRun.requestId,
779			line: `shared ngspice requested exit ${status} (immediate=${Boolean(immediate)})`,
780		});
781	}
782	return 0;
783}
784
785function onData(vecvaluesAllPtr, vectorCount) {
786	const currentTime = readCurrentTimeFromData(vecvaluesAllPtr, vectorCount);
787	postDebug('data-callback', {
788		vecvaluesAllPtr,
789		vectorCount,
790		currentTime,
791		timeVectorName: currentRun?.timeVectorName,
792	});
793	emitProgress(false, currentTime);
794	return 0;
795}
796
797function onDataInit(vecinfoAllPtr) {
798	if (currentRun) {
799		const timeVectorName = findTimeVectorName(vecinfoAllPtr);
800		if (timeVectorName) {
801			currentRun.timeVectorName = timeVectorName;
802		}
803		postDebug('data-init-callback', {
804			vecinfoAllPtr,
805			timeVectorName: currentRun.timeVectorName,
806		});
807	}
808
809	if (currentRun) {
810		self.postMessage({ type: 'status', requestId: currentRun.requestId, message: 'Transient vectors initialized.' });
811	}
812	return 0;
813}
814
815function onBackground() {
816	return 0;
817}
818
819// ---------------------------------------------------------------------------
820// Progress tracking helpers
821// ---------------------------------------------------------------------------
822
823function emitProgress(force, currentTimeOverride = null) {
824	if (!currentRun || !currentRun.finalTime || currentRun.finalTime <= 0) {
825		return;
826	}
827
828	const currentTime = currentTimeOverride ?? readLatestVectorValue(currentRun.timeVectorName || 'time');
829	if (currentTime === null) {
830		return;
831	}
832
833	const now = Date.now();
834	const progress = Math.min(Math.max(currentTime / currentRun.finalTime, currentRun.lastProgress), 1);
835	if (!force && progress - currentRun.lastProgress < 0.001 && now - currentRun.lastEmitAt < 80) {
836		return;
837	}
838
839	currentRun.lastProgress = progress;
840	currentRun.lastCurrentTime = currentTime;
841	currentRun.lastEmitAt = now;
842	self.postMessage({
843		type: 'progress',
844		requestId: currentRun.requestId,
845		currentTime,
846		finalTime: currentRun.finalTime,
847		progress,
848	});
849}
850
851function readLatestVectorValue(vectorName) {
852	let vectorInfoPtr = api.getVecInfo(vectorName);
853	if (!vectorInfoPtr && !vectorName.includes('.')) {
854		const currentPlot = api.curPlot?.();
855		if (currentPlot) {
856			vectorInfoPtr = api.getVecInfo(`${currentPlot}.${vectorName}`);
857		}
858	}
859
860	if (!vectorInfoPtr) {
861		return null;
862	}
863
864	const realDataPtr = HEAPU32[(vectorInfoPtr + VECTOR_INFO_REALDATA_OFFSET) >> 2];
865	const length = HEAP32[(vectorInfoPtr + VECTOR_INFO_LENGTH_OFFSET) >> 2];
866	if (!realDataPtr || length <= 0) {
867		return null;
868	}
869
870	return HEAPF64[(realDataPtr >> 3) + length - 1];
871}
872
873function readCurrentTimeFromData(vecvaluesAllPtr, vectorCount) {
874	if (!vecvaluesAllPtr) {
875		return null;
876	}
877
878	const count = vectorCount || HEAP32[(vecvaluesAllPtr + VECVALUESALL_COUNT_OFFSET) >> 2];
879	const valuesPtr = HEAPU32[(vecvaluesAllPtr + VECVALUESALL_VALUES_OFFSET) >> 2];
880	if (!valuesPtr || count <= 0) {
881		return null;
882	}
883
884	for (let index = 0; index < count; index += 1) {
885		const vecvaluePtr = HEAPU32[(valuesPtr >> 2) + index];
886		if (!vecvaluePtr) {
887			continue;
888		}
889
890		const namePtr = HEAPU32[(vecvaluePtr + VECVALUES_NAME_OFFSET) >> 2];
891		const name = namePtr ? Module.UTF8ToString(namePtr) : '';
892		const isScale = HEAPU8[vecvaluePtr + VECVALUES_IS_SCALE_OFFSET] !== 0;
893		if (!isScale && name !== 'time' && name !== currentRun?.timeVectorName) {
894			continue;
895		}
896
897		return HEAPF64[(vecvaluePtr + VECVALUES_REAL_OFFSET) >> 3];
898	}
899
900	return null;
901}
902
903function findTimeVectorName(vecinfoAllPtr) {
904	if (!vecinfoAllPtr) {
905		return null;
906	}
907
908	const count = HEAP32[(vecinfoAllPtr + VECINFOALL_COUNT_OFFSET) >> 2];
909	const vecsPtr = HEAPU32[(vecinfoAllPtr + VECINFOALL_VECS_OFFSET) >> 2];
910	if (!vecsPtr || count <= 0) {
911		return null;
912	}
913
914	for (let index = 0; index < count; index += 1) {
915		const vecinfoPtr = HEAPU32[(vecsPtr >> 2) + index];
916		if (!vecinfoPtr) {
917			continue;
918		}
919
920		const namePtr = HEAPU32[(vecinfoPtr + VECINFO_NAME_OFFSET) >> 2];
921		if (!namePtr) {
922			continue;
923		}
924
925		const name = Module.UTF8ToString(namePtr);
926		if (name === 'time') {
927			return name;
928		}
929	}
930
931	return null;
932}
933
934// ---------------------------------------------------------------------------
935// Netlist parsing helpers
936// ---------------------------------------------------------------------------
937
938function extractTranFinalTime(netlist) {
939	for (const line of netlist.split(/\r?\n/)) {
940		if (!/^\s*\.?tran\b/i.test(line)) {
941			continue;
942		}
943
944		const tokens = line.trim().split(/\s+/);
945		if (tokens.length < 3) {
946			postDebug('tran-parse-skipped', { line, reason: 'too-few-tokens' });
947			return null;
948		}
949
950		const finalTime = parseScaledNumber(tokens[2]);
951		postDebug('tran-parse-result', { line, tokens, finalTime });
952		return finalTime;
953	}
954
955	postDebug('tran-parse-missed', { reason: 'no-tran-line-found' });
956	return null;
957}
958
959function parseScaledNumber(token) {
960	const match = token.trim().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)([a-zA-Z]+)?$/);
961	if (!match) {
962		return null;
963	}
964
965	const value = Number(match[1]);
966	const suffix = (match[2] || '').toLowerCase();
967	const multipliers = {
968		t: 1e12,
969		g: 1e9,
970		meg: 1e6,
971		k: 1e3,
972		m: 1e-3,
973		u: 1e-6,
974		n: 1e-9,
975		p: 1e-12,
976		f: 1e-15,
977	};
978
979	if (!suffix) {
980		return value;
981	}
982
983	return value * (multipliers[suffix] || 1);
984}
985
986// ---------------------------------------------------------------------------
987// Utility: memory allocation, URLs, filesystem
988// ---------------------------------------------------------------------------
989
990function allocateCStringArray(lines) {
991	const pointerSize = 4;
992	const stringPointers = lines.map((line) => allocateCString(line));
993	const arrayPointer = _malloc((stringPointers.length + 1) * pointerSize);
994
995	stringPointers.forEach((pointer, index) => {
996		HEAPU32[(arrayPointer >> 2) + index] = pointer;
997	});
998	HEAPU32[(arrayPointer >> 2) + stringPointers.length] = 0;
999
1000	return { arrayPointer, stringPointers };
1001}
1002
1003function freeCStringArray({ arrayPointer, stringPointers }) {
1004	stringPointers.forEach((pointer) => _free(pointer));
1005	_free(arrayPointer);
1006}
1007
1008function allocateCString(value) {
1009	const length = Module.lengthBytesUTF8(value) + 1;
1010	const pointer = _malloc(length);
1011	Module.stringToUTF8(value, pointer, length);
1012	return pointer;
1013}
1014
1015function normalizeConfig(config) {
1016	return {
1017		assetBaseUrl: config.assetBaseUrl || './',
1018		moduleScript: config.moduleScript || 'ngspice-lib.js',
1019		wasmFile: config.wasmFile || 'ngspice-lib.wasm',
1020	};
1021}
1022
1023function resolveAssetUrl(basePath, fileName) {
1024	return new URL(`${trimTrailingSlash(basePath)}/${fileName}`, self.location.href).toString();
1025}
1026
1027function trimTrailingSlash(value) {
1028	return value.replace(/\/$/, '');
1029}
1030
1031async function fetchBinary(fileName) {
1032	const response = await fetch(resolveAssetUrl(moduleConfig.assetBaseUrl, fileName));
1033	if (!response.ok) {
1034		throw new Error(`Failed to fetch ${fileName}: ${response.status} ${response.statusText}`);
1035	}
1036	return response.arrayBuffer();
1037}
1038
1039async function fetchText(fileName) {
1040	const response = await fetch(resolveAssetUrl(moduleConfig.assetBaseUrl, fileName));
1041	if (!response.ok) {
1042		throw new Error(`Failed to fetch ${fileName}: ${response.status} ${response.statusText}`);
1043	}
1044	return response.text();
1045}
1046
1047function ensurePath(path) {
1048	const parts = path.split('/').filter(Boolean);
1049	let current = '/';
1050	for (const part of parts) {
1051		FS.createPath(current, part, true, true);
1052		current = current === '/' ? `/${part}` : `${current}/${part}`;
1053	}
1054}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.