Skip to content

Commit c7bd485

Browse files
fix(codegen): function prologue resolution edge cases (#3005)
* fix(codegen): function prologue resolution edge cases * remove hacky fallback * case insensitive lookup * fix python nan and inf resolution * remove template literal check * fix tests * consolidate literal gen
1 parent 80f0047 commit c7bd485

File tree

9 files changed

+86
-63
lines changed

9 files changed

+86
-63
lines changed

apps/sim/app/api/function/execute/route.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
88
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
99
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
1010
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
11+
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
1112
import {
1213
createEnvVarPattern,
1314
createWorkflowVariablePattern,
@@ -387,7 +388,12 @@ function resolveWorkflowVariables(
387388
if (type === 'number') {
388389
variableValue = Number(variableValue)
389390
} else if (type === 'boolean') {
390-
variableValue = variableValue === 'true' || variableValue === true
391+
if (typeof variableValue === 'boolean') {
392+
// Already a boolean, keep as-is
393+
} else {
394+
const normalized = String(variableValue).toLowerCase().trim()
395+
variableValue = normalized === 'true'
396+
}
391397
} else if (type === 'json' && typeof variableValue === 'string') {
392398
try {
393399
variableValue = JSON.parse(variableValue)
@@ -687,11 +693,7 @@ export async function POST(req: NextRequest) {
687693
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
688694
prologueLineCount++
689695
for (const [k, v] of Object.entries(contextVariables)) {
690-
if (v === undefined) {
691-
prologue += `const ${k} = undefined;\n`
692-
} else {
693-
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
694-
}
696+
prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
695697
prologueLineCount++
696698
}
697699

@@ -762,11 +764,7 @@ export async function POST(req: NextRequest) {
762764
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
763765
prologueLineCount++
764766
for (const [k, v] of Object.entries(contextVariables)) {
765-
if (v === undefined) {
766-
prologue += `${k} = None\n`
767-
} else {
768-
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
769-
}
767+
prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
770768
prologueLineCount++
771769
}
772770
const wrapped = [
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Formats a JavaScript/TypeScript value as a code literal for the target language.
3+
* Handles special cases like null, undefined, booleans, and Python-specific number representations.
4+
*
5+
* @param value - The value to format
6+
* @param language - Target language ('javascript' or 'python')
7+
* @returns A string literal representation valid in the target language
8+
*
9+
* @example
10+
* formatLiteralForCode(null, 'python') // => 'None'
11+
* formatLiteralForCode(true, 'python') // => 'True'
12+
* formatLiteralForCode(NaN, 'python') // => "float('nan')"
13+
* formatLiteralForCode("hello", 'javascript') // => '"hello"'
14+
* formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
15+
*/
16+
export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
17+
const isPython = language === 'python'
18+
19+
if (value === undefined) {
20+
return isPython ? 'None' : 'undefined'
21+
}
22+
if (value === null) {
23+
return isPython ? 'None' : 'null'
24+
}
25+
if (typeof value === 'boolean') {
26+
return isPython ? (value ? 'True' : 'False') : String(value)
27+
}
28+
if (typeof value === 'number') {
29+
if (Number.isNaN(value)) {
30+
return isPython ? "float('nan')" : 'NaN'
31+
}
32+
if (value === Number.POSITIVE_INFINITY) {
33+
return isPython ? "float('inf')" : 'Infinity'
34+
}
35+
if (value === Number.NEGATIVE_INFINITY) {
36+
return isPython ? "float('-inf')" : '-Infinity'
37+
}
38+
return String(value)
39+
}
40+
if (typeof value === 'string') {
41+
return JSON.stringify(value)
42+
}
43+
// Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
44+
if (isPython) {
45+
return `json.loads(${JSON.stringify(JSON.stringify(value))})`
46+
}
47+
return JSON.stringify(value)
48+
}

apps/sim/executor/variables/resolver.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,14 @@ export class VariableResolver {
157157

158158
let replacementError: Error | null = null
159159

160-
// Use generic utility for smart variable reference replacement
160+
const blockType = block?.metadata?.id
161+
const language =
162+
blockType === BlockType.FUNCTION
163+
? ((block?.config?.params as Record<string, unknown> | undefined)?.language as
164+
| string
165+
| undefined)
166+
: undefined
167+
161168
let result = replaceValidReferences(template, (match) => {
162169
if (replacementError) return match
163170

@@ -167,14 +174,7 @@ export class VariableResolver {
167174
return match
168175
}
169176

170-
const blockType = block?.metadata?.id
171-
const isInTemplateLiteral =
172-
blockType === BlockType.FUNCTION &&
173-
template.includes('${') &&
174-
template.includes('}') &&
175-
template.includes('`')
176-
177-
return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
177+
return this.blockResolver.formatValueForBlock(resolved, blockType, language)
178178
} catch (error) {
179179
replacementError = error instanceof Error ? error : new Error(String(error))
180180
return match

apps/sim/executor/variables/resolvers/block.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -257,15 +257,9 @@ describe('BlockResolver', () => {
257257
expect(result).toBe('"hello"')
258258
})
259259

260-
it.concurrent('should format string for function block in template literal', () => {
260+
it.concurrent('should format object for function block', () => {
261261
const resolver = new BlockResolver(createTestWorkflow())
262-
const result = resolver.formatValueForBlock('hello', 'function', true)
263-
expect(result).toBe('hello')
264-
})
265-
266-
it.concurrent('should format object for function block in template literal', () => {
267-
const resolver = new BlockResolver(createTestWorkflow())
268-
const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
262+
const result = resolver.formatValueForBlock({ a: 1 }, 'function')
269263
expect(result).toBe('{"a":1}')
270264
})
271265

apps/sim/executor/variables/resolvers/block.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type OutputSchema,
1111
resolveBlockReference,
1212
} from '@/executor/utils/block-reference'
13+
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
1314
import {
1415
navigatePath,
1516
type ResolutionContext,
@@ -159,17 +160,13 @@ export class BlockResolver implements Resolver {
159160
return this.nameToBlockId.get(normalizeName(name))
160161
}
161162

162-
public formatValueForBlock(
163-
value: any,
164-
blockType: string | undefined,
165-
isInTemplateLiteral = false
166-
): string {
163+
public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
167164
if (blockType === 'condition') {
168165
return this.stringifyForCondition(value)
169166
}
170167

171168
if (blockType === 'function') {
172-
return this.formatValueForCodeContext(value, isInTemplateLiteral)
169+
return this.formatValueForCodeContext(value, language)
173170
}
174171

175172
if (blockType === 'response') {
@@ -210,29 +207,7 @@ export class BlockResolver implements Resolver {
210207
return String(value)
211208
}
212209

213-
private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string {
214-
if (isInTemplateLiteral) {
215-
if (typeof value === 'string') {
216-
return value
217-
}
218-
if (typeof value === 'object' && value !== null) {
219-
return JSON.stringify(value)
220-
}
221-
return String(value)
222-
}
223-
224-
if (typeof value === 'string') {
225-
return JSON.stringify(value)
226-
}
227-
if (typeof value === 'object' && value !== null) {
228-
return JSON.stringify(value)
229-
}
230-
if (value === undefined) {
231-
return 'undefined'
232-
}
233-
if (value === null) {
234-
return 'null'
235-
}
236-
return String(value)
210+
private formatValueForCodeContext(value: any, language?: string): string {
211+
return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
237212
}
238213
}

apps/sim/executor/variables/resolvers/reference.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function navigatePath(obj: any, path: string[]): any {
3030
const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
3131
if (arrayMatch) {
3232
const [, prop, bracketsPart] = arrayMatch
33-
current = current[prop]
33+
current =
34+
typeof current === 'object' && current !== null
35+
? (current as Record<string, unknown>)[prop]
36+
: undefined
3437
if (current === undefined || current === null) {
3538
return undefined
3639
}
@@ -49,7 +52,10 @@ export function navigatePath(obj: any, path: string[]): any {
4952
const index = Number.parseInt(part, 10)
5053
current = Array.isArray(current) ? current[index] : undefined
5154
} else {
52-
current = current[part]
55+
current =
56+
typeof current === 'object' && current !== null
57+
? (current as Record<string, unknown>)[part]
58+
: undefined
5359
}
5460
}
5561
return current

apps/sim/lib/execution/isolated-vm-worker.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ async function executeCode(request) {
132132
for (const [key, value] of Object.entries(contextVariables)) {
133133
if (value === undefined) {
134134
await jail.set(key, undefined)
135+
} else if (value === null) {
136+
await jail.set(key, null)
135137
} else {
136138
await jail.set(key, new ivm.ExternalCopy(value).copyInto())
137139
}

apps/sim/lib/workflows/variables/variable-manager.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('VariableManager', () => {
2626
it.concurrent('should handle boolean type variables', () => {
2727
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
2828
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
29-
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true)
29+
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
3030
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
3131
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
3232
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
@@ -128,7 +128,7 @@ describe('VariableManager', () => {
128128
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
129129
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
130130
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
131-
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true)
131+
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
132132
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
133133
})
134134

apps/sim/lib/workflows/variables/variable-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class VariableManager {
6161
// Special case for 'anything else' in the test
6262
if (unquoted === 'anything else') return true
6363
const normalized = String(unquoted).toLowerCase().trim()
64-
return normalized === 'true' || normalized === '1'
64+
return normalized === 'true'
6565
}
6666

6767
case 'object':

0 commit comments

Comments
 (0)