Fix underscore handling in nested pipes and non-function receivers

Add pipeVarStack to track pipe variables for nested pipes, allowing _
to correctly reference the innermost piped value. Also enable piping
to array literals and parenthesized expressions (e.g., `5 | [_ _ _]`).
This commit is contained in:
Chris Wanstrath 2026-01-27 19:12:48 -08:00
parent f5043fe701
commit 248a53c887
2 changed files with 92 additions and 36 deletions

View File

@ -57,6 +57,7 @@ export class Compiler {
loopLabelCount = 0 loopLabelCount = 0
bytecode: Bytecode bytecode: Bytecode
pipeCounter = 0 pipeCounter = 0
pipeVarStack: string[] = [] // Stack of pipe variable names for nested pipes
constructor( constructor(
public input: string, public input: string,
@ -733,56 +734,66 @@ export class Compiler {
const instructions: ProgramItem[] = [] const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(pipedFunctionCall, input)) instructions.push(...this.#compileNode(pipedFunctionCall, input))
// Use a unique variable name for this pipe level to handle nested pipes correctly
this.pipeCounter++ this.pipeCounter++
const pipeValName = `_pipe_value_${this.pipeCounter}` const pipeVarName = `_pipe_${this.pipeCounter}`
this.pipeVarStack.push(pipeVarName)
pipeReceivers.forEach((pipeReceiver) => { pipeReceivers.forEach((pipeReceiver) => {
instructions.push(['STORE', pipeValName]) // Store the piped value in the current pipe's variable
// Also store as `_` for direct access in simple cases
instructions.push(['DUP'])
instructions.push(['STORE', pipeVarName])
instructions.push(['STORE', '_'])
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts( const isFunctionCall =
pipeReceiver, pipeReceiver.type.is('FunctionCall') || pipeReceiver.type.is('FunctionCallOrIdentifier')
input,
)
instructions.push(...this.#compileNode(identifierNode, input)) if (isFunctionCall) {
// Function call receiver: check for explicit _ usage to determine arg handling
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(
pipeReceiver,
input,
)
const isUnderscoreInPositionalArgs = positionalArgs.some((arg) => instructions.push(...this.#compileNode(identifierNode, input))
arg.type.is('Underscore'),
)
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
const { valueNode } = getNamedArgParts(arg, input)
return valueNode.type.is('Underscore')
})
const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs const isUnderscoreInPositionalArgs = positionalArgs.some((arg) =>
arg.type.is('Underscore'),
)
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
const { valueNode } = getNamedArgParts(arg, input)
return valueNode.type.is('Underscore')
})
// If no underscore is explicitly used, add the piped value as the first positional arg const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs
if (shouldPushPositionalArg) {
instructions.push(['LOAD', pipeValName])
}
positionalArgs.forEach((arg) => { // If no underscore is explicitly used, add the piped value as the first positional arg
if (arg.type.is('Underscore')) { if (shouldPushPositionalArg) {
instructions.push(['LOAD', pipeValName]) instructions.push(['LOAD', pipeVarName])
} else { }
positionalArgs.forEach((arg) => {
instructions.push(...this.#compileNode(arg, input)) instructions.push(...this.#compileNode(arg, input))
} })
})
namedArgs.forEach((arg) => { namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input) const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name]) instructions.push(['PUSH', name])
if (valueNode.type.is('Underscore')) {
instructions.push(['LOAD', pipeValName])
} else {
instructions.push(...this.#compileNode(valueNode, input)) instructions.push(...this.#compileNode(valueNode, input))
} })
})
instructions.push(['PUSH', positionalArgs.length + (shouldPushPositionalArg ? 1 : 0)]) instructions.push(['PUSH', positionalArgs.length + (shouldPushPositionalArg ? 1 : 0)])
instructions.push(['PUSH', namedArgs.length]) instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL']) instructions.push(['CALL'])
} else {
// Non-function-call receiver (Array, ParenExpr, etc.): compile directly
// The `_` variable is available for use in nested expressions
instructions.push(...this.#compileNode(pipeReceiver, input))
}
}) })
this.pipeVarStack.pop()
return instructions return instructions
} }
@ -869,6 +880,13 @@ export class Compiler {
return [] // ignore comments return [] // ignore comments
} }
case 'Underscore': {
// _ refers to the piped value for the current (innermost) pipe
// Use the stack to handle nested pipes correctly
const pipeVar = this.pipeVarStack.at(-1) ?? '_'
return [['LOAD', pipeVar]]
}
default: default:
throw new CompilerError( throw new CompilerError(
`Compiler doesn't know how to handle a "${node.type.name}" node.`, `Compiler doesn't know how to handle a "${node.type.name}" node.`,

View File

@ -117,4 +117,42 @@ describe('pipe expressions', () => {
test('dict literals can be piped', () => { test('dict literals can be piped', () => {
expect(`[a=1 b=2 c=3] | dict.values | list.sort | str.join '-'`).toEvaluateTo('1-2-3') expect(`[a=1 b=2 c=3] | dict.values | list.sort | str.join '-'`).toEvaluateTo('1-2-3')
}) })
test('pipe to array literal using _ in nested expressions', () => {
// _ should be accessible inside nested function calls within array literals
const code = `
double = do x: x * 2 end
triple = do x: x * 3 end
5 | [(double _) (triple _)]`
expect(code).toEvaluateTo([10, 15])
})
test('pipe to array literal using _ multiple times', () => {
expect(`10 | [_ _ _]`).toEvaluateTo([10, 10, 10])
})
test('pipe to parenthesized expression using _', () => {
const code = `
double = do x: x * 2 end
5 | (double _)`
expect(code).toEvaluateTo(10)
})
test('pipe chain with array literal receiver', () => {
// Pipe to array, then pipe that array to a function
const code = `
double = do x: x * 2 end
5 | [(double _) _] | list.sum`
expect(code).toEvaluateTo(15) // [10, 5] -> 15
})
test('_ in deeply nested expressions within pipe', () => {
// _ should work in nested function calls within function arguments
const code = `
add = do a b: a + b end
mul = do a b: a * b end
10 | add (mul _ 2) _`
// add(mul(10, 2), 10) = add(20, 10) = 30
expect(code).toEvaluateTo(30)
})
}) })