Fix cross-file const inlining and flag const cycles#1680
Merged
Conversation
When a const's value is an aa-literal (or other complex expression) containing enum or const references, and the const is consumed from a different file than where it was defined, the inner refs were left unresolved in the transpile output. The same-file case worked only by coincidence: the consumer file's pre-transpile pass happened to walk the const's children. Cross-file, those AST nodes live in the defining file and were never visited by the consumer's pass. When the resolved const value is non-literal, walk it and apply the same const/enum substitution to inner refs, scoped to the const statement's own containing namespace so inner refs resolve where they were authored. Fixes #1618
The fix in the parent commit walks all VariableExpression and DottedGetExpression nodes inside a non-literal const value, which includes both the value and key slots of aa members. Cover the computed key path explicitly so a future regression can't slip through.
The cross-file aa-literal fix recursively walks a const's value to
inline inner enum/const refs. With circular const refs whose values are
aggregate (e.g. const A = { x: B }; const B = { y: A }), that walk
recursed forever and overflowed the stack.
Thread a Set<ConstStatement> through the recursion. On cycle detection,
skip both the inner walk and the outer transpile override so the cyclic
ref emits literally instead of producing a transpile-time cycle. This
matches the existing scalar circular const handling (resolveConstValue
returns isCircular=true and the override is skipped).
Side benefit: also short-circuits diamond reference graphs where the
same const is reachable via multiple paths in one inline.
Tests cover three rewalk scenarios: multi-consumer (idempotent override),
diamond graph (skip on revisit), and circular cycle (no infinite loop).
Detect cycles in the const reference graph during scope validation and
emit the existing circularReferenceDetected diagnostic (bs1132). Covers
both scalar cycles (const A = B; const B = A) and aggregate cycles
(const A = { x: B }; const B = { y: A }) — previously the former was
silently broken at runtime and the latter was caught at transpile time
only by the cycle-protection guard, with no user-facing signal.
DFS walks each const's value tree following references that resolve to
other consts; on revisit of an already-stacked const, reports the cycle
on its declaration name. Cycles are deduplicated via the lexicographically-
smallest fullName so an N-cycle reports once instead of N times.
Updates an existing Enum.spec.ts test that incidentally used a circular
const setup to expect the new diagnostic alongside the existing
computed-key error — flagging the root cause is strictly better signal.
TwitchBronBron
approved these changes
Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1618.
When a
const's value is an aa-literal (or other complex expression) containing enum or const references, and the const is consumed from a different file than where it was defined, the inner refs were left unresolved in the transpiled output. For example:' map.bs namespace name.space enum someEnum one = "val1" end enum const myMap = { "key1": someEnum.one } end namespace ' helper.bs — different file namespace name.space class someClass public function someFunc(key as dynamic) as object return name.space.myMap[key] end function end class end namespaceTranspiled output (before this fix):
Why same-file worked but cross-file didn't
BrsFilePreTranspileProcessor.iterateExpressionswalks the consumer file'sparser.references.expressionsand overrides each VariableExpression / DottedGetExpression'stranspileto inline the resolved const/enum literal. When the const and consumer are in the same file, that pass coincidentally also visits the inner enum refs nested inside the const's aa-literal value. Cross-file, those AST nodes live in the defining file and the consumer's pass never visits them, so they emit their defaultsomeEnum.onetext.Fix
In
processExpressionInNamespace, after resolving a const reference, if the resolved value is non-literal, walk it and apply the same const/enum substitution to every innerVariableExpression/DottedGetExpression(skipping inner parts of dotted chains, which are handled at the chain level). The walk is scoped to the const statement's containing namespace, since that's where the inner refs were authored, not the consumer's namespace.The substitution uses the existing
editor.setProperty('transpile', ...)mechanism; the editor undoes those overrides after the consumer file's transpile finishes, so the const's defining file isn't affected on subsequent transpile passes.Tests
Two new tests in
ConstStatement.spec.ts:resolves enum refs inside an aa-literal const used cross-file (issue #1618)— direct repro from the issueresolves enum refs in computed keys of an aa-literal const used cross-file— covers[someEnum.X]: valueform