From a1693078f92c44a3bfdd6f1f173b77f041e78ff2 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 28 Oct 2025 10:11:52 -0700 Subject: [PATCH] Make dot-get work in the compiler AND with parens exprs --- src/compiler/compiler.ts | 14 +++++++++++--- src/compiler/tests/compiler.test.ts | 20 +++++++++++++++++-- src/compiler/utils.ts | 7 +++---- src/editor/editor.tsx | 1 - src/parser/shrimp.grammar | 2 +- src/parser/shrimp.ts | 8 ++++---- src/parser/tests/dot-get.test.ts | 24 +++++++++++++++++++++++ src/testSetup.ts | 30 +++++++++++++---------------- 8 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index b7e6274..21da498 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -94,7 +94,6 @@ export class Compiler { #compileNode(node: SyntaxNode, input: string): ProgramItem[] { const value = input.slice(node.from, node.to) - if (DEBUG) console.log(`🫦 ${node.name}: ${value}`) switch (node.type.id) { @@ -190,10 +189,15 @@ export class Compiler { } case terms.DotGet: { - const { objectName, propertyName } = getDotGetParts(node, input) + const { objectName, property } = getDotGetParts(node, input) const instructions: ProgramItem[] = [] instructions.push(['TRY_LOAD', objectName]) - instructions.push(['PUSH', propertyName]) + if (property.type.id === terms.ParenExpr) { + instructions.push(...this.#compileNode(property, input)) + } else { + const propertyValue = input.slice(property.from, property.to) + instructions.push(['PUSH', propertyValue]) + } instructions.push(['DOT_GET']) return instructions } @@ -265,6 +269,10 @@ export class Compiler { } case terms.FunctionCallOrIdentifier: { + if (node.firstChild?.type.id === terms.DotGet) { + return this.#compileNode(node.firstChild, input) + } + return [['TRY_CALL', value]] } diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 46ee0b7..de5166b 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -96,8 +96,7 @@ describe('compiler', () => { end abc - `) - .toEvaluateTo(true) + `).toEvaluateTo(true) }) test('simple conditionals', () => { @@ -238,3 +237,20 @@ describe('native functions', () => { expect(`add 5 9`).toEvaluateTo(14, { add }) }) }) + +describe('dot get', () => { + const array = (...items: any) => items + const dict = (atNamed: any) => atNamed + + test('access array element', () => { + expect(`arr = array 'a' 'b' 'c'; arr.1`).toEvaluateTo('b', { array }) + }) + + test('access dict element', () => { + expect(`dict = dict a=1 b=2; dict.a`).toEvaluateTo(1, { dict }) + }) + + test('use parens expr with dot-get', () => { + expect(`a = 1; arr = array 'a' 'b' 'c'; arr.(1 + a)`).toEvaluateTo('c', { array }) + }) +}) diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index bcf0587..82b4025 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -203,7 +203,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => { const children = getAllChildren(node) const [object, property] = children - if (children.length !== 2) { + if (!object || !property) { throw new CompilerError( `DotGet expected 2 identifier children, got ${children.length}`, node.from, @@ -219,7 +219,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => { ) } - if (property.type.id !== terms.Identifier && property.type.id !== terms.Number) { + if (![terms.Identifier, terms.Number, terms.ParenExpr].includes(property.type.id)) { throw new CompilerError( `DotGet property must be an Identifier or Number, got ${property.type.name}`, property.from, @@ -228,7 +228,6 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => { } const objectName = input.slice(object.from, object.to) - const propertyName = input.slice(property.from, property.to) - return { objectName, propertyName } + return { objectName, property } } diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 9206fce..ce7e0b1 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -32,7 +32,6 @@ export const Editor = () => { }) multilineModeSignal.connect((isMultiline) => { - console.log(`🌭 hey babe`, isMultiline) view.dispatch({ effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []), }) diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 0968765..14f18a0 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -169,7 +169,7 @@ expression { @skip {} { DotGet { - IdentifierBeforeDot dot (Number | Identifier) + IdentifierBeforeDot dot (Number | Identifier | ParenExpr) } String { "'" stringContent* "'" } diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 40ba69f..3cc8b38 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -7,9 +7,9 @@ import {highlighting} from "./highlight" const spec_Identifier = {__proto__:null,null:64, end:74, if:88, elseif:96, else:100} export const parser = LRParser.deserialize({ version: 14, - states: "/SQYQbOOO!TOpO'#CqO#aQcO'#CtO$ZOSO'#CvO%aQcO'#DsOOQa'#Ds'#DsO&gQcO'#DrO'OQRO'#CuO'^QcO'#DnO'uQbO'#D{OOQ`'#DO'#DOO'}QbO'#CsOOQ`'#Do'#DoO(oQbO'#DnO(}QbO'#EROOQ`'#DX'#DXO)lQRO'#DaOOQ`'#Dn'#DnO)qQQO'#DmOOQ`'#Dm'#DmOOQ`'#Db'#DbQYQbOOO)yObO,59]OOQa'#Dr'#DrOOQ`'#DS'#DSO*RQbO'#DUOOQ`'#EQ'#EQOOQ`'#Df'#DfO*]QbO,59[O*pQbO'#CxO*xQWO'#CyOOOO'#Du'#DuOOOO'#Dc'#DcO+^OSO,59bOOQa,59b,59bO(}QbO,59aO(}QbO,59aOOQ`'#Dd'#DdO+lQbO'#DPO+tQQO,5:gO+yQRO,59_O-`QRO'#CuO-pQRO,59_O-|QQO,59_O.RQQO,59_O.ZQbO'#DgO.fQbO,59ZO.wQRO,5:mO/OQQO,5:mO/TQbO,59{OOQ`,5:X,5:XOOQ`-E7`-E7`OOQa1G.w1G.wOOQ`,59p,59pOOQ`-E7d-E7dOOOO,59d,59dOOOO,59e,59eOOOO-E7a-E7aOOQa1G.|1G.|OOQa1G.{1G.{O/_QcO1G.{OOQ`-E7b-E7bO/yQbO1G0ROOQa1G.y1G.yO(}QbO,59iO(}QbO,59iO!YQbO'#CtO$iQbO'#CpOOQ`,5:R,5:ROOQ`-E7e-E7eO0WQbO1G0XOOQ`1G/g1G/gO0eQbO7+%mO0jQbO7+%nOOQO1G/T1G/TO0zQRO1G/TOOQ`'#DZ'#DZO1UQbO7+%sO1ZQbO7+%tOOQ`<tAN>tO(}QbO'#D]OOQ`'#Dh'#DhO2nQbOAN>zO2yQQO'#D_OOQ`AN>zAN>zO3OQbOAN>zO3TQRO,59wO3[QQO,59wOOQ`-E7f-E7fOOQ`G24fG24fO3aQbOG24fO3fQQO,59yO3kQQO1G/cOOQ`LD*QLD*QO0jQbO1G/eO1ZQbO7+$}OOQ`7+%P7+%POOQ`<tAN>tO(}QbO'#D]OOQ`'#Dh'#DhO2qQbOAN>zO2|QQO'#D_OOQ`AN>zAN>zO3RQbOAN>zO3WQRO,59wO3_QQO,59wOOQ`-E7f-E7fOOQ`G24fG24fO3dQbOG24fO3iQQO,59yO3nQQO1G/cOOQ`LD*QLD*QO0mQbO1G/eO1^QbO7+$}OOQ`7+%P7+%POOQ`< (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 13, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 860 + tokenPrec: 863 }) diff --git a/src/parser/tests/dot-get.test.ts b/src/parser/tests/dot-get.test.ts index bcec05f..f781937 100644 --- a/src/parser/tests/dot-get.test.ts +++ b/src/parser/tests/dot-get.test.ts @@ -274,4 +274,28 @@ end`).toMatchTree(` Identifier heya `) }) + + test('can use the result of a parens expression as the property of dot get', () => { + expect('obj = list 1 2 3; obj.(1 + 2)').toMatchTree(` + Assign + AssignableIdentifier obj + Eq = + FunctionCall + Identifier list + PositionalArg + Number 1 + PositionalArg + Number 2 + PositionalArg + Number 3 + FunctionCallOrIdentifier + DotGet + IdentifierBeforeDot obj + ParenExpr + BinOp + Number 1 + Plus + + Number 2 + `) + }) }) diff --git a/src/testSetup.ts b/src/testSetup.ts index 799dbcb..69c8331 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp' import { $ } from 'bun' import { assert, errorMessage } from '#utils/utils' import { Compiler } from '#compiler/compiler' -import { run, VM } from 'reefvm' +import { run, VM, type TypeScriptFunction } from 'reefvm' import { treeToString, VMResultToValue } from '#utils/tree' const regenerateParser = async () => { @@ -33,13 +33,16 @@ declare module 'bun:test' { toMatchTree(expected: string): T toMatchExpression(expected: string): T toFailParse(): T - toEvaluateTo(expected: unknown, nativeFunctions?: Record): Promise + toEvaluateTo( + expected: unknown, + nativeFunctions?: Record + ): Promise toFailEvaluation(): Promise } } expect.extend({ - toMatchTree(received: unknown, expected: string) { + toMatchTree(received, expected) { assert(typeof received === 'string', 'toMatchTree can only be used with string values') const tree = parser.parse(received) @@ -58,7 +61,7 @@ expect.extend({ } }, - toFailParse(received: unknown) { + toFailParse(received) { assert(typeof received === 'string', 'toFailParse can only be used with string values') try { @@ -93,11 +96,7 @@ expect.extend({ } }, - async toEvaluateTo( - received: unknown, - expected: unknown, - nativeFunctions: Record = {} - ) { + async toEvaluateTo(received, expected, nativeFunctions = {}) { assert(typeof received === 'string', 'toEvaluateTo can only be used with string values') try { @@ -109,13 +108,10 @@ expect.extend({ if (expected instanceof RegExp) expected = String(expected) if (value instanceof RegExp) value = String(value) - if (value === expected) { - return { pass: true } - } else { - return { - message: () => `Expected evaluation to be ${expected}, but got ${value}`, - pass: false, - } + expect(value).toEqual(expected) + return { + message: () => `Expected evaluation to be ${expected}, but got ${value}`, + pass: true, } } catch (error) { return { @@ -125,7 +121,7 @@ expect.extend({ } }, - async toFailEvaluation(received: unknown) { + async toFailEvaluation(received) { assert(typeof received === 'string', 'toFailEvaluation can only be used with string values') try {