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:
@@ -1,7 +1,6 @@
|
||||
package dev.hgh.jai.parser
|
||||
|
||||
import com.intellij.psi.PsiErrorElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiFileFactory
|
||||
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
@@ -9,70 +8,77 @@ import dev.hgh.jai.JaiLanguage
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Scratch harness for grammar work. Not a gate — the gate is [JaiCorpusParserTest].
|
||||
* Edit [snippets] and [corpusFiles] freely while iterating on `Jai.bnf`.
|
||||
* Scratch harness for grammar work. **Not a gate** — the gate is [JaiCorpusParserTest].
|
||||
*
|
||||
* Both lists are empty on purpose, so this is silent until someone is working on
|
||||
* `Jai.bnf`. To iterate: paste failing snippets into [snippets] or corpus-relative
|
||||
* paths into [corpusFiles], run `./jaitest`, and read the printed first error. Add
|
||||
* `-Djai.debug.tree=1` to dump the PSI tree as well.
|
||||
*
|
||||
* The loop this supports is: read the sample errors printed by [JaiCorpusParserTest],
|
||||
* reduce one to a snippet here, fix the grammar, then re-check the corpus percentage.
|
||||
*/
|
||||
class DebugParseTest : BasePlatformTestCase() {
|
||||
private val snippets =
|
||||
listOf(
|
||||
"T :: struct {\n p := .06;\n b: BG;\n b.shape.kind = .ABS;\n b.color = V4.{.00, .10, 1};\n}\n",
|
||||
"T :: struct {\n orientation: enum u8 {\n H :: 0;\n V :: 1;\n }\n\n d: BT;\n}\n",
|
||||
"T :: struct {\n using,except .[\"x\"] orientation: Quaternion;\n}\n",
|
||||
"operator *[] :: (b: *Bucket, index: int) -> *int { return null; }\n",
|
||||
"code :: #code a := Vector3.{1, 1, 1};\n",
|
||||
)
|
||||
/** Jai source snippets to parse. */
|
||||
private val snippets = emptyList<String>()
|
||||
|
||||
/** Corpus files to dump the first error of. */
|
||||
private val corpusFiles =
|
||||
listOf(
|
||||
"modules/GetRect/widgets/color_picker.jai",
|
||||
"modules/GetRect/widgets/slidable_region.jai",
|
||||
"how_to/094_array_operators.jai",
|
||||
"how_to/044_using_advanced/main.jai",
|
||||
"how_to/630_compiler_get_nodes.jai",
|
||||
"modules/Bucket_Array.jai",
|
||||
"modules/Android/Toolchain/adb.jai",
|
||||
"modules/Hash_Table.jai",
|
||||
)
|
||||
/** Paths under `~/.local/jai` to parse. */
|
||||
private val corpusFiles = emptyList<String>()
|
||||
|
||||
fun testSnippets() {
|
||||
val factory = PsiFileFactory.getInstance(project)
|
||||
for (text in snippets) {
|
||||
val file = factory.createFileFromText("scratch.jai", JaiLanguage, text)
|
||||
val error = errorsIn(file).firstOrNull()
|
||||
val label = text.replace("\n", "\\n")
|
||||
if (error == null) {
|
||||
println("ok $label")
|
||||
} else {
|
||||
println("FAIL $label")
|
||||
println(" ${error.errorDescription} | at '${around(text, error.textOffset)}'")
|
||||
}
|
||||
if (System.getProperty("jai.debug.tree") != null) {
|
||||
println(
|
||||
com.intellij.psi.impl.DebugUtil
|
||||
.psiToString(file, true, false),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun testCorpusFiles() {
|
||||
val root = File(System.getProperty("user.home"), ".local/jai")
|
||||
if (!root.exists()) return
|
||||
for (relative in corpusFiles) {
|
||||
val file = File(root, relative)
|
||||
if (file.exists()) dumpFile(file) else println("missing: $relative")
|
||||
}
|
||||
}
|
||||
|
||||
fun testSnippets() {
|
||||
val factory = PsiFileFactory.getInstance(project)
|
||||
var failures = 0
|
||||
for (text in snippets) {
|
||||
val file = factory.createFileFromText("t.jai", JaiLanguage, text)
|
||||
val error = firstError(file)
|
||||
if (error == null) {
|
||||
println("ok ${text.replace("\n", "\\n")}")
|
||||
if (System.getProperty("jai.debug.tree") != null) {
|
||||
println(com.intellij.psi.impl.DebugUtil.psiToString(file, true, false))
|
||||
}
|
||||
} else {
|
||||
failures++
|
||||
val at = text.substring(error.textOffset).take(28).replace("\n", "\\n")
|
||||
println("FAIL ${text.replace("\n", "\\n")}")
|
||||
println(" ${error.errorDescription} | at '$at'")
|
||||
if (System.getProperty("jai.debug.tree") != null) {
|
||||
println(com.intellij.psi.impl.DebugUtil.psiToString(file, true, false))
|
||||
}
|
||||
if (!file.exists()) {
|
||||
println("missing: $relative")
|
||||
continue
|
||||
}
|
||||
val text = file.readText()
|
||||
val psi = PsiFileFactory.getInstance(project).createFileFromText(file.name, JaiLanguage, text)
|
||||
val errors = errorsIn(psi)
|
||||
if (errors.isEmpty()) {
|
||||
println("$relative: clean")
|
||||
continue
|
||||
}
|
||||
println("$relative: ${errors.size} error(s)")
|
||||
for (error in errors.take(REPORTED_ERRORS)) {
|
||||
val line = text.substring(0, error.textOffset).count { it == '\n' } + 1
|
||||
println(" :$line ${error.errorDescription}")
|
||||
println(" at '${around(text, error.textOffset)}'")
|
||||
}
|
||||
}
|
||||
println("$failures/${snippets.size} snippets failed")
|
||||
}
|
||||
|
||||
private fun dumpFile(file: File) {
|
||||
val text = file.readText()
|
||||
val psi = PsiFileFactory.getInstance(project).createFileFromText(file.name, JaiLanguage, text)
|
||||
private fun around(
|
||||
text: String,
|
||||
offset: Int,
|
||||
): String = text.substring(offset).take(SNIPPET_WIDTH).replace("\n", "\\n")
|
||||
|
||||
private fun errorsIn(file: com.intellij.psi.PsiFile): List<PsiErrorElement> {
|
||||
val errors = mutableListOf<PsiErrorElement>()
|
||||
psi.accept(
|
||||
file.accept(
|
||||
object : PsiRecursiveElementWalkingVisitor() {
|
||||
override fun visitErrorElement(element: PsiErrorElement) {
|
||||
errors.add(element)
|
||||
@@ -80,28 +86,11 @@ class DebugParseTest : BasePlatformTestCase() {
|
||||
}
|
||||
},
|
||||
)
|
||||
if (errors.isEmpty()) {
|
||||
println("${file.name}: clean")
|
||||
return
|
||||
}
|
||||
println("${file.name}: ${errors.size} error(s)")
|
||||
for (error in errors.take(6)) {
|
||||
val line = text.substring(0, error.textOffset).count { it == '\n' } + 1
|
||||
println(" :$line ${error.errorDescription}")
|
||||
println(" at '${text.substring(error.textOffset).take(70).replace("\n", "\\n")}'")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private fun firstError(file: PsiFile): PsiErrorElement? {
|
||||
var first: PsiErrorElement? = null
|
||||
file.accept(
|
||||
object : PsiRecursiveElementWalkingVisitor() {
|
||||
override fun visitErrorElement(element: PsiErrorElement) {
|
||||
if (first == null) first = element
|
||||
super.visitErrorElement(element)
|
||||
}
|
||||
},
|
||||
)
|
||||
return first
|
||||
private companion object {
|
||||
const val REPORTED_ERRORS = 6
|
||||
const val SNIPPET_WIDTH = 70
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
package dev.hgh.jai.parser
|
||||
|
||||
/**
|
||||
* Tier 2 (docs/BUILD_PLAN.md §3): golden PSI trees for the constructs listed as
|
||||
* gotchas in `docs/JAI_LANGUAGE_REFERENCE.md` §14.
|
||||
*
|
||||
* The corpus gate proves the grammar *accepts* real code; these prove it builds the
|
||||
* tree we intend, so a refactor that quietly reshapes the PSI shows up as a diff.
|
||||
*
|
||||
* `ParsingTestCase` writes the expected `.txt` when it is missing, which makes adding
|
||||
* a case cheap — but the generated tree must be read before committing, or the test
|
||||
* asserts nothing.
|
||||
*/
|
||||
class JaiParserGoldenTest : JaiParsingTestCase() {
|
||||
fun testSimpleDeclarations() {
|
||||
doTest(true)
|
||||
}
|
||||
fun testDeclarations() = doTest(true)
|
||||
|
||||
fun testExpressions() {
|
||||
doTest(true)
|
||||
}
|
||||
fun testProcedures() = doTest(true)
|
||||
|
||||
fun testTypes() = doTest(true)
|
||||
|
||||
fun testControlFlow() = doTest(true)
|
||||
|
||||
fun testDirectives() = doTest(true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user