Parser: corpus parse rate 69% -> 81%, and stop --tests from sticking

Grammar: 'using,except(x)' modifiers, '#ifx' with block branches, unions with
parameters, multi-value initializers, mixed 'name:, lvalue = call()' targets,
'push_context' with no explicit context, 'operator []=', '#foreign lib "symbol"',
keyword-named arguments such as 'remove={...}', and '#insert (options) body'.
Directive flags now require adjacency: without it, in
'(callback: (*GUID) #c_call, lpContext: *void)' the flag swallowed ', lpContext'.

procModifier is now just directiveExpr — one rule for directives in both
positions, so fixes apply everywhere.

jaitest: a '--tests' filter persisted through Gradle's configuration cache, so a
later unfiltered ./jaitest silently re-ran only that class and printed OK.
Unfiltered runs now disable the configuration cache.

MIN_CLEAN_FILES ratchets the corpus gate at 575/714.
This commit is contained in:
hgranthorner
2026-08-04 12:20:36 -04:00
parent 840a503e40
commit 086ae993c5
24 changed files with 728 additions and 271 deletions

View File

@@ -4,6 +4,7 @@ import com.intellij.lang.PsiBuilder
import com.intellij.lang.parser.GeneratedParserUtilBase
import com.intellij.psi.TokenType
import com.intellij.psi.tree.IElementType
import dev.hgh.jai.lexer.JaiTokenTypes
import dev.hgh.jai.lexer.JaiTokenTypes as T
/**
@@ -170,6 +171,39 @@ object JaiParserUtil : GeneratedParserUtilBase() {
return true
}
/**
* A `,flag` suffix on a directive: `#import,file`, `#library,system,link_always`,
* `#run,stallable`.
*
* Adjacency is required. Without it, in
* `(callback: (*GUID) #c_call, lpContext: *void)` the flag would swallow
* `, lpContext` and the parameter list would fall apart. Every flag in the corpus
* is written tight against the directive; a separator comma always has a space.
*/
@JvmStatic
fun directiveFlag(
b: PsiBuilder,
level: Int,
): Boolean {
if (b.tokenType !== T.COMMA) return false
if (b.rawLookup(-1) === TokenType.WHITE_SPACE) return false
if (b.rawLookup(1) !== T.IDENT) return false
b.advanceLexer()
b.advanceLexer()
return true
}
/** `name =` inside an argument list, where `name` may be a keyword (`remove=`). */
@JvmStatic
fun isNamedArgumentAhead(
b: PsiBuilder,
level: Int,
): Boolean {
val t = b.tokenType ?: return false
if (t !== T.IDENT && t !in JaiTokenTypes.KEYWORDS) return false
return b.lookAhead(1) === T.EQ
}
// ------------------------------------------------------------------ operators
/**
@@ -291,7 +325,8 @@ object JaiParserUtil : GeneratedParserUtilBase() {
* `foo :: () {}` apart from `foo();`.
*
* The prefix skip covers `#as using base: Document;`,
* `#overlay (unknown.vtable) using vtable: *Vtable;` and the macro-exported
* `#overlay (unknown.vtable) using vtable: *Vtable;`,
* `#as using,except(vtable) iunknown: IUnknown;` and the macro-exported
* `` `it := entry.value; ``.
*/
@JvmStatic
@@ -300,10 +335,12 @@ object JaiParserUtil : GeneratedParserUtilBase() {
level: Int,
): Boolean {
var i = 0
var inPrefix = false
loop@ while (true) {
when (b.lookAhead(i)) {
T.DIRECTIVE, T.KW_USING -> {
i++
inPrefix = true
}
T.LPAREN -> {
@@ -311,6 +348,14 @@ object JaiParserUtil : GeneratedParserUtilBase() {
if (i < 0) return false
}
// Only a `,flag` belonging to the prefix, as in `using,except(x)`.
// A comma between declared names is handled by the loop below.
T.COMMA -> {
if (!inPrefix || b.lookAhead(i + 1) !== T.IDENT) break@loop
i += 2
inPrefix = false
}
else -> {
break@loop
}