From 16cb47ddccbd0f4ecc118d7c167dddd95ad74e97 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:16:41 -0800 Subject: [PATCH] `do` allowed in arg/dict values --- src/parser/parser2.ts | 36 ++++++++++++--------- src/parser/tests/functions.test.ts | 52 ++++++++++++++++++++++++++++++ src/parser/tests/literals.test.ts | 16 +++++++++ 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/parser/parser2.ts b/src/parser/parser2.ts index 64f62bc..c0ea85c 100644 --- a/src/parser/parser2.ts +++ b/src/parser/parser2.ts @@ -241,6 +241,22 @@ export class Parser { // parse specific nodes // + // raw determines whether we just want the SyntaxNodes or we want to + // wrap them in a PositionalArg + arg(raw = false): SyntaxNode { + // 'do' is a special function arg - it doesn't need to be wrapped + // in parens. otherwise, args are regular value()s + const val = this.is($T.Keyword, 'do') ? this.do() : this.value() + + if (raw) { + return val + } else { + const arg = new SyntaxNode('PositionalArg', val.from, val.to) + arg.add(val) + return arg + } + } + // [ 1 2 3 ] array(): SyntaxNode { const open = this.expect($T.OpenBracket) @@ -425,10 +441,7 @@ export class Parser { continue } - if (this.is($T.NamedArgPrefix)) - values.push(this.namedArg()) - else - values.push(this.value()) + values.push(this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg()) } const close = this.expect($T.CloseBracket) @@ -549,17 +562,8 @@ export class Parser { const ident = fn ?? this.identifier() const args: SyntaxNode[] = [] - while (!this.isExprEnd() && !this.is($T.Operator, '|')) { - if (this.is($T.NamedArgPrefix)) { - args.push(this.namedArg()) - } else { - // 'do' is the only keyword allowed as a function argument - const val = this.is($T.Keyword, 'do') ? this.do() : this.value() - const arg = new SyntaxNode('PositionalArg', val.from, val.to) - arg.add(val) - args.push(arg) - } - } + while (!this.isExprEnd() && !this.is($T.Operator, '|')) + args.push(this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg()) const node = new SyntaxNode('FunctionCall', ident.from, (args.at(-1) || ident).to) node.push(ident, ...args) @@ -669,7 +673,7 @@ export class Parser { // abc= true namedArg(): SyntaxNode { const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix)) - const val = this.value() + const val = this.arg(true) const node = new SyntaxNode('NamedArg', prefix.from, val.to) return node.push(prefix, val) } diff --git a/src/parser/tests/functions.test.ts b/src/parser/tests/functions.test.ts index d2d7f9f..ff39870 100644 --- a/src/parser/tests/functions.test.ts +++ b/src/parser/tests/functions.test.ts @@ -43,6 +43,58 @@ describe('calling functions', () => { `) }) + test('call with function', () => { + expect(`tail do x: x end`).toMatchTree(` + FunctionCall + Identifier tail + PositionalArg + FunctionDef + Do do + Params + Identifier x + colon : + FunctionCallOrIdentifier + Identifier x + keyword end + `) + }) + + test('call with arg and function', () => { + expect(`tail true do x: x end`).toMatchTree(` + FunctionCall + Identifier tail + PositionalArg + Boolean true + PositionalArg + FunctionDef + Do do + Params + Identifier x + colon : + FunctionCallOrIdentifier + Identifier x + keyword end + `) + }) + + test('call with function in named arg', () => { + expect(`tail callback=do x: x end`).toMatchTree(` + FunctionCall + Identifier tail + NamedArg + NamedArgPrefix callback= + FunctionDef + Do do + Params + Identifier x + colon : + FunctionCallOrIdentifier + Identifier x + keyword end + `) + }) + + test('command with arg that is also a command', () => { expect('tail tail').toMatchTree(` FunctionCall diff --git a/src/parser/tests/literals.test.ts b/src/parser/tests/literals.test.ts index 9173232..ba423ab 100644 --- a/src/parser/tests/literals.test.ts +++ b/src/parser/tests/literals.test.ts @@ -336,6 +336,22 @@ describe('dict literals', () => { `) }) + test('work with functions', () => { + expect(`[trap=do x: x end]`).toMatchTree(` + Dict + NamedArg + NamedArgPrefix trap= + FunctionDef + Do do + Params + Identifier x + colon : + FunctionCallOrIdentifier + Identifier x + keyword end + `) + }) + test('can be nested', () => { expect('[a=one b=[two [c=three]]]').toMatchTree(` Dict