Skip to content

Commit 4a6ced5

Browse files
icecrasher321claude
andcommitted
fix(executor): take the preceding token from the scan, not from a walk back
Reading backwards cannot tell which characters were code: a block comment opens at its first delimiter, so `p./* a /* b */catch(fn)` defeated a search for the nearest `/*` and the call read as a control-flow head again. The scan already knows — it stepped over that comment on the way in — so the two facts the check needs, the token before the parenthesis and whether it followed a property access, are now recorded as it passes and read from there. No search back through the source, and nothing left for a comment body to imitate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 39bb39d commit 4a6ced5

2 files changed

Lines changed: 55 additions & 37 deletions

File tree

apps/sim/executor/variables/resolver.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,9 @@ describe('VariableResolver function block inputs', () => {
984984
code: [
985985
`/* lead */ if (params.a) /['"]/.test('<producer.result>')`,
986986
`const n = params.p./* mid */catch(() => 0) / 2 + Number('<producer.result>')`,
987+
// A comment body may contain another opening delimiter; the comment still ends at
988+
// the first `*/`, which only the scan that passed through it knows.
989+
`const m = params.q./* a /* b */catch(() => 0) / 2 + Number('<producer.result>')`,
987990
].join('\n'),
988991
},
989992
block
@@ -994,6 +997,7 @@ describe('VariableResolver function block inputs', () => {
994997
const code = result.resolvedInputs.code as string
995998
expect(code).toContain(`.test('' + JSON.stringify(globalThis["__blockRef_0"]) + '')`)
996999
expect(code).toContain(`Number('' + JSON.stringify(globalThis["__blockRef_1"]) + '')`)
1000+
expect(code).toContain(`Number('' + JSON.stringify(globalThis["__blockRef_2"]) + '')`)
9971001
})
9981002

9991003
it('does not read a method named after a keyword as a control-flow head', async () => {

apps/sim/executor/variables/resolver.ts

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,47 +1204,34 @@ export class VariableResolver {
12041204
}
12051205

12061206
/**
1207-
* Whether the `(` at this index opens a control-flow head rather than a value.
1207+
* Whether a `(` opens a control-flow head rather than a value.
12081208
*
12091209
* What follows the matching `)` differs entirely between the two — a statement, where a regex
12101210
* literal may begin, versus an operator, where a `/` divides — and the closing parenthesis
1211-
* carries no trace of which it was. Reading the keyword in front of the opening one is what
1212-
* lets `if (a) /re/.test(b)` and `(a + b) / 2` both scan correctly.
1211+
* carries no trace of which it was. The keyword in front of the opening one is what tells
1212+
* them apart, so `if (a) /re/.test(b)` and `(a + b) / 2` both scan correctly.
1213+
*
1214+
* Both inputs come from the forward scan rather than a walk back through the source: the
1215+
* scan already knows which characters were code and which sat inside a comment, and reading
1216+
* backwards cannot recover that — the opener of `/* a /* b *\/` is its first delimiter, not
1217+
* its last, and only the scan that passed through knows the difference.
12131218
*/
1214-
private opensControlFlowHead(template: string, index: number): boolean {
1215-
let end = index
1216-
while (end > 0 && WHITESPACE_CHAR.test(template[end - 1])) {
1217-
end--
1218-
}
1219-
let start = end
1220-
while (start > 0 && this.isJavaScriptIdentifierChar(template[start - 1])) {
1221-
start--
1222-
}
1223-
if (!CONTROL_FLOW_HEAD_KEYWORDS.has(template.slice(start, end))) {
1219+
private opensControlFlowHead(
1220+
template: string,
1221+
previousSignificantIndex: number,
1222+
precededByPropertyAccess: boolean
1223+
): boolean {
1224+
if (precededByPropertyAccess || previousSignificantIndex < 0) {
12241225
return false
12251226
}
1226-
1227-
// `p.catch(fn)` is a method call whose name happens to be a keyword, and what follows its
1228-
// `)` is an operator, not a statement. A control-flow head can never be a property access,
1229-
// and a comment can stand between the two (`p./* c */catch(fn)`), so a comment is stepped
1230-
// over rather than treated as an answer — `/* c */ if (x)` is still a head.
1231-
let before = this.skipWhitespaceBackward(template, start)
1232-
while (template[before - 1] === '/' && template[before - 2] === '*') {
1233-
const opening = template.lastIndexOf('/*', before - 2)
1234-
if (opening < 0) {
1235-
return true
1236-
}
1237-
before = this.skipWhitespaceBackward(template, opening)
1227+
if (!this.isJavaScriptIdentifierChar(template[previousSignificantIndex])) {
1228+
return false
12381229
}
1239-
return template[before - 1] !== '.'
1240-
}
1241-
1242-
private skipWhitespaceBackward(template: string, index: number): number {
1243-
let cursor = index
1244-
while (cursor > 0 && WHITESPACE_CHAR.test(template[cursor - 1])) {
1245-
cursor--
1230+
let start = previousSignificantIndex
1231+
while (start > 0 && this.isJavaScriptIdentifierChar(template[start - 1])) {
1232+
start--
12461233
}
1247-
return cursor
1234+
return CONTROL_FLOW_HEAD_KEYWORDS.has(template.slice(start, previousSignificantIndex + 1))
12481235
}
12491236

12501237
private matchesKeywordAt(template: string, index: number, keyword: string): boolean {
@@ -1376,6 +1363,7 @@ export class VariableResolver {
13761363
const modes: CodeScanMode[] = [{ type: 'normal' }]
13771364
let lastSignificantIndex = -1
13781365
const openParenIsControlHead: boolean[] = []
1366+
let identifierFollowsPropertyAccess = false
13791367
const controlHeadParenCloses = new Set<number>()
13801368
const regexCloseIndices = new Set<number>()
13811369

@@ -1483,8 +1471,21 @@ export class VariableResolver {
14831471
if (!WHITESPACE_CHAR.test(char)) {
14841472
lastSignificantIndex = i
14851473
}
1486-
if (char === '(') {
1487-
openParenIsControlHead.push(this.opensControlFlowHead(template, i))
1474+
if (this.isJavaScriptIdentifierChar(char)) {
1475+
if (
1476+
previousSignificantIndex < 0 ||
1477+
!this.isJavaScriptIdentifierChar(template[previousSignificantIndex])
1478+
) {
1479+
identifierFollowsPropertyAccess = template[previousSignificantIndex] === '.'
1480+
}
1481+
} else if (char === '(') {
1482+
openParenIsControlHead.push(
1483+
this.opensControlFlowHead(
1484+
template,
1485+
previousSignificantIndex,
1486+
identifierFollowsPropertyAccess
1487+
)
1488+
)
14881489
} else if (char === ')') {
14891490
if (openParenIsControlHead.pop()) controlHeadParenCloses.add(i)
14901491
}
@@ -1552,8 +1553,21 @@ export class VariableResolver {
15521553
if (!WHITESPACE_CHAR.test(char)) {
15531554
lastSignificantIndex = i
15541555
}
1555-
if (char === '(') {
1556-
openParenIsControlHead.push(this.opensControlFlowHead(template, i))
1556+
if (this.isJavaScriptIdentifierChar(char)) {
1557+
if (
1558+
previousSignificantIndex < 0 ||
1559+
!this.isJavaScriptIdentifierChar(template[previousSignificantIndex])
1560+
) {
1561+
identifierFollowsPropertyAccess = template[previousSignificantIndex] === '.'
1562+
}
1563+
} else if (char === '(') {
1564+
openParenIsControlHead.push(
1565+
this.opensControlFlowHead(
1566+
template,
1567+
previousSignificantIndex,
1568+
identifierFollowsPropertyAccess
1569+
)
1570+
)
15571571
} else if (char === ')') {
15581572
if (openParenIsControlHead.pop()) controlHeadParenCloses.add(i)
15591573
}

0 commit comments

Comments
 (0)