Make dot-get work in the compiler AND with parens exprs

This commit is contained in:
Corey Johnson 2025-10-28 10:11:52 -07:00
parent bbc9316074
commit a1693078f9
8 changed files with 74 additions and 32 deletions

View File

@ -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]]
}

View File

@ -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 })
})
})

View File

@ -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 }
}

View File

@ -32,7 +32,6 @@ export const Editor = () => {
})
multilineModeSignal.connect((isMultiline) => {
console.log(`🌭 hey babe`, isMultiline)
view.dispatch({
effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []),
})

View File

@ -169,7 +169,7 @@ expression {
@skip {} {
DotGet {
IdentifierBeforeDot dot (Number | Identifier)
IdentifierBeforeDot dot (Number | Identifier | ParenExpr)
}
String { "'" stringContent* "'" }

View File

@ -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`<<IX<<IXOOQ`'#De'#DeO1qQQO'#DeO1vQbO'#EOO2^QbO<<IYOOQ`<<I_<<I_OOQ`'#D['#D[O2cQbO<<I`OOQ`,5:P,5:POOQ`-E7c-E7cOOQ`AN>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`<<Hi<<Hi",
stateData: "3s~O!_OS!`OS~O]QO^`O_TO`POaXOfTOnTOoTOpTO|^O!eZO!hRO!qcO~O!dfO~O]gO_TO`POaXOfTOnTOoTOpTOwhOyiO!eZO!hROzhX!qhX!whX!shXuhX~OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~P!YOkoO!hrO!jmO!knO~O]gO_TO`POaXOfTOnTOoTOpTOwhOyiO!eZO!hRO~OP!gXQ!gXR!gXS!gX!q!gX!w!gXT!gXU!gXV!gXW!gXX!gXY!gXZ!gX[!gX!s!gXu!gX~P$iOP!fXQ!fXR!fXS!fX!q!bX!w!bXu!bX~OPsOQsORtOStO~OPsOQsORtOStO!q!bX!w!bXu!bX~O]uOtsP~O]QO_TO`POaXOfTOnTOoTOpTO!eZO!hRO~Oz}O!q!bX!w!bXu!bX~O]gO_TO`POfTOnTOoTOpTO!eZO!hRO~OV!RO~O!q!SO!w!SO~O]!UOf!UO~OaXOw!VO~P(}Ozda!qda!wda!sdauda~P$iO]!XO!eZO~O!h!YO!j!YO!k!YO!l!YO!m!YO!n!YO~OkoO!h![O!jmO!knO~O]uOtsX~Ot!`O~O!s!aOP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~OT!cOU!cOV!bOW!bOX!bOY!bOZ!bO[!bO~OPsOQsORtOStO~P,tOPsOQsORtOStO!s!aO~Oz}O!s!aO~O]!dO`PO!eZO~Oz}O!qca!wca!scauca~Ot!hO~P,tOt!hO~O^`O|^O~P'}OPsOQsORiiSii!qii!wii!siiuii~O^`O|^O!q!kO~P'}O^`O|^O!q!pO~P'}Ou!qO~O^`O|^O!q!rOu!rP~P'}O!sqitqi~P,tOu!vO~O^`O|^O!q!rOu!rP!Q!rP!S!rP~P'}O!q!yO~O^`O|^O!q!rOu!rX!Q!rX!S!rX~P'}Ou!{O~Ou#QO!Q!|O!S#PO~Ou#VO!Q!|O!S#PO~Ot#XO~Ou#VO~Ot#YO~P,tOt#YO~Ou#ZO~O!q#[O~O!q#]O~Ofo~",
goto: ",`!wPPPPPPPPPPPPPPPPPPP!x#X#gP$V#X$x%_P%x%xPPP%|&Y&sPP&vP&vPP&}P'Z'^'gP'kP&}'q'w'}(T(^(g(nPPPP(t(x)^PP)p*mP+[PPPPP+`+`P+sP+{,S,SdaOe!R!`!h!k!p!t#[#]R{Zi[OZe}!R!`!h!k!p!t#[#]fQOZe!R!`!h!k!p!t#[#]hgQS^ilst!b!c!d!e!|R!d}fSOZe!R!`!h!k!p!t#[#]hTQS^ilst!b!c!d!e!|Q!XmR!e}dWOe!R!`!h!k!p!t#[#]QzZQ!]sR!^t!PTOQSZ^eilst!R!`!b!c!d!e!h!k!p!t!|#[#]ToRqQ{ZQ!Q^Q!l!cR#T!|daOe!R!`!h!k!p!t#[#]YhQSl!d!eQ{ZR!ViRwXZjQSl!d!eeaOe!R!`!h!k!p!t#[#]R!o!hQ!x!pQ#^#[R#_#]T!}!x#OQ#R!xR#W#OQeOR!TeQqRR!ZqQvXR!_vW!t!k!p#[#]R!z!tWlQS!d!eR!WlS!O]|R!g!OQ#O!xR#U#OTdOeSbOeQ!i!RQ!j!`Q!n!hZ!s!k!p!t#[#]d]Oe!R!`!h!k!p!t#[#]Q|ZR!f}dVOe!R!`!h!k!p!t#[#]YhQSl!d!eQyZQ!P^Q!ViQ!]sQ!^tQ!l!bQ!m!cR#S!|dUOe!R!`!h!k!p!t#[#]hgQS^ilst!b!c!d!e!|RxZTpRqsYOQSZeil!R!`!d!e!h!k!p!t#[#]Q!u!kV!w!p#[#]ZkQSl!d!ee_Oe!R!`!h!k!p!t#[#]",
states: "/SQYQbOOO#[QcO'#CtO$UOSO'#CvO%[QcO'#DsOOQa'#Ds'#DsO&bQcO'#DrO&yQRO'#CuO'XQcO'#DnO'pQbO'#D{OOQ`'#DO'#DOO'xQbO'#CsO(jOpO'#CqOOQ`'#Do'#DoO(oQbO'#DnO(}QbO'#EROOQ`'#DX'#DXO)lQRO'#DaOOQ`'#Dn'#DnO)qQQO'#DmOOQ`'#Dm'#DmOOQ`'#Db'#DbQYQbOOOOQa'#Dr'#DrOOQ`'#DS'#DSO)yQbO'#DUOOQ`'#EQ'#EQOOQ`'#Df'#DfO*TQbO,59[O*hQbO'#CxO*pQWO'#CyOOOO'#Du'#DuOOOO'#Dc'#DcO+UOSO,59bOOQa,59b,59bO(}QbO,59aO(}QbO,59aOOQ`'#Dd'#DdO+dQbO'#DPO+lQQO,5:gO+qQRO,59_O-WQRO'#CuO-hQRO,59_O-tQQO,59_O-yQQO,59_O.RObO,59]O.^QbO'#DgO.iQbO,59ZO.zQRO,5:mO/RQQO,5:mO/WQbO,59{OOQ`,5:X,5:XOOQ`-E7`-E7`OOQ`,59p,59pOOQ`-E7d-E7dOOOO,59d,59dOOOO,59e,59eOOOO-E7a-E7aOOQa1G.|1G.|OOQa1G.{1G.{O/bQcO1G.{OOQ`-E7b-E7bO/|QbO1G0ROOQa1G.y1G.yO(}QbO,59iO(}QbO,59iOOQa1G.w1G.wO!TQbO'#CtO$dQbO'#CpOOQ`,5:R,5:ROOQ`-E7e-E7eO0ZQbO1G0XOOQ`1G/g1G/gO0hQbO7+%mO0mQbO7+%nOOQO1G/T1G/TO0}QRO1G/TOOQ`'#DZ'#DZO1XQbO7+%sO1^QbO7+%tOOQ`<<IX<<IXOOQ`'#De'#DeO1tQQO'#DeO1yQbO'#EOO2aQbO<<IYOOQ`<<I_<<I_OOQ`'#D['#D[O2fQbO<<I`OOQ`,5:P,5:POOQ`-E7c-E7cOOQ`AN>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`<<Hi<<Hi",
stateData: "3v~O!_OS!`OS~O]PO^`O_SO`ZOaWOfSOnSOoSOpSO|^O!eYO!hQO!qcO~O]fO_SO`ZOaWOfSOnSOoSOpSOwgOyhO!eYO!hQOzhX!qhX!whX!shXuhX~OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~P!TOknO!hqO!jlO!kmO~O]fO_SO`ZOaWOfSOnSOoSOpSOwgOyhO!eYO!hQO~OP!gXQ!gXR!gXS!gX!q!gX!w!gXT!gXU!gXV!gXW!gXX!gXY!gXZ!gX[!gX!s!gXu!gX~P$dOP!fXQ!fXR!fXS!fX!q!bX!w!bXu!bX~OPrOQrORsOSsO~OPrOQrORsOSsO!q!bX!w!bXu!bX~O]tOtsP~O]PO_SO`ZOaWOfSOnSOoSOpSO!eYO!hQO~O!d|O~Oz}O!q!bX!w!bXu!bX~O]fO_SO`ZOfSOnSOoSOpSO!eYO!hQO~OV!RO~O!q!SO!w!SO~OaWOw!UO~P(}Ozda!qda!wda!sdauda~P$dO]!WO!eYO~O!h!XO!j!XO!k!XO!l!XO!m!XO!n!XO~OknO!h!ZO!jlO!kmO~O]tOtsX~Ot!_O~O!s!`OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~OT!bOU!bOV!aOW!aOX!aOY!aOZ!aO[!aO~OPrOQrORsOSsO~P,lOPrOQrORsOSsO!s!`O~Oz}O!s!`O~O]!cOf!cO!eYO~O]!dO`ZO!eYO~Oz}O!qca!wca!scauca~Ot!hO~P,lOt!hO~O^`O|^O~P'xOPrOQrORiiSii!qii!wii!siiuii~O^`O|^O!q!kO~P'xO^`O|^O!q!pO~P'xOu!qO~O^`O|^O!q!rOu!rP~P'xO!sqitqi~P,lOu!vO~O^`O|^O!q!rOu!rP!Q!rP!S!rP~P'xO!q!yO~O^`O|^O!q!rOu!rX!Q!rX!S!rX~P'xOu!{O~Ou#QO!Q!|O!S#PO~Ou#VO!Q!|O!S#PO~Ot#XO~Ou#VO~Ot#YO~P,lOt#YO~Ou#ZO~O!q#[O~O!q#]O~Ofo~",
goto: ",c!wPPPPPPPPPPPPPPPPPPP!x#X#gP$V#X${%bP%{%{PPP&P&]&vPP&yP&yPP'QP'^'a'jP'nP'Q't'z(Q(W(a(j(qPPPP(w({)aPP)s*pP+_PPPPP+c+cP+vP,O,V,VdaOe!R!_!h!k!p!t#[#]RzYi[OYe}!R!_!h!k!p!t#[#]fPOYe!R!_!h!k!p!t#[#]hfPR^hkrs!a!b!d!e!|R!d}fROYe!R!_!h!k!p!t#[#]hSPR^hkrs!a!b!d!e!|Q!WlQ!c|R!e}dVOe!R!_!h!k!p!t#[#]QyYQ![rR!]s!PSOPRY^ehkrs!R!_!a!b!d!e!h!k!p!t!|#[#]TnQpQzYQ!Q^Q!l!bR#T!|daOe!R!_!h!k!p!t#[#]YgPRk!d!eQzYR!UhRvWZiPRk!d!eeaOe!R!_!h!k!p!t#[#]R!o!hQ!x!pQ#^#[R#_#]T!}!x#OQ#R!xR#W#OQeOR!TeQpQR!YpQuWR!^uW!t!k!p#[#]R!z!tWkPR!d!eR!VkS!O]{R!g!OQ#O!xR#U#OTdOeSbOeQ!i!RQ!j!_Q!n!hZ!s!k!p!t#[#]d]Oe!R!_!h!k!p!t#[#]Q{YR!f}dUOe!R!_!h!k!p!t#[#]YgPRk!d!eQxYQ!P^Q!UhQ![rQ!]sQ!l!aQ!m!bR#S!|dTOe!R!_!h!k!p!t#[#]hfPR^hkrs!a!b!d!e!|RwYToQpsXOPRYehk!R!_!d!e!h!k!p!t#[#]Q!u!kV!w!p#[#]ZjPRk!d!ee_Oe!R!_!h!k!p!t#[#]",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq Neq Lt Lte Gt Gte Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Null ConditionalOp FunctionDef Params colon keyword PositionalArg Underscore NamedArg NamedArgPrefix operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 85,
context: trackScope,
@ -23,5 +23,5 @@ export const parser = LRParser.deserialize({
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!d~~", 11)],
topRules: {"Program":[0,18]},
specialized: [{term: 13, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 13, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
tokenPrec: 860
tokenPrec: 863
})

View File

@ -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
`)
})
})

View File

@ -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<string, Function>): Promise<T>
toEvaluateTo(
expected: unknown,
nativeFunctions?: Record<string, TypeScriptFunction>
): Promise<T>
toFailEvaluation(): Promise<T>
}
}
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<string, Function> = {}
) {
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 {
expect(value).toEqual(expected)
return {
message: () => `Expected evaluation to be ${expected}, but got ${value}`,
pass: false,
}
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 {