From acad4e72dcfb23808662e424188353828d52586d Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 8 Jul 2026 15:38:52 +0200 Subject: [PATCH 1/8] AI: Add skills regarding reference scoping for waves --- .claude/skills/igor-wave-dfref/SKILL.md | 73 ++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/.claude/skills/igor-wave-dfref/SKILL.md b/.claude/skills/igor-wave-dfref/SKILL.md index fc427a3d65..d065481793 100644 --- a/.claude/skills/igor-wave-dfref/SKILL.md +++ b/.claude/skills/igor-wave-dfref/SKILL.md @@ -63,7 +63,77 @@ The type flag /U can be combined with numeric integer type flags. The /C flag can be combined with numeric type flags The /Z flag can be combined with other flags -When WAVE is used without /Z flag and the right side is a null wave reference then the runtime error condition is set. +When a `WAVE` statement (without `/Z`) *actually executes* and its right-hand-side +expression fails to resolve to an existing wave, a runtime error is raised. +This is a different situation from a `WAVE` declaration that is simply never +reached at all due to control flow — see "Scoping and Default Initialization" below. + +### Scoping and Default Initialization + +Igor Pro has **no block scope**. A `WAVE`/`NVAR`/`SVAR`/`DFREF` declaration +written inside an `if`, `for`, or `switch` block is visible for the rest of +the function, exactly like a `Variable` or `String` declared in a block is. +The declaration is allocated for the whole function regardless of where it +textually appears; only the *assignment* is tied to that specific line +actually executing at runtime. + +Reference-typed locals (`WAVE`, `NVAR`, `SVAR`, `DFREF`, `FUNCREF`) are +automatically initialized to a null/non-existent reference at function +entry — the same way a bare `Variable` defaults to 0 and a bare `String` +defaults to a null string (not `""` — a null string is a distinct state, +distinguishable from an empty string via `strlen()`, which returns `NaN` +for a null string but `0` for `""`). If the code path containing the assignment never runs, +the reference is simply left at that safe null default. No lookup is +attempted, so no runtime error occurs: + +```igor +Function/WAVE MaybeGetData(variable condition) + + if(condition) + Make/FREE data + WAVE test1 = data + endif + + // If condition was false, test1 is still a valid, null WAVE reference here — + // not an error, not uninitialized memory. This is safe: + Make/FREE/WAVE wref = {test1} + + return wref +End +``` + +Do **not** confuse this with a `WAVE` statement that *does* execute but whose +right-hand side fails to resolve (e.g. `$name` pointing at a wave that +doesn't exist). That is a different failure mode and, without `/Z`, does +raise a runtime error: + +```igor +// This line executes every time; if "someName" isn't an existing wave, +// this errors (no /Z): +WAVE w = $someName + +// Safe form when the target might not exist: +WAVE/Z w = $someName +if(!WaveExists(w)) + // handle missing wave +endif +``` + +The distinguishing question is not "does the reference look null" but +"did the assignment statement itself run." A conditionally-assigned WAVE +reference that's never reached is a normal, safe null. A WAVE statement +that runs and can't find its target is a runtime error unless guarded +with `/Z`. + +Practical implication: it's a legitimate pattern in this +codebase to conditionally assign a WAVE reference inside a branch and use +it unconditionally afterward (typically feeding into a wave-ref array or +an `if(WaveExists(...))` check), without a separate `WAVE/Z x = $""` +pre-declaration. That pre-declaration is harmless but redundant for this +specific case — it's only necessary when you need the null-default to be +explicit/self-documenting, or when reusing the same variable name across +multiple, non-exclusive branches where the "did it run" tracking gets +less obvious. ### Referencing waves in other data folders @@ -629,6 +699,7 @@ WAVE/T wv = ListToTextWave(listStr, separatorStr) | Programming Overview (functions, parameters) | https://docs.wavemetrics.com/igorpro/programming/programming | | Programming Techniques (DF patterns) | https://docs.wavemetrics.com/igorpro/programming/programming-techniques | | WAVE keyword | https://docs.wavemetrics.com/igorpro/commands/wave | +| Conditionally-assigned WAVE ref used after the block | Safe — defaults to null if the branch didn't run, not an error | | NewFreeWave | https://docs.wavemetrics.com/igorpro/commands/newfreewave | | NewFreeDataFolder | https://docs.wavemetrics.com/igorpro/commands/newfreedatafolder | | GetDataFolderDFR | https://docs.wavemetrics.com/igorpro/commands/getdatafolderdfr | From 27c9af8bac413473ff3df9b607ef9ba47f2c49a1 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 8 Jul 2026 15:59:49 +0200 Subject: [PATCH 2/8] AI: Explain Make default sizing in Igor Pro for AI --- .claude/skills/igor-wave-dfref/SKILL.md | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.claude/skills/igor-wave-dfref/SKILL.md b/.claude/skills/igor-wave-dfref/SKILL.md index d065481793..37a6f4a13e 100644 --- a/.claude/skills/igor-wave-dfref/SKILL.md +++ b/.claude/skills/igor-wave-dfref/SKILL.md @@ -135,6 +135,45 @@ explicit/self-documenting, or when reusing the same variable name across multiple, non-exclusive branches where the "did it run" tracking gets less obvious. +### Default Size When /N Is Omitted + +`Make` (with or without `/FREE`) does not create a 0-point wave when `/N` +is omitted entirely. What size it gets depends on whether an initializer +is also given: + +* **No `/N` and no initializer** — defaults to a 1D wave with **128 + points**: + +```igor + Make/FREE data // data has 128 points, NOT 0 + Make/FREE/WAVE datasets // datasets is a wave-of-waves with 128 elements, NOT 0 + Make numericWave // numericWave has 128 points +``` + +* **No `/N`, but an explicit initializer list is given** — the wave is + sized to match the list, *not* defaulted to 128: + +```igor + Make wv = {1, 2, 3} // wv has exactly 3 points + Make/FREE/T names = {"a", "b"} // names has exactly 2 points +``` + + The initializer form implicitly determines the size; the 128-point + default only applies when there is nothing — no `/N` and no + initializer — to size the wave from. + +Curly-brace initializer lists always require at least one operand +(`Make wv = {1}` is valid, `Make wv = {}` is not) — so there is no +initializer-based way to create a wave with 0 points either. A genuinely +empty wave must be produced via `Make/N=0` or by redimensioning +(`Redimension/N=0`) after creation. + +Before assuming a `Make` call produces an empty/zero-size wave, check for +both `/N` and an initializer list. Only when *both* are absent does the +wave default to 128 points. (If `/N=(...)` is used, every dimension size +must be given explicitly — there is no partial form where some +dimensions are sized and others fall back to 128.) + ### Referencing waves in other data folders ```igor From 98006ecf4015c91e6e7e73ba37a1c5371e65494c Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 8 Jul 2026 19:27:40 +0200 Subject: [PATCH 3/8] AI: Add section for Concatenating waves with zero rows --- .claude/skills/igor-wave-dfref/SKILL.md | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.claude/skills/igor-wave-dfref/SKILL.md b/.claude/skills/igor-wave-dfref/SKILL.md index 37a6f4a13e..5501047258 100644 --- a/.claude/skills/igor-wave-dfref/SKILL.md +++ b/.claude/skills/igor-wave-dfref/SKILL.md @@ -560,6 +560,51 @@ Call it with: WAVE wv = MyWaveGetter() ``` +### Concatenate with Potentially Empty Source Waves + +`Concatenate` (e.g. with `/NP=dim` to accumulate along an existing dimension) +safely creates the destination wave even when the source wave passed to it +has zero rows. If every source wave across repeated/looped `Concatenate` +calls has zero rows, the destination wave still gets created — it ends up +with zero rows, but it is never left as a null/non-existent wave reference. + +Simple example without loop: + +```igor +Make/FREE/T/N=(0) src +Concatenate/FREE/T/NP=(ROWS) {src}, allSrc + +// allSrc is implicitly created by Concatenate and is guaranteed to exist after the call, with DimSize(allSrc, ROWS) == 0 +// same is true if src would be a numeric wave +``` + +Example with loop: + +```igor +// Let `sources` be a wave reference wave with `DimSize(sources, ROWS) > 0` containing text waves. +// Even if every `src` here is a 0-row wave (e.g. from +// ListToTextWave("", ",") — see above), this loop is safe: +for(WAVE/T src : sources) + Concatenate/FREE/T/NP=(ROWS) {src}, allSrc +endfor + +// allSrc is implicitly created by Concatenate and is guaranteed to exist after the loop, with DimSize(allSrc, ROWS) == 0 +// if nothing non-empty was ever concatenated into it. +// Note: this assumes `sources` itself has at least one row — see below for the case where it doesn't. +``` + +Do not assume `allSrc` needs a defensive `WAVE/Z` check or a pre-emptive +`Make/FREE/T/N=(0) allSrc` before the loop purely to guard against the +all-sources-empty case — `Concatenate` already guarantees the destination +exists. + +This must not be confused with `sources` itself having zero rows (i.e. there +is nothing to iterate over at all). In that case the loop body never +executes, `Concatenate` is never called, and `allSrc` is never created — it +remains a null/non-existent wave reference, per the default initialization +described in "Scoping and Default Initialization" (section 2). Referencing +`allSrc` afterward without `/Z` then fails with a runtime error. + ### Global Permanent Waves with Versioning Global permanent waves with versioning are created in wave getter functions that must be located in MIES_WaveDataFolderGetters.ipf. @@ -749,3 +794,4 @@ WAVE/T wv = ListToTextWave(listStr, separatorStr) | GetIndexedObjNameDFR | https://docs.wavemetrics.com/igorpro/commands/getindexedobjnamedfr | | NVAR_Exists | https://docs.wavemetrics.com/igorpro/commands/nvar_exists | | SVAR_Exists | https://docs.wavemetrics.com/igorpro/commands/svar_exists | +| `Concatenate` destination when every source across a loop is 0-row | Destination wave is still created, with 0 rows — never left null | From acded60cb6c5f822297006d98539a3929f4f97e2 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 19 Aug 2026 17:31:02 +0200 Subject: [PATCH 4/8] AI: Add information gathered in recent sessions - not well documented Igor Pro properties, compiler and error handling behavior - sweepformula specifics - explain more rules and conventions --- .claude/skills/igor-gotchas/SKILL.md | 358 ++++++++++++++++++++++++ .claude/skills/igor-wave-dfref/SKILL.md | 94 +++++++ .claude/skills/igortest/advanced.rst | 15 +- .claude/skills/sweepformula/SKILL.md | 165 +++++++++++ .github/copilot-instructions.md | 124 ++++++++ 5 files changed, 753 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/igor-gotchas/SKILL.md create mode 100644 .claude/skills/sweepformula/SKILL.md diff --git a/.claude/skills/igor-gotchas/SKILL.md b/.claude/skills/igor-gotchas/SKILL.md new file mode 100644 index 0000000000..2de7196f41 --- /dev/null +++ b/.claude/skills/igor-gotchas/SKILL.md @@ -0,0 +1,358 @@ +--- +name: igor-gotchas +paths: + - "**/*.ipf" +description: Confirmed Igor Pro runtime/compiler behavior that is not obvious from the command reference alone -- compilation and conditional-compilation mechanics, Execute/deferred-execution restrictions, try/catch/Abort/Debugger interaction, command-line-only restrictions, and specific command quirks (FindValue, WinList, NewPath, CaptureHistory, background tasks, compile hooks, XOP/help-file introspection). Use before writing or debugging any Igor Pro code that touches compilation, error handling, background tasks, or a command whose exact behavior matters and isn't already covered by igor-wave-dfref or igor-10. +--- + +# Igor Pro — Confirmed Runtime and Compiler Gotchas + +This document covers Igor Pro language/runtime behavior that is easy to get +wrong because it isn't obvious from a command's one-line documentation, or +because the documentation itself is ambiguous. Every entry here was confirmed +against real behavior or Igor's own reference documentation, not assumed from +general programming-language intuition. + +See also: `igor-wave-dfref` (WAVE/DFREF reference semantics), `igor-10` +(Igor Pro 10 version differences), `igor-commands` (alphabetical command +link index). + +--- + +## Compilation and Conditional Compilation + +### `#define` symbols for cross-file `#ifdef`/`#ifndef` belong in the main Procedure window + +A `#define` intended to control `#ifdef`/`#ifndef` checks in OTHER procedure +files must be set in the experiment's main Procedure window, not inside an +ordinary `.ipf` file. Per Igor's own documentation, the main procedure window +is always compiled first — a `#define` there is reliably visible to every +other file's `#ifdef` checks; one placed in a regular `.ipf` file has no such +guarantee (compilation order across included files is not something to rely +on for this). + +### `SetIgorOption poundDefine`/`poundUndefine` — the session-scoped, non-compilable alternative + +`SetIgorOption poundDefine=symb` / `poundUndefine=symb` add/remove a +conditional-compilation symbol from a global symbol list available to every +procedure window — broader in scope than a single `#define` line, and usable +without editing any file on disk. Query current state with +`SetIgorOption poundDefine=symb?` → `V_flag` (1 if defined, 0 if not). + +```igor +SetIgorOption poundDefine=IGOR_PRO_BRIDGE? +print V_flag // 0 = undefined + +Execute/P "SetIgorOption poundDefine=IGOR_PRO_BRIDGE" +Execute/P "COMPILEPROCEDURES " +``` + +Important properties: +- **Session-only** — not saved with the experiment, lost on relaunch. Must be + re-set every time a fresh Igor Pro instance/experiment needs it. +- **Not compilable** — "`SetIgorOption` is not compilable. To use it in a + user-defined function, you need to use `Execute`." Only `Execute`/`Execute/P` + can invoke it, never a bare call from inside a `Function`. +- **Triggers a recompile** the same way `RELOAD CHANGED PROCS`/ + `COMPILEPROCEDURES` do — an `#ifdef`/`#ifndef` block gated on the symbol + only takes effect after the next compile. + +### `RELOAD CHANGED PROCS` / `COMPILEPROCEDURES` — separate calls, mandatory trailing space + +These must be issued as two separate `Execute`/`Execute/P` calls, never +joined by `;` into one string, and each needs a mandatory trailing space +(`"RELOAD CHANGED PROCS "`, `"COMPILEPROCEDURES "`). Anything queued via +`Execute/P` immediately after a `COMPILEPROCEDURES` in the same batch may +simply never run — poll the compiled state directly afterward rather than +relying on a follow-up deferred callback to confirm success. + +### Compile hooks: know which one actually fires when + +- `AfterCompiledHook` fires only after a **successful** compile — never on a + failed one. Any cleanup/restart logic that must run regardless of outcome + needs an independent mechanism (e.g. a watchdog background task), not just + this hook. +- `BeforeUncompiledHook(changeCode, procedureWindowTitleStr, textChangeStr)` + fires before procedures uncompile. +- `IgorStartOrNewHook` fires both on Igor launch and on new-experiment + creation — it does not distinguish between the two on its own. + +### `FunctionInfo` resolves unqualified names relative to the CALLING function's own module, not global scope + +`FunctionInfo(functionNameStr)` with an unqualified name resolves relative to +the module the *calling* code is compiled in — not `ProcGlobal`, not global +scope. To check a different module's compile state from code running in an +Independent Module (or any non-`ProcGlobal` module), qualify explicitly: + +```igor +FunctionInfo("ProcGlobal#SomeName") +``` + +### A single compile error anywhere poisons `FunctionInfo` for everything + +When any compile error exists anywhere in the experiment, `FunctionInfo(...)` +reports `"Procedures Not Compiled"` for **every** function queried, including +unrelated, genuinely-fine ones. Do not conclude a specific module/function has +its own problem just because `FunctionInfo` fails on it — check for a compile +error anywhere in the experiment first (e.g. via the compiled-state check +this repo's tooling already uses). + +--- + +## `Execute`, Deferred Execution, and Error Handling + +### `Execute` (unqueued) cannot be called from inside a `Function` at all + +Only `Execute/P` (deferred — runs after the calling function returns control +to Igor's main loop) is legal inside a compiled function. This is a genuine +language restriction, not a tooling limitation. Several operations — +`COMPILEPROCEDURES`, `RELOAD CHANGED PROCS`, `SetIgorOption poundDefine` — +are themselves restricted to only running via `Execute`/`Execute/P` in the +first place, so this combination (`Execute/P "SetIgorOption ..."`) is often +the only legal way to invoke them from procedure code. + +### A `;`-joined command string aborts entirely on its first runtime error + +If `stmt1` errors, `stmt2` in the same `;`-joined string never runs. +Independently *queued* `Execute/P` calls are unaffected by each other's +failure this way — only text joined into one string shares fate. Prefer +separate `Execute/P` calls when a later step should still run even if an +earlier one might fail. + +### `try`/`catch`/`endtry`: a bare runtime error does not jump to `catch` by itself + +Only an explicit `AbortOnRTE` (or `AbortOnValue`) placed right after the +risky call converts a pending runtime error into a catchable abort. Per +Igor's own documentation, "When an abort occurs, execution immediately jumps +to the first statement after `catch`" — but a bare error alone, without that +explicit conversion, does not trigger this. + +```igor +try + RiskyCall() + AbortOnRTE // without this line, a runtime error in RiskyCall() + // does NOT transfer control to catch +catch + // handle it +endtry +``` + +**Put the risky call and its `AbortOnRTE`/`GetRTError(1)` check on the SAME +line** (`dummy = RiskyCall(); AbortOnRTE`), not on separate lines — Igor's +Debug-on-Error checking happens at the END OF EACH LINE, not after each +`;`-separated statement, so a pending error left unacknowledged across a line +boundary can pop a real Debugger window before the guard has a chance to run. + +### `Abort ""` pops a real alert dialog immediately — `try`/`catch` does not suppress it + +Unlike an ordinary runtime error, `Abort` with a message string shows its +alert dialog right away, before any enclosing `try`'s `catch` block runs +(wrapping it in `try`/`catch` only lets you react after the dialog has +already appeared). If a failure is purely logical/internal and must not +block on a popup, set an error status directly instead of calling `Abort` +with a message. + +### A bare `return` is invalid inside an ordinary scalar-returning `Function` + +A value-less `return` (no expression) is only valid inside a Multiple-Return- +Syntax function. In an ordinary `Function`/`Function/S`/`Function/WAVE` +declaration, the returned value's type must always match the declared return +type — `return` alone does not compile there. + +### Checking `GetRTError(1)` after an XOP call that can error + +Check it on the SAME line as the call (`SomeXOPCall(...); err = GetRTError(1)`), +not split across two lines — otherwise an unacknowledged pending error can +surface as an unexpected Debugger popup on a later, unrelated line (same +underlying mechanism as the `try`/`AbortOnRTE` line-boundary issue above). + +--- + +## Command-Line-Only Restrictions + +The following are only restrictions when typed directly at Igor's command +line (interpreted, not compiled) — the identical statement works fine inside +a compiled `Function`: + +- `WAVE/Z w = SomeFunc(...)` — assigning a wave reference from a function + call fails at the command line ("expected wave name, variable name, or + operation"). +- `Make/FREE ...` — free waves have no valid scope outside a function. +- Multiple-return-value destructuring (`[val1, val2] = SomeFunc()`). +- Calling a `static` function by its bare name — it's scoped to its file's + `#pragma ModuleName` and needs `ModuleName#FunctionName` from outside that + module (this restriction is not command-line-specific, but is easy to + mistake for one when it first surfaces there). +- Multi-line control-flow blocks (`if`/`else`/`endif`, `for`/`endfor`) — a + command line containing such a block fails as a whole with a generic error, + even though each line would be valid inside a real function. + +**Workaround for anything needing compiled-only features interactively**: +write/extend a small compiled scratch procedure file, `#include` it, compile, +then call a single compiled helper function from that file via the command +line — everything inside the function body runs as compiled code, so none of +the above restrictions apply. + +A related but distinct fact: variables/strings declared directly on the +command line persist as global command-line variables across separate +command executions within the same Igor session — they are not scoped to +one call. Re-declaring the same name later fails ("the name already exists +as a variable"); assign directly instead of re-declaring. + +--- + +## Specific Command/Function Behavior + +### `FindValue /TXOP` bit flags + +`4` = case-insensitive whole-cell text match (the pervasive default in this +codebase); `5` = `4 | 1` = case-sensitive. Use `TXOP=(1+4)` when case +matters (e.g. matching an SI unit prefix like `m` vs. `M`). + +### `NewPath`/`PathInfo`'s `S_path` always returns Igor's native colon notation, even on Windows + +Confirmed live: normalizing a Windows path via `NewPath` + `PathInfo` +produces `"C:Projects:mies_data:..."`, not backslashes — the same normalized +form MIES stores internally in places like the Analysis Browser's folder-list +wave. Anything comparing/displaying such a value should expect colon +notation regardless of host OS. + +### `WinList`'s `WIN:` bit values + +`1`=graphs, `2`=tables, `4`=layouts, `16`=notebooks, `64`=panels, +`128`=procedure windows, `512`=help windows. (`1024` is not a defined type.) + +### `ProcedureText(funcName, flags, winTitle)` — window title is the THIRD argument + +Passing a window name as the first argument silently returns `""` with no +error. To read an entire window's contents (not a specific function), pass +`funcName=""` and the window name as the third argument. + +### `CaptureHistory(refnum, stopCapturing)` and stale refnums + +Both arguments are required — a one-argument call fails to compile. A saved +numeric refnum from `CaptureHistoryStart()` is only meaningful within the OS +process that created it. If persisted in a global and the experiment is +reloaded (a real process quit+relaunch, not just a recompile), the global +still exists (`NVAR_Exists` succeeds) but the refnum itself is stale — using +it throws a runtime error. Never trust mere existence of a stored refnum-like +handle across a save/reload boundary; validate by trying to use it (wrapped +in `try`/`AbortOnRTE`/`catch`) and recreate if stale. + +### `stopmstimer(-2)` returns microseconds, not milliseconds + +Despite the name, Igor's free-running timer via `stopmstimer(-2)` returns a +value in **microseconds**. + +### `CtrlNamedBackground`'s `start=N` is only a floor, not a guarantee of ordering + +`start=N` (ticks, ~1/60s each) only sets the earliest possible first +invocation — independent of `period`. Background tasks and the deferred +`Execute/P` queue have **no guaranteed ordering relative to each other**; a +task registered "after" some queued work can still tick first. Treat +`start=` only as an empirical safety margin, never as a strict ordering +guarantee. + +### `ThreadGroupRelease(-2)` releases every currently-running thread group + +Useful inside a `BeforeUncompiledHook` to release a stray background thread +before procedures uncompile — a running thread group can otherwise block +`COMPILEPROCEDURES`/`RELOAD CHANGED PROCS` with a modal "still active" +dialog. + +### `DebuggerOptions` creates output globals wherever the current data folder happens to be + +A bare/partial-argument call to `DebuggerOptions` creates its output +variables (`V_enable`, `V_debugOnError`, `V_debugOnAbort`, +`V_NVAR_SVAR_WAVE_Checking`) in whatever data folder is current **at call +time**, regardless of which arguments were actually given. Code that calls it +purely for its toggling side effect should `KillVariables/Z` these four names +in the target folder afterward, or expect stray globals to trip a +`CHECK_EMPTY_FOLDER()`-style assertion downstream (relevant in tests). + +### Auto-indexing order in waveform assignments + +An auto-indexed waveform assignment (e.g. `Make/WAVE/N=(n) w = SomeFunc(p)`) +runs strictly in increasing index order when `Multithread` is **not** used. +With `Multithread`, per-index execution order is not guaranteed and the +right-hand side must be threadsafe. This matters whenever the called +function has order-dependent side effects. + +### Igor Pro on Windows is single-instance-per-user for command-line launches + +Launching `Igor64.exe ` while an instance is already running does +**not** spawn a new process — it signals the existing instance to load the +file, popping an unhandled "save changes?" dialog if that instance has +unsaved changes. If the existing instance is mid-quit when the launch command +runs, the load request can be silently dropped instead. Any programmatic +relaunch logic must confirm the prior process has actually exited from the +OS process list before invoking the executable again — checking that some +IPC channel has merely gone quiet is not sufficient, since that can happen +well before the process itself actually terminates. + +--- + +## Introspecting XOPs and Help Files + +### Locating XOPs, help files, and checking whether something is loaded + +Igor Pro loads XOPs/help files/fonts/procedure files from two merged +locations: under the Igor Pro program folder, and +`/WaveMetrics/Igor Pro User Files/` — both mirror the +same subfolder names (`Igor Extensions`, `Igor Extensions (64-bit)`, +`Igor Fonts`, `Igor Help Files`, `Igor Procedures`, `User Procedures`). Check +both when looking for an XOP's help file; not every XOP ships one. + +To check whether an XOP's functions/operations are actually loaded: +`FunctionList("*", ";", "KIND:4")` lists XOP functions, +`OperationList("*", ";", "external")` lists XOP operations — check both, and +be aware that once an XOP is loaded it can't be toggled/unloaded mid-session, +and neither list attributes a name back to its owning XOP (only naming +convention, or the PE-resource technique below, can do that). + +### `.ihf` help files are themselves Igor notebooks + +Read them with `OpenNotebook/R` (fails with error 251 if that file's +help-window view is already open elsewhere — a help-file view and a plain +notebook view of the same file are mutually exclusive). Export via +`SaveNotebook/O/S=5/H={...}` (HTML) to recover genuine structure: each +paragraph gets a `

` matching WaveMetrics' own semantic style +convention — `Topic`, `Subtopic`/`Subtopic-Indented`, +`TopicBody1`/`TopicBody1a`, `Steps`/`ListNumbered`, +`Code1`/`Code1a`/`Code-Indented1`, `SeeAlso`/`NOTE`/`Table2Col`/`Table3Col`/ +`RelatedTopics`. Useful for reliably parsing structure out of any Igor help +file rather than treating it as flat text. + +### Recovering a closed-source XOP's operations/functions with no vendor docs + +A closed-source `.xop`'s Igor-visible operations/functions are recoverable +from standard Windows PE resources named `"XOPC"` (operations) and `"XOPF"` +(functions), resource ID 1100 — any generic PE resource reader (e.g. Python's +`pefile`) can extract them without vendor documentation: + +- `XOPC` records: `{null-terminated name; int16 LE category bitmask}*`, + terminated by an empty-name record. +- `XOPF` adds a return-type code and per-parameter type codes to each entry. + +Useful for any closed-source XOP in this repo (`MultiClamp700xCommander64.xop`, +`itcXOP2-64.xop`, `MIESUtils-64.xop`, `TUF-64.xop`, `SutterXOP_Win-64.xop`) +with no available documentation. + +--- + +## Reference URLs + +| Topic | URL | +|---|---| +| SetIgorOption | https://docs.wavemetrics.com/igorpro/commands/setigoroption | +| Execute | https://docs.wavemetrics.com/igorpro/commands/execute | +| Abort | https://docs.wavemetrics.com/igorpro/commands/abort | +| GetRTError | https://docs.wavemetrics.com/igorpro/commands/getrterror | +| FunctionInfo | https://docs.wavemetrics.com/igorpro/commands/functioninfo | +| FindValue | https://docs.wavemetrics.com/igorpro/commands/findvalue | +| WinList | https://docs.wavemetrics.com/igorpro/commands/winlist | +| NewPath | https://docs.wavemetrics.com/igorpro/commands/newpath | +| CaptureHistory | https://docs.wavemetrics.com/igorpro/commands/capturehistory | +| CtrlNamedBackground | https://docs.wavemetrics.com/igorpro/commands/ctrlnamedbackground | +| ThreadGroupRelease | https://docs.wavemetrics.com/igorpro/commands/threadgrouprelease | +| DebuggerOptions | https://docs.wavemetrics.com/igorpro/commands/debuggeroptions | diff --git a/.claude/skills/igor-wave-dfref/SKILL.md b/.claude/skills/igor-wave-dfref/SKILL.md index 5501047258..75b63fb3b4 100644 --- a/.claude/skills/igor-wave-dfref/SKILL.md +++ b/.claude/skills/igor-wave-dfref/SKILL.md @@ -749,6 +749,100 @@ WAVE/T wv = ListToTextWave(listStr, separatorStr) // code working with wv ``` +### Wave Versioning Migration Must Use Independent `if` Blocks + +When a wave getter upgrades an old wave layout across multiple versions, use a +sequence of independent `if(WaveVersionIsSmaller(wv, N))` blocks (`N` +increasing), never an exclusive `if/elseif` chain. An `elseif` chain can skip +needed intermediate migration steps for a wave that is several versions behind +the latest. + +When a migration step widens a wave's dimensions, `Redimension` to the new +size **before** writing to any newly-added column/row index -- Igor +bounds-checks wave assignments, so writing to column 3 of a wave still sized +at 3 columns (valid indices 0..2) throws a runtime error instead of migrating: + +```igor +// WRONG -- writes before the wave is big enough: +wv[][3] = someValue // errors if wv only has 3 columns (0..2) +Redimension/N=(-1, 4) wv + +// CORRECT -- resize first, then write: +Redimension/N=(-1, 4) wv +wv[][3] = someValue +``` + +### A `WAVE name = expr` Declaration Cannot Reference Its Own Name in `expr` + +The compiler rejects a `WAVE name = expr` declaration where `name` itself +appears inside `expr` as an argument, even if `name` was already declared +earlier in the function: + +```igor +// WRONG -- does not compile: +WAVE data = SomeFunc(data) + +// CORRECT -- introduce a second reference under a different name: +WAVE/Z tmp = data +WAVE data = SomeFunc(tmp) +``` + +This differs from a destructuring *reassignment* via Multiple Return Syntax +(e.g. `[out, outT] = SomeFunc(out, outT)`), which is legal -- it updates +already-declared references rather than re-declaring them. + +### `Make/N=(...)`: a Trailing Dimension Size of 0 vs. 1 + +An explicit dimension size of `0` in `Make/N=(...)` means "this dimension +does not exist," while `1` creates a real, if trivial, additional dimension. +`Make/N=(n, 1, 1, 1)` is **not** equivalent to a true 1D wave -- +`DimSize(wv, COLS)` is `1`, not `0`, in that case. MIES convention: a +wave-of-waves value that must be strictly 1D (e.g. asserted via +`DimSize(wv, COLS) == 0` in some helpers, or `GetWaveDimensionality(wv) == +ROWS` -- see `MIES_Utilities_WaveHandling.ipf` -- as the idiom for "this wave +is 1D") should be created with `0`/omitted trailing dimensions, never `1`. + +### `Variable/G name = value` Overwrites an Existing Global Every Time + +`Variable/G name = value`, with an explicit initializer, overwrites the +global's current value every time that line executes, even if the global +already exists (per the Igor Reference: "`/G` ... overwrites any existing +variable"). For code that must run unconditionally on every invocation +without resetting an existing value, use the bare form with no initializer: + +```igor +// WRONG if this runs more than once and the value should persist between calls: +Variable/G root:Packages:MyPkg:counter = 0 + +// CORRECT -- creates at 0 only if missing, leaves an existing value untouched: +Variable/G root:Packages:MyPkg:counter +``` + +No `NVAR_Exists`-style guard is needed for this bare form. + +### Never Name a Local After an Igor Built-in Function or Keyword + +The compiler does not stop a local variable/string/`WAVE` reference from +being named after a built-in Igor function or reserved keyword (e.g. +`string log` shadows `log()`). This compiles cleanly but silently breaks any +code in that scope expecting the real built-in behavior. Never name a +variable/string/WAVE reference after an Igor built-in function or reserved +keyword -- check the name against `.claude/skills/igor-commands/SKILL.md` +before choosing it if there's any doubt. Note that `ipt lint`'s +`BugproneReservedKeywordsAsIdentifier` check only flags shadowing a reserved +**keyword/type** (e.g. `variable wave`), not a built-in **function** name +(e.g. `variable abs`) -- it has no symbol-resolution semantics for that case, +so this specific mistake must still be caught by manual review. + +### WAVE/DFREF References Are Not Scoped by Independent Modules + +Independent Modules cannot call functions in other modules except through +`Execute` (per Igor's own "Advanced Topics" documentation on Limitations of +Independent Modules) -- but this restriction does **not** apply to direct +`WAVE`/`DFREF` references, which aren't module-scoped at all. Code in an +Independent Module needing only wave/data-folder access (no function calls) +into/out of another module needs no `Execute` indirection. + --- ## 11. Quick Reference Table diff --git a/.claude/skills/igortest/advanced.rst b/.claude/skills/igortest/advanced.rst index d2903cf686..82b9bd4366 100644 --- a/.claude/skills/igortest/advanced.rst +++ b/.claude/skills/igortest/advanced.rst @@ -317,9 +317,18 @@ Supported types for `arg` are variable, string, complex, Integer64, data folder references and wave references. The type of the returned wave of the attributed data generator function must fit to the argument type that the multi data test case takes. -The data generator function name must be attributed with a comment within four -lines above the test cases Function line. The key word is `IUTF_TD_GENERATOR` with -the data generators function name following as seen in the simple example here. +The data generator function name must be attributed with a comment above the +test case's Function line, using the key word `IUTF_TD_GENERATOR` with the data +generator's function name following, as seen in the simple example here. This +scan is not limited to a fixed number of lines -- confirmed against the +implementation (`GetFunctionTagWave` in +`Packages/igortest/procedures/igortest-functiontags.ipf`): it considers every +comment line between the end of the previous function and the current +`Function` line (matching the "all lines above Function up to the previous +Function" statement above), trying each known tag pattern against every +non-empty line and silently skipping any line that doesn't match. This means +ordinary `///` doc-comment lines can be freely interspersed above a +`// IUTF_TD_GENERATOR ...` tag line without breaking the attribution. If no data generator is given or the format of the test case function does not fit to the wave type then a error message is printed and the test run is aborted. diff --git a/.claude/skills/sweepformula/SKILL.md b/.claude/skills/sweepformula/SKILL.md new file mode 100644 index 0000000000..4fc9b8163b --- /dev/null +++ b/.claude/skills/sweepformula/SKILL.md @@ -0,0 +1,165 @@ +--- +name: sweepformula +paths: + - "**/MIES_SweepFormula*.ipf" + - "**/UTF_SweepFormula*.ipf" +description: Architectural facts about MIES's SweepFormula subsystem (dataset wrapping, array-literal evaluation, plotter targeting, nested-execution source-location tracking) that are not obvious from reading a single operation in isolation. Use before adding or modifying a SweepFormula operation, touching the executor/array-evaluation code, or debugging an error-location/assert-data-stack problem. +--- + +# SweepFormula — Architectural Reference + +SweepFormula (`MIES_SweepFormula*.ipf`) is MIES's scripting language for data +evaluation. This document covers structural conventions that span multiple +files and are easy to violate by only looking at one operation's code. Also +read `Packages/doc/SweepFormula.rst` (the docu skill points you there for any +SweepFormula documentation question) and `.claude/skills/igor-wave-dfref` for +general WAVE/DFREF semantics. + +--- + +## Dataset Wrapping Convention + +Every SweepFormula operation result is a "dataset" — a single-element +`WAVE/WAVE` container (typically built via `SFH_CreateSFRefWave`) carrying an +`SF_META_DATATYPE` JSON wave note (set/read via `JWN_SetStringInWaveNote`/ +`JWN_GetStringFromWaveNote`) that identifies its kind (e.g. +`SF_DATATYPE_SELECTCOMP`, `SF_DATATYPE_SELECTTAG`). + +- `SFH_GetOutputForExecutorSingle(data, ..., dataType=X)` wraps whatever it's + given in a **new** wrapper wave, setting the note on that new wrapper — it + never tags `data` itself. +- `select()` is a deliberate counter-example: it builds its own composite + wrapper directly, sets the note on it, and returns via + `SFH_GetOutputForExecutor` — skipping `SFH_GetOutputForExecutorSingle` + entirely. Don't assume every operation goes through the single-wrap helper. +- `seltag` needs **two** levels of wrapping so the array-literal executor + doesn't misinterpret a multi-tag `seltag([a,b])` result as a plain text + wave and array-expand its elements. The datatype note must be set on the + **inner** wrapper (which becomes `genericElement[0]` inside an array + literal), not just the outer one. + +## Array Literals Mixing Scalars and Datasets + +For an array literal (`[a, b, c]`) that may mix scalar/text elements with +dataset (`WAVE/WAVE`) elements, the executor: + +1. Prescans every element **exactly once** via `SF_ResolveDatasetFromJSON` + (never resolve the same element twice — resolution can execute operations + with side effects) to determine if any element is dataset-kind. +2. If any element is a dataset, the **whole array** is promoted to a uniform + wave-of-datasets accumulator (`outW`), with plain scalar/text elements + individually wrapped in their own single-element `"PromotedArrayElement"` + dataset (no `SF_META_DATATYPE` note on that wrapper). +3. A dataset's own internal dimensionality must never leak into the outer + array's shape — guard any dimension-widening logic with + `if(!WaveExists(outW))` so the dataset accumulator stays strictly 1D + regardless of what's inside each element. + +`SFH_GetArgumentSelect` correspondingly checks `IsWaveRefWave(array)` rather +than `IsTextWave(array)`, since array elements in this path are direct wave +references, not stringified markers. + +## Plotter Targeting Is Entirely Outside the JSON Executor + +`and`/`with` are plotter-targeting keywords, not executor syntax: + +- A SweepFormula expression cannot contain line breaks, and `and`/`with` must + each stand alone on their own line — so they can never appear inside an + expression actually parsed by `SFE_ExecuteFormula`/ + `SFE_ExecuteVariableAssignments`. They are recognized in an earlier, + separate notebook-text-splitting step, before the executor ever sees the + expression text. +- They only control **where the plotter places each expression's result**: + `with` = same sub-window as the previous expression, `and` = a new + sub-window. There is no way to feed `and`/`with` through the executor, even + via a nested/dynamically-generated formula string. +- SweepFormula renders into a separate, dedicated plotter panel — **never** + the host DataBrowser/SweepBrowser's own graph. The window name is + deterministic: `SF_GetDataDisplayWindowName(graph, SF_DISPLAYTYPE_GRAPH, + SF_DM_SUBWINDOWS, 0)` (static, module `MIES_SF` — needs `MIES_SF#` + qualification from outside) returns the fully-qualified subwindow name, + e.g. `"SweepFormula_plotsweepBrowser_graph#graph0"` for host graph + `"SweepBrowser"`. Each SweepBrowser/DataBrowser gets its own independently + named plot window keyed off its own `graph` argument (MIES supports + multiple simultaneous SweepBrowsers). `SF_DM_NORMAL` gives a wrong/ + non-existent name for this case, and the `SF_DM_SUBWINDOWS` result already + includes the `#graph0` suffix — don't append it again. +- Operations accepting a `seltag` argument (e.g. `ivscc_apfrequency()`) + auto-group by existing experiment tags when no explicit `seltag` is given. + +## Composing New Operations Out of Existing Ones + +An operation can implement itself by re-entering the real formula executor +with dynamically-built source text, reusing other real operations as +building blocks, via this pattern: + +1. `Duplicate/FREE` the per-graph `GetSFVarStorage(graph)` (a `WAVE/WAVE` + keyed by variable name) as a backup. +2. Build an ordinary SweepFormula source string on the fly and run it via + `SFE_ExecuteVariableAssignments(graph, formula, allowEmptyCode=1)`, which + mutates the **live** `varStorage` in place. +3. Read back whatever result is needed by name. +4. `Duplicate/O backup, varStorage` to wipe all scratch variables, then + re-add only the specific desired outputs via `SFH_AddVariableToStorage` — + this keeps the operation's own temporary variable names from leaking into + the user's persistent SweepFormula environment. + +Exception safety is a non-issue here: if the nested call aborts, the restore +step (4) is simply skipped, but that's fine — a failed evaluation just means +"no result" (SweepFormula never updates an already-displayed plot in place +on failure), and the next run's `SFE_ExecuteVariableAssignments` unconditionally +wipes `varStorage` back to 0 rows regardless of what a prior aborted run left +behind. + +## Nested-Execution Source-Location Tracking Is a LIFO Stack + +Source-location tracking for (possibly nested) formula execution uses a +stack, not a single flat frame: + +- `GetSFAssertDataStack()` (`MIES_WaveDataFolderGetters.ipf`, `WAVE/WAVE`, + lazily created) holds the stack. `GetSFAssertData()` returns the top frame, + auto-pushing a base frame if the stack is empty. +- `SFH_PushAssertDataFrame()`/`SFH_PopAssertDataFrame()` + (`MIES_SweepFormula_Helpers.ipf`) manage nested execution frames — + centralized via a `newFrame` flag parameter on + `SFE_ExecuteVariableAssignments`/`SFE_ExecuteFormula`, rather than each + caller manually bracketing its own call. +- On abort, the pop is deliberately **skipped** so the frame's data survives + for the aggregate error message. `SFH_PopAssertDataFrame` asserts against + popping the base frame, and deliberately does **not** release JSON ids + itself — a normal return already released them via the ordinary success + path, so releasing again would double-release. +- The live global position trackers (`GetSweepFormulaJSONPathTracker()`/ + `GetSweepFormulaBufferOffsetTracker()`) only ever reflect whatever is + executing *right now* — so each frame's rendered location message is + frozen into a `LOCMSG` field at the moment a deeper frame is pushed on top + of it, while the live trackers still reflect that outer frame's own + position at that time. +- `SFH_GetAssertLocationMessage` walks the stack top-to-bottom, joining more + than one non-empty frame's message with `"\rCalled from:"`. +- Only the **outermost** frame (`SFH_GetOutermostAssertDataFrame()`, i.e. + `stack[0]`) has `LINE`/`OFFSET` that are real, on-screen notebook + positions — `SF_CalculateErrorLocationInNotebook` must read that specific + frame, not whatever is currently on top of the stack. +- `SFH_ResetAssertDataStack()` (called from `SF_ClearSFOutputState()`) + releases every remaining frame's `JSONID`/`SRCLOCID` via + `JSON_Release(..., ignoreErr=1)` then empties the stack — necessary + because stale frames from an aborted run would otherwise corrupt the + *next* run's error-location tracking. **Any test that deliberately aborts + a nested formula must call `SFH_ResetAssertDataStack()` itself afterward** + for the same reason. + +--- + +## Reference + +- `Packages/doc/SweepFormula.rst` — user-facing behavior and array/operation + evaluation semantics (e.g. empty-array and mixed numeric/text handling). +- `Packages/MIES/MIES_SweepFormula_Executor.ipf` — `SFE_FormulaExecutor` + (array/object/string dispatch), `SFE_ConvertNonFiniteElements`. +- `Packages/MIES/MIES_SweepFormula_Helpers.ipf` — dataset resolution + (`SF_ResolveDatasetFromJSON`, `SFH_ResolveDatasetElementFromJSON`), + assert-data-stack management. +- `Packages/MIES/MIES_SweepFormula_Operations.ipf` — individual operation + implementations; the primitive `+ - * /` operators live here + (`SFO_IndexOverDataSetsForPrimitiveOperation`). diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 33ca9a72de..bcf3a660e6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -83,8 +83,86 @@ End - Use `ASSERT_TS()` for internal consistency checks in threadsafe functions - Use `FATAL_ERROR()` for code paths that unconditionally create an error - Use `SFH_ASSERT()` in SweepFormula operations for user-facing errors +- Use `SFH_FATAL_ERROR(message, [jsonId])` instead of `SFH_ASSERT(0, message)` for SweepFormula + code paths that unconditionally abort -- the dedicated name makes "this always aborts" explicit + at the call site - Use `DEBUGPRINT()` for debug output (only active when `DEBUGGING_ENABLED` is defined) +### Constants + +- Constants must be `static` whenever not needed cross-file, and declared at the top of the + procedure file in a dedicated block, not inline near their first use +- Non-static/global constants belong only in `MIES_Constants.ipf` -- a new global constant + introduced elsewhere is a convention violation + +### Entry Guard: `PerformSubsystemEntry()` + +Public (non-`static`) functions in MIES generally start with `PerformSubsystemEntry()` (a +`threadsafe` wrapper around `AssertOnAndClearRTError()`, defined in +`MIES_Utilities_ProgramFlow.ipf`) as their first executable statement: + +- Insert it as the literal first executable statement after only plain `variable`/`string`/ + `STRUCT` declarations with non-function-call initializers (or as the very first line if there + are none) +- Split a declaration whose initializer is a scalar/string function call into a bare declaration + plus a separate assignment placed after the guard; literal initializers don't need splitting +- `WAVE`/`DFREF`/`NVAR` declarations with function-call initializers are never split -- insert the + guard immediately before such a line instead +- Do not add it to: `static` functions; pure public-to-public pass-throughs (the callee gets its + own guard, though a pass-through to a `static` callee still needs it since it is the sole public + entry point); empty prototype/`FUNCREF`-template functions; GUI control-event callbacks; + `PopupMenu value=#` functions; `CtrlNamedBackground proc=` callbacks; `SetWindow + hook(...)=`/`tooltipHook(...)=` functions; functions invoked only via `Menu`/`TablePopup`/ + `userdata(Items)=` popup-extension buttons (these start from interpreted Igor Pro mode, not a + normal function-call chain) + +### Conditional Compilation Branches (`#if exists(...) / #else`) + +When a function is split into two independently-compiled real implementations behind +`#if exists(...) #define X_PRESENT #else ... #endif` (e.g. `AMPLIFIER_XOPS_PRESENT` in +`MIES_AmplifierInteraction.ipf`), both branches are real public functions with the same name and +must receive equal treatment (entry guards, doc comments, etc.) in any repo-wide rollout of a new +convention -- it is easy to update only the branch currently compiled in your own environment and +miss the other one. + +### Scratch Code + +`Packages/MIES/MIES_ClaudeScrapCode.ipf` (`#include`d from `Packages/MIES_Include.ipf`) is a +permanently-uncommitted scratch procedure file for throwaway helper/test code written during +interactive AI-assisted sessions. Neither the file's contents nor its `#include` line are ever +meant to be committed. Check and clean it up when switching branches -- scratch code calling +`static`/module-qualified functions from one branch's task may fail to compile, or silently do +something meaningless, on a different branch's code. + +## Common Pitfalls + +- `GetNotebookText(win, mode=N)`'s `mode` parameter is not interchangeable across call sites -- a + mode value copied from an unrelated call site can silently return an empty string for real + content. Check the specific mode the real code path you're touching actually uses (e.g. + `SF_GetCode` uses `mode=2` for the SweepFormula notebook) rather than assuming a mode number + transfers. +- `SF_SetFormula(win, formula)` (non-static, `MIES_SweepFormula.ipf`) is the documented public API + for setting the SweepFormula notebook's contents (wraps `ReplaceNotebookText`) -- use it instead + of manipulating the notebook directly; pass `""` to clear. +- `PGC_SetAndActivateControl` (`MIES_ProgrammaticGUIControl.ipf`) cannot drive row *selection* on a + "mode=9" (treeview/checkbox-style) `ListBox` -- neither the PGC call nor a raw `ListBox + ..., selRow=row` changes selection bits for this style. Selection must be set directly by + writing the `LISTBOX_SELECT_OR_SHIFT_SELECTION` bit into the control's `selWave` (the mechanism + the existing `ListBoxSelectAll`/Ctrl+A handler already uses). Not every GUI interaction has a + PGC equivalent. +- Never end a `//` comment with a semicolon, regardless of indentation. + `tools/check-code.sh`'s trailing-semicolon regex has a blind spot on indented comment lines + (leading whitespace can be consumed by its backtracking match), so an indented `// comment;` + can slip through locally but still trip the same check elsewhere (e.g. the pre-push hook) -- + treat the rule as absolute rather than relying on the local check catching every case. +- A hardware hook function referenced only by name string (e.g. + `sprintf endFunc, "P_NI_StopDAQ(...)"` for a DAQmx `EOSH=` callback) is not caught by the + compiler if that function is later made `static` or renamed -- this is a real runtime-only risk + worth calling out explicitly during refactors rather than assuming the compiler would catch it. +- A duplicate function name across two independently-included copies of the same file is a real, + specific compile error (`error: the name "X" already exists as a function`) -- relevant if a + procedure file could ever be reachable both by direct load and via `#include` at the same time. + ## Build and Test ### Pre-commit Hooks @@ -94,6 +172,35 @@ The repository uses pre-commit hooks configured in `.pre-commit-config.yaml`: - Run `./tools/run-ipt.sh lint -i --noreturn-func='FATAL_ERROR|SFH_FATAL_ERROR|FAIL'` for IPF formatting - Custom code checks via `./tools/check-code.sh` +### Igor Programming Tool (`ipt`) + +`tools/ipt`/`tools/ipt.exe` (invoke via `tools/run-ipt.sh`) parses real Igor Pro source into a +genuine AST/symbol table without needing a live Igor Pro instance. Reach for it proactively in any +Igor Pro code task, not only when explicitly asked for a parse/AST -- use it as a standing habit +alongside direct reading of the source, not as a replacement for it: + +- `ipt check [--print-ast] ` -- confirm a file/edit actually parses, or get an authoritative + AST (node types, precise line:column spans). +- `ipt lint ` -- catches known bug/style patterns (e.g. + `BugproneReservedKeywordsAsIdentifier`, which flags a variable named after a reserved + **keyword/type** like `variable wave`, but does **not** flag one named after a built-in + **function** like `variable abs`/`string log` -- it has no symbol-resolution semantics for that + case, so built-in-function-name shadowing still needs manual review). +- `ipt rename --print-symbol-table ` (with a full `-f`/`-l`/`-c`/`-n` target to avoid a + known no-target crash) -- a genuine cross-referenced symbol table (every + declaration/read/write/definition point with exact spans, function signatures); also usable for + the rename itself. +- `ipt format` -- run this after every manual edit to a `.ipf` file, every time, not just when + restructuring -- it reformats the whole file consistently (aligned `=` across consecutive + assignment/declaration lines, a blank line separating local declarations from the first + statement), matching this repo's canonical formatting. +- A clean `ipt check`/`ipt lint` pass only confirms syntax/style -- it does **not** confirm test + assertions or logic are correct (e.g. a dimension-mismatched `Make/FREE` between two waves being + compared can parse/lint cleanly and still fail at runtime). Always run new/changed tests live + against Igor Pro, never rely on `ipt` alone as a substitute for that. +- `ipt` only knows about files explicitly passed via `files`/`-f` -- it does not resolve + `#include`s itself, so pass every file actually relevant to the question at hand. + ### Running Tests Tests are run via Igor Pro experiments (.pxp files): @@ -123,6 +230,23 @@ Test categories: - Data generator functions must be in Packages/tests/UTF_DataGenerators.ipf and declared as static - Any new utility function that is created for a generic task in either MIES_Utilities*.ipf or MIES_MiesUtilities*.ipf must have its own test cases - Test cases may call static MIES functions by prefixing the ModuleName defined in the MIES procedure files when AUTOMATED_TESTING is defined +- Run a single test case via `RunWithOpts(testcase="TestName", testsuite="UTF_SomeFile.ipf")` + (MIES's own wrapper, in `Packages/tests/UTF_HelperFunctions.ipf`, around IgorTest's `RunTest`) -- + never call the (`static`, module-scoped) test function directly. A direct call bypasses the + framework's fixture setup/teardown (e.g. leftover windows from earlier manual work can cause + spurious failures that `RunWithOpts` avoids via its normal per-test cleanup) +- `testsuite` defaults to `GetDefaultTestSuitesForExperiment()` if omitted; `testcase` defaults to + every test case in the suite. `enableRegExp=1` matches both `testsuite`/`testcase` as anchored, + case-insensitive regexes, but then `testsuite` must include the `.ipf` extension +- A test that deliberately triggers an internal `ASSERT`/prints `"!!! Assertion FAILED !!!"` + inside its own `try`/`catch` (to verify a function correctly rejects bad input) is expected + console output, not a real failure -- it will not appear in the suite's final failure list +- `TestEndCommon()` (`Packages/tests/UTF_HelperFunctions.ipf`) must never call the ZeroMQ XOP's + raw `zeromq_stop()` unconditionally -- that operation tears down *every* ZeroMQ socket in the + whole Igor Pro instance (not just test-related ones), which can kill an unrelated, live ZeroMQ + connection (e.g. an interactive AI-assisted bridge session) process-wide. Any such cleanup call + must be gated (e.g. behind `#ifndef IGOR_PRO_BRIDGE`) so it still runs in CI but can be excluded + when something else in the same process depends on ZeroMQ staying up ### Building Documentation From 04ba71e61248c249574b7e682f962dbbdf899804 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 19 Aug 2026 18:54:32 +0200 Subject: [PATCH 5/8] AI: Add MultiThread works on every wave type, for(elem : wv) loops --- .claude/skills/igor-gotchas/SKILL.md | 40 +++++++++++++++++++++++ .claude/skills/igor-wave-dfref/SKILL.md | 42 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/.claude/skills/igor-gotchas/SKILL.md b/.claude/skills/igor-gotchas/SKILL.md index 2de7196f41..303c9fcb50 100644 --- a/.claude/skills/igor-gotchas/SKILL.md +++ b/.claude/skills/igor-gotchas/SKILL.md @@ -278,6 +278,46 @@ With `Multithread`, per-index execution order is not guaranteed and the right-hand side must be threadsafe. This matters whenever the called function has order-dependent side effects. +### `MultiThread` works with any wave type -- the restriction is on the expression, not the wave + +The `MultiThread` keyword (in front of a wave assignment statement inside a +function, e.g. `MultiThread w = expr`) has no restriction based on the +destination or source wave's data type. Confirmed against Igor's own +"MultiThread" keyword reference and the "Automatic Parallel Processing with +MultiThread" article (Advanced Topics.ihf): the entire discussion is about +whether the *expression* (and any function it calls) is thread-safe, never +about which wave type is involved. Igor's own docs explicitly cover +`MultiThread` with numeric waves, and separately confirm it for wave +reference waves (`WAVE/WAVE`, "You can use a wave reference wave as a list +of waves for further processing and in multithreaded wave assignment using +the MultiThread keyword") and data folder reference waves (`WAVE/DF`, same +wording) -- Advanced Topics.ihf has dedicated worked examples for both +("Wave Reference MultiThread Example", "Data Folder Reference MultiThread +Example"), plus one for structure arrays ("Structure Array MultiThread +Example"). This repo's own code confirms `MultiThread` with **text** waves +too, e.g. `SFE_ConvertNonFiniteElements` +(`MIES_SweepFormula_Executor.ipf`) reads a `WAVE/T` source +(`subArray[p][q][r][s]`) via `MultiThread`, and +`SFE_FormulaExecutor` writes into a `WAVE/T` destination +(`Multithread outT[index][][][] = outT[index][0][0][0]`). + +The real constraints are about the *expression*, not the wave type: +- It must be thread-safe -- any function it calls (built-in or + user-defined) must be thread-safe; user-defined functions need the + `ThreadSafe` keyword. +- Do not reference any point of the destination wave other than the + current point (`p`/`q`/`r`/`s`) being computed -- e.g. + `wave1 = wave1[p+1] - wave1[p-1]` gives indeterminate results. +- A thread-safe function called from the expression must not resize/ + retype/kill any wave passed to it, write to a text wave passed to it, or + write to a variable passed by reference; any waves/globals it creates + itself disappear when the assignment finishes; and it cannot use `WAVE`/ + `NVAR`/`SVAR` to reach into the main thread's data folder tree (each + thread has its own private data folder tree). +- Only worth the overhead for a destination with a large number of points, + or an expensive expression -- for small waves `MultiThread` can be + slower than the unthreaded assignment. + ### Igor Pro on Windows is single-instance-per-user for command-line launches Launching `Igor64.exe ` while an instance is already running does diff --git a/.claude/skills/igor-wave-dfref/SKILL.md b/.claude/skills/igor-wave-dfref/SKILL.md index 75b63fb3b4..63aac2fd17 100644 --- a/.claude/skills/igor-wave-dfref/SKILL.md +++ b/.claude/skills/igor-wave-dfref/SKILL.md @@ -843,6 +843,48 @@ Independent Modules) -- but this restriction does **not** apply to direct Independent Module needing only wave/data-folder access (no function calls) into/out of another module needs no `Execute` indirection. +### `for(elem : wv)` Range-Based Loops Are Equivalent to an Indexed Loop over `wv[i]` + +A range-based for loop (`for( varName : ) ... endfor`, added in +Igor Pro 9.00) iterates over every element of a wave, regardless of its +dimensionality: + +```igor +for(String s : tw) + Print s +endfor +``` + +- The loop variable's type must match the wave's element type: `string` for + a text wave, `WAVE` for a wave-reference wave, `DFREF` for a data-folder- + reference wave. For a numeric wave the type doesn't need to be an exact + match (e.g. a `variable` loop var is fine even over an integer wave). The + type can be omitted entirely if the loop variable (or the wave expression, + if it's a wave reference) was already declared earlier in the function. +- For a multi-dimensional wave, iteration order is **column-major**: for a + 2D wave, all rows of column 0, then all rows of column 1, and so on. + +This is genuinely equivalent to an indexed loop reading `wv[i]` for +`i = 0` to `numpnts(wv) - 1`, confirmed both by Igor's own documentation and +empirically for a multi-dimensional wave: + +```igor +// Equivalent to: for(v : wv) ... endfor +for(i = 0; i < numpnts(wv); i += 1) + variable elem = wv[i] + // ... +endfor +``` + +This works because Igor's single-bracket point-indexing (`wv[i]`) is not +limited to addressing row `i`, column 0 on a multi-dimensional wave -- +once the index exceeds the row count, it continues into column-major +linear addressing across the wave's remaining dimensions, exactly matching +`numpnts(wv)` and the range-based loop's own traversal order. Confirmed live +against a 3x4 numeric wave: both `for(i=0; i Date: Wed, 19 Aug 2026 19:15:18 +0200 Subject: [PATCH 6/8] AI: Add section for wave indexing --- .claude/skills/igor-wave-dfref/SKILL.md | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.claude/skills/igor-wave-dfref/SKILL.md b/.claude/skills/igor-wave-dfref/SKILL.md index 63aac2fd17..8fc54b2b39 100644 --- a/.claude/skills/igor-wave-dfref/SKILL.md +++ b/.claude/skills/igor-wave-dfref/SKILL.md @@ -885,6 +885,53 @@ against a 3x4 numeric wave: both `for(i=0; i 2; wv2d[2][1] = 2 + 10*1 = 12) +wv2d[2.5][1] // 13 (row rounds 2.5 -> 3; wv2d[3][1] = 3 + 10*1 = 13 -- NOT truncated to 12) +wv2d[2.7][1] // 13 (row rounds 2.7 -> 3) +wv2d[1][2.3] // 21 (col rounds 2.3 -> 2; wv2d[1][2] = 1 + 10*2 = 21) +wv2d[1][2.5] // 31 (col rounds 2.5 -> 3; wv2d[1][3] = 1 + 10*3 = 31 -- NOT truncated to 21) +wv2d[1][2.7] // 31 (col rounds 2.7 -> 3) +``` + +The `2.5`/`2.7` cases are the ones that distinguish rounding from truncation: +truncation would give `12`/`21` (flooring to `2`) in every one of those rows, +but the actual results are `13`/`31` (rounding up to `3`), matching "closest to +the specified index," not "truncated toward zero." + --- ## 11. Quick Reference Table From f9ceba380c75eb04f6b5b094c36b3bf3cf2cf937 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 19 Aug 2026 19:42:32 +0200 Subject: [PATCH 7/8] AI: More accurate formulation of some statements --- .claude/skills/igor-gotchas/SKILL.md | 56 ++++++++++++++++++++-------- .claude/skills/sweepformula/SKILL.md | 35 +++++++++++------ .github/copilot-instructions.md | 10 +++-- 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/.claude/skills/igor-gotchas/SKILL.md b/.claude/skills/igor-gotchas/SKILL.md index 303c9fcb50..2850038809 100644 --- a/.claude/skills/igor-gotchas/SKILL.md +++ b/.claude/skills/igor-gotchas/SKILL.md @@ -53,9 +53,14 @@ Important properties: - **Not compilable** — "`SetIgorOption` is not compilable. To use it in a user-defined function, you need to use `Execute`." Only `Execute`/`Execute/P` can invoke it, never a bare call from inside a `Function`. -- **Triggers a recompile** the same way `RELOAD CHANGED PROCS`/ - `COMPILEPROCEDURES` do — an `#ifdef`/`#ifndef` block gated on the symbol - only takes effect after the next compile. +- **Does NOT itself trigger a recompile** — setting/clearing the symbol only + changes what a *subsequent* `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` call + will see. An `#ifdef`/`#ifndef` block gated on the symbol keeps its old + branch compiled in until a separate, explicit recompile actually runs -- + every real call site in this repo issues the two as separate queued calls + (e.g. `MIES_Debugging.ipf`'s `EnableDebugMode()`/`DisableDebugMode()`/ + `EnableEvilMode()`/etc.: `Execute/P/Q "SetIgorOption poundDefine=..."` + immediately followed by its own separate `Execute/P/Q "COMPILEPROCEDURES "`). ### `RELOAD CHANGED PROCS` / `COMPILEPROCEDURES` — separate calls, mandatory trailing space @@ -101,15 +106,31 @@ this repo's tooling already uses). ## `Execute`, Deferred Execution, and Error Handling -### `Execute` (unqueued) cannot be called from inside a `Function` at all - -Only `Execute/P` (deferred — runs after the calling function returns control -to Igor's main loop) is legal inside a compiled function. This is a genuine -language restriction, not a tooling limitation. Several operations — -`COMPILEPROCEDURES`, `RELOAD CHANGED PROCS`, `SetIgorOption poundDefine` — -are themselves restricted to only running via `Execute`/`Execute/P` in the -first place, so this combination (`Execute/P "SetIgorOption ..."`) is often -the only legal way to invoke them from procedure code. +### Bare `Execute` is legal inside a `Function` — `Execute/P` is only required for operations that can't run while the calling code is still on the stack + +Bare, unqueued `Execute` (no `/P`) *is* legal from inside a compiled +`Function` — Igor's own reference documentation confirms this is in fact the +**standard**, intended use: "The most common use of Execute is to call a +macro or an external operation from a user-defined function. This is +necessary because Igor does not allow you to make such calls directly." +(Igor Reference.ihf, "Execute [/Z] cmdStr"). This repo relies on it directly, +e.g. `WBP_CreateWaveBuilderPanel()`'s `Execute "WaveBuilder()"` +(`MIES_WaveBuilderPanel.ipf`) and `QueryIgorOption()`'s +`Execute/Q "SetIgorOption " + option + "=?"` (`MIES_Debugging.ipf`). + +`Execute/P` (deferred — posted to Igor's operation queue, only running once +"nothing else is happening... Macros and functions must not be running and +the command line must be empty") is specifically required for operations +that cannot run synchronously while the calling function itself is still on +the call stack — most notably `COMPILEPROCEDURES`/`RELOAD CHANGED PROCS` +(recompiling the very procedures the calling function is compiled from), and, +for the same reason, a `SetIgorOption poundDefine`/`poundUndefine` call meant +to take effect before an immediately-following queued compile. For commands +without that constraint, a plain (optionally `/Q`/`/Z`-flagged) `Execute` +from inside a `Function` works fine and is the documented pattern — +`COMPILEPROCEDURES`/`RELOAD CHANGED PROCS`/`SetIgorOption` still need +`Execute`/`Execute/P` rather than a bare compiled call (they are themselves +non-compilable operations), just not exclusively the deferred form. ### A `;`-joined command string aborts entirely on its first runtime error @@ -273,10 +294,13 @@ in the target folder afterward, or expect stray globals to trip a ### Auto-indexing order in waveform assignments An auto-indexed waveform assignment (e.g. `Make/WAVE/N=(n) w = SomeFunc(p)`) -runs strictly in increasing index order when `Multithread` is **not** used. -With `Multithread`, per-index execution order is not guaranteed and the -right-hand side must be threadsafe. This matters whenever the called -function has order-dependent side effects. +evaluates the right-hand side once per destination element, strictly in +increasing linear index order (`0, 1, 2, ..., numpnts(w)-1` — column-major +for a multi-dimensional destination, i.e. all of column 0 before column 1, +matching the `p`/`q`/`r`/`s` symbols' own progression) when `Multithread` is +**not** used. With `Multithread`, per-index execution order is not +guaranteed and the right-hand side must be threadsafe. This matters whenever +the called function has order-dependent side effects. ### `MultiThread` works with any wave type -- the restriction is on the expression, not the wave diff --git a/.claude/skills/sweepformula/SKILL.md b/.claude/skills/sweepformula/SKILL.md index 4fc9b8163b..05502a0d58 100644 --- a/.claude/skills/sweepformula/SKILL.md +++ b/.claude/skills/sweepformula/SKILL.md @@ -19,15 +19,26 @@ general WAVE/DFREF semantics. ## Dataset Wrapping Convention -Every SweepFormula operation result is a "dataset" — a single-element -`WAVE/WAVE` container (typically built via `SFH_CreateSFRefWave`) carrying an -`SF_META_DATATYPE` JSON wave note (set/read via `JWN_SetStringInWaveNote`/ -`JWN_GetStringFromWaveNote`) that identifies its kind (e.g. -`SF_DATATYPE_SELECTCOMP`, `SF_DATATYPE_SELECTTAG`). - -- `SFH_GetOutputForExecutorSingle(data, ..., dataType=X)` wraps whatever it's - given in a **new** wrapper wave, setting the note on that new wrapper — it - never tags `data` itself. +A SweepFormula operation result is a "dataset": a `WAVE/WAVE` container +(typically built via `SFH_CreateSFRefWave(win, opShort, size)`). `size` is +**not** always 1 — most operations size it to match their input (e.g. +`SFH_CreateSFRefWave(exd.graph, opShort, DimSize(input, ROWS))`, one output +row per input row), and only some operations route through the +single-element convenience wrapper `SFH_GetOutputForExecutorSingle` (which +hardcodes `size=1`). + +The `SF_META_DATATYPE` JSON wave note (set/read via `JWN_SetStringInWaveNote`/ +`JWN_GetStringFromWaveNote`) is **optional per-operation metadata**, not a +universal property of every dataset — it identifies specific semantic kinds +(e.g. `SF_DATATYPE_SELECTCOMP`, `SF_DATATYPE_SELECTTAG`) and is set only when +an operation explicitly asks for it: + +- `SFH_GetOutputForExecutorSingle(data, ..., dataType=X)` only sets the note + when the optional `dataType` argument is actually supplied — most calls in + `MIES_SweepFormula_Operations_Select.ipf` do; plenty of other call sites + across the codebase omit `dataType` entirely and get no note at all. When + it does set the note, it wraps `data` in a **new** wrapper wave and tags + that wrapper — it never tags `data` itself. - `select()` is a deliberate counter-example: it builds its own composite wrapper directly, sets the note on it, and returns via `SFH_GetOutputForExecutor` — skipping `SFH_GetOutputForExecutorSingle` @@ -48,8 +59,10 @@ dataset (`WAVE/WAVE`) elements, the executor: with side effects) to determine if any element is dataset-kind. 2. If any element is a dataset, the **whole array** is promoted to a uniform wave-of-datasets accumulator (`outW`), with plain scalar/text elements - individually wrapped in their own single-element `"PromotedArrayElement"` - dataset (no `SF_META_DATATYPE` note on that wrapper). + individually wrapped via `Make/FREE/WAVE promoted = {subArray}` + (`MIES_SweepFormula_Executor.ipf`) into their own single-element + wave-of-waves wrapper — an ad hoc free wave, not a named/tagged dataset + kind (no `SF_META_DATATYPE` note is set on it). 3. A dataset's own internal dimensionality must never leak into the outer array's shape — guard any dimension-widening logic with `if(!WaveExists(outW))` so the dataset accumulator stays strictly 1D diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bcf3a660e6..f4c58cf3bc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -97,9 +97,13 @@ End ### Entry Guard: `PerformSubsystemEntry()` -Public (non-`static`) functions in MIES generally start with `PerformSubsystemEntry()` (a -`threadsafe` wrapper around `AssertOnAndClearRTError()`, defined in -`MIES_Utilities_ProgramFlow.ipf`) as their first executable statement: +Public (non-`static`) functions in MIES generally start with `PerformSubsystemEntry()` +(defined in `MIES_Utilities_ProgramFlow.ipf`) as their first executable statement. +`PerformSubsystemEntry()` itself is **not** `threadsafe` -- it has its own inline +`GetAndClearRTError()`/`BUG()` logic. The `threadsafe` equivalent, for `threadsafe` +functions, is the separate `PerformSubsystemEntry_TS()`, which does wrap +`AssertOnAndClearRTError()`/`BUG_TS()`. Use `PerformSubsystemEntry_TS()` -- not +`PerformSubsystemEntry()` -- as the entry guard for `threadsafe` public functions: - Insert it as the literal first executable statement after only plain `variable`/`string`/ `STRUCT` declarations with non-function-call initializers (or as the very first line if there From 8cddc768d55a38fab76716aaf4cdcab85fa53c38 Mon Sep 17 00:00:00 2001 From: Michael Huth Date: Wed, 19 Aug 2026 19:43:16 +0200 Subject: [PATCH 8/8] AI: Mirror of the SKILLset for github CI - it seems github copilot needs them specifically there --- .claude/skills/sweepformula/SKILL.md | 8 +- .github/copilot-instructions.md | 5 +- .github/skills/README.md | 16 + .github/skills/docu/SKILL.md | 42 + .github/skills/igor-10/SKILL.md | 293 +++++++ .github/skills/igor-commands/SKILL.md | 960 ++++++++++++++++++++++ .github/skills/igor-gotchas/SKILL.md | 422 ++++++++++ .github/skills/igor-python/SKILL.md | 304 +++++++ .github/skills/igor-wave-dfref/SKILL.md | 980 +++++++++++++++++++++++ .github/skills/igortest/SKILL.md | 31 + .github/skills/igortest/advanced.rst | 770 ++++++++++++++++++ .github/skills/igortest/basic.rst | 187 +++++ .github/skills/igortest/examples.rst | 502 ++++++++++++ .github/skills/igortest/flags.rst | 54 ++ .github/skills/igortest/guided-tour.rst | 181 +++++ .github/skills/igortest/introduction.rst | 101 +++ .github/skills/sweepformula/SKILL.md | 180 +++++ 17 files changed, 5032 insertions(+), 4 deletions(-) create mode 100644 .github/skills/README.md create mode 100644 .github/skills/docu/SKILL.md create mode 100644 .github/skills/igor-10/SKILL.md create mode 100644 .github/skills/igor-commands/SKILL.md create mode 100644 .github/skills/igor-gotchas/SKILL.md create mode 100644 .github/skills/igor-python/SKILL.md create mode 100644 .github/skills/igor-wave-dfref/SKILL.md create mode 100644 .github/skills/igortest/SKILL.md create mode 100644 .github/skills/igortest/advanced.rst create mode 100644 .github/skills/igortest/basic.rst create mode 100644 .github/skills/igortest/examples.rst create mode 100644 .github/skills/igortest/flags.rst create mode 100644 .github/skills/igortest/guided-tour.rst create mode 100644 .github/skills/igortest/introduction.rst create mode 100644 .github/skills/sweepformula/SKILL.md diff --git a/.claude/skills/sweepformula/SKILL.md b/.claude/skills/sweepformula/SKILL.md index 05502a0d58..52e7d3729a 100644 --- a/.claude/skills/sweepformula/SKILL.md +++ b/.claude/skills/sweepformula/SKILL.md @@ -97,14 +97,16 @@ references, not stringified markers. multiple simultaneous SweepBrowsers). `SF_DM_NORMAL` gives a wrong/ non-existent name for this case, and the `SF_DM_SUBWINDOWS` result already includes the `#graph0` suffix — don't append it again. -- Operations accepting a `seltag` argument (e.g. `ivscc_apfrequency()`) - auto-group by existing experiment tags when no explicit `seltag` is given. ## Composing New Operations Out of Existing Ones An operation can implement itself by re-entering the real formula executor with dynamically-built source text, reusing other real operations as -building blocks, via this pattern: +building blocks, via this pattern. This is a documented extension pattern +(`Packages/doc/SweepFormula.rst`, "full plotting specification" section) -- +currently exercised by `UTF_SweepFormula.ipf`'s tests, not by a named +production operation, so treat it as the supported way to build a new +operation this way rather than a description of an existing one: 1. `Duplicate/FREE` the per-graph `GetSFVarStorage(graph)` (a `WAVE/WAVE` keyed by variable name) as a backup. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f4c58cf3bc..751770cbb6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -93,7 +93,10 @@ End - Constants must be `static` whenever not needed cross-file, and declared at the top of the procedure file in a dedicated block, not inline near their first use - Non-static/global constants belong only in `MIES_Constants.ipf` -- a new global constant - introduced elsewhere is a convention violation + introduced elsewhere is a convention violation, with one existing exception: + `MIES_ConversionConstants.ipf` (decimal-multiplier constants such as `ONE_TO_MILLI`, + generated by `GenerateMultiplierConstants()`) is its own dedicated, non-static global + constants file by established convention -- do not flag additions there ### Entry Guard: `PerformSubsystemEntry()` diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 0000000000..dd58e772de --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,16 @@ +# Mirror of .claude/skills + +This directory is a byte-for-byte copy of `../../.claude/skills`. + +`.claude/skills` is the authoritative source, read natively by Claude Code / +Claude in Cowork. This copy exists solely because GitHub Copilot's Agent +Skills feature (code review, coding agent, CLI) was observed to fail to find +a skill placed only under `.claude/skills` even though that location is +documented as supported, while a copy under `.github/skills` (GitHub's own +first-party skills location) resolved it. + +**Maintenance**: whenever a file under `.claude/skills` is added, edited, or +removed, apply the same change here. There is currently no automation for +this -- keep the two directories in sync by hand (or script it, e.g. +`rsync -a --delete .claude/skills/ .github/skills/`, in a pre-commit hook or +CI check, if drift becomes a recurring problem). diff --git a/.github/skills/docu/SKILL.md b/.github/skills/docu/SKILL.md new file mode 100644 index 0000000000..0984b399b1 --- /dev/null +++ b/.github/skills/docu/SKILL.md @@ -0,0 +1,42 @@ +--- +name: docu +description: Documentation-consistency guidance for MIES. Use for any task involving behavior, UI text, workflows, analysis, or user-facing output; read and align with the repository's .rst documentation (especially Packages/doc/ and SweepFormula docs) before answering, and flag mismatches between code and docs instead of silently picking one. +--- + +# Copilot Instructions for MIES + +## Documentation Source of Truth + +For any task involving behavior, UI text, workflows, analysis, or user-facing output in MIES: + +1. Read and use all reStructuredText documentation files (`*.rst`) in this repository as authoritative documentation. +2. Prefer documentation under `Packages/doc/` when conflicts arise. +3. If code behavior appears to conflict with `.rst` docs, do not silently choose one: + - Call out the mismatch clearly. + - Propose a fix consistent with existing MIES conventions. +4. When generating explanations, comments, commit messages, PR summaries, or user help text: + - Align terminology with the `.rst` documentation. + - Reuse established names for features, parameters, and workflows. +5. For SweepFormula work specifically, prioritize the SweepFormula `.rst` documentation and keep parser/function wording consistent. + +## File Discovery Guidance + +When starting a task, discover and consider all: +- `**/*.rst` + +Especially include: +- `Packages/doc/**/*.rst` +- Top-level documentation files (for example `README.rst`, if present) +- Any `.rst` near the code being modified + +## Implementation Guidance + +- Do not invent behavior that contradicts `.rst` docs. +- If docs are ambiguous, state assumptions explicitly in the response. +- Keep examples and suggested API usage consistent with documented patterns. +- For user-visible strings, prefer wording style used in docs. + +## Response Requirements + +In your response, briefly list which `.rst` files were used when the task is documentation-sensitive. +If none were found, say that explicitly and proceed with best-effort based on code context. diff --git a/.github/skills/igor-10/SKILL.md b/.github/skills/igor-10/SKILL.md new file mode 100644 index 0000000000..adf0635dce --- /dev/null +++ b/.github/skills/igor-10/SKILL.md @@ -0,0 +1,293 @@ +--- +name: igor-10 +paths: + - "**/*.ipf" +description: Igor Pro 10 changes relevant to code generation: 64-bit only, Python integration, compiler behavior changes, new language features, function and operation changes, and bug fixes that alter results. Use when writing or reviewing Igor Pro code that must run on Igor Pro 10, or when Igor 9 vs 10 behavior differences matter. +--- + +# Igor Pro 10 — Key Changes for Code Generation + +This document summarizes Igor Pro 10 changes that affect how code should be written. +It is intended as reference material for AI-assisted code generation, not as a complete +changelog. For full details, fetch the official documentation: + +- What's New: https://docs.wavemetrics.com/igorpro/igor-pro-10/what-s-new-in-igor-pro-10 +- Changes since 10.00: https://docs.wavemetrics.com/igorpro/igor-pro-10/what-s-changed-since-igor-pro-10-00 + +--- + +## 1. Igor Pro 10 is 64-bit Only + +Igor Pro 10 ships as a 64-bit application only (unlike versions 7–9 which had both). +**All XOPs must also be 64-bit.** Do not suggest or reference 32-bit XOP patterns. +The camera operations `NewCamera`, `ModifyCamera`, and `GetCamera` have been removed. + +--- + +## 2. Python Integration (New in Igor 10) + +Igor 10 introduced direct, bidirectional Python communication via the `igorpro` +module, replacing workarounds like ExecuteScriptText for Python tasks. See the +`igor-python` skill for the full reference (syntax, setup, object model, object +lifetime rules). Version-specific facts not covered there: + +- The `igorpro.fn` submodule (calling Igor functions directly from Python) was + added in 10.01, not 10.00. +- Igor 10.00 supports Python 3.x generally; 10.01 specifically adds Python 3.14 + support (standard installation only, not free-threaded). + +--- + +## 3. Compiler Behavior Changes (Breaking) + +### Unreachable code in switch/strswitch is now a compile error + +Code before `case` labels or after `break` before the next `case` is now rejected: + +```igor +// THIS WILL NOT COMPILE in Igor 10: +switch(val) + Print "this is unreachable" // ERROR: unreachable code + case 1: + // ... + break + Print "also unreachable" // ERROR: unreachable code + case 2: + // ... +endswitch + +// CORRECT: +switch(val) + case 1: + Print "reachable" + break + case 2: + // ... +endswitch +``` + +### ExperimentModified no longer triggered by same-value assignments + +In Igor 10, assigning a global variable or string to its current value no longer +marks the experiment as modified. If your code relies on this side effect to trigger +a save prompt, explicitly call: + +```igor +ExperimentModified 1 +``` + +### #pragma independentModule=ProcGlobal is now a compile error + +This was silently accepted before Igor 10. It is now rejected at compile time. + +### Abort/AbortOnRTE no longer triggers "Debug on User Abort" + +The debugger is now invoked only when the **user** clicks the Abort button. +Programmatic `Abort`, `AbortOnRTE`, and `AbortOnValue` calls no longer invoke +the debugger even when "Debug on User Abort" is enabled. + +--- + +## 4. New Language Features + +### #pragma moduleName now allowed in the main Procedure window + +Previously, `#pragma moduleName` was only valid in included procedure files. +In Igor 10 it can be used in the main Procedure window: + +```igor +#pragma moduleName = MyMainModule + +Static Function helperFunction() + // now callable as MyMainModule#helperFunction() +End +``` + +### DFREF supported in Multiple Return Syntax (MRS) + +Data folder references can now be returned via MRS: + +```igor +Function [DFREF df] GetSubfolder(String name) + DFREF currentDF = GetDataFolderDFR() + NewDataFolder/O currentDF:$name + DFREF df = currentDF:$name +End + +// Calling it: +[DFREF myDF] = GetSubfolder("results") +``` + +### Static constants accessible across modules + +Constants in other modules can now be referenced with double or triple names, +just like static functions: + +```igor +switch(val) + case OtherModule#MY_CONSTANT: + // ... + break + case OtherIM#SubModule#THEIR_CONSTANT: + // ... + break +endswitch +``` + +### Line continuation expanded + +Line continuation with `\` now works nearly everywhere, including after +end-of-line comments, and compound name components can span lines: + +```igor +Function [WAVE w] // output wave \ + Example(String name, // wave name \ + Variable n) // number of points + Make/O/N=(n) $name \ + = gnoise(1) + WAVE w = $name +End +``` + +--- + +## 5. Function and Operation Changes + +### WaveStats — new /W flag options for performance + +In Igor 10, `WaveStats` accepts optional parameters with `/W` that allow +bypassing creation of `V_*` output variables. Use this in tight loops where +you don't need all statistics: + +```igor +// Full stats (creates all V_ variables) — original behavior: +WaveStats/Q myWave + +// Igor 10: bypass V_ variable creation for performance +WaveStats/Q/W=0 myWave // fetch only; see docs for parameter options +``` + +See https://docs.wavemetrics.com/igorpro/commands/wavestats for full syntax. + +### MatrixOP — new functions (Igor 10.00 + 10.01) + +The following functions were added to `MatrixOP` in Igor 10: + +`subtractMin()`, `indexMatch()`, `removeCol()`, `removeCols()`, +`scaleLayers()`, `scaleChunks()`, `subtractRows()`, `subtractCols()`, +`quatFromSpherical()`, `quatInverse()`, `median()`, `zapZeros()`, +`replaceInfs()`, `enoise()`, `setType()`, `rowDiff()`, `binMean()`, +`binVar()`, `limit()`, `not()` + +The `/KCLS` flag was extended to support the third dimension (10.01). + +Do not suggest manual workarounds for these operations — use the built-in +MatrixOP functions instead. + +### New functions: Interp4D and Interp4DPath + +Four-dimensional interpolation is now available: + +```igor +result = Interp4D(w, x0, x1, x2, x3) +``` + +### /K=4 flag on window creation operations + +`Display`, `Edit`, `NewPanel`, `NewGizmo`, `NewLayout`, `NewNotebook`, +`NewWaterfall`, `NewImage`, and `OpenNotebook` now support `/K=4`, which +kills the window without a dialog and does not save it with the experiment. +Useful for temporary tool or progress windows in pipeline code: + +```igor +NewPanel/K=4 as "Processing..." +``` + +### New drawing layers: ProgTop and UserTop + +Two new drawing layers were added that render **above** annotations: +`ProgTop` (for programmatic drawing) and `UserTop` (for user drawing). +Use `SetDrawLayer ProgTop` when you need overlays that sit on top of +annotation text boxes. + +### Say operation (text-to-speech) + +```igor +Say "Analysis complete" +``` + +### printf engineering notation: %W2P and %W3P + +New format specifiers interpret precision as total significant digits +(not fractional digits as %W0P/%W1P do): + +```igor +printf "%.4W3PHz", 12.342E3 // prints "12.34 KHz" +printf "%.2W2PV", -12.342E-3 // prints "-12mV" +``` + +--- + +## 6. Bug Fixes That Affect Results (Important) + +### area() and faverage() — incorrect results fixed (10.01) + +Both functions had a bug where the trapezoidal end-point correction was +discarded, producing slightly incorrect results. **Results from these +functions changed in Igor 10.01.** If you have existing code or saved +results that used `area()` or `faverage()`, the values will differ +slightly after upgrading. + +### CurveFit now accepts wave subranges with X waves (10.01) + +Previously, using a wave subrange in `CurveFit` when an X wave was also +specified was incorrectly rejected. This now works: + +```igor +CurveFit/Q gauss myWave[10,50] /X=xWave /W=weightWave +``` + +### FindPeak crash fixed for waves > 1 million points (10.01) + +`FindPeak` previously crashed on large waves. This is fixed in 10.01 +with up to 40x performance improvement on large waves. + +### AnnotationInfo returns "" instead of error for missing annotations + +In Igor 10, `AnnotationInfo` returns an empty string if the window has +no annotations or the named annotation doesn't exist. Previously this +was a runtime error. Code that used `try/catch` around `AnnotationInfo` +for this case can be simplified. + +### Bitwise operators now use signed integer conversion (10.01) + +`|`, `&`, `~`, and `%^` previously converted operands to unsigned integers. +They now convert to signed integers. This can change results for negative +operands. + +--- + +## 7. Compatibility Notes + +### File compatibility +Experiment files saved by Igor 10 using Igor 10-specific features cannot +be opened by earlier versions. If cross-version compatibility matters, +avoid Igor 10-only syntax. + +### _labels_ keyword no longer translated (10.01) +In localized versions of Igor (e.g. Japanese), the `_labels_` keyword in +`Display`, `AppendToGraph`, and `ReplaceWave` is no longer translated. +Igor always generates `_labels_` regardless of OS language. This is only +relevant when sharing `.pxp` files across language environments. + +--- + +## Reference URLs + +| Topic | URL | +|---|---| +| What's New in Igor 10 | https://docs.wavemetrics.com/igorpro/igor-pro-10/what-s-new-in-igor-pro-10 | +| Changes since 10.00 | https://docs.wavemetrics.com/igorpro/igor-pro-10/what-s-changed-since-igor-pro-10-00 | +| MatrixOP | https://docs.wavemetrics.com/igorpro/commands/matrixop | +| WaveStats | https://docs.wavemetrics.com/igorpro/commands/wavestats | +| Multiple Return Syntax | https://docs.wavemetrics.com/igorpro/programming/programming#multiple-return-syntax | diff --git a/.github/skills/igor-commands/SKILL.md b/.github/skills/igor-commands/SKILL.md new file mode 100644 index 0000000000..b822acf18e --- /dev/null +++ b/.github/skills/igor-commands/SKILL.md @@ -0,0 +1,960 @@ +--- +name: igor-commands +paths: + - "**/*.ipf" +description: Alphabetical reference of built-in Igor Pro commands and functions with links to official WaveMetrics documentation. Use to verify a command name or casing exists, or to look up its full signature, parameters, and flags before generating Igor Pro code. +--- + +# Igor Pro Built-in Commands Reference + +This file lists all built-in Igor Pro commands (operations and functions) alphabetically, +with links to the official WaveMetrics online documentation for each command. + +**Documentation root:** https://docs.wavemetrics.com/igorpro/commands +**Base URL for all commands:** `https://docs.wavemetrics.com/igorpro/commands/` + +When you need the full signature, parameters, flags, or examples for any command, +fetch the documentation page at the URL shown next to that command. + +**Problematic Igor rules:** + +Igor names are case-insensitive for identifiers (variables, waves, functions) + +MatrixOp cannot use p,q,r,s point addressing method + +--- + +## A + +- [Abort](https://docs.wavemetrics.com/igorpro/commands/abort) +- [abs](https://docs.wavemetrics.com/igorpro/commands/abs) +- [acos](https://docs.wavemetrics.com/igorpro/commands/acos) +- [acosh](https://docs.wavemetrics.com/igorpro/commands/acosh) +- [AddFIFOData](https://docs.wavemetrics.com/igorpro/commands/addfifodata) +- [AddFIFOVectData](https://docs.wavemetrics.com/igorpro/commands/addfifovectdata) +- [AddListItem](https://docs.wavemetrics.com/igorpro/commands/addlistitem) +- [AddMovieAudio](https://docs.wavemetrics.com/igorpro/commands/addmovieaudio) +- [AddMovieFrame](https://docs.wavemetrics.com/igorpro/commands/addmovieframe) +- [AddWavesToBoxPlot](https://docs.wavemetrics.com/igorpro/commands/addwavestoboxplot) +- [AddWavesToViolinPlot](https://docs.wavemetrics.com/igorpro/commands/addwavestoviolinplot) +- [AdoptFiles](https://docs.wavemetrics.com/igorpro/commands/adoptfiles) +- [airyA](https://docs.wavemetrics.com/igorpro/commands/airya) +- [airyAD](https://docs.wavemetrics.com/igorpro/commands/airyad) +- [airyB](https://docs.wavemetrics.com/igorpro/commands/airyb) +- [airyBD](https://docs.wavemetrics.com/igorpro/commands/airybd) +- [alog](https://docs.wavemetrics.com/igorpro/commands/alog) +- [AnnotationInfo](https://docs.wavemetrics.com/igorpro/commands/annotationinfo) +- [AnnotationList](https://docs.wavemetrics.com/igorpro/commands/annotationlist) +- [APMath](https://docs.wavemetrics.com/igorpro/commands/apmath) +- [Append](https://docs.wavemetrics.com/igorpro/commands/append) +- [AppendBoxPlot](https://docs.wavemetrics.com/igorpro/commands/appendboxplot) +- [AppendImage](https://docs.wavemetrics.com/igorpro/commands/appendimage) +- [AppendLayoutObject](https://docs.wavemetrics.com/igorpro/commands/appendlayoutobject) +- [AppendMatrixContour](https://docs.wavemetrics.com/igorpro/commands/appendmatrixcontour) +- [AppendText](https://docs.wavemetrics.com/igorpro/commands/appendtext) +- [AppendToGizmo](https://docs.wavemetrics.com/igorpro/commands/appendtogizmo) +- [AppendToGraph](https://docs.wavemetrics.com/igorpro/commands/appendtograph) +- [AppendToLayout](https://docs.wavemetrics.com/igorpro/commands/appendtolayout) +- [AppendToTable](https://docs.wavemetrics.com/igorpro/commands/appendtotable) +- [AppendViolinPlot](https://docs.wavemetrics.com/igorpro/commands/appendviolinplot) +- [AppendXYZContour](https://docs.wavemetrics.com/igorpro/commands/appendxyzcontour) +- [area](https://docs.wavemetrics.com/igorpro/commands/area) +- [areaXY](https://docs.wavemetrics.com/igorpro/commands/areaxy) +- [asin](https://docs.wavemetrics.com/igorpro/commands/asin) +- [asinh](https://docs.wavemetrics.com/igorpro/commands/asinh) +- [atan](https://docs.wavemetrics.com/igorpro/commands/atan) +- [atan2](https://docs.wavemetrics.com/igorpro/commands/atan2) +- [atanh](https://docs.wavemetrics.com/igorpro/commands/atanh) +- [AutoPositionWindow](https://docs.wavemetrics.com/igorpro/commands/autopositionwindow) +- [AxisInfo](https://docs.wavemetrics.com/igorpro/commands/axisinfo) +- [AxisLabel](https://docs.wavemetrics.com/igorpro/commands/axislabel) +- [AxisList](https://docs.wavemetrics.com/igorpro/commands/axislist) +- [AxisValFromPixel](https://docs.wavemetrics.com/igorpro/commands/axisvalfrompixel) + +## B + +- [BackgroundInfo](https://docs.wavemetrics.com/igorpro/commands/backgroundinfo) +- [Base64Decode](https://docs.wavemetrics.com/igorpro/commands/base64decode) +- [Base64Encode](https://docs.wavemetrics.com/igorpro/commands/base64encode) +- [Beep](https://docs.wavemetrics.com/igorpro/commands/beep) +- [Besseli](https://docs.wavemetrics.com/igorpro/commands/besseli) +- [Besselj](https://docs.wavemetrics.com/igorpro/commands/besselj) +- [Besselk](https://docs.wavemetrics.com/igorpro/commands/besselk) +- [Bessely](https://docs.wavemetrics.com/igorpro/commands/bessely) +- [bessI](https://docs.wavemetrics.com/igorpro/commands/bessi) +- [bessJ](https://docs.wavemetrics.com/igorpro/commands/bessj) +- [bessK](https://docs.wavemetrics.com/igorpro/commands/bessk) +- [bessY](https://docs.wavemetrics.com/igorpro/commands/bessy) +- [beta](https://docs.wavemetrics.com/igorpro/commands/beta) +- [betai](https://docs.wavemetrics.com/igorpro/commands/betai) +- [BezierToPolygon](https://docs.wavemetrics.com/igorpro/commands/beziertopolygon) +- [BinarySearch](https://docs.wavemetrics.com/igorpro/commands/binarysearch) +- [BinarySearchInterp](https://docs.wavemetrics.com/igorpro/commands/binarysearchinterp) +- [binomial](https://docs.wavemetrics.com/igorpro/commands/binomial) +- [binomialln](https://docs.wavemetrics.com/igorpro/commands/binomialln) +- [binomialNoise](https://docs.wavemetrics.com/igorpro/commands/binomialnoise) +- [BoundingBall](https://docs.wavemetrics.com/igorpro/commands/boundingball) +- [BoxSmooth](https://docs.wavemetrics.com/igorpro/commands/boxsmooth) +- [BrowseURL](https://docs.wavemetrics.com/igorpro/commands/browseurl) +- [BuildMenu](https://docs.wavemetrics.com/igorpro/commands/buildmenu) +- [Button](https://docs.wavemetrics.com/igorpro/commands/button) + +## C + +- [cabs](https://docs.wavemetrics.com/igorpro/commands/cabs) +- [CaptureHistory](https://docs.wavemetrics.com/igorpro/commands/capturehistory) +- [CaptureHistoryStart](https://docs.wavemetrics.com/igorpro/commands/capturehistorystart) +- [cd](https://docs.wavemetrics.com/igorpro/commands/cd) +- [ceil](https://docs.wavemetrics.com/igorpro/commands/ceil) +- [centerOfMass](https://docs.wavemetrics.com/igorpro/commands/centerofmass) +- [centerOfMassXY](https://docs.wavemetrics.com/igorpro/commands/centerofmassxy) +- [cequal](https://docs.wavemetrics.com/igorpro/commands/cequal) +- [char2num](https://docs.wavemetrics.com/igorpro/commands/char2num) +- [Chart](https://docs.wavemetrics.com/igorpro/commands/chart) +- [chebyshev](https://docs.wavemetrics.com/igorpro/commands/chebyshev) +- [chebyshevU](https://docs.wavemetrics.com/igorpro/commands/chebyshevu) +- [CheckBox](https://docs.wavemetrics.com/igorpro/commands/checkbox) +- [CheckDisplayed](https://docs.wavemetrics.com/igorpro/commands/checkdisplayed) +- [CheckName](https://docs.wavemetrics.com/igorpro/commands/checkname) +- [ChildWindowList](https://docs.wavemetrics.com/igorpro/commands/childwindowlist) +- [ChooseColor](https://docs.wavemetrics.com/igorpro/commands/choosecolor) +- [CleanupName](https://docs.wavemetrics.com/igorpro/commands/cleanupname) +- [Close](https://docs.wavemetrics.com/igorpro/commands/close) +- [CloseHelp](https://docs.wavemetrics.com/igorpro/commands/closehelp) +- [CloseMovie](https://docs.wavemetrics.com/igorpro/commands/closemovie) +- [CloseProc](https://docs.wavemetrics.com/igorpro/commands/closeproc) +- [cmplx](https://docs.wavemetrics.com/igorpro/commands/cmplx) +- [CmpStr](https://docs.wavemetrics.com/igorpro/commands/cmpstr) +- [ColorScale](https://docs.wavemetrics.com/igorpro/commands/colorscale) +- [ColorTab2Wave](https://docs.wavemetrics.com/igorpro/commands/colortab2wave) +- [Concatenate](https://docs.wavemetrics.com/igorpro/commands/concatenate) +- [conj](https://docs.wavemetrics.com/igorpro/commands/conj) +- [ContourInfo](https://docs.wavemetrics.com/igorpro/commands/contourinfo) +- [ContourNameList](https://docs.wavemetrics.com/igorpro/commands/contournamelist) +- [ContourNameToWaveRef](https://docs.wavemetrics.com/igorpro/commands/contournametowaveref) +- [ContourZ](https://docs.wavemetrics.com/igorpro/commands/contourz) +- [ControlBar](https://docs.wavemetrics.com/igorpro/commands/controlbar) +- [ControlInfo](https://docs.wavemetrics.com/igorpro/commands/controlinfo) +- [ControlNameList](https://docs.wavemetrics.com/igorpro/commands/controlnamelist) +- [ControlUpdate](https://docs.wavemetrics.com/igorpro/commands/controlupdate) +- [ConvertGlobalStringTextEncoding](https://docs.wavemetrics.com/igorpro/commands/convertglobalstringtextencoding) +- [ConvertTextEncoding](https://docs.wavemetrics.com/igorpro/commands/converttextencoding) +- [ConvexHull](https://docs.wavemetrics.com/igorpro/commands/convexhull) +- [Convolve](https://docs.wavemetrics.com/igorpro/commands/convolve) +- [CopyDimLabels](https://docs.wavemetrics.com/igorpro/commands/copydimlabels) +- [CopyFile](https://docs.wavemetrics.com/igorpro/commands/copyfile) +- [CopyFolder](https://docs.wavemetrics.com/igorpro/commands/copyfolder) +- [CopyScales](https://docs.wavemetrics.com/igorpro/commands/copyscales) +- [Correlate](https://docs.wavemetrics.com/igorpro/commands/correlate) +- [cos](https://docs.wavemetrics.com/igorpro/commands/cos) +- [cosh](https://docs.wavemetrics.com/igorpro/commands/cosh) +- [CosIntegral](https://docs.wavemetrics.com/igorpro/commands/cosintegral) +- [cot](https://docs.wavemetrics.com/igorpro/commands/cot) +- [coth](https://docs.wavemetrics.com/igorpro/commands/coth) +- [CountObjects](https://docs.wavemetrics.com/igorpro/commands/countobjects) +- [CountObjectsDFR](https://docs.wavemetrics.com/igorpro/commands/countobjectsdfr) +- [cpowi](https://docs.wavemetrics.com/igorpro/commands/cpowi) +- [CreateAliasShortcut](https://docs.wavemetrics.com/igorpro/commands/createaliasshortcut) +- [CreateBrowser](https://docs.wavemetrics.com/igorpro/commands/createbrowser) +- [CreateDataObjectName](https://docs.wavemetrics.com/igorpro/commands/createdataobjectname) +- [CreationDate](https://docs.wavemetrics.com/igorpro/commands/creationdate) +- [Cross](https://docs.wavemetrics.com/igorpro/commands/cross) +- [csc](https://docs.wavemetrics.com/igorpro/commands/csc) +- [csch](https://docs.wavemetrics.com/igorpro/commands/csch) +- [CsrInfo](https://docs.wavemetrics.com/igorpro/commands/csrinfo) +- [CsrWave](https://docs.wavemetrics.com/igorpro/commands/csrwave) +- [CsrWaveRef](https://docs.wavemetrics.com/igorpro/commands/csrwaveref) +- [CsrXWave](https://docs.wavemetrics.com/igorpro/commands/csrxwave) +- [CsrXWaveRef](https://docs.wavemetrics.com/igorpro/commands/csrxwaveref) +- [CTabList](https://docs.wavemetrics.com/igorpro/commands/ctablist) +- [CtrlBackground](https://docs.wavemetrics.com/igorpro/commands/ctrlbackground) +- [CtrlFIFO](https://docs.wavemetrics.com/igorpro/commands/ctrlfifo) +- [CtrlNamedBackground](https://docs.wavemetrics.com/igorpro/commands/ctrlnamedbackground) +- [Cursor](https://docs.wavemetrics.com/igorpro/commands/cursor) +- [CurveFit](https://docs.wavemetrics.com/igorpro/commands/curvefit) +- [CustomControl](https://docs.wavemetrics.com/igorpro/commands/customcontrol) +- [CWT](https://docs.wavemetrics.com/igorpro/commands/cwt) + +## D + +- [DataFolderDir](https://docs.wavemetrics.com/igorpro/commands/datafolderdir) +- [DataFolderExists](https://docs.wavemetrics.com/igorpro/commands/datafolderexists) +- [DataFolderList](https://docs.wavemetrics.com/igorpro/commands/datafolderlist) +- [DataFolderRefChanges](https://docs.wavemetrics.com/igorpro/commands/datafolderrefchanges) +- [DataFolderRefsEqual](https://docs.wavemetrics.com/igorpro/commands/datafolderrefsequal) +- [DataFolderRefStatus](https://docs.wavemetrics.com/igorpro/commands/datafolderrefstatus) +- [date](https://docs.wavemetrics.com/igorpro/commands/date) +- [date2secs](https://docs.wavemetrics.com/igorpro/commands/date2secs) +- [DateTime](https://docs.wavemetrics.com/igorpro/commands/datetime) +- [dateToJulian](https://docs.wavemetrics.com/igorpro/commands/datetojulian) +- [dawson](https://docs.wavemetrics.com/igorpro/commands/dawson) +- [Debugger](https://docs.wavemetrics.com/igorpro/commands/debugger) +- [DebuggerOptions](https://docs.wavemetrics.com/igorpro/commands/debuggeroptions) +- [DefaultFont](https://docs.wavemetrics.com/igorpro/commands/defaultfont) +- [DefaultGUIControls](https://docs.wavemetrics.com/igorpro/commands/defaultguicontrols) +- [DefaultGUIFont](https://docs.wavemetrics.com/igorpro/commands/defaultguifont) +- [DefaultTextEncoding](https://docs.wavemetrics.com/igorpro/commands/defaulttextencoding) +- [defined](https://docs.wavemetrics.com/igorpro/commands/defined) +- [DefineGuide](https://docs.wavemetrics.com/igorpro/commands/defineguide) +- [DelayUpdate](https://docs.wavemetrics.com/igorpro/commands/delayupdate) +- [DeleteAnnotations](https://docs.wavemetrics.com/igorpro/commands/deleteannotations) +- [DeleteFile](https://docs.wavemetrics.com/igorpro/commands/deletefile) +- [DeleteFolder](https://docs.wavemetrics.com/igorpro/commands/deletefolder) +- [DeletePoints](https://docs.wavemetrics.com/igorpro/commands/deletepoints) +- [deltax](https://docs.wavemetrics.com/igorpro/commands/deltax) +- [Differentiate](https://docs.wavemetrics.com/igorpro/commands/differentiate) +- [digamma](https://docs.wavemetrics.com/igorpro/commands/digamma) +- [Dilogarithm](https://docs.wavemetrics.com/igorpro/commands/dilogarithm) +- [DimDelta](https://docs.wavemetrics.com/igorpro/commands/dimdelta) +- [DimOffset](https://docs.wavemetrics.com/igorpro/commands/dimoffset) +- [DimSize](https://docs.wavemetrics.com/igorpro/commands/dimsize) +- [Dir](https://docs.wavemetrics.com/igorpro/commands/dir) +- [Display](https://docs.wavemetrics.com/igorpro/commands/display) +- [DisplayHelpTopic](https://docs.wavemetrics.com/igorpro/commands/displayhelptopic) +- [DisplayProcedure](https://docs.wavemetrics.com/igorpro/commands/displayprocedure) +- [DoAlert](https://docs.wavemetrics.com/igorpro/commands/doalert) +- [DoIgorMenu](https://docs.wavemetrics.com/igorpro/commands/doigormenu) +- [DoUpdate](https://docs.wavemetrics.com/igorpro/commands/doupdate) +- [DoWindow](https://docs.wavemetrics.com/igorpro/commands/dowindow) +- [DoXOPIdle](https://docs.wavemetrics.com/igorpro/commands/doxopidle) +- [DPSS](https://docs.wavemetrics.com/igorpro/commands/dpss) +- [DrawAction](https://docs.wavemetrics.com/igorpro/commands/drawaction) +- [DrawArc](https://docs.wavemetrics.com/igorpro/commands/drawarc) +- [DrawBezier](https://docs.wavemetrics.com/igorpro/commands/drawbezier) +- [DrawLine](https://docs.wavemetrics.com/igorpro/commands/drawline) +- [DrawOval](https://docs.wavemetrics.com/igorpro/commands/drawoval) +- [DrawPICT](https://docs.wavemetrics.com/igorpro/commands/drawpict) +- [DrawPoly](https://docs.wavemetrics.com/igorpro/commands/drawpoly) +- [DrawRect](https://docs.wavemetrics.com/igorpro/commands/drawrect) +- [DrawRRect](https://docs.wavemetrics.com/igorpro/commands/drawrrect) +- [DrawText](https://docs.wavemetrics.com/igorpro/commands/drawtext) +- [DrawUserShape](https://docs.wavemetrics.com/igorpro/commands/drawusershape) +- [DSPDetrend](https://docs.wavemetrics.com/igorpro/commands/dspdetrend) +- [DSPPeriodogram](https://docs.wavemetrics.com/igorpro/commands/dspperiodogram) +- [Duplicate](https://docs.wavemetrics.com/igorpro/commands/duplicate) +- [DuplicateDataFolder](https://docs.wavemetrics.com/igorpro/commands/duplicatedatafolder) +- [DWT](https://docs.wavemetrics.com/igorpro/commands/dwt) + +## E + +- [e](https://docs.wavemetrics.com/igorpro/commands/e) +- [EdgeStats](https://docs.wavemetrics.com/igorpro/commands/edgestats) +- [Edit](https://docs.wavemetrics.com/igorpro/commands/edit) +- [ei](https://docs.wavemetrics.com/igorpro/commands/ei) +- [EllipticE](https://docs.wavemetrics.com/igorpro/commands/elliptice) +- [EllipticK](https://docs.wavemetrics.com/igorpro/commands/elliptick) +- [enoise](https://docs.wavemetrics.com/igorpro/commands/enoise) +- [EqualWaves](https://docs.wavemetrics.com/igorpro/commands/equalwaves) +- [erf](https://docs.wavemetrics.com/igorpro/commands/erf) +- [erfc](https://docs.wavemetrics.com/igorpro/commands/erfc) +- [erfcw](https://docs.wavemetrics.com/igorpro/commands/erfcw) +- [erfcx](https://docs.wavemetrics.com/igorpro/commands/erfcx) +- [ErrorBars](https://docs.wavemetrics.com/igorpro/commands/errorbars) +- [EstimatePeakSizes](https://docs.wavemetrics.com/igorpro/commands/estimatepeaksizes) +- [Execute](https://docs.wavemetrics.com/igorpro/commands/execute) +- [ExecuteScriptText](https://docs.wavemetrics.com/igorpro/commands/executescripttext) +- [exists](https://docs.wavemetrics.com/igorpro/commands/exists) +- [exp](https://docs.wavemetrics.com/igorpro/commands/exp) +- [ExperimentInfo](https://docs.wavemetrics.com/igorpro/commands/experimentinfo) +- [ExperimentModified](https://docs.wavemetrics.com/igorpro/commands/experimentmodified) +- [expInt](https://docs.wavemetrics.com/igorpro/commands/expint) +- [ExpIntegralE1](https://docs.wavemetrics.com/igorpro/commands/expintegrale1) +- [expNoise](https://docs.wavemetrics.com/igorpro/commands/expnoise) +- [ExportGizmo](https://docs.wavemetrics.com/igorpro/commands/exportgizmo) +- [Extract](https://docs.wavemetrics.com/igorpro/commands/extract) + +## F + +- [factorial](https://docs.wavemetrics.com/igorpro/commands/factorial) +- [Faddeeva](https://docs.wavemetrics.com/igorpro/commands/faddeeva) +- [FakeData](https://docs.wavemetrics.com/igorpro/commands/fakedata) +- [FastGaussTransform](https://docs.wavemetrics.com/igorpro/commands/fastgausstransform) +- [FastOp](https://docs.wavemetrics.com/igorpro/commands/fastop) +- [faverage](https://docs.wavemetrics.com/igorpro/commands/faverage) +- [faverageXY](https://docs.wavemetrics.com/igorpro/commands/faveragexy) +- [FBInRead](https://docs.wavemetrics.com/igorpro/commands/fbinread) +- [FBInWrite](https://docs.wavemetrics.com/igorpro/commands/fbinwrite) +- [FetchURL](https://docs.wavemetrics.com/igorpro/commands/fetchurl) +- [FFT](https://docs.wavemetrics.com/igorpro/commands/fft) +- [FGetPos](https://docs.wavemetrics.com/igorpro/commands/fgetpos) +- [FIFO2Wave](https://docs.wavemetrics.com/igorpro/commands/fifo2wave) +- [FIFOStatus](https://docs.wavemetrics.com/igorpro/commands/fifostatus) +- [FilterFIR](https://docs.wavemetrics.com/igorpro/commands/filterfir) +- [FilterIIR](https://docs.wavemetrics.com/igorpro/commands/filteriir) +- [FindAPeak](https://docs.wavemetrics.com/igorpro/commands/findapeak) +- [FindContour](https://docs.wavemetrics.com/igorpro/commands/findcontour) +- [FindDimLabel](https://docs.wavemetrics.com/igorpro/commands/finddimlabel) +- [FindDuplicates](https://docs.wavemetrics.com/igorpro/commands/findduplicates) +- [FindLevel](https://docs.wavemetrics.com/igorpro/commands/findlevel) +- [FindLevels](https://docs.wavemetrics.com/igorpro/commands/findlevels) +- [FindListItem](https://docs.wavemetrics.com/igorpro/commands/findlistitem) +- [FindPeak](https://docs.wavemetrics.com/igorpro/commands/findpeak) +- [FindPointsInPoly](https://docs.wavemetrics.com/igorpro/commands/findpointsinpoly) +- [FindRoots](https://docs.wavemetrics.com/igorpro/commands/findroots) +- [FindSequence](https://docs.wavemetrics.com/igorpro/commands/findsequence) +- [FindValue](https://docs.wavemetrics.com/igorpro/commands/findvalue) +- [floor](https://docs.wavemetrics.com/igorpro/commands/floor) +- [FMaxFlat](https://docs.wavemetrics.com/igorpro/commands/fmaxflat) +- [FontList](https://docs.wavemetrics.com/igorpro/commands/fontlist) +- [FontSizeHeight](https://docs.wavemetrics.com/igorpro/commands/fontsizeheight) +- [FontSizeStringWidth](https://docs.wavemetrics.com/igorpro/commands/fontsizestringwidth) +- [FPClustering](https://docs.wavemetrics.com/igorpro/commands/fpclustering) +- [fprintf](https://docs.wavemetrics.com/igorpro/commands/fprintf) +- [FReadLine](https://docs.wavemetrics.com/igorpro/commands/freadline) +- [fresnelCos](https://docs.wavemetrics.com/igorpro/commands/fresnelcos) +- [fresnelCS](https://docs.wavemetrics.com/igorpro/commands/fresnelcs) +- [fresnelSin](https://docs.wavemetrics.com/igorpro/commands/fresnelsin) +- [FSetPos](https://docs.wavemetrics.com/igorpro/commands/fsetpos) +- [FStatus](https://docs.wavemetrics.com/igorpro/commands/fstatus) +- [FTPCreateDirectory](https://docs.wavemetrics.com/igorpro/commands/ftpcreatedirectory) +- [FTPDelete](https://docs.wavemetrics.com/igorpro/commands/ftpdelete) +- [FTPDownload](https://docs.wavemetrics.com/igorpro/commands/ftpdownload) +- [FTPUpload](https://docs.wavemetrics.com/igorpro/commands/ftpupload) +- [FuncFit](https://docs.wavemetrics.com/igorpro/commands/funcfit) +- [FuncFitMD](https://docs.wavemetrics.com/igorpro/commands/funcfitmd) +- [FuncRefInfo](https://docs.wavemetrics.com/igorpro/commands/funcrefinfo) +- [FunctionInfo](https://docs.wavemetrics.com/igorpro/commands/functioninfo) +- [FunctionList](https://docs.wavemetrics.com/igorpro/commands/functionlist) +- [FunctionPath](https://docs.wavemetrics.com/igorpro/commands/functionpath) +- [Functions_Intro](https://docs.wavemetrics.com/igorpro/commands/functions_intro) + +## G + +- [gamma](https://docs.wavemetrics.com/igorpro/commands/gamma) +- [gammaEuler](https://docs.wavemetrics.com/igorpro/commands/gammaeuler) +- [gammaInc](https://docs.wavemetrics.com/igorpro/commands/gammainc) +- [gammaNoise](https://docs.wavemetrics.com/igorpro/commands/gammanoise) +- [gammln](https://docs.wavemetrics.com/igorpro/commands/gammln) +- [gammp](https://docs.wavemetrics.com/igorpro/commands/gammp) +- [gammq](https://docs.wavemetrics.com/igorpro/commands/gammq) +- [Gauss](https://docs.wavemetrics.com/igorpro/commands/gauss) +- [Gauss1D](https://docs.wavemetrics.com/igorpro/commands/gauss1d) +- [Gauss2D](https://docs.wavemetrics.com/igorpro/commands/gauss2d) +- [GBLoadWave](https://docs.wavemetrics.com/igorpro/commands/gbloadwave) +- [gcd](https://docs.wavemetrics.com/igorpro/commands/gcd) +- [GeometricMean](https://docs.wavemetrics.com/igorpro/commands/geometricmean) +- [GetAxis](https://docs.wavemetrics.com/igorpro/commands/getaxis) +- [GetBrowserLine](https://docs.wavemetrics.com/igorpro/commands/getbrowserline) +- [GetBrowserSelection](https://docs.wavemetrics.com/igorpro/commands/getbrowserselection) +- [GetCamera](https://docs.wavemetrics.com/igorpro/commands/getcamera) +- [GetDataFolder](https://docs.wavemetrics.com/igorpro/commands/getdatafolder) +- [GetDataFolderDFR](https://docs.wavemetrics.com/igorpro/commands/getdatafolderdfr) +- [GetDefaultFont](https://docs.wavemetrics.com/igorpro/commands/getdefaultfont) +- [GetDefaultFontSize](https://docs.wavemetrics.com/igorpro/commands/getdefaultfontsize) +- [GetDefaultFontStyle](https://docs.wavemetrics.com/igorpro/commands/getdefaultfontstyle) +- [GetDimLabel](https://docs.wavemetrics.com/igorpro/commands/getdimlabel) +- [GetEnvironmentVariable](https://docs.wavemetrics.com/igorpro/commands/getenvironmentvariable) +- [GetErrMessage](https://docs.wavemetrics.com/igorpro/commands/geterrmessage) +- [GetFileFolderInfo](https://docs.wavemetrics.com/igorpro/commands/getfilefolderinfo) +- [GetFormula](https://docs.wavemetrics.com/igorpro/commands/getformula) +- [GetGizmo](https://docs.wavemetrics.com/igorpro/commands/getgizmo) +- [GetIndependentModuleName](https://docs.wavemetrics.com/igorpro/commands/getindependentmodulename) +- [GetIndexedObjName](https://docs.wavemetrics.com/igorpro/commands/getindexedobjname) +- [GetIndexedObjNameDFR](https://docs.wavemetrics.com/igorpro/commands/getindexedobjnamedfr) +- [GetKeyState](https://docs.wavemetrics.com/igorpro/commands/getkeystate) +- [GetLastUserMenuInfo](https://docs.wavemetrics.com/igorpro/commands/getlastusermenuinfo) +- [GetMarquee](https://docs.wavemetrics.com/igorpro/commands/getmarquee) +- [GetMouse](https://docs.wavemetrics.com/igorpro/commands/getmouse) +- [GetRTErrMessage](https://docs.wavemetrics.com/igorpro/commands/getrterrmessage) +- [GetRTError](https://docs.wavemetrics.com/igorpro/commands/getrterror) +- [GetRTLocation](https://docs.wavemetrics.com/igorpro/commands/getrtlocation) +- [GetRTLocInfo](https://docs.wavemetrics.com/igorpro/commands/getrtlocinfo) +- [GetRTStackInfo](https://docs.wavemetrics.com/igorpro/commands/getrtstackinfo) +- [GetScrapText](https://docs.wavemetrics.com/igorpro/commands/getscraptext) +- [GetSelection](https://docs.wavemetrics.com/igorpro/commands/getselection) +- [GetUserData](https://docs.wavemetrics.com/igorpro/commands/getuserdata) +- [GetWavesDataFolder](https://docs.wavemetrics.com/igorpro/commands/getwavesdatafolder) +- [GetWavesDataFolderDFR](https://docs.wavemetrics.com/igorpro/commands/getwavesdatafolderdfr) +- [GetWindow](https://docs.wavemetrics.com/igorpro/commands/getwindow) +- [GetWindowBrowserSelection](https://docs.wavemetrics.com/igorpro/commands/getwindowbrowserselection) +- [GizmoInfo](https://docs.wavemetrics.com/igorpro/commands/gizmoinfo) +- [GizmoScale](https://docs.wavemetrics.com/igorpro/commands/gizmoscale) +- [gnoise](https://docs.wavemetrics.com/igorpro/commands/gnoise) +- [graphemeLength](https://docs.wavemetrics.com/igorpro/commands/graphemelength) +- [GraphNormal](https://docs.wavemetrics.com/igorpro/commands/graphnormal) +- [GraphWaveDraw](https://docs.wavemetrics.com/igorpro/commands/graphwavedraw) +- [GraphWaveEdit](https://docs.wavemetrics.com/igorpro/commands/graphwaveedit) +- [Grep](https://docs.wavemetrics.com/igorpro/commands/grep) +- [GrepList](https://docs.wavemetrics.com/igorpro/commands/greplist) +- [GrepString](https://docs.wavemetrics.com/igorpro/commands/grepstring) +- [GroupBox](https://docs.wavemetrics.com/igorpro/commands/groupbox) +- [GuideInfo](https://docs.wavemetrics.com/igorpro/commands/guideinfo) +- [GuideNameList](https://docs.wavemetrics.com/igorpro/commands/guidenamelist) + +## H + +- [Hanning](https://docs.wavemetrics.com/igorpro/commands/hanning) +- [Hash](https://docs.wavemetrics.com/igorpro/commands/hash) +- [HCluster](https://docs.wavemetrics.com/igorpro/commands/hcluster) +- [hcsr](https://docs.wavemetrics.com/igorpro/commands/hcsr) +- [HDF5AttributeInfo](https://docs.wavemetrics.com/igorpro/commands/hdf5attributeinfo) +- [HDF5CloseFile](https://docs.wavemetrics.com/igorpro/commands/hdf5closefile) +- [HDF5CloseGroup](https://docs.wavemetrics.com/igorpro/commands/hdf5closegroup) +- [HDF5Control](https://docs.wavemetrics.com/igorpro/commands/hdf5control) +- [HDF5CreateFile](https://docs.wavemetrics.com/igorpro/commands/hdf5createfile) +- [HDF5CreateGroup](https://docs.wavemetrics.com/igorpro/commands/hdf5creategroup) +- [HDF5CreateLink](https://docs.wavemetrics.com/igorpro/commands/hdf5createlink) +- [HDF5DatasetInfo](https://docs.wavemetrics.com/igorpro/commands/hdf5datasetinfo) +- [HDF5DimensionScale](https://docs.wavemetrics.com/igorpro/commands/hdf5dimensionscale) +- [HDF5Dump](https://docs.wavemetrics.com/igorpro/commands/hdf5dump) +- [HDF5DumpErrors](https://docs.wavemetrics.com/igorpro/commands/hdf5dumperrors) +- [HDF5FlushFile](https://docs.wavemetrics.com/igorpro/commands/hdf5flushfile) +- [HDF5LibraryInfo](https://docs.wavemetrics.com/igorpro/commands/hdf5libraryinfo) +- [HDF5LinkInfo](https://docs.wavemetrics.com/igorpro/commands/hdf5linkinfo) +- [HDF5ListAttributes](https://docs.wavemetrics.com/igorpro/commands/hdf5listattributes) +- [HDF5ListGroup](https://docs.wavemetrics.com/igorpro/commands/hdf5listgroup) +- [HDF5LoadData](https://docs.wavemetrics.com/igorpro/commands/hdf5loaddata) +- [HDF5LoadGroup](https://docs.wavemetrics.com/igorpro/commands/hdf5loadgroup) +- [HDF5LoadImage](https://docs.wavemetrics.com/igorpro/commands/hdf5loadimage) +- [HDF5OpenFile](https://docs.wavemetrics.com/igorpro/commands/hdf5openfile) +- [HDF5OpenGroup](https://docs.wavemetrics.com/igorpro/commands/hdf5opengroup) +- [HDF5SaveData](https://docs.wavemetrics.com/igorpro/commands/hdf5savedata) +- [HDF5SaveGroup](https://docs.wavemetrics.com/igorpro/commands/hdf5savegroup) +- [HDF5SaveImage](https://docs.wavemetrics.com/igorpro/commands/hdf5saveimage) +- [HDF5TypeInfo](https://docs.wavemetrics.com/igorpro/commands/hdf5typeinfo) +- [HDF5UnlinkObject](https://docs.wavemetrics.com/igorpro/commands/hdf5unlinkobject) +- [hermite](https://docs.wavemetrics.com/igorpro/commands/hermite) +- [hermiteGauss](https://docs.wavemetrics.com/igorpro/commands/hermitegauss) +- [HideIgorMenus](https://docs.wavemetrics.com/igorpro/commands/hideigormenus) +- [HideInfo](https://docs.wavemetrics.com/igorpro/commands/hideinfo) +- [HideProcedures](https://docs.wavemetrics.com/igorpro/commands/hideprocedures) +- [HideTools](https://docs.wavemetrics.com/igorpro/commands/hidetools) +- [HilbertTransform](https://docs.wavemetrics.com/igorpro/commands/hilberttransform) +- [Histogram](https://docs.wavemetrics.com/igorpro/commands/histogram) +- [hyperG0F1](https://docs.wavemetrics.com/igorpro/commands/hyperg0f1) +- [hyperG1F1](https://docs.wavemetrics.com/igorpro/commands/hyperg1f1) +- [hyperG2F1](https://docs.wavemetrics.com/igorpro/commands/hyperg2f1) +- [hyperGNoise](https://docs.wavemetrics.com/igorpro/commands/hypergnoise) +- [hyperGPFQ](https://docs.wavemetrics.com/igorpro/commands/hypergpfq) + +## I + +- [i](https://docs.wavemetrics.com/igorpro/commands/i) +- [ICA](https://docs.wavemetrics.com/igorpro/commands/ica) +- [IFFT](https://docs.wavemetrics.com/igorpro/commands/ifft) +- [IgorInfo](https://docs.wavemetrics.com/igorpro/commands/igorinfo) +- [IgorVersion](https://docs.wavemetrics.com/igorpro/commands/igorversion) +- [ilim](https://docs.wavemetrics.com/igorpro/commands/ilim) +- [imag](https://docs.wavemetrics.com/igorpro/commands/imag) +- [ImageAnalyzeParticles](https://docs.wavemetrics.com/igorpro/commands/imageanalyzeparticles) +- [ImageBlend](https://docs.wavemetrics.com/igorpro/commands/imageblend) +- [ImageBoundaryToMask](https://docs.wavemetrics.com/igorpro/commands/imageboundarytomask) +- [ImageComposite](https://docs.wavemetrics.com/igorpro/commands/imagecomposite) +- [ImageEdgeDetection](https://docs.wavemetrics.com/igorpro/commands/imageedgedetection) +- [ImageFileInfo](https://docs.wavemetrics.com/igorpro/commands/imagefileinfo) +- [ImageFilter](https://docs.wavemetrics.com/igorpro/commands/imagefilter) +- [ImageFocus](https://docs.wavemetrics.com/igorpro/commands/imagefocus) +- [ImageFromXYZ](https://docs.wavemetrics.com/igorpro/commands/imagefromxyz) +- [ImageGenerateROIMask](https://docs.wavemetrics.com/igorpro/commands/imagegenerateroimask) +- [ImageGLCM](https://docs.wavemetrics.com/igorpro/commands/imageglcm) +- [ImageHistModification](https://docs.wavemetrics.com/igorpro/commands/imagehistmodification) +- [ImageHistogram](https://docs.wavemetrics.com/igorpro/commands/imagehistogram) +- [ImageInfo](https://docs.wavemetrics.com/igorpro/commands/imageinfo) +- [ImageInterpolate](https://docs.wavemetrics.com/igorpro/commands/imageinterpolate) +- [ImageLineProfile](https://docs.wavemetrics.com/igorpro/commands/imagelineprofile) +- [ImageLoad](https://docs.wavemetrics.com/igorpro/commands/imageload) +- [ImageMorphology](https://docs.wavemetrics.com/igorpro/commands/imagemorphology) +- [ImageNameList](https://docs.wavemetrics.com/igorpro/commands/imagenamelist) +- [ImageNameToWaveRef](https://docs.wavemetrics.com/igorpro/commands/imagenametowaveref) +- [ImageRegistration](https://docs.wavemetrics.com/igorpro/commands/imageregistration) +- [ImageRemoveBackground](https://docs.wavemetrics.com/igorpro/commands/imageremovebackground) +- [ImageRestore](https://docs.wavemetrics.com/igorpro/commands/imagerestore) +- [ImageRotate](https://docs.wavemetrics.com/igorpro/commands/imagerotate) +- [ImageSave](https://docs.wavemetrics.com/igorpro/commands/imagesave) +- [ImageSeedFill](https://docs.wavemetrics.com/igorpro/commands/imageseedfill) +- [ImageSkeleton3D](https://docs.wavemetrics.com/igorpro/commands/imageskeleton3d) +- [ImageSnake](https://docs.wavemetrics.com/igorpro/commands/imagesnake) +- [ImageStats](https://docs.wavemetrics.com/igorpro/commands/imagestats) +- [ImageThreshold](https://docs.wavemetrics.com/igorpro/commands/imagethreshold) +- [ImageTransform](https://docs.wavemetrics.com/igorpro/commands/imagetransform) +- [ImageUnwrapPhase](https://docs.wavemetrics.com/igorpro/commands/imageunwrapphase) +- [ImageWindow](https://docs.wavemetrics.com/igorpro/commands/imagewindow) +- [IndependentModuleList](https://docs.wavemetrics.com/igorpro/commands/independentmodulelist) +- [IndexedDir](https://docs.wavemetrics.com/igorpro/commands/indexeddir) +- [IndexedFile](https://docs.wavemetrics.com/igorpro/commands/indexedfile) +- [IndexSort](https://docs.wavemetrics.com/igorpro/commands/indexsort) +- [IndexToScale](https://docs.wavemetrics.com/igorpro/commands/indextoscale) +- [Inf](https://docs.wavemetrics.com/igorpro/commands/inf) +- [InsertPoints](https://docs.wavemetrics.com/igorpro/commands/insertpoints) +- [InstantFrequency](https://docs.wavemetrics.com/igorpro/commands/instantfrequency) +- [Integrate](https://docs.wavemetrics.com/igorpro/commands/integrate) +- [Integrate1D](https://docs.wavemetrics.com/igorpro/commands/integrate1d) +- [Integrate2D](https://docs.wavemetrics.com/igorpro/commands/integrate2d) +- [IntegrateODE](https://docs.wavemetrics.com/igorpro/commands/integrateode) +- [interp](https://docs.wavemetrics.com/igorpro/commands/interp) +- [Interp2D](https://docs.wavemetrics.com/igorpro/commands/interp2d) +- [Interp3D](https://docs.wavemetrics.com/igorpro/commands/interp3d) +- [Interp3DPath](https://docs.wavemetrics.com/igorpro/commands/interp3dpath) +- [Interp4D](https://docs.wavemetrics.com/igorpro/commands/interp4d) +- [Interp4DPath](https://docs.wavemetrics.com/igorpro/commands/interp4dpath) +- [Interpolate2](https://docs.wavemetrics.com/igorpro/commands/interpolate2) +- [Interpolate3D](https://docs.wavemetrics.com/igorpro/commands/interpolate3d) +- [inverseErf](https://docs.wavemetrics.com/igorpro/commands/inverseerf) +- [inverseErfc](https://docs.wavemetrics.com/igorpro/commands/inverseerfc) +- [ItemsInList](https://docs.wavemetrics.com/igorpro/commands/itemsinlist) + +## J + +- [j](https://docs.wavemetrics.com/igorpro/commands/j) +- [JacobiCn](https://docs.wavemetrics.com/igorpro/commands/jacobicn) +- [JacobiSn](https://docs.wavemetrics.com/igorpro/commands/jacobisn) +- [JCAMPLoadWave](https://docs.wavemetrics.com/igorpro/commands/jcamploadwave) +- [jlim](https://docs.wavemetrics.com/igorpro/commands/jlim) +- [JointHistogram](https://docs.wavemetrics.com/igorpro/commands/jointhistogram) +- [JulianToDate](https://docs.wavemetrics.com/igorpro/commands/juliantodate) + +## K + +- [KillBackground](https://docs.wavemetrics.com/igorpro/commands/killbackground) +- [KillControl](https://docs.wavemetrics.com/igorpro/commands/killcontrol) +- [KillDataFolder](https://docs.wavemetrics.com/igorpro/commands/killdatafolder) +- [KillFIFO](https://docs.wavemetrics.com/igorpro/commands/killfifo) +- [KillFreeAxis](https://docs.wavemetrics.com/igorpro/commands/killfreeaxis) +- [KillPath](https://docs.wavemetrics.com/igorpro/commands/killpath) +- [KillPICTs](https://docs.wavemetrics.com/igorpro/commands/killpicts) +- [KillStrings](https://docs.wavemetrics.com/igorpro/commands/killstrings) +- [KillVariables](https://docs.wavemetrics.com/igorpro/commands/killvariables) +- [KillWaves](https://docs.wavemetrics.com/igorpro/commands/killwaves) +- [KillWindow](https://docs.wavemetrics.com/igorpro/commands/killwindow) +- [KMeans](https://docs.wavemetrics.com/igorpro/commands/kmeans) + +## L + +- [Label](https://docs.wavemetrics.com/igorpro/commands/label) +- [laguerre](https://docs.wavemetrics.com/igorpro/commands/laguerre) +- [laguerreA](https://docs.wavemetrics.com/igorpro/commands/laguerrea) +- [laguerreGauss](https://docs.wavemetrics.com/igorpro/commands/laguerregauss) +- [LambertW](https://docs.wavemetrics.com/igorpro/commands/lambertw) +- [Layout](https://docs.wavemetrics.com/igorpro/commands/layout) +- [LayoutInfo](https://docs.wavemetrics.com/igorpro/commands/layoutinfo) +- [LayoutPageAction](https://docs.wavemetrics.com/igorpro/commands/layoutpageaction) +- [LayoutSlideShow](https://docs.wavemetrics.com/igorpro/commands/layoutslideshow) +- [leftx](https://docs.wavemetrics.com/igorpro/commands/leftx) +- [Legend](https://docs.wavemetrics.com/igorpro/commands/legend) +- [legendreA](https://docs.wavemetrics.com/igorpro/commands/legendrea) +- [limit](https://docs.wavemetrics.com/igorpro/commands/limit) +- [LinearFeedbackShiftRegister](https://docs.wavemetrics.com/igorpro/commands/linearfeedbackshiftregister) +- [ListBox](https://docs.wavemetrics.com/igorpro/commands/listbox) +- [ListMatch](https://docs.wavemetrics.com/igorpro/commands/listmatch) +- [ListToTextWave](https://docs.wavemetrics.com/igorpro/commands/listtotextwave) +- [ListToWaveRefWave](https://docs.wavemetrics.com/igorpro/commands/listtowaverefwave) +- [ln](https://docs.wavemetrics.com/igorpro/commands/ln) +- [LoadData](https://docs.wavemetrics.com/igorpro/commands/loaddata) +- [LoadPackagePreferences](https://docs.wavemetrics.com/igorpro/commands/loadpackagepreferences) +- [LoadPICT](https://docs.wavemetrics.com/igorpro/commands/loadpict) +- [LoadWave](https://docs.wavemetrics.com/igorpro/commands/loadwave) +- [Loess](https://docs.wavemetrics.com/igorpro/commands/loess) +- [log](https://docs.wavemetrics.com/igorpro/commands/log) +- [logNormalNoise](https://docs.wavemetrics.com/igorpro/commands/lognormalnoise) +- [LombPeriodogram](https://docs.wavemetrics.com/igorpro/commands/lombperiodogram) +- [lorentzianNoise](https://docs.wavemetrics.com/igorpro/commands/lorentziannoise) +- [LowerStr](https://docs.wavemetrics.com/igorpro/commands/lowerstr) + +## M + +- [MacroInfo](https://docs.wavemetrics.com/igorpro/commands/macroinfo) +- [MacroList](https://docs.wavemetrics.com/igorpro/commands/macrolist) +- [MacroPath](https://docs.wavemetrics.com/igorpro/commands/macropath) +- [magsqr](https://docs.wavemetrics.com/igorpro/commands/magsqr) +- [Make](https://docs.wavemetrics.com/igorpro/commands/make) +- [MakeIndex](https://docs.wavemetrics.com/igorpro/commands/makeindex) +- [MandelbrotPoint](https://docs.wavemetrics.com/igorpro/commands/mandelbrotpoint) +- [MarcumQ](https://docs.wavemetrics.com/igorpro/commands/marcumq) +- [MarkPerfTestTime](https://docs.wavemetrics.com/igorpro/commands/markperftesttime) +- [MatrixBalance](https://docs.wavemetrics.com/igorpro/commands/matrixbalance) +- [MatrixCondition](https://docs.wavemetrics.com/igorpro/commands/matrixcondition) +- [MatrixConvolve](https://docs.wavemetrics.com/igorpro/commands/matrixconvolve) +- [MatrixCorr](https://docs.wavemetrics.com/igorpro/commands/matrixcorr) +- [MatrixDet](https://docs.wavemetrics.com/igorpro/commands/matrixdet) +- [MatrixDot](https://docs.wavemetrics.com/igorpro/commands/matrixdot) +- [MatrixEigenV](https://docs.wavemetrics.com/igorpro/commands/matrixeigenv) +- [MatrixFactor](https://docs.wavemetrics.com/igorpro/commands/matrixfactor) +- [MatrixFilter](https://docs.wavemetrics.com/igorpro/commands/matrixfilter) +- [MatrixGaussJ](https://docs.wavemetrics.com/igorpro/commands/matrixgaussj) +- [MatrixGLM](https://docs.wavemetrics.com/igorpro/commands/matrixglm) +- [MatrixInverse](https://docs.wavemetrics.com/igorpro/commands/matrixinverse) +- [MatrixLinearSolve](https://docs.wavemetrics.com/igorpro/commands/matrixlinearsolve) +- [MatrixLinearSolveTD](https://docs.wavemetrics.com/igorpro/commands/matrixlinearsolvetd) +- [MatrixLLS](https://docs.wavemetrics.com/igorpro/commands/matrixlls) +- [MatrixLUBkSub](https://docs.wavemetrics.com/igorpro/commands/matrixlubksub) +- [MatrixLUD](https://docs.wavemetrics.com/igorpro/commands/matrixlud) +- [MatrixLUDTD](https://docs.wavemetrics.com/igorpro/commands/matrixludtd) +- [MatrixMultiply](https://docs.wavemetrics.com/igorpro/commands/matrixmultiply) +- [MatrixMultiplyAdd](https://docs.wavemetrics.com/igorpro/commands/matrixmultiplyadd) +- [MatrixOp](https://docs.wavemetrics.com/igorpro/commands/matrixop) +- [MatrixRank](https://docs.wavemetrics.com/igorpro/commands/matrixrank) +- [MatrixReverseBalance](https://docs.wavemetrics.com/igorpro/commands/matrixreversebalance) +- [MatrixSchur](https://docs.wavemetrics.com/igorpro/commands/matrixschur) +- [MatrixSolve](https://docs.wavemetrics.com/igorpro/commands/matrixsolve) +- [MatrixSparse](https://docs.wavemetrics.com/igorpro/commands/matrixsparse) +- [MatrixSVBkSub](https://docs.wavemetrics.com/igorpro/commands/matrixsvbksub) +- [MatrixSVD](https://docs.wavemetrics.com/igorpro/commands/matrixsvd) +- [MatrixTrace](https://docs.wavemetrics.com/igorpro/commands/matrixtrace) +- [MatrixTranspose](https://docs.wavemetrics.com/igorpro/commands/matrixtranspose) +- [max](https://docs.wavemetrics.com/igorpro/commands/max) +- [mean](https://docs.wavemetrics.com/igorpro/commands/mean) +- [MeasureStyledText](https://docs.wavemetrics.com/igorpro/commands/measurestyledtext) +- [median](https://docs.wavemetrics.com/igorpro/commands/median) +- [min](https://docs.wavemetrics.com/igorpro/commands/min) +- [MLLoadWave](https://docs.wavemetrics.com/igorpro/commands/mlloadwave) +- [mod](https://docs.wavemetrics.com/igorpro/commands/mod) +- [ModDate](https://docs.wavemetrics.com/igorpro/commands/moddate) +- [Modify](https://docs.wavemetrics.com/igorpro/commands/modify) +- [ModifyBoxPlot](https://docs.wavemetrics.com/igorpro/commands/modifyboxplot) +- [ModifyBrowser](https://docs.wavemetrics.com/igorpro/commands/modifybrowser) +- [ModifyCamera](https://docs.wavemetrics.com/igorpro/commands/modifycamera) +- [ModifyContour](https://docs.wavemetrics.com/igorpro/commands/modifycontour) +- [ModifyControl](https://docs.wavemetrics.com/igorpro/commands/modifycontrol) +- [ModifyControlList](https://docs.wavemetrics.com/igorpro/commands/modifycontrollist) +- [ModifyFreeAxis](https://docs.wavemetrics.com/igorpro/commands/modifyfreeaxis) +- [ModifyGizmo](https://docs.wavemetrics.com/igorpro/commands/modifygizmo) +- [ModifyGraph](https://docs.wavemetrics.com/igorpro/commands/modifygraph) +- [ModifyImage](https://docs.wavemetrics.com/igorpro/commands/modifyimage) +- [ModifyLayout](https://docs.wavemetrics.com/igorpro/commands/modifylayout) +- [ModifyPanel](https://docs.wavemetrics.com/igorpro/commands/modifypanel) +- [ModifyProcedure](https://docs.wavemetrics.com/igorpro/commands/modifyprocedure) +- [ModifyTable](https://docs.wavemetrics.com/igorpro/commands/modifytable) +- [ModifyViolinPlot](https://docs.wavemetrics.com/igorpro/commands/modifyviolinplot) +- [ModifyWaterfall](https://docs.wavemetrics.com/igorpro/commands/modifywaterfall) +- [MoveDataFolder](https://docs.wavemetrics.com/igorpro/commands/movedatafolder) +- [MoveFile](https://docs.wavemetrics.com/igorpro/commands/movefile) +- [MoveFolder](https://docs.wavemetrics.com/igorpro/commands/movefolder) +- [MoveString](https://docs.wavemetrics.com/igorpro/commands/movestring) +- [MoveSubwindow](https://docs.wavemetrics.com/igorpro/commands/movesubwindow) +- [MoveVariable](https://docs.wavemetrics.com/igorpro/commands/movevariable) +- [MoveWave](https://docs.wavemetrics.com/igorpro/commands/movewave) +- [MoveWindow](https://docs.wavemetrics.com/igorpro/commands/movewindow) +- [MPFXEMGPeak](https://docs.wavemetrics.com/igorpro/commands/mpfxemgpeak) +- [MPFXExpConvExpPeak](https://docs.wavemetrics.com/igorpro/commands/mpfxexpconvexppeak) +- [MPFXGaussPeak](https://docs.wavemetrics.com/igorpro/commands/mpfxgausspeak) +- [MPFXLorentzianPeak](https://docs.wavemetrics.com/igorpro/commands/mpfxlorentzianpeak) +- [MPFXVoigtPeak](https://docs.wavemetrics.com/igorpro/commands/mpfxvoigtpeak) +- [MultiTaperPSD](https://docs.wavemetrics.com/igorpro/commands/multitaperpsd) +- [MultiThreadingControl](https://docs.wavemetrics.com/igorpro/commands/multithreadingcontrol) + +## N + +- [NameOfWave](https://docs.wavemetrics.com/igorpro/commands/nameofwave) +- [NaN](https://docs.wavemetrics.com/igorpro/commands/nan) +- [NeuralNetworkRun](https://docs.wavemetrics.com/igorpro/commands/neuralnetworkrun) +- [NeuralNetworkTrain](https://docs.wavemetrics.com/igorpro/commands/neuralnetworktrain) +- [NewCamera](https://docs.wavemetrics.com/igorpro/commands/newcamera) +- [NewDataFolder](https://docs.wavemetrics.com/igorpro/commands/newdatafolder) +- [NewFIFO](https://docs.wavemetrics.com/igorpro/commands/newfifo) +- [NewFIFOChan](https://docs.wavemetrics.com/igorpro/commands/newfifochan) +- [NewFreeAxis](https://docs.wavemetrics.com/igorpro/commands/newfreeaxis) +- [NewFreeDataFolder](https://docs.wavemetrics.com/igorpro/commands/newfreedatafolder) +- [NewFreeWave](https://docs.wavemetrics.com/igorpro/commands/newfreewave) +- [NewGizmo](https://docs.wavemetrics.com/igorpro/commands/newgizmo) +- [NewImage](https://docs.wavemetrics.com/igorpro/commands/newimage) +- [NewLayout](https://docs.wavemetrics.com/igorpro/commands/newlayout) +- [NewMovie](https://docs.wavemetrics.com/igorpro/commands/newmovie) +- [NewNotebook](https://docs.wavemetrics.com/igorpro/commands/newnotebook) +- [NewPanel](https://docs.wavemetrics.com/igorpro/commands/newpanel) +- [NewPath](https://docs.wavemetrics.com/igorpro/commands/newpath) +- [NewWaterFall](https://docs.wavemetrics.com/igorpro/commands/newwaterfall) +- [norm](https://docs.wavemetrics.com/igorpro/commands/norm) +- [NormalizeUnicode](https://docs.wavemetrics.com/igorpro/commands/normalizeunicode) +- [note (function)](https://docs.wavemetrics.com/igorpro/commands/note_function) +- [Note (operation)](https://docs.wavemetrics.com/igorpro/commands/note_operation) +- [Notebook](https://docs.wavemetrics.com/igorpro/commands/notebook) +- [NotebookAction](https://docs.wavemetrics.com/igorpro/commands/notebookaction) +- [num2char](https://docs.wavemetrics.com/igorpro/commands/num2char) +- [num2istr](https://docs.wavemetrics.com/igorpro/commands/num2istr) +- [num2str](https://docs.wavemetrics.com/igorpro/commands/num2str) +- [NumberByKey](https://docs.wavemetrics.com/igorpro/commands/numberbykey) +- [numpnts](https://docs.wavemetrics.com/igorpro/commands/numpnts) +- [numtype](https://docs.wavemetrics.com/igorpro/commands/numtype) +- [NumVarOrDefault](https://docs.wavemetrics.com/igorpro/commands/numvarordefault) +- [NVAR_Exists](https://docs.wavemetrics.com/igorpro/commands/nvar_exists) + +## O + +- [Open](https://docs.wavemetrics.com/igorpro/commands/open) +- [OpenHelp](https://docs.wavemetrics.com/igorpro/commands/openhelp) +- [OpenNotebook](https://docs.wavemetrics.com/igorpro/commands/opennotebook) +- [OpenProc](https://docs.wavemetrics.com/igorpro/commands/openproc) +- [OperationList](https://docs.wavemetrics.com/igorpro/commands/operationlist) +- [Optimize](https://docs.wavemetrics.com/igorpro/commands/optimize) + +## P + +- [p](https://docs.wavemetrics.com/igorpro/commands/p) +- [p2rect](https://docs.wavemetrics.com/igorpro/commands/p2rect) +- [PadString](https://docs.wavemetrics.com/igorpro/commands/padstring) +- [PanelResolution](https://docs.wavemetrics.com/igorpro/commands/panelresolution) +- [ParamIsDefault](https://docs.wavemetrics.com/igorpro/commands/paramisdefault) +- [ParseFilePath](https://docs.wavemetrics.com/igorpro/commands/parsefilepath) +- [ParseOperationTemplate](https://docs.wavemetrics.com/igorpro/commands/parseoperationtemplate) +- [PathInfo](https://docs.wavemetrics.com/igorpro/commands/pathinfo) +- [PathList](https://docs.wavemetrics.com/igorpro/commands/pathlist) +- [PauseForUser](https://docs.wavemetrics.com/igorpro/commands/pauseforuser) +- [PauseUpdate](https://docs.wavemetrics.com/igorpro/commands/pauseupdate) +- [PCA](https://docs.wavemetrics.com/igorpro/commands/pca) +- [pcsr](https://docs.wavemetrics.com/igorpro/commands/pcsr) +- [Pi](https://docs.wavemetrics.com/igorpro/commands/pi) +- [PICTInfo](https://docs.wavemetrics.com/igorpro/commands/pictinfo) +- [PICTList](https://docs.wavemetrics.com/igorpro/commands/pictlist) +- [PixelFromAxisVal](https://docs.wavemetrics.com/igorpro/commands/pixelfromaxisval) +- [PlayMovie](https://docs.wavemetrics.com/igorpro/commands/playmovie) +- [PlayMovieAction](https://docs.wavemetrics.com/igorpro/commands/playmovieaction) +- [PlaySound](https://docs.wavemetrics.com/igorpro/commands/playsound) +- [pnt2x](https://docs.wavemetrics.com/igorpro/commands/pnt2x) +- [poissonNoise](https://docs.wavemetrics.com/igorpro/commands/poissonnoise) +- [poly](https://docs.wavemetrics.com/igorpro/commands/poly) +- [poly2D](https://docs.wavemetrics.com/igorpro/commands/poly2d) +- [PolygonArea](https://docs.wavemetrics.com/igorpro/commands/polygonarea) +- [PolygonOp](https://docs.wavemetrics.com/igorpro/commands/polygonop) +- [PopupContextualMenu](https://docs.wavemetrics.com/igorpro/commands/popupcontextualmenu) +- [PopupMenu](https://docs.wavemetrics.com/igorpro/commands/popupmenu) +- [PossiblyQuoteName](https://docs.wavemetrics.com/igorpro/commands/possiblyquotename) +- [Preferences](https://docs.wavemetrics.com/igorpro/commands/preferences) +- [PrimeFactors](https://docs.wavemetrics.com/igorpro/commands/primefactors) +- [Print](https://docs.wavemetrics.com/igorpro/commands/print) +- [printf](https://docs.wavemetrics.com/igorpro/commands/printf) +- [PrintGraphs](https://docs.wavemetrics.com/igorpro/commands/printgraphs) +- [PrintLayout](https://docs.wavemetrics.com/igorpro/commands/printlayout) +- [PrintNotebook](https://docs.wavemetrics.com/igorpro/commands/printnotebook) +- [PrintSettings](https://docs.wavemetrics.com/igorpro/commands/printsettings) +- [PrintTable](https://docs.wavemetrics.com/igorpro/commands/printtable) +- [ProcedureText](https://docs.wavemetrics.com/igorpro/commands/proceduretext) +- [ProcedureVersion](https://docs.wavemetrics.com/igorpro/commands/procedureversion) +- [Project](https://docs.wavemetrics.com/igorpro/commands/project) +- [PulseStats](https://docs.wavemetrics.com/igorpro/commands/pulsestats) +- [PutScrapText](https://docs.wavemetrics.com/igorpro/commands/putscraptext) +- [pwd](https://docs.wavemetrics.com/igorpro/commands/pwd) +- [Python](https://docs.wavemetrics.com/igorpro/commands/python) +- [PythonEnv](https://docs.wavemetrics.com/igorpro/commands/pythonenv) +- [PythonFile](https://docs.wavemetrics.com/igorpro/commands/pythonfile) + +## Q + +- [q](https://docs.wavemetrics.com/igorpro/commands/q) +- [qcsr](https://docs.wavemetrics.com/igorpro/commands/qcsr) +- [Quit](https://docs.wavemetrics.com/igorpro/commands/quit) + +## R + +- [r](https://docs.wavemetrics.com/igorpro/commands/r) +- [r2polar](https://docs.wavemetrics.com/igorpro/commands/r2polar) +- [RatioFromNumber](https://docs.wavemetrics.com/igorpro/commands/ratiofromnumber) +- [ReadVariables](https://docs.wavemetrics.com/igorpro/commands/readvariables) +- [real](https://docs.wavemetrics.com/igorpro/commands/real) +- [Redimension](https://docs.wavemetrics.com/igorpro/commands/redimension) +- [Remez](https://docs.wavemetrics.com/igorpro/commands/remez) +- [Remove](https://docs.wavemetrics.com/igorpro/commands/remove) +- [RemoveByKey](https://docs.wavemetrics.com/igorpro/commands/removebykey) +- [RemoveContour](https://docs.wavemetrics.com/igorpro/commands/removecontour) +- [RemoveEnding](https://docs.wavemetrics.com/igorpro/commands/removeending) +- [RemoveFromGizmo](https://docs.wavemetrics.com/igorpro/commands/removefromgizmo) +- [RemoveFromGraph](https://docs.wavemetrics.com/igorpro/commands/removefromgraph) +- [RemoveFromLayout](https://docs.wavemetrics.com/igorpro/commands/removefromlayout) +- [RemoveFromList](https://docs.wavemetrics.com/igorpro/commands/removefromlist) +- [RemoveFromTable](https://docs.wavemetrics.com/igorpro/commands/removefromtable) +- [RemoveImage](https://docs.wavemetrics.com/igorpro/commands/removeimage) +- [RemoveLayoutObjects](https://docs.wavemetrics.com/igorpro/commands/removelayoutobjects) +- [RemoveListItem](https://docs.wavemetrics.com/igorpro/commands/removelistitem) +- [RemovePath](https://docs.wavemetrics.com/igorpro/commands/removepath) +- [Rename](https://docs.wavemetrics.com/igorpro/commands/rename) +- [RenameDataFolder](https://docs.wavemetrics.com/igorpro/commands/renamedatafolder) +- [RenamePath](https://docs.wavemetrics.com/igorpro/commands/renamepath) +- [RenamePICT](https://docs.wavemetrics.com/igorpro/commands/renamepict) +- [RenameWindow](https://docs.wavemetrics.com/igorpro/commands/renamewindow) +- [ReorderImages](https://docs.wavemetrics.com/igorpro/commands/reorderimages) +- [ReorderTraces](https://docs.wavemetrics.com/igorpro/commands/reordertraces) +- [ReplaceNumberByKey](https://docs.wavemetrics.com/igorpro/commands/replacenumberbykey) +- [ReplaceString](https://docs.wavemetrics.com/igorpro/commands/replacestring) +- [ReplaceStringByKey](https://docs.wavemetrics.com/igorpro/commands/replacestringbykey) +- [Resample](https://docs.wavemetrics.com/igorpro/commands/resample) +- [ResumeUpdate](https://docs.wavemetrics.com/igorpro/commands/resumeupdate) +- [Reverse](https://docs.wavemetrics.com/igorpro/commands/reverse) +- [IFFT](https://docs.wavemetrics.com/igorpro/commands/ifft) +- [root](https://docs.wavemetrics.com/igorpro/commands/root) +- [Rotate](https://docs.wavemetrics.com/igorpro/commands/rotate) +- [round](https://docs.wavemetrics.com/igorpro/commands/round) + +## S + +- [s](https://docs.wavemetrics.com/igorpro/commands/s) +- [Save](https://docs.wavemetrics.com/igorpro/commands/save) +- [SaveData](https://docs.wavemetrics.com/igorpro/commands/savedata) +- [SaveExperiment](https://docs.wavemetrics.com/igorpro/commands/saveexperiment) +- [SaveGizmo](https://docs.wavemetrics.com/igorpro/commands/savegizmo) +- [SaveGraphCopy](https://docs.wavemetrics.com/igorpro/commands/savegraphcopy) +- [SaveNotebook](https://docs.wavemetrics.com/igorpro/commands/savenotebook) +- [SavePackagePreferences](https://docs.wavemetrics.com/igorpro/commands/savepackagepreferences) +- [SavePICT](https://docs.wavemetrics.com/igorpro/commands/savepict) +- [SaveTableCopy](https://docs.wavemetrics.com/igorpro/commands/savetablecopy) +- [ScaleToIndex](https://docs.wavemetrics.com/igorpro/commands/scaletoindex) +- [sec](https://docs.wavemetrics.com/igorpro/commands/sec) +- [sech](https://docs.wavemetrics.com/igorpro/commands/sech) +- [SetAxis](https://docs.wavemetrics.com/igorpro/commands/setaxis) +- [SetBackground](https://docs.wavemetrics.com/igorpro/commands/setbackground) +- [SetDimLabel](https://docs.wavemetrics.com/igorpro/commands/setdimlabel) +- [SetDrawEnv](https://docs.wavemetrics.com/igorpro/commands/setdrawenv) +- [SetDrawLayer](https://docs.wavemetrics.com/igorpro/commands/setdrawlayer) +- [SetFileFolder](https://docs.wavemetrics.com/igorpro/commands/setfilefolder) +- [SetFormula](https://docs.wavemetrics.com/igorpro/commands/setformula) +- [SetIgorHook](https://docs.wavemetrics.com/igorpro/commands/setigorhook) +- [SetIgorMenuMode](https://docs.wavemetrics.com/igorpro/commands/setigormenumode) +- [SetIgorOption](https://docs.wavemetrics.com/igorpro/commands/setigoroption) +- [SetMarquee](https://docs.wavemetrics.com/igorpro/commands/setmarquee) +- [SetProcessSleep](https://docs.wavemetrics.com/igorpro/commands/setprocesssleep) +- [SetRandomSeed](https://docs.wavemetrics.com/igorpro/commands/setrandomseed) +- [SetScale](https://docs.wavemetrics.com/igorpro/commands/setscale) +- [SetUserData](https://docs.wavemetrics.com/igorpro/commands/setuserdata) +- [SetVariable](https://docs.wavemetrics.com/igorpro/commands/setvariable) +- [SetWaveLock](https://docs.wavemetrics.com/igorpro/commands/setwavelock) +- [SetWindow](https://docs.wavemetrics.com/igorpro/commands/setwindow) +- [ShowIgorMenus](https://docs.wavemetrics.com/igorpro/commands/showigormenus) +- [ShowInfo](https://docs.wavemetrics.com/igorpro/commands/showinfo) +- [ShowTools](https://docs.wavemetrics.com/igorpro/commands/showtools) +- [sin](https://docs.wavemetrics.com/igorpro/commands/sin) +- [sinc](https://docs.wavemetrics.com/igorpro/commands/sinc) +- [sinh](https://docs.wavemetrics.com/igorpro/commands/sinh) +- [SinIntegral](https://docs.wavemetrics.com/igorpro/commands/sinintegral) +- [SmoothCustom](https://docs.wavemetrics.com/igorpro/commands/smoothcustom) +- [Smooth](https://docs.wavemetrics.com/igorpro/commands/smooth) +- [Sort](https://docs.wavemetrics.com/igorpro/commands/sort) +- [SortColumns](https://docs.wavemetrics.com/igorpro/commands/sortcolumns) +- [SoundInRecord](https://docs.wavemetrics.com/igorpro/commands/soundinrecord) +- [SoundInSet](https://docs.wavemetrics.com/igorpro/commands/soundinset) +- [SoundInStartChart](https://docs.wavemetrics.com/igorpro/commands/soundinstartchart) +- [SoundInStatus](https://docs.wavemetrics.com/igorpro/commands/soundinstatus) +- [SoundInStopChart](https://docs.wavemetrics.com/igorpro/commands/soundinstopchart) +- [SplitString](https://docs.wavemetrics.com/igorpro/commands/splitstring) +- [SplitWave](https://docs.wavemetrics.com/igorpro/commands/splitwave) +- [sprintf](https://docs.wavemetrics.com/igorpro/commands/sprintf) +- [sqrt](https://docs.wavemetrics.com/igorpro/commands/sqrt) +- [sscanf](https://docs.wavemetrics.com/igorpro/commands/sscanf) +- [Stack](https://docs.wavemetrics.com/igorpro/commands/stack) +- [StackWindows](https://docs.wavemetrics.com/igorpro/commands/stackwindows) +- [StatsANOVA1Test](https://docs.wavemetrics.com/igorpro/commands/statsanova1test) +- [StatsANOVA2NRTest](https://docs.wavemetrics.com/igorpro/commands/statsanova2nrtest) +- [StatsANOVA2RMTest](https://docs.wavemetrics.com/igorpro/commands/statsanova2rmtest) +- [StatsANOVA2Test](https://docs.wavemetrics.com/igorpro/commands/statsanova2test) +- [StatsAngularMean](https://docs.wavemetrics.com/igorpro/commands/statsangularmean) +- [StatsChiTest](https://docs.wavemetrics.com/igorpro/commands/statschittest) +- [StatsCircularMean](https://docs.wavemetrics.com/igorpro/commands/statscircularmean) +- [StatsCorrelation](https://docs.wavemetrics.com/igorpro/commands/statscorrelation) +- [StatsDIPTest](https://docs.wavemetrics.com/igorpro/commands/statsdiptest) +- [StatsDunnettTest](https://docs.wavemetrics.com/igorpro/commands/statsdunnetttest) +- [StatsFTest](https://docs.wavemetrics.com/igorpro/commands/statsftest) +- [StatsHodgesAjneTest](https://docs.wavemetrics.com/igorpro/commands/statshodgesajnetest) +- [StatsJBTest](https://docs.wavemetrics.com/igorpro/commands/statsjbtest) +- [StatsKDE](https://docs.wavemetrics.com/igorpro/commands/statskde) +- [StatsKendallTauTest](https://docs.wavemetrics.com/igorpro/commands/statskendalltest) +- [StatsKSTest](https://docs.wavemetrics.com/igorpro/commands/statskstest) +- [StatsKWTest](https://docs.wavemetrics.com/igorpro/commands/statskwtest) +- [StatsLinearCorrelationTest](https://docs.wavemetrics.com/igorpro/commands/statslinearcorrelationtest) +- [StatsLMTest](https://docs.wavemetrics.com/igorpro/commands/statslmtest) +- [StatsMedianTest](https://docs.wavemetrics.com/igorpro/commands/statsmediantest) +- [StatsMultiCorrelationTest](https://docs.wavemetrics.com/igorpro/commands/statsmulticorrelationtest) +- [StatsNPMCTest](https://docs.wavemetrics.com/igorpro/commands/statsnpmctest) +- [StatsNPNominalSRTest](https://docs.wavemetrics.com/igorpro/commands/statsnpnominalsrtest) +- [StatsQuantiles](https://docs.wavemetrics.com/igorpro/commands/statsquantiles) +- [StatsRayleighTest](https://docs.wavemetrics.com/igorpro/commands/statsrayleightest) +- [StatsResample](https://docs.wavemetrics.com/igorpro/commands/statsresample) +- [StatsSample](https://docs.wavemetrics.com/igorpro/commands/statssample) +- [StatsShapiroWilkTest](https://docs.wavemetrics.com/igorpro/commands/statsshapirowiliktest) +- [StatsSignTest](https://docs.wavemetrics.com/igorpro/commands/statssigntest) +- [StatsSpearmansRho](https://docs.wavemetrics.com/igorpro/commands/statsspearmansrho) +- [StatsStudentTest](https://docs.wavemetrics.com/igorpro/commands/statsstudenttest) +- [StatsTTest](https://docs.wavemetrics.com/igorpro/commands/statsttest) +- [StatsTukeyTest](https://docs.wavemetrics.com/igorpro/commands/statstukeytest) +- [StatsVariancesTest](https://docs.wavemetrics.com/igorpro/commands/statsvariancestest) +- [StatsWatsonTest](https://docs.wavemetrics.com/igorpro/commands/statswatsontest) +- [StatsWatsonWilliamsTest](https://docs.wavemetrics.com/igorpro/commands/statswatsonwilliamstest) +- [StatsWheelerWatsonTest](https://docs.wavemetrics.com/igorpro/commands/statswheelerwatsontest) +- [StatsWilcoxonRankTest](https://docs.wavemetrics.com/igorpro/commands/statswilcoxonranktest) +- [StatsWRCorrelationTest](https://docs.wavemetrics.com/igorpro/commands/statswrcorrelationtest) +- [str2num](https://docs.wavemetrics.com/igorpro/commands/str2num) +- [StrLen](https://docs.wavemetrics.com/igorpro/commands/strlen) +- [StringByKey](https://docs.wavemetrics.com/igorpro/commands/stringbykey) +- [StringFromList](https://docs.wavemetrics.com/igorpro/commands/stringfromlist) +- [StringList](https://docs.wavemetrics.com/igorpro/commands/stringlist) +- [StringMatch](https://docs.wavemetrics.com/igorpro/commands/stringmatch) +- [StructGet](https://docs.wavemetrics.com/igorpro/commands/structget) +- [StructPut](https://docs.wavemetrics.com/igorpro/commands/structput) +- [SVAR_Exists](https://docs.wavemetrics.com/igorpro/commands/svar_exists) + +## T + +- [t](https://docs.wavemetrics.com/igorpro/commands/t) +- [TableInfo](https://docs.wavemetrics.com/igorpro/commands/tableinfo) +- [TagVal](https://docs.wavemetrics.com/igorpro/commands/tagval) +- [tan](https://docs.wavemetrics.com/igorpro/commands/tan) +- [tanh](https://docs.wavemetrics.com/igorpro/commands/tanh) +- [TextFile](https://docs.wavemetrics.com/igorpro/commands/textfile) +- [TextWaveToList](https://docs.wavemetrics.com/igorpro/commands/textwavetolist) +- [TileWindows](https://docs.wavemetrics.com/igorpro/commands/tilewindows) +- [time](https://docs.wavemetrics.com/igorpro/commands/time) +- [Triangulate3D](https://docs.wavemetrics.com/igorpro/commands/triangulate3d) +- [Trunc](https://docs.wavemetrics.com/igorpro/commands/trunc) + +## U + +- [URLDecode](https://docs.wavemetrics.com/igorpro/commands/urldecode) +- [URLEncode](https://docs.wavemetrics.com/igorpro/commands/urlencode) +- [UpperStr](https://docs.wavemetrics.com/igorpro/commands/upperstr) + +## V + +- [Variable](https://docs.wavemetrics.com/igorpro/commands/variable) +- [Variance](https://docs.wavemetrics.com/igorpro/commands/variance) +- [vcsr](https://docs.wavemetrics.com/igorpro/commands/vcsr) + +## W + +- [Wave](https://docs.wavemetrics.com/igorpro/commands/wave) +- [WaveInfo](https://docs.wavemetrics.com/igorpro/commands/waveinfo) +- [WaveList](https://docs.wavemetrics.com/igorpro/commands/wavelist) +- [WaveMeanStdv](https://docs.wavemetrics.com/igorpro/commands/wavemeanstdv) +- [WaveMin](https://docs.wavemetrics.com/igorpro/commands/wavemin) +- [WaveMax](https://docs.wavemetrics.com/igorpro/commands/wavemax) +- [WaveRefIndexed](https://docs.wavemetrics.com/igorpro/commands/waverefindexed) +- [WaveRefIndexedDFR](https://docs.wavemetrics.com/igorpro/commands/waverefindexeddfr) +- [WaveRefsEqual](https://docs.wavemetrics.com/igorpro/commands/waverefsequal) +- [WaveStats](https://docs.wavemetrics.com/igorpro/commands/wavestats) +- [WaveTransform](https://docs.wavemetrics.com/igorpro/commands/wavetransform) +- [WaveType](https://docs.wavemetrics.com/igorpro/commands/wavetype) +- [WaveUnits](https://docs.wavemetrics.com/igorpro/commands/waveunits) +- [WhichListItem](https://docs.wavemetrics.com/igorpro/commands/whichlistitem) +- [WignerVille](https://docs.wavemetrics.com/igorpro/commands/wignerville) +- [WindowBounds](https://docs.wavemetrics.com/igorpro/commands/windowbounds) +- [WindowFunction](https://docs.wavemetrics.com/igorpro/commands/windowfunction) +- [WindowInfo](https://docs.wavemetrics.com/igorpro/commands/windowinfo) +- [WindowList](https://docs.wavemetrics.com/igorpro/commands/windowlist) +- [WindowNamesToWave](https://docs.wavemetrics.com/igorpro/commands/windownamestowave) +- [WMAppendToXYZContour](https://docs.wavemetrics.com/igorpro/commands/wmappendtoxyzcontour) +- [WMNewContourPlot](https://docs.wavemetrics.com/igorpro/commands/wmnewcontourplot) +- [WMXYZContourConfig](https://docs.wavemetrics.com/igorpro/commands/wmxyzcontourconfig) +- [WMXYZDeletePts](https://docs.wavemetrics.com/igorpro/commands/wmxyzdeletepts) +- [WMXYZScatterConfig](https://docs.wavemetrics.com/igorpro/commands/wmxyzscatterconfig) +- [WMXYZTranformPts](https://docs.wavemetrics.com/igorpro/commands/wmxyztranformpts) +- [WriteFile](https://docs.wavemetrics.com/igorpro/commands/writefile) + +## X + +- [x2pnt](https://docs.wavemetrics.com/igorpro/commands/x2pnt) +- [XWaveRefFromTrace](https://docs.wavemetrics.com/igorpro/commands/xwaverefromtrace) + +## Z + +- [ZeroPad](https://docs.wavemetrics.com/igorpro/commands/zeropad) + +--- + +## How to Look Up a Command's Full Signature + +Each command name above links directly to its documentation page. +The URL pattern is always: + +``` +https://docs.wavemetrics.com/igorpro/commands/ +``` + +For example, to look up `WaveStats`: +- URL: https://docs.wavemetrics.com/igorpro/commands/wavestats + +The documentation pages include: full syntax, all flags/keywords, output variables set (e.g. `V_*`), and usage examples. diff --git a/.github/skills/igor-gotchas/SKILL.md b/.github/skills/igor-gotchas/SKILL.md new file mode 100644 index 0000000000..2850038809 --- /dev/null +++ b/.github/skills/igor-gotchas/SKILL.md @@ -0,0 +1,422 @@ +--- +name: igor-gotchas +paths: + - "**/*.ipf" +description: Confirmed Igor Pro runtime/compiler behavior that is not obvious from the command reference alone -- compilation and conditional-compilation mechanics, Execute/deferred-execution restrictions, try/catch/Abort/Debugger interaction, command-line-only restrictions, and specific command quirks (FindValue, WinList, NewPath, CaptureHistory, background tasks, compile hooks, XOP/help-file introspection). Use before writing or debugging any Igor Pro code that touches compilation, error handling, background tasks, or a command whose exact behavior matters and isn't already covered by igor-wave-dfref or igor-10. +--- + +# Igor Pro — Confirmed Runtime and Compiler Gotchas + +This document covers Igor Pro language/runtime behavior that is easy to get +wrong because it isn't obvious from a command's one-line documentation, or +because the documentation itself is ambiguous. Every entry here was confirmed +against real behavior or Igor's own reference documentation, not assumed from +general programming-language intuition. + +See also: `igor-wave-dfref` (WAVE/DFREF reference semantics), `igor-10` +(Igor Pro 10 version differences), `igor-commands` (alphabetical command +link index). + +--- + +## Compilation and Conditional Compilation + +### `#define` symbols for cross-file `#ifdef`/`#ifndef` belong in the main Procedure window + +A `#define` intended to control `#ifdef`/`#ifndef` checks in OTHER procedure +files must be set in the experiment's main Procedure window, not inside an +ordinary `.ipf` file. Per Igor's own documentation, the main procedure window +is always compiled first — a `#define` there is reliably visible to every +other file's `#ifdef` checks; one placed in a regular `.ipf` file has no such +guarantee (compilation order across included files is not something to rely +on for this). + +### `SetIgorOption poundDefine`/`poundUndefine` — the session-scoped, non-compilable alternative + +`SetIgorOption poundDefine=symb` / `poundUndefine=symb` add/remove a +conditional-compilation symbol from a global symbol list available to every +procedure window — broader in scope than a single `#define` line, and usable +without editing any file on disk. Query current state with +`SetIgorOption poundDefine=symb?` → `V_flag` (1 if defined, 0 if not). + +```igor +SetIgorOption poundDefine=IGOR_PRO_BRIDGE? +print V_flag // 0 = undefined + +Execute/P "SetIgorOption poundDefine=IGOR_PRO_BRIDGE" +Execute/P "COMPILEPROCEDURES " +``` + +Important properties: +- **Session-only** — not saved with the experiment, lost on relaunch. Must be + re-set every time a fresh Igor Pro instance/experiment needs it. +- **Not compilable** — "`SetIgorOption` is not compilable. To use it in a + user-defined function, you need to use `Execute`." Only `Execute`/`Execute/P` + can invoke it, never a bare call from inside a `Function`. +- **Does NOT itself trigger a recompile** — setting/clearing the symbol only + changes what a *subsequent* `RELOAD CHANGED PROCS`/`COMPILEPROCEDURES` call + will see. An `#ifdef`/`#ifndef` block gated on the symbol keeps its old + branch compiled in until a separate, explicit recompile actually runs -- + every real call site in this repo issues the two as separate queued calls + (e.g. `MIES_Debugging.ipf`'s `EnableDebugMode()`/`DisableDebugMode()`/ + `EnableEvilMode()`/etc.: `Execute/P/Q "SetIgorOption poundDefine=..."` + immediately followed by its own separate `Execute/P/Q "COMPILEPROCEDURES "`). + +### `RELOAD CHANGED PROCS` / `COMPILEPROCEDURES` — separate calls, mandatory trailing space + +These must be issued as two separate `Execute`/`Execute/P` calls, never +joined by `;` into one string, and each needs a mandatory trailing space +(`"RELOAD CHANGED PROCS "`, `"COMPILEPROCEDURES "`). Anything queued via +`Execute/P` immediately after a `COMPILEPROCEDURES` in the same batch may +simply never run — poll the compiled state directly afterward rather than +relying on a follow-up deferred callback to confirm success. + +### Compile hooks: know which one actually fires when + +- `AfterCompiledHook` fires only after a **successful** compile — never on a + failed one. Any cleanup/restart logic that must run regardless of outcome + needs an independent mechanism (e.g. a watchdog background task), not just + this hook. +- `BeforeUncompiledHook(changeCode, procedureWindowTitleStr, textChangeStr)` + fires before procedures uncompile. +- `IgorStartOrNewHook` fires both on Igor launch and on new-experiment + creation — it does not distinguish between the two on its own. + +### `FunctionInfo` resolves unqualified names relative to the CALLING function's own module, not global scope + +`FunctionInfo(functionNameStr)` with an unqualified name resolves relative to +the module the *calling* code is compiled in — not `ProcGlobal`, not global +scope. To check a different module's compile state from code running in an +Independent Module (or any non-`ProcGlobal` module), qualify explicitly: + +```igor +FunctionInfo("ProcGlobal#SomeName") +``` + +### A single compile error anywhere poisons `FunctionInfo` for everything + +When any compile error exists anywhere in the experiment, `FunctionInfo(...)` +reports `"Procedures Not Compiled"` for **every** function queried, including +unrelated, genuinely-fine ones. Do not conclude a specific module/function has +its own problem just because `FunctionInfo` fails on it — check for a compile +error anywhere in the experiment first (e.g. via the compiled-state check +this repo's tooling already uses). + +--- + +## `Execute`, Deferred Execution, and Error Handling + +### Bare `Execute` is legal inside a `Function` — `Execute/P` is only required for operations that can't run while the calling code is still on the stack + +Bare, unqueued `Execute` (no `/P`) *is* legal from inside a compiled +`Function` — Igor's own reference documentation confirms this is in fact the +**standard**, intended use: "The most common use of Execute is to call a +macro or an external operation from a user-defined function. This is +necessary because Igor does not allow you to make such calls directly." +(Igor Reference.ihf, "Execute [/Z] cmdStr"). This repo relies on it directly, +e.g. `WBP_CreateWaveBuilderPanel()`'s `Execute "WaveBuilder()"` +(`MIES_WaveBuilderPanel.ipf`) and `QueryIgorOption()`'s +`Execute/Q "SetIgorOption " + option + "=?"` (`MIES_Debugging.ipf`). + +`Execute/P` (deferred — posted to Igor's operation queue, only running once +"nothing else is happening... Macros and functions must not be running and +the command line must be empty") is specifically required for operations +that cannot run synchronously while the calling function itself is still on +the call stack — most notably `COMPILEPROCEDURES`/`RELOAD CHANGED PROCS` +(recompiling the very procedures the calling function is compiled from), and, +for the same reason, a `SetIgorOption poundDefine`/`poundUndefine` call meant +to take effect before an immediately-following queued compile. For commands +without that constraint, a plain (optionally `/Q`/`/Z`-flagged) `Execute` +from inside a `Function` works fine and is the documented pattern — +`COMPILEPROCEDURES`/`RELOAD CHANGED PROCS`/`SetIgorOption` still need +`Execute`/`Execute/P` rather than a bare compiled call (they are themselves +non-compilable operations), just not exclusively the deferred form. + +### A `;`-joined command string aborts entirely on its first runtime error + +If `stmt1` errors, `stmt2` in the same `;`-joined string never runs. +Independently *queued* `Execute/P` calls are unaffected by each other's +failure this way — only text joined into one string shares fate. Prefer +separate `Execute/P` calls when a later step should still run even if an +earlier one might fail. + +### `try`/`catch`/`endtry`: a bare runtime error does not jump to `catch` by itself + +Only an explicit `AbortOnRTE` (or `AbortOnValue`) placed right after the +risky call converts a pending runtime error into a catchable abort. Per +Igor's own documentation, "When an abort occurs, execution immediately jumps +to the first statement after `catch`" — but a bare error alone, without that +explicit conversion, does not trigger this. + +```igor +try + RiskyCall() + AbortOnRTE // without this line, a runtime error in RiskyCall() + // does NOT transfer control to catch +catch + // handle it +endtry +``` + +**Put the risky call and its `AbortOnRTE`/`GetRTError(1)` check on the SAME +line** (`dummy = RiskyCall(); AbortOnRTE`), not on separate lines — Igor's +Debug-on-Error checking happens at the END OF EACH LINE, not after each +`;`-separated statement, so a pending error left unacknowledged across a line +boundary can pop a real Debugger window before the guard has a chance to run. + +### `Abort ""` pops a real alert dialog immediately — `try`/`catch` does not suppress it + +Unlike an ordinary runtime error, `Abort` with a message string shows its +alert dialog right away, before any enclosing `try`'s `catch` block runs +(wrapping it in `try`/`catch` only lets you react after the dialog has +already appeared). If a failure is purely logical/internal and must not +block on a popup, set an error status directly instead of calling `Abort` +with a message. + +### A bare `return` is invalid inside an ordinary scalar-returning `Function` + +A value-less `return` (no expression) is only valid inside a Multiple-Return- +Syntax function. In an ordinary `Function`/`Function/S`/`Function/WAVE` +declaration, the returned value's type must always match the declared return +type — `return` alone does not compile there. + +### Checking `GetRTError(1)` after an XOP call that can error + +Check it on the SAME line as the call (`SomeXOPCall(...); err = GetRTError(1)`), +not split across two lines — otherwise an unacknowledged pending error can +surface as an unexpected Debugger popup on a later, unrelated line (same +underlying mechanism as the `try`/`AbortOnRTE` line-boundary issue above). + +--- + +## Command-Line-Only Restrictions + +The following are only restrictions when typed directly at Igor's command +line (interpreted, not compiled) — the identical statement works fine inside +a compiled `Function`: + +- `WAVE/Z w = SomeFunc(...)` — assigning a wave reference from a function + call fails at the command line ("expected wave name, variable name, or + operation"). +- `Make/FREE ...` — free waves have no valid scope outside a function. +- Multiple-return-value destructuring (`[val1, val2] = SomeFunc()`). +- Calling a `static` function by its bare name — it's scoped to its file's + `#pragma ModuleName` and needs `ModuleName#FunctionName` from outside that + module (this restriction is not command-line-specific, but is easy to + mistake for one when it first surfaces there). +- Multi-line control-flow blocks (`if`/`else`/`endif`, `for`/`endfor`) — a + command line containing such a block fails as a whole with a generic error, + even though each line would be valid inside a real function. + +**Workaround for anything needing compiled-only features interactively**: +write/extend a small compiled scratch procedure file, `#include` it, compile, +then call a single compiled helper function from that file via the command +line — everything inside the function body runs as compiled code, so none of +the above restrictions apply. + +A related but distinct fact: variables/strings declared directly on the +command line persist as global command-line variables across separate +command executions within the same Igor session — they are not scoped to +one call. Re-declaring the same name later fails ("the name already exists +as a variable"); assign directly instead of re-declaring. + +--- + +## Specific Command/Function Behavior + +### `FindValue /TXOP` bit flags + +`4` = case-insensitive whole-cell text match (the pervasive default in this +codebase); `5` = `4 | 1` = case-sensitive. Use `TXOP=(1+4)` when case +matters (e.g. matching an SI unit prefix like `m` vs. `M`). + +### `NewPath`/`PathInfo`'s `S_path` always returns Igor's native colon notation, even on Windows + +Confirmed live: normalizing a Windows path via `NewPath` + `PathInfo` +produces `"C:Projects:mies_data:..."`, not backslashes — the same normalized +form MIES stores internally in places like the Analysis Browser's folder-list +wave. Anything comparing/displaying such a value should expect colon +notation regardless of host OS. + +### `WinList`'s `WIN:` bit values + +`1`=graphs, `2`=tables, `4`=layouts, `16`=notebooks, `64`=panels, +`128`=procedure windows, `512`=help windows. (`1024` is not a defined type.) + +### `ProcedureText(funcName, flags, winTitle)` — window title is the THIRD argument + +Passing a window name as the first argument silently returns `""` with no +error. To read an entire window's contents (not a specific function), pass +`funcName=""` and the window name as the third argument. + +### `CaptureHistory(refnum, stopCapturing)` and stale refnums + +Both arguments are required — a one-argument call fails to compile. A saved +numeric refnum from `CaptureHistoryStart()` is only meaningful within the OS +process that created it. If persisted in a global and the experiment is +reloaded (a real process quit+relaunch, not just a recompile), the global +still exists (`NVAR_Exists` succeeds) but the refnum itself is stale — using +it throws a runtime error. Never trust mere existence of a stored refnum-like +handle across a save/reload boundary; validate by trying to use it (wrapped +in `try`/`AbortOnRTE`/`catch`) and recreate if stale. + +### `stopmstimer(-2)` returns microseconds, not milliseconds + +Despite the name, Igor's free-running timer via `stopmstimer(-2)` returns a +value in **microseconds**. + +### `CtrlNamedBackground`'s `start=N` is only a floor, not a guarantee of ordering + +`start=N` (ticks, ~1/60s each) only sets the earliest possible first +invocation — independent of `period`. Background tasks and the deferred +`Execute/P` queue have **no guaranteed ordering relative to each other**; a +task registered "after" some queued work can still tick first. Treat +`start=` only as an empirical safety margin, never as a strict ordering +guarantee. + +### `ThreadGroupRelease(-2)` releases every currently-running thread group + +Useful inside a `BeforeUncompiledHook` to release a stray background thread +before procedures uncompile — a running thread group can otherwise block +`COMPILEPROCEDURES`/`RELOAD CHANGED PROCS` with a modal "still active" +dialog. + +### `DebuggerOptions` creates output globals wherever the current data folder happens to be + +A bare/partial-argument call to `DebuggerOptions` creates its output +variables (`V_enable`, `V_debugOnError`, `V_debugOnAbort`, +`V_NVAR_SVAR_WAVE_Checking`) in whatever data folder is current **at call +time**, regardless of which arguments were actually given. Code that calls it +purely for its toggling side effect should `KillVariables/Z` these four names +in the target folder afterward, or expect stray globals to trip a +`CHECK_EMPTY_FOLDER()`-style assertion downstream (relevant in tests). + +### Auto-indexing order in waveform assignments + +An auto-indexed waveform assignment (e.g. `Make/WAVE/N=(n) w = SomeFunc(p)`) +evaluates the right-hand side once per destination element, strictly in +increasing linear index order (`0, 1, 2, ..., numpnts(w)-1` — column-major +for a multi-dimensional destination, i.e. all of column 0 before column 1, +matching the `p`/`q`/`r`/`s` symbols' own progression) when `Multithread` is +**not** used. With `Multithread`, per-index execution order is not +guaranteed and the right-hand side must be threadsafe. This matters whenever +the called function has order-dependent side effects. + +### `MultiThread` works with any wave type -- the restriction is on the expression, not the wave + +The `MultiThread` keyword (in front of a wave assignment statement inside a +function, e.g. `MultiThread w = expr`) has no restriction based on the +destination or source wave's data type. Confirmed against Igor's own +"MultiThread" keyword reference and the "Automatic Parallel Processing with +MultiThread" article (Advanced Topics.ihf): the entire discussion is about +whether the *expression* (and any function it calls) is thread-safe, never +about which wave type is involved. Igor's own docs explicitly cover +`MultiThread` with numeric waves, and separately confirm it for wave +reference waves (`WAVE/WAVE`, "You can use a wave reference wave as a list +of waves for further processing and in multithreaded wave assignment using +the MultiThread keyword") and data folder reference waves (`WAVE/DF`, same +wording) -- Advanced Topics.ihf has dedicated worked examples for both +("Wave Reference MultiThread Example", "Data Folder Reference MultiThread +Example"), plus one for structure arrays ("Structure Array MultiThread +Example"). This repo's own code confirms `MultiThread` with **text** waves +too, e.g. `SFE_ConvertNonFiniteElements` +(`MIES_SweepFormula_Executor.ipf`) reads a `WAVE/T` source +(`subArray[p][q][r][s]`) via `MultiThread`, and +`SFE_FormulaExecutor` writes into a `WAVE/T` destination +(`Multithread outT[index][][][] = outT[index][0][0][0]`). + +The real constraints are about the *expression*, not the wave type: +- It must be thread-safe -- any function it calls (built-in or + user-defined) must be thread-safe; user-defined functions need the + `ThreadSafe` keyword. +- Do not reference any point of the destination wave other than the + current point (`p`/`q`/`r`/`s`) being computed -- e.g. + `wave1 = wave1[p+1] - wave1[p-1]` gives indeterminate results. +- A thread-safe function called from the expression must not resize/ + retype/kill any wave passed to it, write to a text wave passed to it, or + write to a variable passed by reference; any waves/globals it creates + itself disappear when the assignment finishes; and it cannot use `WAVE`/ + `NVAR`/`SVAR` to reach into the main thread's data folder tree (each + thread has its own private data folder tree). +- Only worth the overhead for a destination with a large number of points, + or an expensive expression -- for small waves `MultiThread` can be + slower than the unthreaded assignment. + +### Igor Pro on Windows is single-instance-per-user for command-line launches + +Launching `Igor64.exe ` while an instance is already running does +**not** spawn a new process — it signals the existing instance to load the +file, popping an unhandled "save changes?" dialog if that instance has +unsaved changes. If the existing instance is mid-quit when the launch command +runs, the load request can be silently dropped instead. Any programmatic +relaunch logic must confirm the prior process has actually exited from the +OS process list before invoking the executable again — checking that some +IPC channel has merely gone quiet is not sufficient, since that can happen +well before the process itself actually terminates. + +--- + +## Introspecting XOPs and Help Files + +### Locating XOPs, help files, and checking whether something is loaded + +Igor Pro loads XOPs/help files/fonts/procedure files from two merged +locations: under the Igor Pro program folder, and +`/WaveMetrics/Igor Pro User Files/` — both mirror the +same subfolder names (`Igor Extensions`, `Igor Extensions (64-bit)`, +`Igor Fonts`, `Igor Help Files`, `Igor Procedures`, `User Procedures`). Check +both when looking for an XOP's help file; not every XOP ships one. + +To check whether an XOP's functions/operations are actually loaded: +`FunctionList("*", ";", "KIND:4")` lists XOP functions, +`OperationList("*", ";", "external")` lists XOP operations — check both, and +be aware that once an XOP is loaded it can't be toggled/unloaded mid-session, +and neither list attributes a name back to its owning XOP (only naming +convention, or the PE-resource technique below, can do that). + +### `.ihf` help files are themselves Igor notebooks + +Read them with `OpenNotebook/R` (fails with error 251 if that file's +help-window view is already open elsewhere — a help-file view and a plain +notebook view of the same file are mutually exclusive). Export via +`SaveNotebook/O/S=5/H={...}` (HTML) to recover genuine structure: each +paragraph gets a `

` matching WaveMetrics' own semantic style +convention — `Topic`, `Subtopic`/`Subtopic-Indented`, +`TopicBody1`/`TopicBody1a`, `Steps`/`ListNumbered`, +`Code1`/`Code1a`/`Code-Indented1`, `SeeAlso`/`NOTE`/`Table2Col`/`Table3Col`/ +`RelatedTopics`. Useful for reliably parsing structure out of any Igor help +file rather than treating it as flat text. + +### Recovering a closed-source XOP's operations/functions with no vendor docs + +A closed-source `.xop`'s Igor-visible operations/functions are recoverable +from standard Windows PE resources named `"XOPC"` (operations) and `"XOPF"` +(functions), resource ID 1100 — any generic PE resource reader (e.g. Python's +`pefile`) can extract them without vendor documentation: + +- `XOPC` records: `{null-terminated name; int16 LE category bitmask}*`, + terminated by an empty-name record. +- `XOPF` adds a return-type code and per-parameter type codes to each entry. + +Useful for any closed-source XOP in this repo (`MultiClamp700xCommander64.xop`, +`itcXOP2-64.xop`, `MIESUtils-64.xop`, `TUF-64.xop`, `SutterXOP_Win-64.xop`) +with no available documentation. + +--- + +## Reference URLs + +| Topic | URL | +|---|---| +| SetIgorOption | https://docs.wavemetrics.com/igorpro/commands/setigoroption | +| Execute | https://docs.wavemetrics.com/igorpro/commands/execute | +| Abort | https://docs.wavemetrics.com/igorpro/commands/abort | +| GetRTError | https://docs.wavemetrics.com/igorpro/commands/getrterror | +| FunctionInfo | https://docs.wavemetrics.com/igorpro/commands/functioninfo | +| FindValue | https://docs.wavemetrics.com/igorpro/commands/findvalue | +| WinList | https://docs.wavemetrics.com/igorpro/commands/winlist | +| NewPath | https://docs.wavemetrics.com/igorpro/commands/newpath | +| CaptureHistory | https://docs.wavemetrics.com/igorpro/commands/capturehistory | +| CtrlNamedBackground | https://docs.wavemetrics.com/igorpro/commands/ctrlnamedbackground | +| ThreadGroupRelease | https://docs.wavemetrics.com/igorpro/commands/threadgrouprelease | +| DebuggerOptions | https://docs.wavemetrics.com/igorpro/commands/debuggeroptions | diff --git a/.github/skills/igor-python/SKILL.md b/.github/skills/igor-python/SKILL.md new file mode 100644 index 0000000000..32a7e948b5 --- /dev/null +++ b/.github/skills/igor-python/SKILL.md @@ -0,0 +1,304 @@ +--- +name: igor-python +paths: + - "**/*.ipf" + - "**/*.py" +description: Reference for the igorpro Python module used to communicate between Python and Igor Pro 10. Use when writing Python code that runs from within Igor Pro (via Python, PythonFile, or the Python Console), or Igor code that calls into Python. +--- + +# Igor Pro — Python Integration (`igorpro` module) + +This skill covers writing Python code that communicates with Igor Pro 10 +using the `igorpro` module. This module is proprietary to Igor Pro and has +no presence in general Python training data — always use this reference +when generating Python code intended to run from within Igor. + +**Critical constraint:** The `igorpro` module can ONLY be used from within +Igor Pro itself. Scripts must be launched from Igor via `Python`, `PythonFile`, +or the Python Console. You cannot connect to Igor from an external session. + +Official docs: +- https://docs.wavemetrics.com/igorpro/python/python-overview +- https://docs.wavemetrics.com/igorpro/python/python-module-reference + +--- + +## 1. Three Ways to Run Python from Igor + +```igor +// Inline statement +Python "import numpy as np" + +// Run a .py file +PythonFile file = "MyProject/analysis.py" + +// With path symbolic name +NewPath/O scriptPath, "/path/to/scripts/" +PythonFile/P=scriptPath file = "analysis.py" +``` + +Python Console: Python menu → Open Console. Multi-line: Ctrl-Enter. Interrupt: Ctrl-C. + +--- + +## 2. Setup + +**Supported versions:** Python 3.11–3.14 (standard only, not free-threaded). + +```igor +// Activate virtual environment programmatically +NewPath/O envPath, "path/to/parent/" +PythonEnv/P=envPath activate = "myEnv" +``` + +Warning: Cannot change environments without restarting Igor once a session starts. + +**Auto sys.path locations:** +- `~/Documents/WaveMetrics/Igor Pro 10 User Files/Python Scripts` +- `~/Documents/WaveMetrics/Igor Pro 10 User Files/User Procedures` +- `C:/Program Files/WaveMetrics/Igor Pro 10 Folder/Python Scripts` + +Subdirectories are NOT auto-added. Use `from MyProject import analysis`. + +**VSCode autocomplete:** Add to settings.json: +```json +"python.analysis.extraPaths": [ + "C:/Program Files/WaveMetrics/Igor Pro 10 Folder/IgorBinaries_x64/Python" +] +``` + +--- + +## 3. Top-Level Functions + +```python +import igorpro + +igorpro.execute("Make/O/N=100 myWave") +igorpro.execute("WaveStats/Q myWave", ignoreErrors=True) +igorpro.print("Analysis complete") # prints to Igor history +igorpro.version() +``` + +--- + +## 4. igorpro.wave + +### Access existing +```python +w = igorpro.wave('root:myWave') +w = igorpro.wave('myWave') # relative to current DF +df = igorpro.folder('root:data') +w = igorpro.wave('intensity', df) # using folder context +``` + +### Create +```python +w = igorpro.wave.create('newWave') # 128 pts, float32 +w = igorpro.wave.create('qVec', 500, type=igorpro.float64) +w = igorpro.wave.create('mat', (256,256), value=0.0, type=igorpro.float32) +w = igorpro.wave.create('result', 100, overwrite=True) + +# From NumPy/list (type inferred) +import numpy as np +w = igorpro.wave.createfrom('qWave', np.linspace(0.001, 0.5, 200)) +w = igorpro.wave.createfrom('labels', ['s1', 's2', 's3']) + +# In specific folder +w = igorpro.wave.create('fit', 200, folder=igorpro.folder('root:results')) +``` + +### Read +```python +arr = w.asarray() # numpy ndarray (numeric/text; copies data) +n = w.points() +shape = w.shape() # e.g. (200,) or (256,256) +ndims = w.dims() +wname = w.name() +wpath = w.path() +wtype = w.type() # igorpro.WaveType enum +ux = w.units('x') +ud = w.units('d') # data units ('d' or -1) +note = w.note() +alive = w.exists() +start, delta = w.scale('x') + +val = w[0] # indexing (zero-based) +w[5] = 3.14 +val = w[10, 20] # 2D: [row, col] +``` + +### Modify +```python +w.set_data(np.sqrt(w.asarray())) + +w.set_scale('x', 0.001, 0.5, 'range') # start to end +w.set_scale('x', 0.001, 0.002, 'delta') # start + step + +w.set_units('x', '1/A') +w.set_units('d', 'cm-1') + +w.set_label('x', 0, 'first_point') +w.set_label('x', -1, 'Q') # overall label + +w.redimension(500) +w.redimension((100, 100)) + +w.kill() +``` + +### Stats +```python +s = w.stats() # dict: V_avg, V_sdev, V_min, V_max, ... +s = w.stats((0.01, 0.1)) # tuple = scaled x range +s = w.stats([10, 50]) # list = point index range +s = w.stats((..., 0.05)) # up to x=0.05 +``` + +--- + +## 5. igorpro.WaveType + +```python +igorpro.float32 / float64 +igorpro.int8 / uint8 / int16 / uint16 / int32 / uint32 +igorpro.int64 / uint64 # Igor 10+ +igorpro.complex64 / complex128 +igorpro.WaveType.text +``` + +**numpy.float16 is NOT supported — convert to float32 first.** + +NumPy dtype → Igor type: float32→32-bit float, float64→64-bit float, +complex64→32-bit complex, complex128→64-bit complex, bool→uint8, str→text. +Integer types map directly to same-width Igor type. + +--- + +## 6. igorpro.folder + +```python +df = igorpro.folder('root:data') +df = igorpro.folder.current() +df = igorpro.folder.create('root:results') +df = igorpro.folder.create('root:results', overwrite=True) + +parent = df.parent() +sub = df.subfolder('fits') +subs = df.subfolders() # list[igorpro.folder] +waves = df.waves() # list[igorpro.wave] + +df.name(); df.path(); df.exists() +df.num_subfolders(); df.num_waves() + +df.set() # set as current DF (restore afterwards!) +df.kill(ignoreErrors=True) +``` + +--- + +## 7. igorpro.variable + +```python +v = igorpro.variable('root:Packages:MyPkg:temp') +val = v.value(); v.set(298.15) +v.exists(); v.real(); v.imag(); v.iscomplex() + +v = igorpro.variable.create('root:Packages:MyPkg:Rg', 42.0) +v = igorpro.variable.create('root:myVar', 3+4j) +v = igorpro.variable.create('root:myVar', 0.0, overwrite=True) +``` + +--- + +## 8. igorpro.string + +```python +s = igorpro.string('root:Packages:MyPkg:sampleName') +val = s.value(); s.set('new_name') +s.exists(); s.name(); s.path() + +s = igorpro.string.create('root:Packages:MyPkg:fileName', 'data.h5') +s = igorpro.string.create('root:myStr', '', overwrite=True) +``` + +--- + +## 9. igorpro.fn — Call Igor Functions from Python + +Case-insensitive. Calls built-in, XOP, or user-defined functions. + +```python +# Built-ins +igorpro.fn.sqrt(2.0) # → float +igorpro.fn.num2str(3.14159) # → str +igorpro.fn.cmplx(3, -4) # → complex +igorpro.fn.sortlist('x;c;e;g;d;a', ';', 1) # → str + +# Pass a wave +w = igorpro.wave('root:data:intensity') +med = igorpro.fn.median(w) # → float + +# Returns wave reference +tw = igorpro.fn.traceNameToWaveRef('Graph0', 'yWave') # → igorpro.wave + +# User-defined function +result = igorpro.fn.MyAnalysisFunc(w, 0.01, 0.1) + +# Optional arguments: keyword=value syntax +igorpro.fn.greet('Jan') +igorpro.fn.greet('Jan', optionalPlace='Argonne') +``` + +### Return types +| Igor return | Python receives | +|-------------|-----------------| +| Variable (real) | float | +| Variable (complex) | complex | +| String | str | +| Wave reference | igorpro.wave | +| DFREF | igorpro.folder | + +### igorpro.fn CANNOT call: +- Functions with pass-by-reference parameters +- Functions with Structure parameters +- Functions using Multiple Return Syntax `[a, b] = func()` +- Functions taking object names as bare identifiers (e.g. CsrInfo) +- Functions with FUNCREF arguments +- Igor operations (use `igorpro.execute` or wrap in a user function) +- `p`, `q`, `r`, `s`, `x`, `y`, `z`, `t` (wave loop variables) +- Functions returning free waves or free data folders + +### Wrapping an operation +```igor +// Igor wrapper for an operation: +Function/WAVE RunWaveStats(Wave w) + WaveStats/Q w + Make/FREE/N=4 result = {V_avg, V_sdev, V_min, V_max} + return result +End +``` +```python +stats = igorpro.fn.RunWaveStats(w).asarray() +avg, sdev, vmin, vmax = stats +``` + +--- + +## 10. Object Lifetime Rules + +- Objects created in Console or via operations are global, persist until Igor closes. +- Killing an Igor wave while Python holds it → exception on next use. +- Deleting `igorpro.wave` in Python does NOT kill the Igor wave. +- Opening a new Igor experiment invalidates all existing `igorpro` objects. +- Free waves and free data folders are NOT supported. + +--- + +## 11. Stability Warnings + +- Python crashes crash Igor — enable Igor auto-save. +- Avoid importing Qt for Python (Igor is Qt-based). +- Avoid non-blocking event loops. + +--- diff --git a/.github/skills/igor-wave-dfref/SKILL.md b/.github/skills/igor-wave-dfref/SKILL.md new file mode 100644 index 0000000000..8fc54b2b39 --- /dev/null +++ b/.github/skills/igor-wave-dfref/SKILL.md @@ -0,0 +1,980 @@ +--- +name: igor-wave-dfref +paths: + - "**/*.ipf" +description: Reference for Igor Pro's WAVE and DFREF reference syntax: wave vs WAVE reference distinctions, declaration forms, data folder navigation, and free waves. Use before generating any Igor Pro code involving waves passed as parameters, data folder references, or free waves, since this is where AI-generated Igor code most commonly contains subtle errors. +--- + +# Igor Pro — WAVE and DFREF Reference Syntax + +This document covers the reference system for waves, global variables, strings, +and data folders. These are the areas where AI-generated Igor code most commonly +contains subtle errors. Read this before generating any code involving waves +passed as parameters, data folder navigation, or free waves. + +Official reference: https://docs.wavemetrics.com/igorpro/programming/programming + +--- + +## 1. The Core Distinction: Wave vs. WAVE + +Igor has two completely different things that look similar: + +- **A wave** — an array of data stored in a data folder (a global object) +- **A WAVE reference** — a local variable inside a function that *points to* a wave + +You never operate on a wave directly by name inside a function. You always +declare a WAVE reference first, then use that reference. + +```igor +// WRONG — this does not work inside a function: +Function BadExample() + myWave[] = myWave[p] * 2 // Error: myWave is not declared +End + +// CORRECT: +Function GoodExample() + WAVE myWave = GlobalWaveGetterFunction() // get reference to wave from a wave getter function + myWave[] = myWave[p] * 2 // now valid +End +``` + +--- + +## 2. WAVE Reference Declarations + +### Basic forms + +```igor +WAVE w // reference to a wave of any type +WAVE/C w // reference to a complex wave +WAVE/T w // reference to a text wave +WAVE/D w // reference to a double-precision wave (rarely needed explicitly) +WAVE/I w // reference to a 32-bit integer wave +WAVE/L w // reference to a 64-bit integer wave (int64) +WAVE/L/U w // reference to an unsigned 64-bit integer wave (uint64) +WAVE/B w // reference to a byte (8-bit) wave +WAVE/W w // reference to a 16-bit integer wave +WAVE/Z w // reference a wave that can also be a null wave +WAVE/WAVE w // reference a wave containing wave references +WAVE/DF w // reference a wave containing data folder references +``` +The type flag /U can be combined with numeric integer type flags. +The /C flag can be combined with numeric type flags +The /Z flag can be combined with other flags + +When a `WAVE` statement (without `/Z`) *actually executes* and its right-hand-side +expression fails to resolve to an existing wave, a runtime error is raised. +This is a different situation from a `WAVE` declaration that is simply never +reached at all due to control flow — see "Scoping and Default Initialization" below. + +### Scoping and Default Initialization + +Igor Pro has **no block scope**. A `WAVE`/`NVAR`/`SVAR`/`DFREF` declaration +written inside an `if`, `for`, or `switch` block is visible for the rest of +the function, exactly like a `Variable` or `String` declared in a block is. +The declaration is allocated for the whole function regardless of where it +textually appears; only the *assignment* is tied to that specific line +actually executing at runtime. + +Reference-typed locals (`WAVE`, `NVAR`, `SVAR`, `DFREF`, `FUNCREF`) are +automatically initialized to a null/non-existent reference at function +entry — the same way a bare `Variable` defaults to 0 and a bare `String` +defaults to a null string (not `""` — a null string is a distinct state, +distinguishable from an empty string via `strlen()`, which returns `NaN` +for a null string but `0` for `""`). If the code path containing the assignment never runs, +the reference is simply left at that safe null default. No lookup is +attempted, so no runtime error occurs: + +```igor +Function/WAVE MaybeGetData(variable condition) + + if(condition) + Make/FREE data + WAVE test1 = data + endif + + // If condition was false, test1 is still a valid, null WAVE reference here — + // not an error, not uninitialized memory. This is safe: + Make/FREE/WAVE wref = {test1} + + return wref +End +``` + +Do **not** confuse this with a `WAVE` statement that *does* execute but whose +right-hand side fails to resolve (e.g. `$name` pointing at a wave that +doesn't exist). That is a different failure mode and, without `/Z`, does +raise a runtime error: + +```igor +// This line executes every time; if "someName" isn't an existing wave, +// this errors (no /Z): +WAVE w = $someName + +// Safe form when the target might not exist: +WAVE/Z w = $someName +if(!WaveExists(w)) + // handle missing wave +endif +``` + +The distinguishing question is not "does the reference look null" but +"did the assignment statement itself run." A conditionally-assigned WAVE +reference that's never reached is a normal, safe null. A WAVE statement +that runs and can't find its target is a runtime error unless guarded +with `/Z`. + +Practical implication: it's a legitimate pattern in this +codebase to conditionally assign a WAVE reference inside a branch and use +it unconditionally afterward (typically feeding into a wave-ref array or +an `if(WaveExists(...))` check), without a separate `WAVE/Z x = $""` +pre-declaration. That pre-declaration is harmless but redundant for this +specific case — it's only necessary when you need the null-default to be +explicit/self-documenting, or when reusing the same variable name across +multiple, non-exclusive branches where the "did it run" tracking gets +less obvious. + +### Default Size When /N Is Omitted + +`Make` (with or without `/FREE`) does not create a 0-point wave when `/N` +is omitted entirely. What size it gets depends on whether an initializer +is also given: + +* **No `/N` and no initializer** — defaults to a 1D wave with **128 + points**: + +```igor + Make/FREE data // data has 128 points, NOT 0 + Make/FREE/WAVE datasets // datasets is a wave-of-waves with 128 elements, NOT 0 + Make numericWave // numericWave has 128 points +``` + +* **No `/N`, but an explicit initializer list is given** — the wave is + sized to match the list, *not* defaulted to 128: + +```igor + Make wv = {1, 2, 3} // wv has exactly 3 points + Make/FREE/T names = {"a", "b"} // names has exactly 2 points +``` + + The initializer form implicitly determines the size; the 128-point + default only applies when there is nothing — no `/N` and no + initializer — to size the wave from. + +Curly-brace initializer lists always require at least one operand +(`Make wv = {1}` is valid, `Make wv = {}` is not) — so there is no +initializer-based way to create a wave with 0 points either. A genuinely +empty wave must be produced via `Make/N=0` or by redimensioning +(`Redimension/N=0`) after creation. + +Before assuming a `Make` call produces an empty/zero-size wave, check for +both `/N` and an initializer list. Only when *both* are absent does the +wave default to 128 points. (If `/N=(...)` is used, every dimension size +must be given explicitly — there is no partial form where some +dimensions are sized and others fall back to 128.) + +### Referencing waves in other data folders + +```igor +WAVE w = root:myData:myWave // full absolute path + +DFREF dfr = root:myData +WAVE w = dfr:myWave // using a DFREF variable (see section 5) + +string wavePath = "root:myData:myWave" +WAVE w = $wavePath // path constructed at runtime in a string +``` +References to global permanent waves should be retrieved through wave-getter functions. + +### Checking if a reference is valid + +Always use `/Z` and then check `WaveExists()` when the wave may not exist: + +```igor +WAVE/Z w = FunctionReturningPossibleNullWave() +if(!WaveExists(w)) + Print "Wave not found" + return NaN +endif +``` + +**Never** assume a WAVE declaration succeeded without checking — if the wave +doesn't exist and you didn't use /Z, the function aborts with an error. + +### Getting a wave reference from a name string + +```igor +string name = "myWave" +WAVE w = $name // wave in current data folder +WAVE w = root:myData:$name // wave in specific folder — WRONG syntax +string fullPath = "root:myData:" + name +WAVE w = $fullPath // build full path in string +``` + +The `$` operator dereferences a string into a name. It cannot be used +mid-path — build the full path string first, then apply `$` once. + +--- + +## 3. Passing Waves to and from Functions + +### Passing waves as parameters + +Waves are always passed by reference (the reference is copied, not the data): + +```igor +// Declare parameter as WAVE in both the parameter list and declaration +Function ProcessWave(WAVE w) + w[] = w[p] * 2 +End + +// Typed wave parameters: +Function ProcessTextWave(WAVE/T tw) + Print tw[0] +End + +Function ProcessComplexWave(WAVE/C cw) + // ... +End +``` + +### Returning a wave reference from a function + +Use `Function/WAVE` return type: + +```igor +Function/WAVE MakeResultWave(variable n) + Make/FREE/N=(n) resultWave + + return resultWave +End + +// Calling it: +WAVE result = MakeResultWave(100) +``` + +Or use Multiple Return Syntax (Igor 8+): + +```igor +Function [WAVE w1, WAVE w2] MakeResultWaves(variable n) + Make/FREE/N=(n) resultWave1, resultWave2 + + return [resultWave1, resultWave2] +End + +// Calling it: +[WAVE result1, WAVE result2] = MakeResultWaves(100) +``` + +### Returning a wave reference to a wave in a specific data folder + +```igor +Function/WAVE GetMyWave(DFREF dfr, string name) + WAVE/Z w = dfr:$name + + return w +End + +// Check for null on the receiving side: +WAVE/Z result = GetMyWave(myDFR, "data") +if(!WaveExists(result)) + Abort "Wave not found" +endif +``` + +--- + +## 4. NVAR and SVAR — Global Variable References + +Global numeric variables and global strings are **not** directly accessible +inside functions by name. You must declare a reference first. + +```igor +// WRONG — globals are not automatically visible in functions: +Function BadGlobal() + myGlobalVar = 42 // Error: not declared +End + +// CORRECT: +Function GoodGlobal() + NVAR myGlobalVar // reference to global numeric variable in current DF + myGlobalVar = 42 +End + +Function GoodGlobalString() + SVAR myGlobalStr // reference to global string in current DF + myGlobalStr = "hello" +End +``` + +### Referencing globals in other data folders + +```igor +NVAR v = root:Packages:MyPkg:settingValue +SVAR s = root:Packages:MyPkg:settingName +``` + +### Checking existence before use + +```igor +NVAR/Z v = root:Packages:MyPkg:counter +if (!NVAR_Exists(v)) + variable/G root:Packages:MyPkg:counter + NVAR v = root:Packages:MyPkg:counter +endif +``` + +Similarly for strings: `SVAR/Z s = ...` then `SVAR_Exists(s)`. + +--- + +## 5. DFREF — Data Folder References + +`DFREF` is a reference to a data folder, analogous to WAVE for waves. +It is the preferred way to write folder-aware code in Igor 7+. + +### Declaring and obtaining a DFREF + +```igor +DFREF dfr = root:myData // absolute path +DFREF dfr = :subFolder // relative to current DF +DFREF dfr = GetDataFolderDFR() // current data folder +DFREF dfr = NewFreeDataFolder() // anonymous free data folder (not in tree) +DFREF dfr = $("root:myData:" + name) // dynamic path +``` + +### Checking if a DFREF is valid + +```igor +DFREF dfr = root:mayNotExist +if (DataFolderRefStatus(dfr) == 0) + Print "Data folder does not exist" + return +endif +``` + +`DataFolderRefStatus` returns: +- `0` — invalid (folder doesn't exist) +- `1` — refers to a regular data folder +- `3` — refers to a free data folder + +### Using DFREF to access waves and variables + +```igor +DFREF dfr = root:myData +WAVE w = dfr:intensity // wave in that folder +NVAR n = dfr:temperature // global variable in that folder +SVAR s = dfr:sampleName // global string in that folder +``` + +### Passing DFREF as a function parameter + +```igor +Function AnalyzeFolder(DFREF dfr) + WAVE/Z w = dfr:data + if (!WaveExists(w)) + Abort "No data wave found" + endif + WaveStats/Q w + Print "Mean =", V_avg +End +``` + +### Saving and restoring the current data folder + +**Always** save and restore the current data folder if your function changes it: + +```igor +Function DoSomethingInFolder(String path) + DFREF saveDF = GetDataFolderDFR() // save current DF + SetDataFolder path + // ... do work ... + SetDataFolder saveDF // restore +End +``` + +Failure to restore the current data folder is one of the most common bugs +in Igor procedures. Use this pattern every time you call `SetDataFolder`. + +### Returning a DFREF (Igor 10+) + +```igor +Function [DFREF df] GetOrCreateFolder(String name) + DFREF root = GetDataFolderDFR() + NewDataFolder/O root:$name + DFREF df = root:$name + + return [df] +End + +// Calling: +[DFREF myDF] = GetOrCreateFolder("results") +``` + +--- + +## 6. Free Waves + +Free waves exist only in memory, are not in any data folder, and are +automatically destroyed when no references point to them. They are ideal +for temporary intermediate results in functions. + +### Creating free waves + +```igor +// Make with /FREE flag: +Make/FREE/N=100 tempWave +Make/FREE/N=(n, m) tempMatrix + +// Duplicate with /FREE: +Duplicate/FREE sourceWave, tempCopy + +// NewFreeWave function: +WAVE tempWave = NewFreeWave(2, 100) // type 2 = single precision float, 100 points +``` + +Wave type codes for `NewFreeWave`: 0=double, 1=complex, 2=single, 4=int8, +8=int16, 16=int32, 32=unsigned int, 64=int64, 128=unsigned int64, 512=text. +Add these together for combinations (e.g. 4+32=36 for unsigned int8). + +### Key rules for free waves + +- Free waves cannot be the target of GUI related functions that permanently cause the data to be displayed, like `Display`, `Edit`, `AppendToGraph`, etc. + Create a permanent wave in a wave getter function for anything that needs to be plotted persistently. +- Free waves **can** be passed to operations like `WaveStats`, `FFT`, + `CurveFit` (with /NOINT or structure-based approaches), `MatrixOP`, etc. +- A free wave is destroyed as soon as the last WAVE reference to it goes + out of scope (end of function, or explicitly set to a different reference). + +--- + +## 7. Free Data Folders + +Free data folders are in-memory containers not attached to the data folder +tree. Useful for bundling temporary waves inside a function. + +```igor +DFREF freeDFR = NewFreeDataFolder() +Make/O freeDFR:tempWave/N=100 +WAVE w = freeDFR:tempWave +w = gnoise(1) +// freeDFR and all its contents are destroyed when freeDFR goes out of scope +``` + +--- + +## 8. The $ Operator — Name-to-Reference Resolution + +`$` converts a string expression into an Igor object reference. It is used +whenever the name of a wave, variable, or data folder is constructed at runtime. + +```igor +string name = "myWave" +WAVE w = $name // resolve name to wave in current DF + +string path = "root:myData:myWave" +WAVE w = $path // resolve full path + +Make/O $(name + "_result") // create wave with constructed name +WAVE result = $(name + "_result") +``` + +### $ in wave arithmetic (assignment to dynamically named wave) + +```igor +string outName = "processedData" +Make/O/N=100 $outName +WAVE out = $outName +out[] = p^2 +``` + +### $ cannot be used mid-path — build the full path string first + +```igor +// WRONG: +WAVE w = root:myData:$waveName // syntax error + +// CORRECT: +DFREF dfr = root:myData +WAVE w = dfr:$waveName // $ at the end of a dfr: prefix IS valid +``` + +--- + +## 9. WaveExists, DataFolderExists, NVAR_Exists, SVAR_Exists + +Use these to check validity before using references. Never assume an object +exists without checking, especially in general-purpose functions. + +```igor +// Waves: +if(WaveExists(w)) // w is a WAVE reference (use after WAVE/Z) + +// Data folders: +if(DataFolderExists("root:myData")) // string path + +DFREF/Z dfr = root:myData +if(DataFolderRefStatus(dfr) != 0) // using a DFREF + +// Global variables: +NVAR/Z v = myGlobalVar +if(NVAR_Exists(v)) + +// Global strings: +SVAR/Z s = myGlobalStr +if(SVAR_Exists(s)) +``` + +--- + +## 10. Common Patterns + +### Global Permanent Waves + +Global permanent waves are created in wave getter functions that must be located in MIES_WaveDataFolderGetters.ipf. + +A wave getter function has the form: +``` +Function/WAVE MyWaveGetter() + + string name = "waveName" + + DFREF dfr = GetDataFolderPath() + WAVE/Z/SDFR=dfr wv = $name + + if(WaveExists(wv)) + return wv + endif + + Make/D/N=(10) dfr:$name/WAVE=wv + + return wv +End +``` + +A wave getter function always returns a valid WAVE reference. + +Call it with: +``` +WAVE wv = MyWaveGetter() +``` + +### Concatenate with Potentially Empty Source Waves + +`Concatenate` (e.g. with `/NP=dim` to accumulate along an existing dimension) +safely creates the destination wave even when the source wave passed to it +has zero rows. If every source wave across repeated/looped `Concatenate` +calls has zero rows, the destination wave still gets created — it ends up +with zero rows, but it is never left as a null/non-existent wave reference. + +Simple example without loop: + +```igor +Make/FREE/T/N=(0) src +Concatenate/FREE/T/NP=(ROWS) {src}, allSrc + +// allSrc is implicitly created by Concatenate and is guaranteed to exist after the call, with DimSize(allSrc, ROWS) == 0 +// same is true if src would be a numeric wave +``` + +Example with loop: + +```igor +// Let `sources` be a wave reference wave with `DimSize(sources, ROWS) > 0` containing text waves. +// Even if every `src` here is a 0-row wave (e.g. from +// ListToTextWave("", ",") — see above), this loop is safe: +for(WAVE/T src : sources) + Concatenate/FREE/T/NP=(ROWS) {src}, allSrc +endfor + +// allSrc is implicitly created by Concatenate and is guaranteed to exist after the loop, with DimSize(allSrc, ROWS) == 0 +// if nothing non-empty was ever concatenated into it. +// Note: this assumes `sources` itself has at least one row — see below for the case where it doesn't. +``` + +Do not assume `allSrc` needs a defensive `WAVE/Z` check or a pre-emptive +`Make/FREE/T/N=(0) allSrc` before the loop purely to guard against the +all-sources-empty case — `Concatenate` already guarantees the destination +exists. + +This must not be confused with `sources` itself having zero rows (i.e. there +is nothing to iterate over at all). In that case the loop body never +executes, `Concatenate` is never called, and `allSrc` is never created — it +remains a null/non-existent wave reference, per the default initialization +described in "Scoping and Default Initialization" (section 2). Referencing +`allSrc` afterward without `/Z` then fails with a runtime error. + +### Global Permanent Waves with Versioning + +Global permanent waves with versioning are created in wave getter functions that must be located in MIES_WaveDataFolderGetters.ipf. +Versioning is required if the wave data is read by MIES after an experiment with MIES data was loaded because the loaded MIES data could be created with an older version of MIES. + +A wave getter function with versioning has the form: +``` +Function/WAVE GetMyWave(string device) + + string name = "myWave" + DFREF dfr = GetMyPath(device) + variable versionOfNewWave = 2 + + WAVE/Z/SDFR=dfr wv = $name + + if(ExistsWithCorrectLayoutVersion(wv, versionOfNewWave)) + return wv + elseif(WaveExists(wv)) // handle upgrade + if(WaveVersionIsAtLeast(wv, 1)) // upgrade version 1 to 2 + Redimension/D wv + else + // upgrade version 0 to 2 + // change the required dimensions and leave all others untouched with -1 + // the extended dimensions are initialized with zero + Redimension/D/N=(10, -1, -1, -1) wv + endif + else + Make/R/N=(10, 2) dfr:$name/WAVE=wv + endif + + SetWaveVersion(wv, versionOfNewWave) + + return wv +End +``` + +The wave getter function upgrades the wave on demand. The wave getter function always returns a valid wave reference with a wave of the latest version. + +Call it with: +``` +WAVE wv = GetMyWave(device) +``` + +### Global Datafolders + +Global datafolders are created in data folder getter functions that must be located in MIES_WaveDataFolderGetters.ipf + +A data folder getter function has the form: + +``` +threadsafe Function/S GetDatafolderPathAsString() + + return GetParentDatafolderPathAsString() + ":myDataFolder" +End + +threadsafe Function/DF GetMyDataFolderPath() + + return createDFWithAllParents(GetDatafolderPathAsString()) +End +``` + +The functions are always pairs, where one function returns the data folder as string and the other as data folder reference. +The function that returns the data folder path as string refers internally to the getter function for the parent data folder. +The topmost data folder of MIES can be retrieved with `GetMiesPath()` or as string with `GetMiesPathAsString()`. +Data folder getter functions always return a valid data folder reference. + +Call it with: +``` +DFREF dfr = GetMyDataFolderPath() +``` + +### Global Strings + +Global strings are created in getter functions that must be located in MIES_GlobalStringAndVariableAccess.ipf + +A global string getter function has the form: +``` +threadsafe Function/S GetMyGlobalString() + + return GetNVARAsString(GetMyDataFolderPath(), "stringName", initialValue = "new string") +End +``` + +The getter function always returns a valid path to the global string. The initialValue is optional and depends on how the string is used in MIES. + +Call it with: +``` +SVAR myGlobalString = $GetMyGlobalString() +``` + +### Global Variables + +Global variables are created in getter functions that must be located in MIES_GlobalStringAndVariableAccess.ipf + +A global variable getter function has the form: +``` +threadsafe Function/S GetMyGlobalVariable() + + return GetNVARAsString(GetMyDataFolderPath(), "variableName", initialValue = 1337) +End +``` + +The getter function always returns a valid path to the global variable. The initialValue is optional and depends on how the variable is used in MIES. + +Call it with: +``` +NVAR myGlobalVariable = $GetMyGlobalVariable() +``` + +### Prefer Existing Utility Functions over Reimplementation + +The procedure files `MIES_Utilities_*.ipf` and `MIES_MiesUtilities_*.ipf` contain utility functions for common tasks. Always prefer an already existing utility function +over reimplementing the same functionality. If for a task there is no utility function but an extension of a utility function would solve the task prefer the extension of an already existing function. + +WRONG: +``` +if(DataFolderRefStatus(dfr) != 0) +``` + +CORRECT: +``` +if(DataFolderExistsDFR(dfr)) +``` + +--- + +### ListToTextWave + +The Igor Pro integrated function `ListToTextWave` never returns a null wave. If the `listStr` argument is an empty string then a text wave with zero rows is returned. + +WRONG: +``` +WAVE/Z/T wv = ListToTextWave(listStr, separatorStr) +if(WaveExists(wv)) + // code working with wv +endif +``` + +CORRECT: +``` +WAVE/T wv = ListToTextWave(listStr, separatorStr) +// code working with wv +``` + +### Wave Versioning Migration Must Use Independent `if` Blocks + +When a wave getter upgrades an old wave layout across multiple versions, use a +sequence of independent `if(WaveVersionIsSmaller(wv, N))` blocks (`N` +increasing), never an exclusive `if/elseif` chain. An `elseif` chain can skip +needed intermediate migration steps for a wave that is several versions behind +the latest. + +When a migration step widens a wave's dimensions, `Redimension` to the new +size **before** writing to any newly-added column/row index -- Igor +bounds-checks wave assignments, so writing to column 3 of a wave still sized +at 3 columns (valid indices 0..2) throws a runtime error instead of migrating: + +```igor +// WRONG -- writes before the wave is big enough: +wv[][3] = someValue // errors if wv only has 3 columns (0..2) +Redimension/N=(-1, 4) wv + +// CORRECT -- resize first, then write: +Redimension/N=(-1, 4) wv +wv[][3] = someValue +``` + +### A `WAVE name = expr` Declaration Cannot Reference Its Own Name in `expr` + +The compiler rejects a `WAVE name = expr` declaration where `name` itself +appears inside `expr` as an argument, even if `name` was already declared +earlier in the function: + +```igor +// WRONG -- does not compile: +WAVE data = SomeFunc(data) + +// CORRECT -- introduce a second reference under a different name: +WAVE/Z tmp = data +WAVE data = SomeFunc(tmp) +``` + +This differs from a destructuring *reassignment* via Multiple Return Syntax +(e.g. `[out, outT] = SomeFunc(out, outT)`), which is legal -- it updates +already-declared references rather than re-declaring them. + +### `Make/N=(...)`: a Trailing Dimension Size of 0 vs. 1 + +An explicit dimension size of `0` in `Make/N=(...)` means "this dimension +does not exist," while `1` creates a real, if trivial, additional dimension. +`Make/N=(n, 1, 1, 1)` is **not** equivalent to a true 1D wave -- +`DimSize(wv, COLS)` is `1`, not `0`, in that case. MIES convention: a +wave-of-waves value that must be strictly 1D (e.g. asserted via +`DimSize(wv, COLS) == 0` in some helpers, or `GetWaveDimensionality(wv) == +ROWS` -- see `MIES_Utilities_WaveHandling.ipf` -- as the idiom for "this wave +is 1D") should be created with `0`/omitted trailing dimensions, never `1`. + +### `Variable/G name = value` Overwrites an Existing Global Every Time + +`Variable/G name = value`, with an explicit initializer, overwrites the +global's current value every time that line executes, even if the global +already exists (per the Igor Reference: "`/G` ... overwrites any existing +variable"). For code that must run unconditionally on every invocation +without resetting an existing value, use the bare form with no initializer: + +```igor +// WRONG if this runs more than once and the value should persist between calls: +Variable/G root:Packages:MyPkg:counter = 0 + +// CORRECT -- creates at 0 only if missing, leaves an existing value untouched: +Variable/G root:Packages:MyPkg:counter +``` + +No `NVAR_Exists`-style guard is needed for this bare form. + +### Never Name a Local After an Igor Built-in Function or Keyword + +The compiler does not stop a local variable/string/`WAVE` reference from +being named after a built-in Igor function or reserved keyword (e.g. +`string log` shadows `log()`). This compiles cleanly but silently breaks any +code in that scope expecting the real built-in behavior. Never name a +variable/string/WAVE reference after an Igor built-in function or reserved +keyword -- check the name against `.claude/skills/igor-commands/SKILL.md` +before choosing it if there's any doubt. Note that `ipt lint`'s +`BugproneReservedKeywordsAsIdentifier` check only flags shadowing a reserved +**keyword/type** (e.g. `variable wave`), not a built-in **function** name +(e.g. `variable abs`) -- it has no symbol-resolution semantics for that case, +so this specific mistake must still be caught by manual review. + +### WAVE/DFREF References Are Not Scoped by Independent Modules + +Independent Modules cannot call functions in other modules except through +`Execute` (per Igor's own "Advanced Topics" documentation on Limitations of +Independent Modules) -- but this restriction does **not** apply to direct +`WAVE`/`DFREF` references, which aren't module-scoped at all. Code in an +Independent Module needing only wave/data-folder access (no function calls) +into/out of another module needs no `Execute` indirection. + +### `for(elem : wv)` Range-Based Loops Are Equivalent to an Indexed Loop over `wv[i]` + +A range-based for loop (`for( varName : ) ... endfor`, added in +Igor Pro 9.00) iterates over every element of a wave, regardless of its +dimensionality: + +```igor +for(String s : tw) + Print s +endfor +``` + +- The loop variable's type must match the wave's element type: `string` for + a text wave, `WAVE` for a wave-reference wave, `DFREF` for a data-folder- + reference wave. For a numeric wave the type doesn't need to be an exact + match (e.g. a `variable` loop var is fine even over an integer wave). The + type can be omitted entirely if the loop variable (or the wave expression, + if it's a wave reference) was already declared earlier in the function. +- For a multi-dimensional wave, iteration order is **column-major**: for a + 2D wave, all rows of column 0, then all rows of column 1, and so on. + +This is genuinely equivalent to an indexed loop reading `wv[i]` for +`i = 0` to `numpnts(wv) - 1`, confirmed both by Igor's own documentation and +empirically for a multi-dimensional wave: + +```igor +// Equivalent to: for(v : wv) ... endfor +for(i = 0; i < numpnts(wv); i += 1) + variable elem = wv[i] + // ... +endfor +``` + +This works because Igor's single-bracket point-indexing (`wv[i]`) is not +limited to addressing row `i`, column 0 on a multi-dimensional wave -- +once the index exceeds the row count, it continues into column-major +linear addressing across the wave's remaining dimensions, exactly matching +`numpnts(wv)` and the range-based loop's own traversal order. Confirmed live +against a 3x4 numeric wave: both `for(i=0; i 2; wv2d[2][1] = 2 + 10*1 = 12) +wv2d[2.5][1] // 13 (row rounds 2.5 -> 3; wv2d[3][1] = 3 + 10*1 = 13 -- NOT truncated to 12) +wv2d[2.7][1] // 13 (row rounds 2.7 -> 3) +wv2d[1][2.3] // 21 (col rounds 2.3 -> 2; wv2d[1][2] = 1 + 10*2 = 21) +wv2d[1][2.5] // 31 (col rounds 2.5 -> 3; wv2d[1][3] = 1 + 10*3 = 31 -- NOT truncated to 21) +wv2d[1][2.7] // 31 (col rounds 2.7 -> 3) +``` + +The `2.5`/`2.7` cases are the ones that distinguish rounding from truncation: +truncation would give `12`/`21` (flooring to `2`) in every one of those rows, +but the actual results are `13`/`31` (rounding up to `3`), matching "closest to +the specified index," not "truncated toward zero." + +--- + +## 11. Quick Reference Table + +| Goal | Syntax | +|---|---| +| Retrieve wave from wave getter function | `WAVE w = MyWaveGetter()` | +| Retrieve wave from function that can return a null wave | `WAVE/Z w = MyFunction()` | +| Reference wave via DFREF (only used in wave getter function) | `WAVE w = dfr:myWave` | +| Check wave exists | `WaveExists(w)` after `WAVE/Z` | +| Reference text wave | `WAVE/T tw = myTextWave` | +| Reference complex wave | `WAVE/C cw = myComplexWave` | +| Reference global variable from getter function | `NVAR v = $MyNVARGetter()` | +| Reference global string from getter function | `SVAR s = $MySVARGetter()` | +| Get current DF as DFREF | `DFREF dfr = GetDataFolderDFR()` | +| Reference data folder | `DFREF dfr = root:myData` | +| Check DFREF is valid | `if(DataFolderExistsDFR(dfr))` | +| Save/restore current DF | `DFREF saveDF = GetDataFolderDFR()` ... `SetDataFolder saveDF` | +| Create free wave | `Make/FREE/N=(n) w` | +| Create free data folder | `DFREF dfr = NewFreeDataFolder()` | +| Get DF containing a wave | `DFREF dfr = GetWavesDataFolderDFR(w)` | +| Get name of a wave | `string name = NameOfWave(w)` | +| Return wave from function | `Function/WAVE Foo()` ... `return w` | +| Return DFREF from function (Igor 10) | `Function [DFREF df] Foo()` ... | + +--- + +## Reference URLs + +| Topic | URL | +|---|---| +| Programming Overview (functions, parameters) | https://docs.wavemetrics.com/igorpro/programming/programming | +| Programming Techniques (DF patterns) | https://docs.wavemetrics.com/igorpro/programming/programming-techniques | +| WAVE keyword | https://docs.wavemetrics.com/igorpro/commands/wave | +| Conditionally-assigned WAVE ref used after the block | Safe — defaults to null if the branch didn't run, not an error | +| NewFreeWave | https://docs.wavemetrics.com/igorpro/commands/newfreewave | +| NewFreeDataFolder | https://docs.wavemetrics.com/igorpro/commands/newfreedatafolder | +| GetDataFolderDFR | https://docs.wavemetrics.com/igorpro/commands/getdatafolderdfr | +| GetWavesDataFolderDFR | https://docs.wavemetrics.com/igorpro/commands/getwavesdatafolderdfr | +| DataFolderRefStatus | https://docs.wavemetrics.com/igorpro/commands/datafolderrefstatus | +| WaveExists | https://docs.wavemetrics.com/igorpro/commands/waveexists | +| CountObjectsDFR | https://docs.wavemetrics.com/igorpro/commands/countobjectsdfr | +| GetIndexedObjNameDFR | https://docs.wavemetrics.com/igorpro/commands/getindexedobjnamedfr | +| NVAR_Exists | https://docs.wavemetrics.com/igorpro/commands/nvar_exists | +| SVAR_Exists | https://docs.wavemetrics.com/igorpro/commands/svar_exists | +| `Concatenate` destination when every source across a loop is 0-row | Destination wave is still created, with 0 rows — never left null | diff --git a/.github/skills/igortest/SKILL.md b/.github/skills/igortest/SKILL.md new file mode 100644 index 0000000000..613437ee9a --- /dev/null +++ b/.github/skills/igortest/SKILL.md @@ -0,0 +1,31 @@ +--- +name: igortest +description: Reference documentation for IgorTest, the Igor Universal Testing Framework (IUTF) used for all MIES unit tests. Use when writing, reviewing, or debugging test cases under Packages/tests, when choosing assertions or logical flags, when setting up test suites or data generators, or when questions come up about test hooks and test execution. +--- + +# IgorTest (Igor Universal Testing Framework) + +IgorTest is the testing framework MIES uses for all automated tests (see the +`Writing Tests` section in the project instructions for MIES-specific +conventions layered on top of this framework, such as test cases needing to +be `static` and free of wave leaks). + +This skill only covers the framework itself: test suites, test cases, +assertions, flags, and advanced features like test hooks. Read the relevant +file below rather than guessing at API details or assertion names. + +## Reference files + +- [introduction.rst](introduction.rst) - What the framework is for and why tests matter; start here if unfamiliar with IgorTest. +- [basic.rst](basic.rst) - Core structure: Test Suites, Test Cases, and Assertions, and how they relate. +- [guided-tour.rst](guided-tour.rst) - Step-by-step walkthrough of creating and executing a first test. +- [examples.rst](examples.rst) - Worked examples of writing tests with this framework. +- [advanced.rst](advanced.rst) - Advanced usage, including test hooks that run at the start/end of a test run, suite, or case. +- [flags.rst](flags.rst) - Reference for logical flags used to modify assertion behavior (e.g. wave comparison flags), which can be combined. + +## When to load which file + +- Writing a brand-new test file or unsure of the basic layout: read `introduction.rst` and `basic.rst` first, then `guided-tour.rst`. +- Need a concrete pattern to copy: read `examples.rst`. +- Implementing setup/teardown logic, or anything that runs before/after a test run, suite, or case: read `advanced.rst`. +- Picking the right assertion or unsure what a flag does: read `flags.rst`. diff --git a/.github/skills/igortest/advanced.rst b/.github/skills/igortest/advanced.rst new file mode 100644 index 0000000000..82b9bd4366 --- /dev/null +++ b/.github/skills/igortest/advanced.rst @@ -0,0 +1,770 @@ +.. vim: set et sts=3 sw=3 tw=79: + +.. _advanced: + +Advanced Usage +============== + +.. _TestHooks: + +Test Hooks +---------- + +A Test Run can be extended with user-defined code at specific points during its +execution. These pre-defined injection points are at the beginning and +respectively at the end of a complete :ref:`Test Run`, a +:ref:`TestSuite`, and a :ref:`TestCase`. + +The following functions are reserved for user code injections: + +.. cpp:function:: TEST_BEGIN_OVERRIDE() + +Executed at the **begin** of a :cpp:func:`Test Run`. + +.. cpp:function:: TEST_END_OVERRIDE() + +Executed at the **end** of a :cpp:func:`Test Run`. + +.. cpp:function:: TEST_SUITE_BEGIN_OVERRIDE() + +Executed at the **begin** of a :ref:`TestSuite`. + +.. cpp:function:: TEST_SUITE_END_OVERRIDE() + +Executed at the **end** of a :ref:`TestSuite`. + +.. cpp:function:: TEST_CASE_BEGIN_OVERRIDE() + +Executed at the **begin** of a :ref:`TestCase`. + +.. cpp:function:: TEST_CASE_END_OVERRIDE() + +Executed at the **end** of a :ref:`TestCase`. + +.. note:: + + :cpp:func:`TEST_END_OVERRIDE()` is executed at the very end of a test run + so that the Igor debugger state is already reset to the state it had before + :cpp:func:`RunTest()` was executed. + +.. note:: + + The functions :cpp:func:`TEST_SUITE_BEGIN_OVERRIDE()` and + :cpp:func:`TEST_SUITE_END_OVERRIDE()` as well as + :cpp:func:`TEST_CASE_BEGIN_OVERRIDE()` and + :cpp:func:`TEST_CASE_END_OVERRIDE()` can also be defined locally in a test + suite with the `static` keyword. :ref:`example2` shows how `static` + functions are called the framework. + +These functions are executed automatically if they are defined anywhere in +global or local context. For example, :cpp:func:`TEST_CASE_BEGIN_OVERRIDE` gets +executed at the beginning of each :ref:`TestCase`. Locally defined functions +always override globally defined ones of the same name. To visualize this +behavior, take a look at the following scenario: A user would like to have code +executed only in a specific :ref:`TestSuite`. Then the functions +:cpp:func:`TEST_SUITE_BEGIN_OVERRIDE` and :cpp:func:`TEST_SUITE_END_OVERRIDE` +can be defined locally within the current :ref:`TestSuite` by declaring them +`static` to the current Test Suite. The local (`static`) functions then replace +any previously defined global functions. The functionality with additional user +code at certain points of a Test Run is demonstrated in :ref:`example5`. + +To give a possible use case, take a look at the following scenario: By default, +each :ref:`TestCase` is executed in its own temporary data folder. +:cpp:func:`TEST_CASE_BEGIN_OVERRIDE` can be used to set the data folder to +`root:`. This will result that each Test Case gets executed in `root:` and no +cleanup is done afterward. The *next* Test Case then starts with the data the +*previous* Test Case left in `root:`. + +.. note:: + By default the Igor debugger is disabled during the execution of a test run. + +Assertions can be used in test hooks. However it is enforced by the IUTF that +the test case itself must contain at least one assertion. If a CHECK or WARN +assertion in a test hook fails the test run is still executed normally. If a +test hook exits with a pending RTE (runtime exception) or an abort the execution +of the test run will be cancelled as this is considered an invalid test setup. + +.. _JUNITOutput: + +JUNIT Output +------------ + +All common continuous integration frameworks support input as JUNIT XML files. +The Igor Pro Universal Testing Framework supports output of test run results in +JUNIT XML format. The output can be enabled by adding the optional parameter +:code:`enableJU=1` to :cpp:func:`RunTest()`. + +The XML output files are written to the experiments `home` directory with naming +`JU_Experiment_Date_Time.xml`. If a file with the same name already exists a +three digit number is added to the name. The JUNIT Output includes the results +and history log of each test case and test suite. + +The format reference that the IUTF uses is described in the section +:ref:`junit_reference`. + +If the function tag ``// IUTF_SKIP`` is preceding the test case function then the test case is skipped (not executed) +and counted for JUNIT as `skipped`. + +Test Anything Protocol Output +----------------------------- + +Output according to the `Test Anything Protocol (TAP) standard 13 +`__ can be enabled +with the optional parameter `enableTAP = 1` of :cpp:func:`RunTest()`. + +.. todo:: + + reference function parameters with their breathe links + +The output is written into a file in the experiment folder with a unique +generated name `tap_'time'.log`. This prevents accidental overwrites of +previous test runs. A TAP output file combines all Test Cases from all Test +Suites given in :cpp:func:`RunTest()`. Additional TAP compliant descriptions +and directives for each Test Case can be added in the lines preceding the +function of a Test Case (all lines above :code:`Function` up to the previous +:code:`Function` are considered as tags, every tag in separate line): + +.. code-block:: igor + + // TAPDescription: My description here + // TAPDirective: My directive here + +For directives two additional keywords are defined that can be written at the +beginning of the directive message. + +- `TODO` indicates a Test that includes a part of the program still in + development. Failures here will be ignored by a TAP consumer. + +- `SKIP` indicates a Test that should be skipped. A Test with this directive + keyword is not executed and reported always as 'ok'. + +If the function tag ``// IUTF_SKIP`` is preceding the test case function then the test case is skipped (not executed) +and evaluated for TAP the same as if ``// TAPDirective: SKIP`` was set. + +Examples: +^^^^^^^^^ + +.. code-block:: igor + + // TAPDirective: TODO routine that should be tested is still under development + +or + +.. code-block:: igor + + // TAPDirective: SKIP this test gets skipped + +See the Experiment in the TAP_Example folder for reference. + +.. todo:: + + add reference to the example, include example code + +.. _automate: + +Automate Test Runs +------------------ + +To further simplify test execution it is possible to automate test runs from +the command line. + +Steps to do that include: + +- Implement a function called `run()` in `ProcGlobal` context (or an independent + module with IUTF included) taking no parameters. This function must perform + all necessary steps for test execution, which is at least one call to + :cpp:func:`RunTest`. + +- Put the test experiment together with your :ref:`Test Suites` and + the script `helper/autorun-test.bat` into its own folder. + +- Run the batch file `autorun-test.bat`. + +- Inspect the created log file. + +The example batch files for autorun create a file named `DO_AUTORUN.TXT` before +starting Igor Pro. This enables autorun mode. After the `run()` function is +executed and returned the log is saved in a file on disk and Igor Pro quits. + +A different autorun mode is enabled if the file is named +`DO_AUTORUN_PLAIN.TXT`. In this mode no log file is saved after the test +execution and Igor Pro does not quit. This mode also does not use the Operation +Queue. + +See also :ref:`example6`. + +Running in an Independent Module +-------------------------------- + +The universal testing framework can be run itself in an independent module. +This can be required in very rare cases when the `ProcGlobal` procedures +might not always be compiled. + +See also :ref:`example9`. + +Handling of Abort Code +---------------------- + +The universal testing framework continues with the next test case after catching +`Abort` and logs the abort code. Currently differentiation of different abort +conditions include manual user aborts, stack overflow and an encountered +`Abort` in the code. The framework is terminated when manually pressing the +Abort button. + +.. note:: + + Igor Pro 6 can not differentiate between manual user aborts and programmatic + abort codes. Pressing the Abort button in Igor Pro 6 will therefore + terminate only the current test case and continue with the next queued test + case. + +.. _tests_with_background_activity: + +Test Cases with Background Activity +----------------------------------- + +There exist situations where a test case needs to return temporary to the Igor +command prompt and continue after a background task has finished. A real world +use case is for example a testing code that runs data acquisition in a +background task and the test case should continue after the acquisition finished. + +The universal testing framework supports such cases with a feature that allows to +register one or more background tasks that should be monitored. A procedure name +can be given that is called when the monitored background tasks finish. After the +current test case procedure finishes the framework will return to Igors command +prompt. This allows the users background task(s) to do its job. After the +task(s) finish the framework continues the test case with the registered procedure. + +The registration is done by calling :cpp:func:`RegisterIUTFMonitor()` from a +test case or a BEGIN hook. The registration allows to give a list of +background tasks that should be monitored. The mode parameter sets if all or one +task has to finish to continue test execution. Optional a timeout can be set +after the test continues independently of the user task(s) state. + +It might happen that while a test case executes it turns out that a previously +registered background monitor is not needed any more, e.g. if requirements for +further parts of the test case are not met. Then an already registered background +monitor can be unregistered by calling :cpp:func:`UnRegisterIUTFMonitor()` from +the test case or BEGIN hook. The function takes no arguments. + +See also :ref:`flags_IUTFBackgroundMonModes`. + +Function definition of RegisterIUTFMonitor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenfunction:: RegisterIUTFMonitor + +The function that is registered to continue the test execution must have the +same format as a test case function and the name has to end with `_REENTRY`. +When the universal testing framework temporary drops to Igors command line and +resumes later no begin/end hooks are executed. Logically the universal testing +frame work stays in the same test case. It is allowed to register another +monitoring in the `_REENTRY` function. + +Multiple subsequent calls to :cpp:func:`RegisterIUTFMonitor()` in the same +function overwrite the previous registration. + +Test Cases with background activity are supported from multi data test cases, see +`Multi Data Test Cases with Background Activity`_. + + See also :ref:`example11`. + + See also :ref:`example12`. + +.. _multi_data_test_cases: + +Multi Data Test Cases +--------------------- + +Often the same test should be run multiple times with different sets of data. +The universal testing framework offers direct support for such tests. Test cases +that are run with multiple data take one optional argument. To the test case a +data generator function is attributed that returns a wave. For each element of +that wave the test case is run. This sketches a simple multi data test case: + +.. code-block:: igor + + // IUTF_TD_GENERATOR DataGeneratorFunction + Function myTestCase([arg]) + variable arg + // add checks here + End + + Function/WAVE DataGeneratorFunction() + Make/FREE data = {1, 2, 3, 4} + return data + End + +To the test case `myTestCase` a data generator function name is attributed with the +comment line above following the tag word `IUTF_TD_GENERATOR`. +All lines above :code:`Function` up to the previous :code:`Function` are considered +as tags with every tag in separate line. +If the data generator function is not found in the current procedure file it is searched +in all procedure files of the current compilation unit as a non-static function. (ProcGlobal context) +Also a static data generator function in another procedure file can be specified by +adding the Module name in the specification. There is no search in other procedure +files if such specified function is not found. + +.. code-block:: igor + + // IUTF_TD_GENERATOR GeneratorModule#DataGeneratorFunction + +The data generator `DataGeneratorFunction` returns a wave of numeric type and the +test case takes one optional argument of numeric type. When run `myTestCase` is +executed four times with argument arg 1, 2, 3 and 4. + +Supported types for `arg` are variable, string, complex, Integer64, data folder +references and wave references. The type of the returned wave of the attributed +data generator function must fit to the argument type that the multi data test +case takes. +The data generator function name must be attributed with a comment above the +test case's Function line, using the key word `IUTF_TD_GENERATOR` with the data +generator's function name following, as seen in the simple example here. This +scan is not limited to a fixed number of lines -- confirmed against the +implementation (`GetFunctionTagWave` in +`Packages/igortest/procedures/igortest-functiontags.ipf`): it considers every +comment line between the end of the previous function and the current +`Function` line (matching the "all lines above Function up to the previous +Function" statement above), trying each known tag pattern against every +non-empty line and silently skipping any line that doesn't match. This means +ordinary `///` doc-comment lines can be freely interspersed above a +`// IUTF_TD_GENERATOR ...` tag line without breaking the attribution. +If no data generator is given or the format of the test case function does not fit +to the wave type then a error message is printed and the test run is aborted. + +The test case names are by default extended with `:num` where num is the index +of the wave returned from the data generator. For convenience in the data generator +dimension labels can be set for each wave element that are used instead of the index. + +.. code-block:: igor + + Function/WAVE DataGeneratorFunction() + Make/FREE data = {1, 2, 3, 4} + SetDimLabel 0, 0, first, data + SetDimLabel 0, 1, second, data + SetDimLabel 0, 2, third, data + SetDimLabel 0, 3, fourth, data + return data + End + +The test case names would now be `myTestCase:first`, `myTestCase:second` and so on. + +The optional argument of the test case function is always given from the data +generator wave elements. Thus the case that `ParamIsDefault(arg)` is true never +happens. + +When setting up a multi data test case with a data generator returning wave +references then the test case can also use typed waves. Supported are +text waves (``WAVE/T``), waves with data folder references (``WAVE/DF``) and +waves with wave references (``WAVE/WAVE``). For such a test case or reentry +function the associated data generator must return a wave reference wave where +each wave element refers to a wave of the fitting type. +For a test case setup with the generic ``WAVE`` the type is not fixed for all +elements of from the data generator. + + See also :ref:`example13`. + +Assertions can be used in data generators. If a CHECK or WARN assertion in a +data generator fails the test run is still executed normally. If a data +generator exits with a pending RTE (runtime exception) or an abort the +execution of the test run will be cancelled as this is considered an invalid +test setup. + +Multi Data Test Cases with Background Activity +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multi data test cases that register a background task to be monitored are +supported. For a multi data test case each reentry function can have one of two +different formats: + +- Function fun_REENTRY() with no argument as described in `Test Cases with Background Activity`_ +- Function fun_REENTRY([arg]) with the same argument type as the originating multi data test case. + +For the second case, the reentry function is called with the same wave element as argument as +when the multi data test case was started. + +If the reentry function uses a different argument type than the test case entry +function then on reentry to the universal testing framework an error is printed +and further test execution is aborted. + +.. code-block:: igor + + // IUTF_TD_GENERATOR DataGeneratorFunction + Function myTestCase([var]) + variable var + + CtrlNamedBackGround testtask, proc=UserTask, period=1, start + RegisterIUTFMonitor("testtask", 1, "testCase_REENTRY") + CHECK(var == 1 || var == 5) + End + + Function UserTask(s) + STRUCT WMBackgroundStruct &s + + return !mod(trunc(datetime), 5) + End + + Function/WAVE DataGeneratorFunction() + Make/FREE data = {5, 1} + SetDimLabel 0, 0, first, data + SetDimLabel 0, 1, second, data + return data + End + + Function testCase_REENTRY([var]) + variable var + + print "Reentered test case with argument ", var + PASS() + End + +.. _multi_multi_data_test_cases: + +Multi-Multi Data Test Cases +--------------------------- + +Multi-Multi-Data test cases are an extension of multi-data test cases. They allow to specify more than one variable with corresponding data generator. + +.. code-block:: igor + + Function/WAVE GeneratorStr() + + Make/FREE/T/N=2 data = num2istr(p) + SetDimlabel UTF_ROW, 0, ROW0, data + SetDimlabel UTF_ROW, 1, ROW1, data + + return data + End + + Function/WAVE GeneratorVar() + + Make/FREE/N=2 data = p + SetDimlabel UTF_ROW, 0, ROW0, data + SetDimlabel UTF_ROW, 1, ROW1, data + + return data + End + + // IUTF_TD_GENERATOR v0:GeneratorVar + // IUTF_TD_GENERATOR s2:GeneratorStr + // IUTF_TD_GENERATOR v1:GeneratorVar + // IUTF_TD_GENERATOR v2:GeneratorVar + // IUTF_TD_GENERATOR v3:GeneratorVar + static Function TC_MMD_Part1([md]) + STRUCT IUTF_mData &md + + CHECK(md.v0 >= 0 && md.v0 < 2) + print md.v0, md.v1, md.v2, md.v3 + print md.s2 + End + +The basic functionality works the same as for the regular multi-data test cases. +In Multi-Multi-Data test cases the changing variables are elements of the structure ``IUTF_mData``. Each variable can have a data generator function set with the +``IUTF_TD_GENERATOR`` directive. The tag syntax is ``varName:DataGeneratorName``. The test case is called for all permutations of setup data generators values of all variables. +In the upper example these are 32 test case calls. The structure defines the following variables: + +.. code-block:: igor + + Structure IUTF_mData + variable v0 + variable v1 + variable v2 + variable v3 + variable v4 + string s0 + string s1 + string s2 + string s3 + string s4 + DFREF dfr0 + DFREF dfr1 + DFREF dfr2 + DFREF dfr3 + DFREF dfr4 + WAVE/WAVE w0 + WAVE/WAVE w1 + WAVE/WAVE w2 + WAVE/WAVE w3 + WAVE/WAVE w4 + variable/C c0 + variable/C c1 + variable/C c2 + variable/C c3 + variable/C c4 + int64 i0 + int64 i1 + int64 i2 + int64 i3 + int64 i4 + EndStructure + +Note: The int64 variables are only available for Igor Pro 7+. + +Any combination of v, s, c, w, dfr and i variables can be set. Currently for each type the structure offers 5 different variables. +Variables that are not set by a data generator have their respective default value, 0 or null. +The test case name is suffixed by the current index of the data generator wave or if set by the current dimension label. +The order of the suffixes equals the order of the variables in the structure ``IUTF_mData``. +The indices are changed for all setup variables. The first variables changes fastest, that is in the upper example for ``v0``. +If Multi-Multi-Data test cases are combined with functions with background activity the reentry function must have the same +signature. + +.. _code_coverage: + +Code Coverage Determination +--------------------------- + +When running Igor Pro 9 or newer the Igor Pro Universal Testing Framework offers +the feature to obtain code coverage information. When enabled the IUTF adds to +functions in target procedure files code to track execution. At the end of the +test run the IUTF outputs files in HTML format with coverage information. + +This feature is enabled when the optional parameter ``traceWinList`` is set and non-empty when calling ``RunTest``. +Before the actual tests are executed the given procedure files are modified on disk where additional function calls are inserted. +The additional code does not change the execution of the original code. This step is named ``Instrumentation``. +The coverage results are output as HTML files in the experiments folder for each procedure file in the form: + +.. + To create htmloutput.txt run the tests from Various.pxp. Then a file test-tracing2.htm is created in the folder of the experiment file. + For htmloutput.txt the content from the first function Workload is taken and from the second function TracingTest the first + Make/MultiThread block as well as the second if block with if/elseif/else/endif. + +.. literalinclude:: htmloutput.txt + +The code is prefixed with three columns where the number in the first column is the count how many times the line was executed. +In second and third column is counted, when the code contained an ``if`` conditional. For that case the second column counts +the execution for the case the condition was ``true`` and the third column counts when the condition was ``false`` respectively. + +IUTF does also support the output in `Cobertura format `_. To do this you have to add +``COBERTURA:1`` to ``traceOptions`` in ``RunTest``. This will output an xml file for each instrumented procedure file. + +Details +^^^^^^^ + +The optional parameter ``traceOptions`` for ``RunTest`` allows to tune execution with code coverage. This parameter is a list +with key-value pairs that can be set using the Igor functions ``ReplaceNumberByKey`` or ``ReplaceStringByKey`` respectively. +For each settings key a constant is defined in ``TraceOptionKeyStrings``. The following keys are available: + +* ``UTF_KEY_REGEXP`` (``REGEXP:boolean``) When set the parameter ``traceWinList`` is parsed as a regular expression for all procedure window names. +* ``UTF_KEY_HTMLCREATION`` (``HTMLCREATION:boolean``) When set to zero no HTML files are created after the test run. + HTML files can be created by calling ``IUTF_Tracing#AnalyzeTracingResult()`` manually after a test run. +* ``UTF_KEY_INSTRUMENTATIONONLY`` (``INSTRUMENTONLY:boolean``) When set the IUTF will only do the code instrumentation and then return. No tests get executed. +* ``UTF_KEY_COBERTURA``(``COBERTURA:boolean``) When set IUTF will additionally output the reports in Cobertura format. +* ``UTF_KEY_COBERTURA_SOURCES`` (``COBERTURA_SOURCES:string``) A comma ``,`` delimited + list of directory paths that should be used as source paths for the procedure files. If this list + is empty or this option not set IUTF will use the current home directory of the experiment as the + source path for all procedure files. +* ``UTF_KEY_COBERTURA_OUT`` (``COBERTURA_OUT:string``) The output directory where all generated + cobertura file should be written to. This helps to organize your project directory. You have to + provide the absolute path to the directory with a trailing directory delimiter (``\`` in Windows, + ``:`` with Macintosh). If this option is not defined or empty IUTF will store all generated files + in the home directory at the start of ``RunTest`` which is usually the same directory as your + experiment file. + +Additionally function and macros can be excluded from instrumentation by adding the special comment ``// IUTF_NOINSTRUMENTATION`` before the first line of the function. +Excluding basic functions or macros that are called very often can speed up the execution of instrumented code. + +Static functions in procedure files can only be instrumented, if the procedure file has the pragma ModuleName set, e.g. ``#pragma ModuleName=myUtilities``. +For static functions that exist in a given procedure file without ModuleName a warning is printed to history. These function are not instrumented and +appear in the coverage result file with zero executions. + +Instrumented code runs roughly 30% slower. In special cases a stronger slowdown can occur. In such cases it should be considered to exclude +very often called functions from the instrumentation with the special comment ``// IUTF_NOINSTRUMENTATION`` as described above. + +Coverage logging also works for threadsafe functions and functions that are executed in preemptive threads. + +The instrumented code that is written to disk and executed with code coverage logging is based on the current code within Igor Pro at the time when ``RunTest`` is called. +The evaluation of gathered coverage data refers to the procedure file content on disk when ``RunTest`` was called. Thus, unsaved changes +in procedure files that are targeted for instrumentation will result in incorrect result files. It is strongly recommended to save all +procedure file changes to disk before running a test with code coverage logging. + +At the end of a run with code coverage determination Igor Pro outputs the global coverage to stdout in the form ``Coverage: 12.3%``. +The following regular expression can be used in CI services (e.g. in GitLab) to retrieve the number +``(?:^Coverage: )(\d+.\d+)(?:%$)``. + +After the test run the user can call ``IUTF_RestoreTracing()`` to restore the +instrumented procedure files back to their original version. It is recommended +to call this manually after the test run. + +.. _coverage_statistics: + +Statistics +^^^^^^^^^^ + +After running the code coverage the user can print a table with the most called functions to the history using +``ShowTopFunctions``. This function accepts as the first parameter the maximum number of entries that should be +printed. If all entries should be printed this parameter should be set to to a large number or ``Inf``. + +The optional parameter ``mode`` can be set to ``UTF_ANALYTICS_LINES`` to print the statistics for each line instead of +each function (``UTF_ANALYTICS_FUNCTIONS``). The optional parameter ``sorting`` defines the column that should be +sorted for. Currently supported are ``UTF_ANALYTICS_CALLS`` (default) to sort for all direct calls and +``UTF_ANALYTICS_SUM`` to sort for the sum of all called lines inside the function. ``UTF_ANALYTICS_SUM`` can not +combined with the mode ``UTF_ANALYTICS_LINES``. + +The data is also available as a global wave in ``root:Packages:igortest:TracingAnalyticResult``. + +Limitations +^^^^^^^^^^^ + +The function that calls RunTest with tracing enabled must return to the Igor Pro +command line afterwards to allow recompilation of the instrumented code. It is +not allowed to have another RunTest call in between. The Igor Pro Universal +Testing Framework will abort with an error in that case. + +If the full autorun feature is enabled through ``DO_AUTORUN.TXT`` the RunTest call with instrumentation must be the only call in the experiment. +Specifically, if a RunTest call without tracing is placed before then the RunTest call with tracing will not execute tests. + +The output of the statistics can currently not be automated as such as the automation requires the return to the Igor +Pro command line. + +Examples +^^^^^^^^ + +.. literalinclude:: ../../examples/CoverageDemoCode.ipf + :caption: Sets up a test, enables coverage determination for all procedure files that start with ``CODE_``. + :name: IUTF_Coverage_example1 + :language: igor + :start-after: // IUTF_Coverage_example1_begin + :end-before: // IUTF_Coverage_example1_end + :dedent: + :tab-width: 4 + +.. literalinclude:: ../../examples/CoverageDemoCode.ipf + :caption: Enables coverage determination for all procedure files that start with ``CODE_``, but stops after instrumentation of the code. + :name: IUTF_Coverage_example2 + :language: igor + :start-after: // IUTF_Coverage_example2_begin + :end-before: // IUTF_Coverage_example2_end + :dedent: + :tab-width: 4 + +.. literalinclude:: ../../examples/CoverageDemoCode.ipf + :caption: Output the results of coverage determination as Cobertura and disables the HTML output. Only the test suite will be instrumented. + :name: IUTF_Coverage_example3 + :language: igor + :start-after: // IUTF_Coverage_example3_begin + :end-before: // IUTF_Coverage_example3_end + :dedent: + :tab-width: 4 + +.. _flaky_tests: + +Flaky Tests +----------- + +Certainly a flaky test is something that needs to be avoided and fixed. Tests +should always pass and if not they need to be worked on. However we don't live +in a perfect world and thus it might be helpful to identify tests that fail +every now and then. + +To allow rerun failed flaky tests in the Igor Universal Testing Framework you +have to call ``RunTest`` with the optional ``retry`` parameter set to +``IUTF_RETRY_FAILED_UNTIL_PASS``. After that all flaky tests need to be marked +with the function tag ``IUTF_RETRY_FAILED``. IUTF will now rerun these test +cases up to 10 times if they exits with a failed CHECK assertion. + +You can also set the ``IUTF_RETRY_MARK_ALL_AS_RETRY`` flag in the ``retry`` +parameter of ``RunTest`` to rerun all failed tests in the test run. This treats +all tests cases as if they are marked with the function tag +``IUTF_RETRY_FAILED``. + +If you want that all failed REQUIRE assertions will be retried as well you have +to set the ``IUTF_RETRY_REQUIRES`` flag in the ``retry`` parameter of +``RunTest``. Be careful as this will also retry other cases which would normally +abort the test run like invalid reentry function signatures. The best thing is +not to use REQUIRE assertions for conditions that are flaky. + +You can also change the maximum number of retries to a lower limit using the +optional parameter ``retryMaxCount``. However it is not possible to set this +number to a higher value than 10 (``IUTF_MAX_SUPPORTED_RETRY``). + +Rerunning a flaky test case will also re-execute the test case begin and end +hook each time. If a multi-data testcase or a multi-multi-data testcase is +marked as flaky and one iteration failed it will retry the single failed +iteration with the same arguments and not all previous runs. + +.. code-block:: igor + + // IUTF_RETRY_FAILED + Function FlakyTest() + // doing some stuff that can fail for some reasons but will succeed if it + // will be retried some times. + variable err = SetupTestWhichIsFlaky() + CHECK_EQUAL_VAR(err, 0) + if(err) + return NaN + endif + + // perform the real test + // ... + End + +.. _shuffle_test_case_order: + +Shuffle test case order +----------------------- + +The Igor universal testing framework executes all test suites one by one by +their appearance in the ``RunTest`` call. If the optional parameter +``enableRegExp`` was set it will execute the found test suites alphabetically. +If you want a random order each time you execute ``RunTest`` you have to set the +flag ``IUTF_SHUFFLE_TEST_SUITES`` of the optional parameter ``shuffle`` in the +``RunTest`` call. + +When a test suite is executed it will execute all of its test cases. There is no +interleaving with other test suites. By default are all test cases executed in +order of their line number in the procedure file. To randomize the order of the +execution of test cases you have to set the flag ``IUTF_SHUFFLE_TEST_CASES`` of +the optional parameter ``shuffle`` in the ``RunTest`` call. This will shuffle +all test cases for each test suite. If this is not intended for single test +suites (e.g. the test cases depend on each other) you can opt-out these test +suites by setting the procedure tag ``// IUTF_NO_SHUFFLE_TEST_CASE`` somewhere +in the file. + +.. caution:: + Procedure tags can only be placed in the first 20 lines of a file. They are + one-line comments like function tags (e.g. ``// IUTF_SKIP``) and ignore any + conditional compilation with ``#if``. + +If you want to shuffle everything you can set the optional parameter ``shuffle`` +to ``IUTF_SHUFFLE_ALL``. + +.. _Jenkins XUnit plugin: https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd + +.. _junit_reference: + +JUNIT Reference +--------------- + +The JUNIT implementation in the IUTF is based on the XML scheme definition from `Jenkins XUnit plugin`_. + +Example XML reference file. + +.. literalinclude:: junit.xml + :caption: Example XML file with attributes used also supported by the Jenkins JUnit plugin based on the file published at . + :name: JUNIT_XML_Example + :language: xml + :force: + :dedent: + :tab-width: 4 + +.. literalinclude:: junit.xsd + :caption: XSD (XML scheme definition) file for JUNIT + :name: JUNIT_XSD + :language: xml + :dedent: + :tab-width: 4 + +.. _cobertura_reference: + +Cobertura Reference +------------------- + +The Cobertura implementation in the IUTF is based on the DTD scheme definition +`coverage-04.dtd `_. + +.. literalinclude:: coverage-04.dtd + :caption: Cobertura DTD schema ``coverage-04.dtd`` + :name: COBERTURA_DTD + :language: dtd + :dedent: + :tab-width: 4 diff --git a/.github/skills/igortest/basic.rst b/.github/skills/igortest/basic.rst new file mode 100644 index 0000000000..861205063e --- /dev/null +++ b/.github/skills/igortest/basic.rst @@ -0,0 +1,187 @@ +.. vim: set et sts=3 sw=3 tw=79: + +.. _basic: + +Basic Structure +=============== + +The interface design and naming is inspired by the `Boost Test Library +`__. Following this naming scheme, the universal +testing package consists of three basic structural elements: + +- :ref:`Test Suites ` +- :ref:`Test Cases ` +- :ref:`Assertions ` + +The basic building blocks of this Igor Pro Universal Testing Framework are +assertions. Assertions are used for checking if a condition is true. See +:ref:`AssertionTypes` for a clarification of the difference between the three +assertion types. Assertions are grouped into single test cases and test cases +are organized in test suites. + +A :ref:`test suite ` is a group of test cases that live in a single +procedure file. You can group multiple test suites in a named test environment +by using the optional parameter :code:`name` of :cpp:func:`RunTest()`. + +For a list of all objects see :ref:`genindex` or use the :ref:`search`. + +.. _RunTest: + +Test Run +-------- + +A Test Run is executed using :cpp:func:`RunTest` with only a single mandatory +parameter which is the :ref:`TestSuite`. + +Function definition of RunTest +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenfunction:: RunTest + +.. _TestSuite: + +Test Suite +---------- + +A Test Suite is a group of :ref:`Test Cases` which should belong +together. All :ref:`test functions` are defined in a single +procedure file, :cpp:func:`RunTest` calls them from top to bottom. Generally speaking, +a Test Suite is equal to a procedure file. +Therefore tests suites can not be nested, although multiple test suites can be +run with one command by supplying a list to the parameter :code:`procWinList` in +:cpp:func:`RunTest`. + +.. note:: + + Although possible, a test suite should not live inside the main program. It + should be separated from the rest of the project into its own procedure + file. This also allows to load only the necessary parts of your program + into the unit test. + +.. _TestCase: + +Test Case +--------- + +A Test Case is one of the basic building blocks grouping :ref:`assertions +` together. A function is considered a test case if it +fulfills all of the following properties: + +1. It takes no parameters. +2. It returns a numeric value (Igor Pro default). +3. Its name does not end with `_IGNORE` or `_REENTRY`. +4. It is either non-static, or static and part of a regular module. + +The first rule is making the test case callable in automated test environments. + +The second rule is reserving the `_IGNORE` namespace to allow advanced users to +add their own helper functions. It is advised to define all test cases as +static functions and to create one regular distinctive module per procedure +file. This will keep the Test Cases in their own namespace and thus not +interfere with user-defined functions in `ProcGlobal`. + +A defined list of test cases in a test suite can be run using the optional +parameter :code:`testCase` of :cpp:func:`RunTest`. When executing multiple test +suites and a test case is found in more than one test suite, it is executed in +every matching test suite. + +Test cases can be marked to expect failures. The assertions are executed as +normal and the error counter is reset to zero if one or more assertions failed +during the execution of this test case. Only if the test case finished without +any failed assertion the test case itself is considered as failed. To mark a +test case as expected failure write the keyword in the comment above (all lines +above :code:`Function` up to the previous :code:`Function` are considered as +tags, every tag in separate line): + +.. code-block:: igor + + // IUTF_EXPECTED_FAILURE + Function TestCase_NotWorkingYet() + +All assertions in a test case are marked as expected failures. If the test case +ends due to an :code:`Abort`, :code:`AbortOnRTE` or pending RTE this is also +considered as expected failure and neither the error counter is increased or +test case failed. + +Example: +^^^^^^^^ + +In Test Suite `TestSuite_1.ipf` the Test Cases `static Duplicate()` and `static Unique_1()` +are defined. In Test Suite `TestSuite_2.ipf` the Test Cases `static Duplicate()`, +`static Unique_2()` are defined. + +.. code-block:: igor + + Runtest("TestSuite_1.ipf;TestSuite_2.ipf", testCase="Unique_1;Unique_2;Duplicate") + +The command will run the two test suites `TestSuite_1.ipf` and +`TestSuite_2.ipf` separately. Within every test suites two test cases are +execute: the `Unique*` test case and the `Duplicate` test case. The `Duplicate` +test cases do not interfere with each other since they are static to the +corresponding procedure files. Since the duplicate test cases are found in both +test suites, they are also executed in both. + +.. note:: + + The Test Run will not execute if the one of the specified test cases can not be + found in the given list of test suites. This is also applies if no test case + could be found using a regular expression pattern. + +.. _AssertionTypes: + +Assertion Types +--------------- + +An assertion checks that a given condition is true or in more general terms +that an entity fulfills specific properties. Test assertions are defined for +strings, variables and waves and have :code:`ALL_CAPS` names. The assertion +group is specified with a prefix to the assertion name using one of `WARN`, +`CHECK` or `REQUIRE`. Assertions usually come in these triplets which differ +only in how they react on a failed assertion. The following table clarifies the +difference between the three assertion prefix groups: + ++-----------+----------------------+-------------------------+-------------------------------+ +| Type | Create Log Message | Increment Error Count | Abort execution immediately | ++===========+======================+=========================+===============================+ +| WARN | YES | NO | NO | ++-----------+----------------------+-------------------------+-------------------------------+ +| CHECK | YES | YES | NO | ++-----------+----------------------+-------------------------+-------------------------------+ +| REQUIRE | YES | YES | YES | ++-----------+----------------------+-------------------------+-------------------------------+ + +The most simple assertion is :cpp:func:`CHECK` which tests if its argument is +true. If you do not want to increase the error count, you could use the +corresponding :cpp:func:`WARN` function and if you want to Abort the execution +of the current test case if the supplied argument is false, you can use the +:cpp:func:`REQUIRE` variant for this. + +Similar to these simple assertions there are many different checks for typical +use cases. Comparing two variables, for example, can be done with +:cpp:func:`WARN_EQUAL_VAR`, or :cpp:func:`REQUIRE_EQUAL_VAR`. Take a look at +:ref:`example10` for a test case with various assertions. + +.. note:: + + See :ref:`group_assertions` for a complete list of all available checks. If + in doubt use the `CHECK` variant. + +Assertions with only one variant are :cpp:func:`PASS` and :cpp:func:`FAIL`. +If you want to know more about how to use these two special assertions, take a +look at :ref:`example7`. + +.. _AbortingTestRun: + +Aborting the test run +--------------------- + +You can abort the execution of the test run by clicking the Abort button in the +status bar or pressing the following user abort key combinations: + ++--------------+----------------+ +| Command-dot | Macintosh only | ++--------------+----------------+ +| Ctrl+Break | Windows only | ++--------------+----------------+ +| Shift+Escape | All Platforms | ++--------------+----------------+ diff --git a/.github/skills/igortest/examples.rst b/.github/skills/igortest/examples.rst new file mode 100644 index 0000000000..730406733f --- /dev/null +++ b/.github/skills/igortest/examples.rst @@ -0,0 +1,502 @@ +.. vim: set et sts=3 sw=3 tw=79: + +.. _examples: + +Examples +======== + +The example section shows the usage of the Igor Pro Universal Testing Framework. +If you are just starting to use this framework, consider taking the :ref:`tour`. + +.. _example1: + +Example1 +-------- + +This example is showing the basic working principle of the compare assertion. +Constant values are given as input to the unit :code:`abs()` and the output is +checked for equality. + +This unit test makes sure that the function :code:`abs()` behaves as expected. +For example if you use the unit :code:`abs()` in a function and you give +:code:`NaN` as an input value the output value will also be :code:`NaN`. The +function is also capable of handling :code:`INF` singularities. + +.. literalinclude:: ../../examples/example1-plain.ipf + :caption: example1-plain.ipf + :tab-width: 4 + :linenos: + :emphasize-lines: 11 + +The test suite can be executed using the following command: + +.. code-block:: igor + :caption: command + + RunTest("example1-plain.ipf") + +By looking at line 10 in this example it becomes clear that +:cpp:func:`CHECK_EQUAL_VAR` is a better way of comparing numeric variables than +the plain :cpp:func:`CHECK` assertion since :code:`NaN == NaN` is false. The +error is skipped by using the :cpp:func:`WARN` variant and will not raise the +error counter. If you want to know up to what extent those methods differ, take +a look at the section on :ref:`AssertionTypes`. + +.. note:: + + It is recommended to take a look at the :doc:`complete list of assertions + `. This will help in choosing the right assertion type for a + comparison. + + The definition for the assertions in this test suite: + + * :cpp:func:`CHECK_EQUAL_VAR` + * :cpp:func:`WARN` + +.. _example2: + +Example2 +-------- + +This test suite has its own run routine. The :code:`run_IGNORE` function serves +as an entry point for :code:`"example2-plain.ipf"`. By using the +:code:`_IGNORE` suffix, the function itself will be ignored as a test case. +This is also explained in the section about :ref:`Test Cases`. It is +important to note that calling :cpp:func:`RunTest` would otherwise lead to a +recursion error. + +There are multiple calls to :cpp:func:`RunTest` in :code:`run_IGNORE` to +demonstrate the use of optional arguments. Calling the function without any +optional argument will lead to a search for all available test cases in the +procedure file. You can also execute specific test cases by supplying them with +the :code:`testCase` parameter. + +The optional parameter :code:`name` is especially useful for bundling more than +one procedure file into a single test run. + +The test suite itself lives in a module and all test cases are static to that +module. This is the recommended environment for a test suite. When using the +static keyword, you also have to define a module with :code:`#pragma +ModuleName=Example2` + +.. literalinclude:: ../../examples/example2-plain.ipf + :caption: example2-plain.ipf + :tab-width: 4 + :name: example2-code + :emphasize-lines: 3 + +.. code-block:: igor + :caption: command + + run_IGNORE() + +.. note:: + + The definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`CHECK_EQUAL_STR` + * :cpp:func:`CHECK_NEQ_STR` + * :cpp:func:`CHECK_EMPTY_STR` + * :cpp:func:`CHECK_NULL_STR` + +.. _example3: + +Example3 +-------- + +This test suite emphasizes the difference between the :cpp:func:`WARN`, +:cpp:func:`CHECK`, and :cpp:func:`REQUIRE` assertion variants. + +The :cpp:func:`WARN_* ` variant does not increment the error count if the +executed assertion fails. :cpp:func:`CHECK_* ` variants increase the +error count. :cpp:func:`REQUIRE_* ` variants also increment the error +count but will stop the execution of the test run immediately if the assertion +fails. + +Even if a test has failed, the test end hook is still executed. See +:ref:`example5` for more details on hooks. + +.. literalinclude:: ../../examples/example3-plain.ipf + :caption: example3-plain.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + print RunTest("example3-plain.ipf") + +The error count this test suite returns is 2 + +.. note:: + + See also the section on :ref:`AssertionTypes`. + + * :cpp:func:`CHECK` + * :cpp:func:`WARN` + * :cpp:func:`REQUIRE` + +.. _example4: + +Example4 +-------- + +This test suite shows the use of test assertions for waves. + +The type of a wave can be checked with :cpp:func:`CHECK_WAVE` and +binary flags for the :ref:`flags_testwave_minor` and +:ref:`flags_testwave_major`. All flags are defined in :ref:`flags_testwave` and +can be concatenated as shown in line 45. If the comparison is done against such a +concatenation, it will fail if a single flag is not true. This is also shown in +line 47 where the free wave does not exist but as proven before, it is +definitely numeric. + +It is noteworthy that each test case is executed in a fresh and empty +datafolder. There is no need to use :code:`KillWaves` or :code:`Make/O` here. + +.. literalinclude:: ../../examples/example4-wavechecking.ipf + :caption: example4-wavechecking.ipf + :tab-width: 4 + :linenos: + :emphasize-lines: 10,45,47 + +.. code-block:: igor + :caption: command + + print RunTest("example4-wavechecking.ipf") + +Helper functions to check wave types and compare with reference waves are also +provided in :doc:`assertions`. + +.. note:: + + The definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`CHECK_EMPTY_FOLDER` + * :cpp:func:`CHECK_WAVE` + * :cpp:func:`CHECK_EQUAL_VAR` + * :cpp:func:`CHECK_EMPTY_STR` + * :cpp:func:`CHECK_EQUAL_WAVES` + +.. _example5: + +Example5 +-------- + +The two test suites show how to use test hook overrides. + +Here is shown how user code can be added to the Test Run at certain points. In +this test suite, additional code can be executed at the beginning and end of +the test cases. This is done by declaring the :code:`TEST_CASE_BEGIN_OVERRIDE` +or :code:`TEST_CASE_END_OVERRIDE` function :code:`'static'`. Functions with +this specific naming and the :code:`_OVERRIDE` suffix are automatically found +and registered as hooks. + +Be aware that a :code:`'static'` defined hook overrides any global +:code:`TEST_CASE_BEGIN_OVERRIDE` functions for this Test Suite. If you want to +execute the global :code:`TEST_CASE_BEGIN_OVERRIDE` as well add this code to +the static override function: + +.. code-block:: igor + + FUNCREF USER_HOOK_PROTO tcbegin_global = $"ProcGlobal#TEST_CASE_BEGIN_OVERRIDE" + tcbegin_global(name) + +The second procedure file :ref:`example5-code-2` is in :code:`ProcGlobal` +context so the test hook extensions are also global. + +.. literalinclude:: ../../examples/example5-extensionhooks.ipf + :caption: example5-extensionhooks.ipf + :tab-width: 4 + :name: example5-code-1 + +.. literalinclude:: ../../examples/example5-extensionhooks-otherSuite.ipf + :caption: example5-extensionhooks-otherSuite.ipf + :tab-width: 4 + :name: example5-code-2 + +.. code-block:: igor + :caption: command + + RunTest("example5-extensionhooks.ipf;example5-extensionhooks-otherSuite.ipf") + +Each hook will output a message starting with :code:`>>`. After the Test Run +has finished you can see at which points the additional user code was executed. + +.. note:: + + Also take a look at the :ref:`TestHooks` section. + + The definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`CHECK_EQUAL_VAR` + * :cpp:func:`CHECK_CLOSE_VAR` + +.. _example6: + +Example6 +-------- + +This test suite shows the automatic execution of test runs from the command line. +On Windows, call the "autorun-test-xxx.bat" from the helper folder. + +The autorun batch script executes test runs for all pxp experiment files in the +current folder. After the run, a log file is created in the folder. The log +file includes the history of the Igor Pro Experiment. See also the section +on :ref:`automate`. + +.. literalinclude:: ../../examples/Example6/example6-automatic-invocation.ipf + :caption: example6-automatic-invocation.ipf + :tab-width: 4 + :name: example6-code-1 + +.. literalinclude:: ../../examples/Example6/example6-runner.ipf + :caption: example6-runner.ipf + :tab-width: 4 + :name: example6-code-2 + +In this example, the automatic invocation of the Igor Pro Universal Testing +Framework is also producing :ref:`JUNITOutput`. This allows the framework to be +used in automated CI/CD Pipelines. + +.. note:: + + The definition for the :doc:`assertion ` in this test suite: + + * :cpp:func:`CHECK_EQUAL_VAR` + +.. _example7: + +Example7 +-------- + +This test suite is showing how unhandled aborts in the test cases are displayed. + +The Test environment catches such conditions and treats them accordingly. This +works with :code:`Abort`, :code:`AbortOnValue` and :code:`AbortOnRTE` (see +:ref:`example8`). + +.. literalinclude:: ../../examples/example7-uncaught-aborts.ipf + :caption: example7-uncaught-aborts.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + RunTest("example7-uncaught-aborts.ipf") + +.. note:: + + Relevant definitions for the :doc:`assertions` in this test suite: + + * :cpp:func:`PASS` + +.. _example8: + +Example8 +-------- + +This test suite shows the behaviour of the universal testing environment if user +code generates an uncaught Runtime Error (RTE). The test environment catches +this condition and gives a detailed error message in the history. The runtime +error is of course treated as an error. + +In this example, the highlighted lines generate such a RTE due to a +missing references. Be aware that for multiple runtime errors without +:code:`AbortOnRTE`, only the message of the first RTE gets displayed. To find +every RTE at its correct line you can open the debugger with: + +.. code-block:: igor + :caption: command + + RunTest(..., debugMode = IUTF_DEBUG_ON_ERROR) + +There might be situations when the user wants to check if certain functions or +statements return a runtime error and handle it. For this exists +:code:`CHECK_RTE`, :code:`CHECK_ANY_RTE` and :code:`CHECK_NO_RTE` that can help +in this situation. These assertions check the current RTE state and create an +error if the current state is unexpected. They will also clear any pending RTE +so its safe to continue execution. + +These assertions are shown in the second function. This function also includes +an example how the user can check for RTEs and aborts at the same time. + +When using :code:`CHECK_RTE`, :code:`CHECK_ANY_RTE` or :code:`CHECK_NO_RTE` the +user has to keep in mind that any :code:`INFO` has to be called before the +critical statement as :code:`INFO` does nothing when a pending RTE exists to +keep the error state unchanged. + +.. literalinclude:: ../../examples/example8-uncaught-runtime-errors.ipf + :caption: example8-uncaught-runtime-errors + :tab-width: 4 + :linenos: + :emphasize-lines: 10,14,21,25,34 + +.. code-block:: igor + :caption: command + + RunTest("example8-uncaught-runtime-errors.ipf") + +.. note:: + + Relevant definitions for the :doc:`assertions` in this test suite: + + * :cpp:func:`PASS` + * :cpp:func:`FAIL` + +.. _example9: + +Example9 +-------- + +This examples shows how the whole framework can be run in an independent +module. + +Please note that when calling the test suite, the procedure window name does +*not* need to include any independent module specification. + +.. literalinclude:: ../../examples/example9-IM.ipf + :caption: example9-IM.ipf + :tab-width: 4 + :emphasize-lines: 3 + +.. code-block:: igor + :caption: command + + Example9#RunTest("example9-IM.ipf") + +.. note:: + + Definition for the :doc:`assertion ` in this test suite: + + * :cpp:func:`CHECK_EQUAL_VAR` + +.. _example10: + +Example10 +--------- + +This example tests the functionality of a peak find library found `on +github `__. It +demonstrates that by defining a unit test, we can rely on the functionality of +an external library. Even though we can not see the code itself from this unit, +we can test it and see if it fits our needs. Keep in mind that a program is +only as good as the unit test the define it. + +.. literalinclude:: ../../examples/example10-peakfind.ipf + :caption: example10-peakfind.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + RunTest("example10-peakfind.ipf") + +.. note:: + + Definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`CHECK_WAVE` + * :cpp:func:`CHECK_EQUAL_VAR` + * :cpp:func:`CHECK_CLOSE_VAR` + +.. _example11: + +Example11 +--------- + +This example demonstrates the usage of the igortest background +monitor. It contains a single test case that registers a user task to be +monitored. After the initial test case procedure finishes the universal testing +framework drops to Igors command line. After the user task finishes the +universal testing framework resumes the test case in the given `_REENTRY` +function. To emphasize that this feature can be chained the first `_REENTRY` +function registers the same user task again with another `_REENTRY` function to +resume. + +.. literalinclude:: ../../examples/example11-background.ipf + :caption: example11-background.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + RunTest("example11-background.ipf") + +.. note:: + + Definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`WARN_EQUAL_VAR` + +.. _example12: + +Example12 +--------- + +This example demonstrates the usage of the igortest background +monitor from a :cpp:func:`TEST_CASE_BEGIN_OVERRIDE` hook, see :ref:`TestHooks`. +The background monitor registration can be called from any begin hook. + +.. literalinclude:: ../../examples/example12-background-using-hooks.ipf + :caption: example12-background-using-hooks.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + RunTest("example12-background-using-hooks.ipf") + +.. note:: + + Definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`WARN_EQUAL_STR` + +.. _example13: + +Example13 +--------- + +This example shows how test cases are used with data generators. It includes +test cases that take one argument that is provided by a data generator function. +The data generator function returns a wave of that argument type and the test +case is called for each element of that wave. + +.. literalinclude:: ../../examples/example13-multi-test-data.ipf + :caption: example13-multi-test-data.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + RunTest("example13-multi-test-data.ipf") + +.. note:: + + Definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`CHECK` + +.. _example14: + +Example14 +--------- + +This example shows how to attach information to the next called assertion. If +this assertion fails the information is printed to the output to provide more +context to the assertion. + +.. literalinclude:: ../../examples/example14-info.ipf + :caption: example14-info.ipf + :tab-width: 4 + +.. code-block:: igor + :caption: command + + RunTest("example14-info.ipf") + +.. note:: + + Definition for the :doc:`assertions` in this test suite: + + * :cpp:func:`INFO` diff --git a/.github/skills/igortest/flags.rst b/.github/skills/igortest/flags.rst new file mode 100644 index 0000000000..0debfc517d --- /dev/null +++ b/.github/skills/igortest/flags.rst @@ -0,0 +1,54 @@ +.. vim: set et sts=3 sw=3 tw=79: + +.. _flags: + +Logical Flags +------------- + +The following flags are binary set. One or more of them can apply at the same +time. + +.. _flags_equalwave: + +Equal Wave Flags +^^^^^^^^^^^^^^^^ + +These flags are used in :cpp:func:`CHECK_EQUAL_WAVES` + +.. doxygengroup:: EqualWaveFlags + :content-only: + +.. _flags_testwave: + +Test Wave Flags +^^^^^^^^^^^^^^^ + +The following flags are used in :cpp:func:`CHECK_WAVE`. Note that there is a +minor and a major wave type. + +.. _flags_testwave_major: + +MajorType +""""""""" + +.. doxygengroup:: TestWaveFlagsMajor + :content-only: + +.. _flags_testwave_minor: + +MinorType +""""""""" + +.. doxygengroup:: TestWaveFlagsMinor + :content-only: + +.. _flags_IUTFBackgroundMonModes: + +Background Monitor Modes +^^^^^^^^^^^^^^^^^^^^^^^^ + +The following constants are used with :cpp:func:`RegisterIUTFMonitor`. They define +the condition how multiple user tasks states are evaluated. + +.. doxygengroup:: IUTFBackgroundMonModes + :content-only: diff --git a/.github/skills/igortest/guided-tour.rst b/.github/skills/igortest/guided-tour.rst new file mode 100644 index 0000000000..9fa9c7ae36 --- /dev/null +++ b/.github/skills/igortest/guided-tour.rst @@ -0,0 +1,181 @@ +.. vim: set et sts=3 sw=3 tw=79: + +.. _tour: + +Guided Tour +=========== + +To visualize the functionality of the Igor Pro Universal Testing Framework, we +will start with a guided tour in which we create our first unit and test it with +the Igor Pro Universal Testing Framework. The tour will cover the following +steps: + +* :ref:`tour_create` +* :ref:`tour_test` +* :ref:`tour_execute` +* :ref:`tour_extend` + +Please make sure that the framework has been properly installed if you wish to +follow the guide. For the framework to work, the files from the `procedures folder +`__ +should be placed into the `User Procedures` Folder of your Igor Pro setup. + +.. _tour_create: + +Creating a unit +--------------- + +We will start by creating a simple unit. + +The following formula gives the diameter :math:`d` of a carbon nanotube: + +.. math:: + + d = \frac{a_0}{\pi}\cdot\sqrt{n^2+m^2+nm} + +The natural numbers :math:`n` and :math:`m` define the carbon nanotube type. +:math:`a_0` is the unit cell lattice constant of graphene (Understanding the +background of the above formula is not required here). + +The formula is easily translated into Igor Pro code: + +.. code-block:: igorpro + :linenos: + :caption: Procedure + + #pragma TextEncoding = "UTF-8" + #pragma rtGlobals=3 + + // calculate carbon nanotube diameters + Function diameter(n, m) + Variable n, m + + return 0.144 / 3.1415 * (3 * (n^2 + n*m + m^2))^(0.5) + End + +.. _tour_test: + +Testing the unit +---------------- + +If we want to rely on this formula with other calculations, we have to test if +the output of this function is both correct and within our required accuracy +range. To perform these two tests, we define a :ref:`TestCase`. + +.. code-block:: igorpro + :linenos: + :caption: test0 + + #pragma TextEncoding = "UTF-8" + #pragma rtGlobals=3 + + #include "igortest" + + Function testDiameter() + // the (6,5) type is 0.757nm in diameter + REQUIRE_CLOSE_VAR(diameter(6, 5), 0.757, tol=1e-3) + // this is the same value as for the (9,1) type. + REQUIRE_EQUAL_VAR(diameter(6, 5), diameter(9, 1)) + End + +The test case :code:`testDiameter` contains two checks. Both are required to +pass the test suite. In the context of this framework we will refer to them as +:ref:`assertions `. The first assertion +:code:`REQUIRE_CLOSE_VAR` compares the two floating point numbers within the +given tolerance of 0.001nm. The second :code:`REQUIRE_EQUAL_VAR` uses a +mathematical peculiarity of the above formula to check if the calculation gives +correct output. + +The test case function can be placed anywhere inside the main procedure file, +but it can be considered good practice to separate test cases into a procedure +file of their own. Such a separate procedure file that only contains test cases +is called a :ref:`TestSuite`. A test suite can for example perform all the necessary +tests for a unit. + +.. _tour_execute: + +Executing the test +------------------ + +To execute the test suite we use the :cpp:func:`RunTest` directive. It accepts +the name of our test suite (the procedure window) as an argument. In our +example we have named the procedure window :code:`"test0"`. + +.. code-block:: console + :emphasize-lines: 8 + + •RunTest("test0") + Start of test "Unnamed" + Entering test suite "Unnamed" + Entering test case "testDiameter" + Leaving test case "testDiameter" + Finished with no errors + Leaving test suite "test0" + Test finished with no errors + End of test "Unnamed" + +In the console output above, the highlighted line indicates that all tests +within the current test suite have passed successfully. The unit is working +properly. The full Igor Pro environment with our unit test should look like +this: + +.. image:: _static/introduction-demo.png + +.. _tour_extend: + +Extending the test +------------------ + +Note, that we have defined a test case for the current capabilities of our +function :code:`diameter()`. The calculation is only exact up to the specified +error range. The high error is caused by a fixated value of +:code:`pi=3.1415`. To emphasize this, we can add an assertion to the test case +that will fail but will not affect the error counter. Such an assertion is done +with a `WARN_*` directive. Every `REQUIRE_*` assertion also has a +`WARN_*` variant, see:ref:`AssertionTypes` for a summary. + +.. code-block:: igorpro + :emphasize-lines: 6,7 + + Function testDiameter() + // the (6,5) type is 0.757nm in diameter + REQUIRE_CLOSE_VAR(diameter(6, 5), 0.757, tol=1e-3) + // this is the same value as for the (9,1) type. + REQUIRE_EQUAL_VAR(diameter(6, 5), diameter(9, 1)) + // warn if accuracy is not exact + WARN_CLOSE_VAR(diameter(6, 5), 0.7573453, tol=1e-7) + End + +The output of :cpp:func:`RunTest` will now include a warning assertion without +failing the test case: + +.. code-block:: console + :emphasize-lines: 6,7,9 + + •RunTest("test0") + Start of test "Unnamed" + Entering test suite "Unnamed" + Entering test case "testDiameter" + Entering test case "testDiameter" + 0.757368 ~ 0.757345 with strong check and tol 1e-07: is false + Assertion "WARN_CLOSE_VAR(diameter(6, 5), 0.7573453, tol=1e-7)" failed in line 11, procedure "test0" + Leaving test case "testDiameter" + Finished with no errors + Leaving test suite "test0" + Test finished with no errors + End of test "Unnamed" + +If the program should be extended to a higher level of accuracy, this warning +can be set to the corresponding :cpp:func:`REQUIRE` +assertion. The program :code:`diameter` then has to be changed to reflect the +new requirement. In the current example, :math:`pi` would need to be used +instead of only a handful of decimal places hardcoded. + +In a test-driven workflow, the unit tests get extended before even changing +anything at the code base. Defining the test case prior to any code production +assures that the software development is not producing unnecessary (and +untested) code. + +A more elaborate example for defining a peak find functionality can be found in +the :ref:`examples section `. For a quick start, also have a look at +the :ref:`first example`. diff --git a/.github/skills/igortest/introduction.rst b/.github/skills/igortest/introduction.rst new file mode 100644 index 0000000000..7a8d065484 --- /dev/null +++ b/.github/skills/igortest/introduction.rst @@ -0,0 +1,101 @@ +.. vim: set et sts=3 sw=3 tw=79: + +.. _introduction: + +What is a Universal Testing Framework? +====================================== + +The purpose of every program is to ensure that a specific task is performed +reliably in a defined matter. Therefore, programming is all about testing and +quality control of the produced source code. These two workflow tasks are +entirely optional but are especially important when it comes to hazard and +risk-sensitive tasks, as well as security-relevant features of software with +critical to catastrophic consequences. More generally speaking, it contributes +to a clean, professional look and better working experience if software works +in a defined way and unit tests help to define this way. + +Testing +------- + +A program gets tested in various ways during development: A first test usually +involves the syntactic correctness and the correct usage of external libraries. +It ensures that the program compiles and that it produces output for a given +task. Complex scenarios typically afford a much larger codebase and a more +profound investigation of the involved interfaces. The more complex the +scenarios a program can handle, the more time is involved in its production. +Therefore it is crucial to define the program's interface to indicate what it +is capable of, and what not, to prevent it getting used in the wrong context. + +One standard in quality control is the four-eyes-check by two persons. Writing +professional code in a lean and agile, continuous delivery software +environment, usually involves this additional peer review step. The review step +is an attempt to separate code production, and testing to separate persons as +the perception of the tester adds valuable input to the code leading to quicker +deployment of quality software. + +A review typically involves testing the functionality of the code output for +different inputs. These tests are equally performed during code production and +review stages. The problem, this review step is targeting onto, is that a +programmer typically does not think of all critical test situations. The +tester, in turn, does not know about the code and its context and therefore the +reviewer needs time to understand the context of the program. In an attempt to +save valuable time, review and code production have to be based on a definition +for the produced functionality which can be for example the creation of a valid +file format. Such a definition allows the tester to perform tests without +necessarily needing to hack into the code base. Defining these tests somewhere +records the current functionality of the program and protects it against +changes. + +Even though, the review process guarantees a higher level of quality, the +additional assessment requires an assignment of double the developing resources +and those resources are usually considered precious. In this context, automated +test environments minimize production time and ensure a consistent level of +quality. This level of quality can then consistently get maintained over time +when further changes are introduced to the unit. + +Unit Tests +---------- + +To be able to perform automated tests, the code is typically organized in +functional units. A unit is a part of software inside a project that performs a +particular task. Typically this unit is isolated and runs on a linearly +independent path inside the code. The unit communicates via an interface which +accepts inputs and produces outputs. + +.. code:: + + ######## + input --> # unit # --> output + ######## + +In the most simple case, a unit is a function. The parameters which get passed +to the function define the input interface, and the return value is the output +interface. In a more complex scenario, such a unit could be responsible for +converting one file to another format. + +A unit can be checked for valid output by defining a :ref:`suite of tests +`. The test suite is further grouped into atomically small tests +which are called :ref:`Test Cases `. A test case typically checks +that an entity fulfills specific properties and a unit produces valid output +for a given input. Within these checks, the result of defined inputs is +compared against defined outputs. The comparisons are performed using different +types of :ref:`Assertions `. As long as all test cases inside a +test suite are executed correctly, the tested functionality of the unit is maintained. +Performing these checks on a regular basis also ensures that a consistent level +of quality and a defined functionality is maintained upon changes to the code. + +Agile Development +----------------- + +When using version control systems like `git `_, the +introduced changes are typically tested with test pipelines before applying +the changes by using apps like `jenkins `_ or `gitlab +`_. These automated tests introduce a step prior +to the review process which makes the review more clear and transparent and +allow a quicker code review. `This Framework +`_ enables unit tests for +continuous integration and continuous delivery environments in `Igor Pro +`_. Do not hesitate to `contact us +`_ if you need further assistance +in creating a professional CI/CD workflow for your Igor Pro project to ensure a +higher level of quality in your code. diff --git a/.github/skills/sweepformula/SKILL.md b/.github/skills/sweepformula/SKILL.md new file mode 100644 index 0000000000..52e7d3729a --- /dev/null +++ b/.github/skills/sweepformula/SKILL.md @@ -0,0 +1,180 @@ +--- +name: sweepformula +paths: + - "**/MIES_SweepFormula*.ipf" + - "**/UTF_SweepFormula*.ipf" +description: Architectural facts about MIES's SweepFormula subsystem (dataset wrapping, array-literal evaluation, plotter targeting, nested-execution source-location tracking) that are not obvious from reading a single operation in isolation. Use before adding or modifying a SweepFormula operation, touching the executor/array-evaluation code, or debugging an error-location/assert-data-stack problem. +--- + +# SweepFormula — Architectural Reference + +SweepFormula (`MIES_SweepFormula*.ipf`) is MIES's scripting language for data +evaluation. This document covers structural conventions that span multiple +files and are easy to violate by only looking at one operation's code. Also +read `Packages/doc/SweepFormula.rst` (the docu skill points you there for any +SweepFormula documentation question) and `.claude/skills/igor-wave-dfref` for +general WAVE/DFREF semantics. + +--- + +## Dataset Wrapping Convention + +A SweepFormula operation result is a "dataset": a `WAVE/WAVE` container +(typically built via `SFH_CreateSFRefWave(win, opShort, size)`). `size` is +**not** always 1 — most operations size it to match their input (e.g. +`SFH_CreateSFRefWave(exd.graph, opShort, DimSize(input, ROWS))`, one output +row per input row), and only some operations route through the +single-element convenience wrapper `SFH_GetOutputForExecutorSingle` (which +hardcodes `size=1`). + +The `SF_META_DATATYPE` JSON wave note (set/read via `JWN_SetStringInWaveNote`/ +`JWN_GetStringFromWaveNote`) is **optional per-operation metadata**, not a +universal property of every dataset — it identifies specific semantic kinds +(e.g. `SF_DATATYPE_SELECTCOMP`, `SF_DATATYPE_SELECTTAG`) and is set only when +an operation explicitly asks for it: + +- `SFH_GetOutputForExecutorSingle(data, ..., dataType=X)` only sets the note + when the optional `dataType` argument is actually supplied — most calls in + `MIES_SweepFormula_Operations_Select.ipf` do; plenty of other call sites + across the codebase omit `dataType` entirely and get no note at all. When + it does set the note, it wraps `data` in a **new** wrapper wave and tags + that wrapper — it never tags `data` itself. +- `select()` is a deliberate counter-example: it builds its own composite + wrapper directly, sets the note on it, and returns via + `SFH_GetOutputForExecutor` — skipping `SFH_GetOutputForExecutorSingle` + entirely. Don't assume every operation goes through the single-wrap helper. +- `seltag` needs **two** levels of wrapping so the array-literal executor + doesn't misinterpret a multi-tag `seltag([a,b])` result as a plain text + wave and array-expand its elements. The datatype note must be set on the + **inner** wrapper (which becomes `genericElement[0]` inside an array + literal), not just the outer one. + +## Array Literals Mixing Scalars and Datasets + +For an array literal (`[a, b, c]`) that may mix scalar/text elements with +dataset (`WAVE/WAVE`) elements, the executor: + +1. Prescans every element **exactly once** via `SF_ResolveDatasetFromJSON` + (never resolve the same element twice — resolution can execute operations + with side effects) to determine if any element is dataset-kind. +2. If any element is a dataset, the **whole array** is promoted to a uniform + wave-of-datasets accumulator (`outW`), with plain scalar/text elements + individually wrapped via `Make/FREE/WAVE promoted = {subArray}` + (`MIES_SweepFormula_Executor.ipf`) into their own single-element + wave-of-waves wrapper — an ad hoc free wave, not a named/tagged dataset + kind (no `SF_META_DATATYPE` note is set on it). +3. A dataset's own internal dimensionality must never leak into the outer + array's shape — guard any dimension-widening logic with + `if(!WaveExists(outW))` so the dataset accumulator stays strictly 1D + regardless of what's inside each element. + +`SFH_GetArgumentSelect` correspondingly checks `IsWaveRefWave(array)` rather +than `IsTextWave(array)`, since array elements in this path are direct wave +references, not stringified markers. + +## Plotter Targeting Is Entirely Outside the JSON Executor + +`and`/`with` are plotter-targeting keywords, not executor syntax: + +- A SweepFormula expression cannot contain line breaks, and `and`/`with` must + each stand alone on their own line — so they can never appear inside an + expression actually parsed by `SFE_ExecuteFormula`/ + `SFE_ExecuteVariableAssignments`. They are recognized in an earlier, + separate notebook-text-splitting step, before the executor ever sees the + expression text. +- They only control **where the plotter places each expression's result**: + `with` = same sub-window as the previous expression, `and` = a new + sub-window. There is no way to feed `and`/`with` through the executor, even + via a nested/dynamically-generated formula string. +- SweepFormula renders into a separate, dedicated plotter panel — **never** + the host DataBrowser/SweepBrowser's own graph. The window name is + deterministic: `SF_GetDataDisplayWindowName(graph, SF_DISPLAYTYPE_GRAPH, + SF_DM_SUBWINDOWS, 0)` (static, module `MIES_SF` — needs `MIES_SF#` + qualification from outside) returns the fully-qualified subwindow name, + e.g. `"SweepFormula_plotsweepBrowser_graph#graph0"` for host graph + `"SweepBrowser"`. Each SweepBrowser/DataBrowser gets its own independently + named plot window keyed off its own `graph` argument (MIES supports + multiple simultaneous SweepBrowsers). `SF_DM_NORMAL` gives a wrong/ + non-existent name for this case, and the `SF_DM_SUBWINDOWS` result already + includes the `#graph0` suffix — don't append it again. + +## Composing New Operations Out of Existing Ones + +An operation can implement itself by re-entering the real formula executor +with dynamically-built source text, reusing other real operations as +building blocks, via this pattern. This is a documented extension pattern +(`Packages/doc/SweepFormula.rst`, "full plotting specification" section) -- +currently exercised by `UTF_SweepFormula.ipf`'s tests, not by a named +production operation, so treat it as the supported way to build a new +operation this way rather than a description of an existing one: + +1. `Duplicate/FREE` the per-graph `GetSFVarStorage(graph)` (a `WAVE/WAVE` + keyed by variable name) as a backup. +2. Build an ordinary SweepFormula source string on the fly and run it via + `SFE_ExecuteVariableAssignments(graph, formula, allowEmptyCode=1)`, which + mutates the **live** `varStorage` in place. +3. Read back whatever result is needed by name. +4. `Duplicate/O backup, varStorage` to wipe all scratch variables, then + re-add only the specific desired outputs via `SFH_AddVariableToStorage` — + this keeps the operation's own temporary variable names from leaking into + the user's persistent SweepFormula environment. + +Exception safety is a non-issue here: if the nested call aborts, the restore +step (4) is simply skipped, but that's fine — a failed evaluation just means +"no result" (SweepFormula never updates an already-displayed plot in place +on failure), and the next run's `SFE_ExecuteVariableAssignments` unconditionally +wipes `varStorage` back to 0 rows regardless of what a prior aborted run left +behind. + +## Nested-Execution Source-Location Tracking Is a LIFO Stack + +Source-location tracking for (possibly nested) formula execution uses a +stack, not a single flat frame: + +- `GetSFAssertDataStack()` (`MIES_WaveDataFolderGetters.ipf`, `WAVE/WAVE`, + lazily created) holds the stack. `GetSFAssertData()` returns the top frame, + auto-pushing a base frame if the stack is empty. +- `SFH_PushAssertDataFrame()`/`SFH_PopAssertDataFrame()` + (`MIES_SweepFormula_Helpers.ipf`) manage nested execution frames — + centralized via a `newFrame` flag parameter on + `SFE_ExecuteVariableAssignments`/`SFE_ExecuteFormula`, rather than each + caller manually bracketing its own call. +- On abort, the pop is deliberately **skipped** so the frame's data survives + for the aggregate error message. `SFH_PopAssertDataFrame` asserts against + popping the base frame, and deliberately does **not** release JSON ids + itself — a normal return already released them via the ordinary success + path, so releasing again would double-release. +- The live global position trackers (`GetSweepFormulaJSONPathTracker()`/ + `GetSweepFormulaBufferOffsetTracker()`) only ever reflect whatever is + executing *right now* — so each frame's rendered location message is + frozen into a `LOCMSG` field at the moment a deeper frame is pushed on top + of it, while the live trackers still reflect that outer frame's own + position at that time. +- `SFH_GetAssertLocationMessage` walks the stack top-to-bottom, joining more + than one non-empty frame's message with `"\rCalled from:"`. +- Only the **outermost** frame (`SFH_GetOutermostAssertDataFrame()`, i.e. + `stack[0]`) has `LINE`/`OFFSET` that are real, on-screen notebook + positions — `SF_CalculateErrorLocationInNotebook` must read that specific + frame, not whatever is currently on top of the stack. +- `SFH_ResetAssertDataStack()` (called from `SF_ClearSFOutputState()`) + releases every remaining frame's `JSONID`/`SRCLOCID` via + `JSON_Release(..., ignoreErr=1)` then empties the stack — necessary + because stale frames from an aborted run would otherwise corrupt the + *next* run's error-location tracking. **Any test that deliberately aborts + a nested formula must call `SFH_ResetAssertDataStack()` itself afterward** + for the same reason. + +--- + +## Reference + +- `Packages/doc/SweepFormula.rst` — user-facing behavior and array/operation + evaluation semantics (e.g. empty-array and mixed numeric/text handling). +- `Packages/MIES/MIES_SweepFormula_Executor.ipf` — `SFE_FormulaExecutor` + (array/object/string dispatch), `SFE_ConvertNonFiniteElements`. +- `Packages/MIES/MIES_SweepFormula_Helpers.ipf` — dataset resolution + (`SF_ResolveDatasetFromJSON`, `SFH_ResolveDatasetElementFromJSON`), + assert-data-stack management. +- `Packages/MIES/MIES_SweepFormula_Operations.ipf` — individual operation + implementations; the primitive `+ - * /` operators live here + (`SFO_IndexOverDataSetsForPrimitiveOperation`).