Skip to content

Fix cross-file const inlining and flag const cycles#1680

Merged
chrisdp merged 4 commits intomasterfrom
fix/issue-1618-const-aa-literal-cross-file
Apr 27, 2026
Merged

Fix cross-file const inlining and flag const cycles#1680
chrisdp merged 4 commits intomasterfrom
fix/issue-1618-const-aa-literal-cross-file

Conversation

@chrisdp
Copy link
Copy Markdown
Contributor

@chrisdp chrisdp commented Apr 27, 2026

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 namespace

Transpiled output (before this fix):

return ({
    "key1": someEnum.one  ' ← never replaced; crashes at runtime
})[key]

Why same-file worked but cross-file didn't

BrsFilePreTranspileProcessor.iterateExpressions walks the consumer file's parser.references.expressions and overrides each VariableExpression / DottedGetExpression's transpile to 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 default someEnum.one text.

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 inner VariableExpression / 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 issue
  • resolves enum refs in computed keys of an aa-literal const used cross-file — covers [someEnum.X]: value form

chrisdp added 2 commits April 27, 2026 13:27
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.
chrisdp added 2 commits April 27, 2026 14:00
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.
@chrisdp chrisdp changed the title Fix cross-file const inlining for aa-literal values containing enum refs (#1618) Fix cross-file const inlining and flag const cycles Apr 27, 2026
@chrisdp chrisdp merged commit c79159d into master Apr 27, 2026
7 checks passed
@chrisdp chrisdp deleted the fix/issue-1618-const-aa-literal-cross-file branch April 27, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Constants not properly transpiling across files

2 participants