# F-01 — variableValidator.js: Three instances of AST compensation via regex

**Severity:** Major  
**File:** `core/src/validators/variableValidator.js`  
**Category:** Validator compensating for incomplete AST use  
**Remediation appropriate:** Yes — Tier 1 and Tier 2

---

## Context

`variableValidator.js` validates variable declarations, assignments, and references. It uses
the old string-level `parser.js` functions (`parseAssignment`, `extractVariables`, `stripQuotedStrings`)
rather than the AST, even though the AST now provides typed statement nodes for all constructs
the validator cares about.

---

## Instance 1 — `DO_ITERATOR_RE` compensates for AST `DoStatement.iterator`

**Lines:** 31, 245–252

```javascript
// Line 31 — module-level regex:
const DO_ITERATOR_RE = /^\s*DO\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/i;

// Lines 245–252 — usage:
const doMatch = lineText.match(DO_ITERATOR_RE);
if (doMatch) {
    const iterVar = doMatch[1];
    if (isVariableInScope(iterVar, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
        initializedVariables.add(getScopedInitKey(iterVar, lineIndex, functionScopes));
    }
}
```

**What this does:** Detects `DO varName = ...` iterator syntax to record that `varName` is
implicitly initialised by the loop.

**What the AST already provides:** The AST parser produces `DoStatement` nodes with
`kind: "iterator"` and `iterator: { variable: string, start: TokenSpan, to: TokenSpan|null, by: TokenSpan|null }`.
The variable name is `stmt.iterator.variable` — no regex required.

**The gap:** `documentFacts.js` builds `functionScopes` from the AST but does not expose the
iterator variable per line. The fix requires `documentFacts` to either:
(a) Include DO-iterator variables in the `locals` map of the enclosing scope (since an iterator
   variable acts as a local for the loop body), or
(b) Expose a per-line initialised-variable set derived from the AST walk.

**Recommended fix:**
In `documentFacts.js::collectLocals()`, extend the walk to detect `DoStatement` nodes with
`kind: "iterator"` and record `stmt.iterator.variable` as an initialised variable (not in
`locals` — it is not *declared*, only *implicitly initialised*). Expose this as
`doIteratorsByLine: Map<lineIndex, string>` in facts. The validator then does:
```javascript
const iterVar = facts.doIteratorsByLine?.get(lineIndex);
if (iterVar && isVariableInScope(iterVar, ...)) {
    initializedVariables.add(getScopedInitKey(iterVar, ...));
}
```
No regex, no raw line text access.

---

## Instance 2 — `parseAssignment()` compensates for AST `SetStatement`/`AssignStatement`

**Lines:** 242–243 (and implicitly throughout the assignment-scanning loop)

```javascript
const assignment = parseAssignment(lineText);
if (!assignment) { ... }
```

`parseAssignment` (from `parser.js`) uses:
```javascript
const match = text.match(/^\s*#?\s*(?:SET\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/i);
```

**What the AST already provides:**
- `SetStatement`: `{ type: "SetStatement", target: string, value: TokenSpan, line: number }`
- `AssignStatement`: `{ type: "AssignStatement", target: string, value: TokenSpan, line: number }`

Both give the assignment target (string) and value (TokenSpan) directly from AST traversal.

**Why this is a problem:** The validator currently iterates line-by-line using `document.lineAt(lineIndex).text`,
then calls `parseAssignment()` on the raw text. This is a full secondary parse of what the AST
already computed. The regex is also fragile: it can match inside output lines or comment-adjacent text
if line classification is not perfect.

**Recommended fix (Tier 2):**
Have `documentFacts.js` expose assignment nodes keyed by line number:
```javascript
facts.assignmentsByLine: Map<number, {target: string, valueSpan: TokenSpan}>
```
Built from a walk of all `SetStatement` and `AssignStatement` nodes at any depth in the AST.
The validator then: `const assignment = facts.assignmentsByLine?.get(lineIndex);`

This is a larger change because it requires threading the assignment map through `collectDiagnostics.js`
into `validateVariableUsage`. Acceptable as a Tier 2 item.

---

## Instance 3 — `getParamModeVariables()` regex on expression text

**Lines:** 158–180

```javascript
function getParamModeVariables(expression) {
    const callMatch = expression.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
    if (!callMatch) return new Set();
    const entry = BuiltInFunctions[callMatch[1].toUpperCase()];
    if (!entry || !entry.paramModes) return new Set();
    // ...
    const argText = extractArgAt(expression, i);
    // ...
    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
        outVars.add(trimmed);
    }
}
```

This uses regex on the raw expression string to:
1. Identify the function name (first identifier before `(`)
2. Validate that each `VAR_init_not_required` argument is a plain identifier

**What the expression parser provides:**
`parseExpression(span)` would return a `Call` node:
```javascript
{ type: "Call", callee: Identifier("functionName"), args: [ExprNode, ...], ... }
```
The function name is `callNode.callee.name`. Each argument is an `ExprNode`. A plain variable
reference is an `Identifier` node — no regex needed to confirm it's a simple name.

**Recommended fix (Tier 1):**
Import `parseExpression` from `expressionParser.js`. Pass the assignment's `value` TokenSpan:
```javascript
const expr = parseExpression(assignment.value);
if (expr?.type === ExprNodeType.Call) {
    const funcName = expr.callee.name;
    const entry = BuiltInFunctions[funcName.toUpperCase()];
    // check paramModes, then check expr.args[i].type === ExprNodeType.Identifier
}
```
This eliminates all three regex patterns in `getParamModeVariables` and gives exact positions
for argument nodes if they are ever needed for diagnostics.

---

## Summary

| Instance | Regex / String-level | AST/Expression equivalent | Tier |
|---|---|---|---|
| DO_ITERATOR_RE | `lineText.match(/^\s*DO\s+.../i)` | `DoStatement.kind === "iterator"`, `.iterator.variable` | 1 (via documentFacts extension) |
| parseAssignment | `lineText.match(/...SET...=.../i)` | `SetStatement.target` / `AssignStatement.target` | 2 |
| getParamModeVariables | `expression.match(/...\(/)` + `extractArgAt` | `parseExpression(span).type === Call` | 1 |
