diff --git a/.gitignore b/.gitignore index c3cfc7c4..732e3ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ lib/ types/ node_modules/ .vscode/ -coverage/ \ No newline at end of file +coverage/ +.DS_Store \ No newline at end of file diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 1558a2a4..456143a8 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -74,6 +74,7 @@ Object.freeze(defaultExecutionOptions); export class Interpreter implements Expr.Visitor, Stmt.Visitor { private _environment = new Environment(); + private _lastDotGetAA: RoAssociativeArray = this._environment.getRootM(); private coverageCollector: CoverageCollector | null = null; private _manifest: PP.Manifest | undefined; @@ -1093,21 +1094,27 @@ export class Interpreter implements Expr.Visitor, Stmt.Visitor expression.callee instanceof Expr.DottedGet || expression.callee instanceof Expr.IndexedGet ) { - let maybeM = this.evaluate(expression.callee.obj); - maybeM = isBoxable(maybeM) ? maybeM.box() : maybeM; - - if (maybeM.kind === ValueKind.Object) { - if (maybeM instanceof RoAssociativeArray) { - mPointer = maybeM; - } + if (expression.callee.obj instanceof Expr.Call) { + mPointer = this._lastDotGetAA; } else { - return this.addError( - new BrsError( - "Attempted to retrieve a function from a primitive value", - expression.closingParen.location - ) - ); + let maybeM = this.evaluate(expression.callee.obj); + maybeM = isBoxable(maybeM) ? maybeM.box() : maybeM; + + if (maybeM.kind === ValueKind.Object) { + if (maybeM instanceof RoAssociativeArray) { + mPointer = maybeM; + } + } else { + return this.addError( + new BrsError( + "Attempted to retrieve a function from a primitive value", + expression.closingParen.location + ) + ); + } } + } else { + this._lastDotGetAA = this.environment.getRootM(); } return this.inSubEnv((subInterpreter) => { @@ -1182,6 +1189,9 @@ export class Interpreter implements Expr.Visitor, Stmt.Visitor if (isIterable(source)) { try { + if (source instanceof RoAssociativeArray) { + this._lastDotGetAA = source; + } return source.get(new BrsString(expression.name.text)); } catch (err: any) { return this.addError(new BrsError(err.message, expression.name.location)); diff --git a/test/e2e/Syntax.test.js b/test/e2e/Syntax.test.js index c4f10650..f55fc39b 100644 --- a/test/e2e/Syntax.test.js +++ b/test/e2e/Syntax.test.js @@ -201,9 +201,8 @@ describe("end to end syntax", () => { "run", "stop", "then", - "promise-like resolved to 'foo'", "Hello from line ", - "18", + "14", ]); }); @@ -216,4 +215,14 @@ describe("end to end syntax", () => { "14", // arr = [13]: arr[0]++ ]); }); + + test("dot-chaining.brs", async () => { + await execute([resourceFile("dot-chaining.brs")], outputStreams); + + expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([ + "removed number '3' from array, remaining 2", + "promise-like resolved to 'foo'", + "optional chaining works", + ]); + }); }); diff --git a/test/e2e/resources/dot-chaining.brs b/test/e2e/resources/dot-chaining.brs new file mode 100644 index 00000000..73404f86 --- /dev/null +++ b/test/e2e/resources/dot-chaining.brs @@ -0,0 +1,36 @@ +sub main() + a = [" 1 ", " 2 ", " 3 "] + b = a.pop().trim().toInt() + print "removed number '" + b.toStr() + "' from array, remaining " + a.count().toStr() + m.__result = "bad" + immediatePromise("foo").then(sub(result) + print "promise-like resolved to '" + result + "'" + end sub) + print "optional chaining " + testOptionalChaining() +end sub + +' A simple promise-like function that immediately resolves to the provided value. +' You probably don't want to use it in production. +' @param {*} val the value this promise-like should immediately resolve to +' @returns {AssociativeArray} an associative array contianing a `then` property, used to chain +' promise-like constructs. +function immediatePromise(val as dynamic) as object + return { + __result: val + then: sub(resolved as function) + resolved(m.__result) + end sub + } +end function + +function testOptionalChaining() + responseData = { + data:{ + metricsData:{ + addOnsStepStart : "works" + } + } + } + result = responseData?.data.metricsData?.addOnsStepStart + return result +end function diff --git a/test/e2e/resources/multi-file/test1.brs b/test/e2e/resources/multi-file/test1.brs index 39a633c5..8b6146b0 100644 --- a/test/e2e/resources/multi-file/test1.brs +++ b/test/e2e/resources/multi-file/test1.brs @@ -2,7 +2,6 @@ sub Main() print("function in same file: " + sameFileFunc()) print("function in different file: " + differentFileFunc()) print("function with dependency: " + dependentFunc()) - print("result" + testOptionalChaining()) end sub function sameFileFunc() @@ -13,14 +12,3 @@ function dependencyFunc() return "from dependencyFunc()" end function -function testOptionalChaining() - result = responseData?.data.metricsData?.addOnsStepStart - responseData = { - data:{ - metricsData:{ - addOnsStepStart : "print" - } - } - } - return result -end function diff --git a/test/e2e/resources/reserved-words.brs b/test/e2e/resources/reserved-words.brs index d078ea20..a66f5429 100644 --- a/test/e2e/resources/reserved-words.brs +++ b/test/e2e/resources/reserved-words.brs @@ -11,23 +11,5 @@ sub main() print word end for - immediatePromise("foo").then(sub(result) - print "promise-like resolved to '" + result + "'" - end sub) - print "Hello from line " LINE_NUM end sub - -' A simple promise-like function that immediately resolves to the provided value. -' You probably don't want to use it in production. -' @param {*} val the value this promise-like should immediately resolve to -' @returns {AssociativeArray} an associative array contianing a `then` property, used to chain -' promise-like constructs. -function immediatePromise(val as dynamic) as object - return { - __result: val - then: sub(resolved as function) - resolved(m.__result) - end sub - } -end function