SnippetParser
Parses a snippet as what execute_code promises it is: imports, then a FUNCTION BODY.
KotlinParsing.parseScript parses the same text under class-body rules — a property may carry a getter or a setter, statements may share a line — and the analysis then treats every top-level declaration as a member, so no smart cast crosses from one statement to the next and a top-level var never smart-casts at all. The one entry that parses statements under function-body rules, parseBlockCodeFragment, belongs to the debugger and rejects imports written into the text: its code fragments take them out of band. So this is parseScript with its single switch flipped, and one marker added:
KT_FILE
├─ IMPORT_LIST …
└─ SCRIPT
└─ BLOCK
└─ SCRIPT_INITIALIZER ← ONE, around the whole body
├─ PROPERTY ← parsed as a local: no accessors, same-line statements need a `;`
└─ …That one marker is what lets the stock light-tree converter do all the rest: it turns the initializer into a block the way it turns any function body, and every "where am I" check it makes reads the statements as local. parsePsi builds the same tree as PSI, for analysis, with the two changes the PSI converter forces.
Three members are out of reach. The two parser factories are package-private: createForTopLevelNonLazy, the only way to a non-lazy parser — the lazy one collapses function bodies into nodes light tree never expands — and createForTopLevel, the lazy one PSI needs. parsePreamble is private, which no compiler flag or package placement can open. All three are resolved once, by the constructor, and PlainEngine and the analysis session each hold an instance for the life of the process. The constructor rather than a static initializer: a lookup that fails there throws out of warm-up with its real cause every time it is retried, where a failed class initializer is reported on JDK 17 as a bare NoClassDefFoundError from its second use on. The compiler is our own pinned copy, so they can only go missing when this build moves Kotlin — and then warm-up fails on its first line, naming the member.
invoke, not invokeExact: Kotlin gives a polymorphic call its return type only from an as applied directly to it, and parsePreamble returns void, which no Kotlin expression spells. Once per eval, so the adaptation invoke does costs nothing that matters.
Functions
Parse code into a PSI tree, for analysis: the same statements under the same rules, with two changes the PSI converter forces. The parse is lazy, as the stock PSI parse is: a lambda's element type makes its PSI only out of a collapsed node, so an eagerly built lambda fails the first time anything asks it for its PSI. And the body gets a BLOCK of its own inside the initializer: KtScriptInitializer.body is the initializer's first expression child, so without one a body of two statements would convert as its first. Syntax errors stay in the tree as error elements, as in any PSI parse. collectDiagnostics never reports them: whoever shows diagnostics walks the tree for them, as the CLI's SyntaxErrorReporter does.