Compare commits

..

7 Commits

Author SHA1 Message Date
Vikhyath Mondreti
12239a0803 consolidate literal gen 2026-01-26 01:33:06 -08:00
Vikhyath Mondreti
c264481361 fix tests 2026-01-26 01:14:07 -08:00
Vikhyath Mondreti
0e9bcdf1b2 remove template literal check 2026-01-26 01:10:30 -08:00
Vikhyath Mondreti
e20ec7ae3c fix python nan and inf resolution 2026-01-26 00:26:48 -08:00
Vikhyath Mondreti
87dcd53b98 case insensitive lookup 2026-01-26 00:23:33 -08:00
Vikhyath Mondreti
8aabf06f62 remove hacky fallback 2026-01-26 00:09:59 -08:00
Vikhyath Mondreti
b45fc62e7b fix(codegen): function prologue resolution edge cases 2026-01-26 00:03:15 -08:00
9 changed files with 86 additions and 63 deletions

View File

@@ -8,6 +8,7 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants' import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference' import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import { import {
createEnvVarPattern, createEnvVarPattern,
createWorkflowVariablePattern, createWorkflowVariablePattern,
@@ -387,7 +388,12 @@ function resolveWorkflowVariables(
if (type === 'number') { if (type === 'number') {
variableValue = Number(variableValue) variableValue = Number(variableValue)
} else if (type === 'boolean') { } else if (type === 'boolean') {
variableValue = variableValue === 'true' || variableValue === true if (typeof variableValue === 'boolean') {
// Already a boolean, keep as-is
} else {
const normalized = String(variableValue).toLowerCase().trim()
variableValue = normalized === 'true'
}
} else if (type === 'json' && typeof variableValue === 'string') { } else if (type === 'json' && typeof variableValue === 'string') {
try { try {
variableValue = JSON.parse(variableValue) variableValue = JSON.parse(variableValue)
@@ -687,11 +693,7 @@ export async function POST(req: NextRequest) {
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++ prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) { for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) { prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
prologue += `const ${k} = undefined;\n`
} else {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
}
prologueLineCount++ prologueLineCount++
} }
@@ -762,11 +764,7 @@ export async function POST(req: NextRequest) {
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n` prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++ prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) { for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) { prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
prologue += `${k} = None\n`
} else {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
}
prologueLineCount++ prologueLineCount++
} }
const wrapped = [ const wrapped = [

View File

@@ -0,0 +1,48 @@
/**
* Formats a JavaScript/TypeScript value as a code literal for the target language.
* Handles special cases like null, undefined, booleans, and Python-specific number representations.
*
* @param value - The value to format
* @param language - Target language ('javascript' or 'python')
* @returns A string literal representation valid in the target language
*
* @example
* formatLiteralForCode(null, 'python') // => 'None'
* formatLiteralForCode(true, 'python') // => 'True'
* formatLiteralForCode(NaN, 'python') // => "float('nan')"
* formatLiteralForCode("hello", 'javascript') // => '"hello"'
* formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
*/
export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
const isPython = language === 'python'
if (value === undefined) {
return isPython ? 'None' : 'undefined'
}
if (value === null) {
return isPython ? 'None' : 'null'
}
if (typeof value === 'boolean') {
return isPython ? (value ? 'True' : 'False') : String(value)
}
if (typeof value === 'number') {
if (Number.isNaN(value)) {
return isPython ? "float('nan')" : 'NaN'
}
if (value === Number.POSITIVE_INFINITY) {
return isPython ? "float('inf')" : 'Infinity'
}
if (value === Number.NEGATIVE_INFINITY) {
return isPython ? "float('-inf')" : '-Infinity'
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
// Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
if (isPython) {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`
}
return JSON.stringify(value)
}

View File

@@ -157,7 +157,14 @@ export class VariableResolver {
let replacementError: Error | null = null let replacementError: Error | null = null
// Use generic utility for smart variable reference replacement const blockType = block?.metadata?.id
const language =
blockType === BlockType.FUNCTION
? ((block?.config?.params as Record<string, unknown> | undefined)?.language as
| string
| undefined)
: undefined
let result = replaceValidReferences(template, (match) => { let result = replaceValidReferences(template, (match) => {
if (replacementError) return match if (replacementError) return match
@@ -167,14 +174,7 @@ export class VariableResolver {
return match return match
} }
const blockType = block?.metadata?.id return this.blockResolver.formatValueForBlock(resolved, blockType, language)
const isInTemplateLiteral =
blockType === BlockType.FUNCTION &&
template.includes('${') &&
template.includes('}') &&
template.includes('`')
return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
} catch (error) { } catch (error) {
replacementError = error instanceof Error ? error : new Error(String(error)) replacementError = error instanceof Error ? error : new Error(String(error))
return match return match

View File

@@ -257,15 +257,9 @@ describe('BlockResolver', () => {
expect(result).toBe('"hello"') expect(result).toBe('"hello"')
}) })
it.concurrent('should format string for function block in template literal', () => { it.concurrent('should format object for function block', () => {
const resolver = new BlockResolver(createTestWorkflow()) const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock('hello', 'function', true) const result = resolver.formatValueForBlock({ a: 1 }, 'function')
expect(result).toBe('hello')
})
it.concurrent('should format object for function block in template literal', () => {
const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
expect(result).toBe('{"a":1}') expect(result).toBe('{"a":1}')
}) })

View File

@@ -10,6 +10,7 @@ import {
type OutputSchema, type OutputSchema,
resolveBlockReference, resolveBlockReference,
} from '@/executor/utils/block-reference' } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import { import {
navigatePath, navigatePath,
type ResolutionContext, type ResolutionContext,
@@ -159,17 +160,13 @@ export class BlockResolver implements Resolver {
return this.nameToBlockId.get(normalizeName(name)) return this.nameToBlockId.get(normalizeName(name))
} }
public formatValueForBlock( public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
value: any,
blockType: string | undefined,
isInTemplateLiteral = false
): string {
if (blockType === 'condition') { if (blockType === 'condition') {
return this.stringifyForCondition(value) return this.stringifyForCondition(value)
} }
if (blockType === 'function') { if (blockType === 'function') {
return this.formatValueForCodeContext(value, isInTemplateLiteral) return this.formatValueForCodeContext(value, language)
} }
if (blockType === 'response') { if (blockType === 'response') {
@@ -210,29 +207,7 @@ export class BlockResolver implements Resolver {
return String(value) return String(value)
} }
private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string { private formatValueForCodeContext(value: any, language?: string): string {
if (isInTemplateLiteral) { return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
if (typeof value === 'string') {
return value
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
if (value === undefined) {
return 'undefined'
}
if (value === null) {
return 'null'
}
return String(value)
} }
} }

View File

@@ -30,7 +30,10 @@ export function navigatePath(obj: any, path: string[]): any {
const arrayMatch = part.match(/^([^[]+)(\[.+)$/) const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
if (arrayMatch) { if (arrayMatch) {
const [, prop, bracketsPart] = arrayMatch const [, prop, bracketsPart] = arrayMatch
current = current[prop] current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[prop]
: undefined
if (current === undefined || current === null) { if (current === undefined || current === null) {
return undefined return undefined
} }
@@ -49,7 +52,10 @@ export function navigatePath(obj: any, path: string[]): any {
const index = Number.parseInt(part, 10) const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined current = Array.isArray(current) ? current[index] : undefined
} else { } else {
current = current[part] current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[part]
: undefined
} }
} }
return current return current

View File

@@ -132,6 +132,8 @@ async function executeCode(request) {
for (const [key, value] of Object.entries(contextVariables)) { for (const [key, value] of Object.entries(contextVariables)) {
if (value === undefined) { if (value === undefined) {
await jail.set(key, undefined) await jail.set(key, undefined)
} else if (value === null) {
await jail.set(key, null)
} else { } else {
await jail.set(key, new ivm.ExternalCopy(value).copyInto()) await jail.set(key, new ivm.ExternalCopy(value).copyInto())
} }

View File

@@ -26,7 +26,7 @@ describe('VariableManager', () => {
it.concurrent('should handle boolean type variables', () => { it.concurrent('should handle boolean type variables', () => {
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
@@ -128,7 +128,7 @@ describe('VariableManager', () => {
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false) expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true) expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false) expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true) expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false) expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
}) })

View File

@@ -61,7 +61,7 @@ export class VariableManager {
// Special case for 'anything else' in the test // Special case for 'anything else' in the test
if (unquoted === 'anything else') return true if (unquoted === 'anything else') return true
const normalized = String(unquoted).toLowerCase().trim() const normalized = String(unquoted).toLowerCase().trim()
return normalized === 'true' || normalized === '1' return normalized === 'true'
} }
case 'object': case 'object':