From 248a53c887205249226854e031922006d1b88cad Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 27 Jan 2026 19:12:48 -0800 Subject: [PATCH] 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 | [_ _ _]`). --- src/compiler/compiler.ts | 90 ++++++++++++++++++++------------- src/compiler/tests/pipe.test.ts | 38 ++++++++++++++ 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 68c198c..f310bd9 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -57,6 +57,7 @@ export class Compiler { loopLabelCount = 0 bytecode: Bytecode pipeCounter = 0 + pipeVarStack: string[] = [] // Stack of pipe variable names for nested pipes constructor( public input: string, @@ -733,56 +734,66 @@ export class Compiler { const instructions: ProgramItem[] = [] instructions.push(...this.#compileNode(pipedFunctionCall, input)) + // Use a unique variable name for this pipe level to handle nested pipes correctly this.pipeCounter++ - const pipeValName = `_pipe_value_${this.pipeCounter}` + const pipeVarName = `_pipe_${this.pipeCounter}` + this.pipeVarStack.push(pipeVarName) + 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( - pipeReceiver, - input, - ) + const isFunctionCall = + pipeReceiver.type.is('FunctionCall') || pipeReceiver.type.is('FunctionCallOrIdentifier') - 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) => - arg.type.is('Underscore'), - ) - const isUnderscoreInNamedArgs = namedArgs.some((arg) => { - const { valueNode } = getNamedArgParts(arg, input) - return valueNode.type.is('Underscore') - }) + instructions.push(...this.#compileNode(identifierNode, input)) - 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 - if (shouldPushPositionalArg) { - instructions.push(['LOAD', pipeValName]) - } + const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs - positionalArgs.forEach((arg) => { - if (arg.type.is('Underscore')) { - instructions.push(['LOAD', pipeValName]) - } else { + // If no underscore is explicitly used, add the piped value as the first positional arg + if (shouldPushPositionalArg) { + instructions.push(['LOAD', pipeVarName]) + } + + positionalArgs.forEach((arg) => { instructions.push(...this.#compileNode(arg, input)) - } - }) + }) - namedArgs.forEach((arg) => { - const { name, valueNode } = getNamedArgParts(arg, input) - instructions.push(['PUSH', name]) - if (valueNode.type.is('Underscore')) { - instructions.push(['LOAD', pipeValName]) - } else { + namedArgs.forEach((arg) => { + const { name, valueNode } = getNamedArgParts(arg, input) + instructions.push(['PUSH', name]) instructions.push(...this.#compileNode(valueNode, input)) - } - }) + }) - instructions.push(['PUSH', positionalArgs.length + (shouldPushPositionalArg ? 1 : 0)]) - instructions.push(['PUSH', namedArgs.length]) - instructions.push(['CALL']) + instructions.push(['PUSH', positionalArgs.length + (shouldPushPositionalArg ? 1 : 0)]) + instructions.push(['PUSH', namedArgs.length]) + 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 } @@ -869,6 +880,13 @@ export class Compiler { 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: throw new CompilerError( `Compiler doesn't know how to handle a "${node.type.name}" node.`, diff --git a/src/compiler/tests/pipe.test.ts b/src/compiler/tests/pipe.test.ts index 4d1669f..d6f7a0b 100644 --- a/src/compiler/tests/pipe.test.ts +++ b/src/compiler/tests/pipe.test.ts @@ -117,4 +117,42 @@ describe('pipe expressions', () => { test('dict literals can be piped', () => { 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) + }) }) -- 2.50.1