Support conditional compilation (#if) inside class bodies - #1744
Conversation
In v0, conditional compilation was handled by a preprocessor that ran
before parsing, so #if blocks worked anywhere - including class bodies.
In v1, #if became a first-class AST statement handled by the parser at
block and file-root level, but class bodies were never taught about it,
producing a cascade of parse errors ("expected-end-keyword", "Unexpected
token '#end if'", etc.) for previously-valid code.
Changes:
- Parser: class bodies now accept #if blocks. Branches are parsed in a
class-member context (methods, fields, annotations, nested #if) and
follow the same token-position contract as regular conditional compile
blocks.
- ClassStatement: methods/fields/memberMap now register members found
inside conditional compile branches, so the type system and tooling
can see them.
- Transpile: methods and builder assignments are emitted wrapped in the
equivalent #if/#else if/#else/#end if directives. Field initializers
injected into the constructor are wrapped in an equivalent conditional
compile statement. Field-only branches don't emit empty directives in
the methods/builder sections.
markwpearce
left a comment
There was a problem hiding this comment.
Ran a review pass on this, a few things worth a look:
src/parser/Statement.ts:2849 (getConstructorFunction) - this only looks at the top-level this.body, so it doesn't find a new() that's only declared inside a #if block. Combined with registerMembers now descending into #if branches, you can end up with a class like:
class Animal
#if DEBUG
sub new()
end sub
#end if
end class
getConstructorFunction() says "no constructor exists", so a synthetic empty one gets injected... but the conditional one still transpiles too. Actually tried this - when DEBUG is true you get two sub __Animal_method_new() defs in the output. That's not just a "known limitation", it's invalid brs that won't compile on-device, and nothing flags it.
src/parser/Parser.ts:728 (classMemberDeclaration) - the early return for #if happens before the annotation-consumption logic at the bottom of the function. So an annotation right before a #if block doesn't attach to anything inside it - it just leaks onto whatever comes after the block instead. Confirmed this: @deprecated before a #if-wrapped method silently attaches to the next method instead, no diagnostic at all. Worse than the "annotation not attached to anything" case since there's zero signal something went wrong.
src/parser/Statement.ts:2692 (registerMembers) / :3087 (getTranspiledConditionalCompileMembers) - more of a maintainability thing than a bug: the #if/#else if/#else branch tree gets walked by three separate hand-rolled recursive functions now (registration, transpile, field-initializer injection). Any future fix to how elseBranch chains work has to be applied in all three or one of them silently breaks. Also registerMembers flattens conditional members into methods/fields/memberMap once at construction - if body gets mutated later (plugins, editor splices) those arrays go stale.
Not blocking necessarily, but the first two are silent-bad-output bugs, would want those fixed or at least diagnosed before merge.
Problem
In v0, conditional compilation was handled by a preprocessor that ran before parsing, so
#ifblocks worked anywhere — including class bodies. In v1,#ifbecame a first-class AST statement (ConditionalCompileStatement) handled by the parser at block and file-root level, butclassDeclaration()was never taught about it. Code like this (valid in v0, common in test suites and debug tooling) produces a cascade of parse errors in v1 (expected-end-keyword,Unexpected token '#end if',unexpected-statement, etc.):Changes
#ifblocks. The class-member parsing was extracted intoclassMemberDeclaration()and reused by a newclassBodyConditionalCompileBlock(), which parses branch contents in a class-member context (methods, fields, annotations, nested#if). It plugs into the existingconditionalCompileStatement()machinery via an injected branch parser, so#else ifchains,notconditions, false-branch diagnostic suppression, and the token-position contract all behave exactly like function-body#if.methods/fields/memberMapnow register members found inside conditional compile branches, so the type system and tooling can see them (e.g. calling a conditional method viam.from another method validates cleanly).#if/#else if/#else/#end ifdirectives (resolved by the Roku compiler from manifestbs_const, same as function-body#if)ConditionalCompileStatement#if/#end ifshells in the methods/builder sectionsExample transpile output:
Known limitations
sub new()) inside#ifblocks are not supported — builder/class-function generation assumes a single known constructor signature (unchanged from current behavior).getTypedef()does not yet emit conditional members into.d.bstypedefs.Testing
#else ifchains, nesting, empty blocks, annotations on conditional members, class-type membership, unterminated-block diagnostics