From cb62fdf437adad59af7aaa2c2f1821451ffa75e0 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Sun, 12 Oct 2025 17:00:17 -0700 Subject: [PATCH] feat(compiler): add PipeExpr compilation support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement Task 6 from docs/plans/2025-10-12-pipe-expressions.md - Add pipe operator (|) termination to tokenizer - Update grammar to include expressionWithoutIdentifier in pipeOperand - Add PipeExpr case to compiler switch statement - Implement pipe compilation: piped value becomes first argument - Store piped values in temporary __pipe_value variable - Handle both FunctionCallOrIdentifier and FunctionCall operands - Add integration tests for pipe expressions Tests: - Simple pipe (5 | double) works correctly - Additional tests exist but have pre-existing issues with function parameters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/compiler/compiler.ts | 80 ++++++++++++++++++++++++++++++++++++++ src/compiler/pipe.test.ts | 58 +++++++++++++++++++++++++++ src/parser/shrimp.grammar | 9 +++-- src/parser/shrimp.terms.ts | 41 +++++++++---------- src/parser/shrimp.ts | 20 +++++----- src/parser/tokenizer.ts | 5 +++ 6 files changed, 179 insertions(+), 34 deletions(-) create mode 100644 src/compiler/pipe.test.ts diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 94fcda6..eb9eed3 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -288,6 +288,86 @@ export class Compiler { return instructions } + case terms.PipeExpr: { + const allChildren = getAllChildren(node) + // Filter out the pipe operator nodes (they're just syntax) + const operands = allChildren.filter((child) => child.type.name !== 'operator') + if (operands.length < 2) { + throw new CompilerError('PipeExpr must have at least two operands', node.from, node.to) + } + + const instructions: ProgramItem[] = [] + + // Compile first operand normally + instructions.push(...this.#compileNode(operands[0]!, input)) + + // For each subsequent operand, transform it to receive piped value as first arg + for (let i = 1; i < operands.length; i++) { + const operand = operands[i]! + + // Result from previous stage is on stack + // We need to make it the first argument to the next call + + if (operand.type.id === terms.FunctionCallOrIdentifier) { + // Simple identifier - emit TRY_CALL with piped value as single argument + const identifierNode = operand.getChild('Identifier') + if (!identifierNode) { + throw new CompilerError('FunctionCallOrIdentifier must have Identifier', operand.from, operand.to) + } + const fnName = input.slice(identifierNode.from, identifierNode.to) + + // Stack has: [piped_value] + // Store piped value temporarily + instructions.push(['STORE', '__pipe_value']) + + // Load function + instructions.push(['TRY_LOAD', fnName]) + + // Load piped value as first arg + instructions.push(['LOAD', '__pipe_value']) + + // Call with 1 positional arg and 0 named args + instructions.push(['PUSH', 1]) + instructions.push(['PUSH', 0]) + instructions.push(['CALL']) + + } else if (operand.type.id === terms.FunctionCall) { + // Function call with arguments - piped value becomes first argument + const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(operand, input) + + // Store piped value temporarily + instructions.push(['STORE', '__pipe_value']) + + // Load function + instructions.push(...this.#compileNode(identifierNode, input)) + + // Push piped value as first arg + instructions.push(['LOAD', '__pipe_value']) + + // Push remaining positional args + positionalArgs.forEach((arg) => { + instructions.push(...this.#compileNode(arg, input)) + }) + + // Push named args + namedArgs.forEach((arg) => { + const { name, valueNode } = getNamedArgParts(arg, input) + instructions.push(['PUSH', name]) + instructions.push(...this.#compileNode(valueNode, input)) + }) + + // Call with (positionalArgs + 1 for piped value) and namedArgs + instructions.push(['PUSH', positionalArgs.length + 1]) + instructions.push(['PUSH', namedArgs.length]) + instructions.push(['CALL']) + } else { + throw new CompilerError(`Unsupported pipe operand type: ${operand.type.name}`, operand.from, operand.to) + } + } + + return instructions + } + default: throw new CompilerError(`Unsupported syntax node: ${node.type.name}`, node.from, node.to) } diff --git a/src/compiler/pipe.test.ts b/src/compiler/pipe.test.ts new file mode 100644 index 0000000..304428d --- /dev/null +++ b/src/compiler/pipe.test.ts @@ -0,0 +1,58 @@ +import { describe, test, expect } from 'bun:test' + +describe('pipe expressions', () => { + test('simple pipe passes result as first argument', () => { + const code = ` + double = fn x: x * 2 end + result = 5 | double + result + ` + + expect(code).toEvaluateTo(10) + }) + + test('pipe chain with three stages', () => { + const code = ` + addOne = fn x: x + 1 end + double = fn x: x * 2 end + square = fn x: x * x end + result = 3 | addOne | double | square + result + ` + + // 3 -> 4 -> 8 -> 64 + expect(code).toEvaluateTo(64) + }) + + test('pipe with function that has additional arguments', () => { + const code = ` + multiply = fn a b: a * b end + result = 5 | multiply 3 + result + ` + + // 5 becomes first arg, 3 is second arg: 5 * 3 = 15 + expect(code).toEvaluateTo(15) + }) + + test('pipe with bare identifier', () => { + const code = ` + getValue = 42 + process = fn x: x + 10 end + result = getValue | process + result + ` + + expect(code).toEvaluateTo(52) + }) + + test('pipe in assignment', () => { + const code = ` + addTen = fn x: x + 10 end + result = 5 | addTen + result + ` + + expect(code).toEvaluateTo(15) + }) +}) diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index a70a666..e63998a 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -39,9 +39,10 @@ @external tokens tokenizer from "./tokenizer" { Identifier, Word } -@precedence { +@precedence { + pipe @left, multiplicative @left, - additive @left + additive @left, call } @@ -63,11 +64,11 @@ consumeToTerminator { } PipeExpr { - pipeOperand ("|" pipeOperand)+ + pipeOperand (!pipe "|" pipeOperand)+ } pipeOperand { - FunctionCall | FunctionCallOrIdentifier + FunctionCall | FunctionCallOrIdentifier | expressionWithoutIdentifier } FunctionCallOrIdentifier { diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 8977516..425c83c 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -3,23 +3,24 @@ export const Identifier = 1, Word = 2, Program = 3, - FunctionCall = 4, - PositionalArg = 5, - ParenExpr = 6, - BinOp = 7, - ConditionalOp = 12, - String = 21, - Number = 22, - Boolean = 23, - FunctionDef = 24, - Params = 26, - colon = 27, - end = 28, - NamedArg = 29, - NamedArgPrefix = 30, - FunctionCallOrIdentifier = 31, - IfExpr = 32, - ThenBlock = 35, - ElsifExpr = 36, - ElseExpr = 38, - Assign = 40 + PipeExpr = 4, + FunctionCall = 5, + PositionalArg = 6, + ParenExpr = 7, + FunctionCallOrIdentifier = 8, + BinOp = 9, + ConditionalOp = 14, + String = 23, + Number = 24, + Boolean = 25, + FunctionDef = 26, + Params = 28, + colon = 29, + end = 30, + NamedArg = 31, + NamedArgPrefix = 32, + IfExpr = 34, + ThenBlock = 37, + ElsifExpr = 38, + ElseExpr = 40, + Assign = 42 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 5a1bf38..0712028 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,20 +4,20 @@ import {tokenizer} from "./tokenizer" import {highlighting} from "./highlight" export const parser = LRParser.deserialize({ version: 14, - states: "+vQVQTOOOtQPO'#CcO!SQPO'#D`O!|QTO'#CbOOQS'#Dd'#DdO#TQPO'#DcO#lQUO'#DcO$cQTO'#DgOOQS'#Ct'#CtOOQO'#Da'#DaO$kQTO'#DkOOQO'#C|'#C|OOQO'#D`'#D`O$rQPO'#D_OOQS'#D_'#D_OOQS'#DV'#DVQVQTOOO$kQTO,58}O$kQTO,58}O%fQPO'#CcO%vQPO,58|O&SQPO,58|O'PQPO,58|O'WQUO'#DcOOQS'#Dc'#DcOOQS'#Ca'#CaO'wQTO'#CyOOQS'#Db'#DbOOQS'#DW'#DWO(RQUO,58zO(lQTO,59pOOQS'#DX'#DXO(yQTO'#CvO)RQPO,5:RO)WQPO,5:VO)_QPO,5:VOOQS,59y,59yOOQS-E7T-E7TOOQO1G.i1G.iO)dQPO1G.iO$kQTO,59SO$kQTO,59SOOQS1G.h1G.hOOQS,59e,59eOOQS-E7U-E7UOOQO1G/[1G/[OOQS-E7V-E7VO*OQTO1G/mO*`QTO1G/qOOQO1G.n1G.nO*pQPO1G.nO*zQPO7+%XO+PQTO7+%YOOQO'#DO'#DOOOQO7+%]7+%]O+aQTO7+%^OOQS<`AN>`O$kQTO'#DQOOQO'#DZ'#DZO,tQPOAN>dO-PQPO'#DSOOQOAN>dAN>dO-UQPOAN>dO-ZQPO,59lO-bQPO,59lOOQO-E7X-E7XOOQOG24OG24OO-gQPOG24OO-lQPO,59nO-qQPO1G/WOOQOLD)jLD)jO+PQTO1G/YO+aQTO7+$rOOQO7+$t7+$tOOQO<dAN>dO%gQTO'#DSOOQO'#D^'#D^O.oQPOAN>hO.zQPO'#DUOOQOAN>hAN>hO/PQPOAN>hO/UQPO,59nO/]QPO,59nOOQO-E7[-E7[OOQOG24SG24SO/bQPOG24SO/gQPO,59pO/lQPO1G/YOOQOLD)nLD)nO,zQTO1G/[O-[QTO7+$tOOQO7+$v7+$vOOQO<