Skip to content

Support conditional compilation (#if) inside class bodies#1744

Draft
chrisdp wants to merge 1 commit into
v1from
fix/conditional-compile-in-class-body
Draft

Support conditional compilation (#if) inside class bodies#1744
chrisdp wants to merge 1 commit into
v1from
fix/conditional-compile-in-class-body

Conversation

@chrisdp

@chrisdp chrisdp commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

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 (ConditionalCompileStatement) handled by the parser at block and file-root level, but classDeclaration() 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.):

class Person
    sub speak()
    end sub

    #if DEBUG
        sub debugSpeak()
            print "debug"
        end sub
    #end if
end class

Changes

  • Parser: class bodies now accept #if blocks. The class-member parsing was extracted into classMemberDeclaration() and reused by a new classBodyConditionalCompileBlock(), which parses branch contents in a class-member context (methods, fields, annotations, nested #if). It plugs into the existing conditionalCompileStatement() machinery via an injected branch parser, so #else if chains, not conditions, false-branch diagnostic suppression, and the token-position contract all behave exactly like function-body #if.
  • ClassStatement: methods/fields/memberMap now register members found inside conditional compile branches, so the type system and tooling can see them (e.g. calling a conditional method via m. from another method validates cleanly).
  • Transpile:
    • methods and builder assignments are emitted wrapped in the equivalent #if/#else if/#else/#end if directives (resolved by the Roku compiler from manifest bs_const, same as function-body #if)
    • field initializers injected into the constructor are wrapped in an equivalent ConditionalCompileStatement
    • field-only branches don't emit empty #if/#end if shells in the methods/builder sections

Example transpile output:

sub __Animal_method_new()
    m.name = "generic"
    #if DEBUG
        m.logLevel = 4
    #else
        m.logLevel = 0
    #end if
end sub
#if DEBUG
    sub __Animal_method_debugSpeak()
        print "debug"
    end sub
#end if
function __Animal_builder()
    instance = {}
    instance.new = __Animal_method_new
    #if DEBUG
        instance.debugSpeak = __Animal_method_debugSpeak
    #end if
    return instance
end function

Known limitations

  • Constructors (sub new()) inside #if blocks 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.bs typedefs.

Testing

  • 12 new tests (8 parser, 4 transpile/validation): methods, fields, #else if chains, nesting, empty blocks, annotations on conditional members, class-type membership, unterminated-block diagnostics
  • full suite passes (4264 passing, 0 failures), lint clean
  • validated against a large production codebase (fubo's Roku app) where the v0→v1 migration surfaced this: the parse-error cascade is resolved with the full plugin stack loaded

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.
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.

1 participant