Tier 2 golden trees, and #scope_file is a statement not a modifier

isDeclarationAhead used to skip *any* directive, so '#scope_file' was absorbed as
a modifier of the declaration after it. That misreports scope, since a scope
directive governs everything that follows it. Only #as, #overlay, #add_context
and #no_reset actually prefix a declaration in the corpus; the rest are
statements now, which the regenerated Directives.txt shows.

Golden fixtures cover the reference doc's gotcha list: declaration forms,
procedures (named/multiple/variadic/polymorphic/foreign returns), structs, enums,
unions, array and pointer types, the 'if x == { case }' switch form, for
modifiers, ifx, and directives including a here-string.

DebugParseTest is now inert by default and documented as the scratch loop.

generateParser wipes its output through a Delete task rather than a doFirst,
because a lambda added from a Kotlin build script captures the script object and
breaks the configuration cache ('./jaigradle check' failed to store it).

check and verifyPlugin (IU-253/261/262) both green.
This commit is contained in:
hgranthorner
2026-08-04 12:48:30 -04:00
parent 47a2c5805e
commit e3ff13f9f3
20 changed files with 1050 additions and 221 deletions

View File

@@ -91,6 +91,17 @@ object JaiParserUtil : GeneratedParserUtilBase() {
"this",
)
/**
* Directives that modify the declaration that follows them, rather than standing
* alone as a statement.
*
* The distinction matters for the tree shape: `#scope_file` changes visibility for
* everything *after* it, so attaching it to the next declaration would misreport
* that declaration's scope. Taken from the corpus, where only these four appear
* directly in front of a `name :` / `name ::` / `name :=`.
*/
private val DIRECTIVES_ON_DECLARATIONS = setOf("as", "overlay", "add_context", "no_reset")
// ---------------------------------------------------------------- directives
/**
@@ -343,7 +354,13 @@ object JaiParserUtil : GeneratedParserUtilBase() {
var inPrefix = false
loop@ while (true) {
when (b.lookAhead(i)) {
T.DIRECTIVE, T.KW_USING -> {
T.DIRECTIVE -> {
if (!isDeclarationDirective(b, i)) break@loop
i++
inPrefix = true
}
T.KW_USING -> {
i++
inPrefix = true
}
@@ -378,6 +395,20 @@ object JaiParserUtil : GeneratedParserUtilBase() {
}
}
/**
* Whether the DIRECTIVE at lookahead [index] modifies the declaration that
* follows. Only the first token can be read as text through the builder, which is
* enough: a declaration-modifying directive is always the first thing we look at.
*/
private fun isDeclarationDirective(
b: PsiBuilder,
index: Int,
): Boolean {
if (index != 0) return true
val text = b.tokenText ?: return false
return text.removePrefix("#") in DIRECTIVES_ON_DECLARATIONS
}
/**
* Index just past the closer matching the bracket at [start], or -1 if unbalanced.
* Covers `(…)`, `[…]`, `.[…]` and `.{…}`, which all appear in declaration
@@ -479,6 +510,22 @@ object JaiParserUtil : GeneratedParserUtilBase() {
}
}
/**
* True when the next token cannot start a polymorph capture.
*
* An unparenthesised return list is comma-separated, and so is the parameter list
* that may enclose it. A `$T` after the comma can only be a parameter, so the
* return list has to stop there.
*/
@JvmStatic
fun notPolyParamAhead(
b: PsiBuilder,
level: Int,
): Boolean {
val t = b.tokenType
return t !== T.DOLLAR && t !== T.DOUBLE_DOLLAR
}
/** `IDENT ':'` — a loop label. */
@JvmStatic
fun isLabelAhead(