package dev.hgh.jai.parser 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 /** * Hand-written helpers referenced from `Jai.bnf` as external rules. * * These exist because a few Jai constructs are not expressible as pure BNF over our * token set: * * - `#ident` is a single DIRECTIVE token, so "is this `#if`?" is a *text* test. * - `if x == { case ...; }` reuses `==` as a switch introducer, so the expression * parser must refuse `==` when a `{` follows. * - `(` starts both a parenthesised expression and a procedure header. * - A declaration has no introducer keyword, so `foo :: ...` vs `foo(...)` needs * lookahead. * * Everything here is pure lookahead except the `dir*` family, which consume exactly * one DIRECTIVE token. */ object JaiParserUtil : GeneratedParserUtilBase() { /** * Directives whose operand is a brace-delimited block, e.g. `#asm { ... }`. * Tried before [DIRECTIVES_WITH_OPERAND] so `#run { ... }` takes the block form * while `#run foo()` falls through to the expression form. */ private val DIRECTIVES_WITH_BLOCK = setOf( "asm", "modify", "run", "insert", "code", "run_and_insert", "compile_time", "bytes", "no_reset", ) /** * Directives that take a single expression operand, e.g. `#import "Basic"`. * The operand is optional: `#foreign` appears both bare and as `#foreign lib`. * Anything not listed here is parsed as a bare directive so that, for example, * `#scope_file` cannot swallow the declaration that follows it. */ private val DIRECTIVES_WITH_OPERAND = setOf( "import", "load", "run", "assert", "insert", "code", "type", "char", "library", "system_library", "foreign_library", "foreign_system_library", "align", "placeholder", "placeholders", "bake", "bake_arguments", "bake_constants", "deprecated", "add_context", "module_parameters", "place", "overlay", "procedure_of_call", "body_text", "version", "poke_name", "dump", "foreign", "define", "include", "undef", "ifdef", "pragma", "elsewhere", "expand", "specified", "location", "this", ) // ---------------------------------------------------------------- directives /** * Consumes the DIRECTIVE token if its text matches exactly. * * [text] includes the `#`: Grammar-Kit rewrites a `<>` argument into a * reference to the `if` *token* because `'if'` is a declared token text, so the * grammar has to say `<>`. */ @JvmStatic fun dir( b: PsiBuilder, level: Int, text: String, ): Boolean { if (b.tokenType !== T.DIRECTIVE) return false if (b.tokenText != text) return false b.advanceLexer() return true } /** True (without consuming) if the current token is the directive [text] (with `#`). */ @JvmStatic fun dirAhead( b: PsiBuilder, level: Int, text: String, ): Boolean = b.tokenType === T.DIRECTIVE && b.tokenText == text @JvmStatic fun dirWithBlock( b: PsiBuilder, level: Int, ): Boolean = consumeDirectiveIn(b, DIRECTIVES_WITH_BLOCK) @JvmStatic fun dirWithOperand( b: PsiBuilder, level: Int, ): Boolean = consumeDirectiveIn(b, DIRECTIVES_WITH_OPERAND) /** Any DIRECTIVE token; used for the bare `#complete` / `#scope_file` form. */ @JvmStatic fun dirPlain( b: PsiBuilder, level: Int, ): Boolean { if (b.tokenType !== T.DIRECTIVE) return false b.advanceLexer() return true } /** * A directive that takes neither an operand nor a block, so it stands alone as a * statement without a `;` — `#scope_file`, `#no_padding`, `#compiler`. */ @JvmStatic fun dirBare( b: PsiBuilder, level: Int, ): Boolean { if (b.tokenType !== T.DIRECTIVE) return false val text = b.tokenText ?: return false val name = text.removePrefix("#") if (name in DIRECTIVES_WITH_OPERAND || name in DIRECTIVES_WITH_BLOCK) return false b.advanceLexer() return true } private fun consumeDirectiveIn( b: PsiBuilder, names: Set, ): Boolean { if (b.tokenType !== T.DIRECTIVE) return false val text = b.tokenText ?: return false if (text.length < 2 || text.substring(1) !in names) return false b.advanceLexer() 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 /** * An expression that stops short of `=`. * * Needed wherever a *type* is followed by `= value`: in `a : float = 37.0` the * type slot must yield `float`, not the assignment `float = 37.0`. Grammar-Kit * only ever calls the expression root at the lowest priority from BNF, so this * re-enters it one level up. */ @JvmStatic fun exprNoAssign( b: PsiBuilder, level: Int, ): Boolean = JaiParser.expr(b, level, ASSIGNMENT_PRIORITY) /** * Consumes `==` unless a `{` follows it, because `if x == { case ...; }` is the * switch form and the `==` belongs to the statement, not to an equality expression. */ @JvmStatic fun eqEqNotSwitch( b: PsiBuilder, level: Int, ): Boolean { if (b.tokenType !== T.EQ_EQ) return false if (b.lookAhead(1) === T.LBRACE) return false b.advanceLexer() return true } /** * True if the previous token already closes a construct, so no `;` is needed: * `}` ends a block, struct or enum body, a here-string ends on its own terminator * line (`BODY :: #string DONE … DONE`), and a `;` was already taken by a nested * statement (`code :: #code a := 1;`). */ @JvmStatic fun prevEndsConstruct( b: PsiBuilder, level: Int, ): Boolean { var i = -1 while (true) { val t: IElementType = b.rawLookup(i) ?: return false if (t !== TokenType.WHITE_SPACE && t !== T.LINE_COMMENT && t !== T.BLOCK_COMMENT) { return t === T.RBRACE || t === T.HERE_STRING || t === T.SEMICOLON } i-- } } // ------------------------------------------------------------------ lookahead /** * `(`…`)` followed by `->`, a directive, or a `{` body, where the parameter list * is either empty or contains a `:`/`$` at depth 1. Distinguishes a procedure * header from `if (x) { }`, whose parenthesised condition looks similar. */ @JvmStatic fun isProcHeaderAhead( b: PsiBuilder, level: Int, ): Boolean { if (b.tokenType !== T.LPAREN) return false var depth = 0 var i = 0 var sawParamMarker = false var empty = true while (true) { val t = b.lookAhead(i) ?: return false when (t) { T.LPAREN, T.LBRACKET, T.BEGIN_ARRAY_LITERAL, T.LBRACE, T.BEGIN_STRUCT_LITERAL -> { depth++ } T.RPAREN -> { depth-- if (depth == 0) { // A `name:` or `$T` inside the parentheses can only be a // parameter list, so the header stands on its own: a bare // proc *type* like `proc: (info: *Info, data: T)` has no // arrow, directive or body after it. if (sawParamMarker) return true val next = b.lookAhead(i + 1) return when (next) { T.RIGHT_ARROW, T.DIRECTIVE -> true T.LBRACE -> empty else -> false } } } T.RBRACKET, T.RBRACE -> { depth-- } else -> { if (depth == 1) { empty = false if (t === T.COLON || t === T.DOLLAR || t === T.DOUBLE_DOLLAR || t === T.COLON_COLON || t === T.COLON_EQ ) { sawParamMarker = true } } } } i++ if (i > MAX_LOOKAHEAD) return false } } /** `(` that starts a parenthesised *return list*, i.e. not a procedure type. */ @JvmStatic fun isReturnGroupAhead( b: PsiBuilder, level: Int, ): Boolean = b.tokenType === T.LPAREN && !isProcHeaderAhead(b, level) /** * `[#directive [(…)] | using | `]* IDENT (',' IDENT)* (':' | '::' | ':=')` — a * declaration. Jai declarations have no introducer keyword, so this is what tells * `foo :: () {}` apart from `foo();`. * * The prefix skip covers `#as using base: Document;`, * `#overlay (unknown.vtable) using vtable: *Vtable;`, * `#as using,except(vtable) iunknown: IUnknown;` and the macro-exported * `` `it := entry.value; ``. */ @JvmStatic fun isDeclarationAhead( b: PsiBuilder, 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, T.LBRACKET, T.BEGIN_ARRAY_LITERAL, T.BEGIN_STRUCT_LITERAL -> { i = skipBalancedGroup(b, i) 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 } } } while (true) { if (b.lookAhead(i) === T.BACKTICK) i++ if (b.lookAhead(i) !== T.IDENT) return false i++ when (b.lookAhead(i)) { T.COLON, T.COLON_COLON, T.COLON_EQ -> return true T.COMMA -> i++ else -> return false } } } /** * Index just past the closer matching the bracket at [start], or -1 if unbalanced. * Covers `(…)`, `[…]`, `.[…]` and `.{…}`, which all appear in declaration * prefixes: `#overlay (x) using …`, `using,except .["x", "y"] …`. */ private fun skipBalancedGroup( b: PsiBuilder, start: Int, ): Int { var depth = 0 var i = start while (i - start <= MAX_LOOKAHEAD) { when (b.lookAhead(i) ?: return -1) { T.LPAREN, T.LBRACKET, T.BEGIN_ARRAY_LITERAL, T.LBRACE, T.BEGIN_STRUCT_LITERAL -> { depth++ } T.RPAREN, T.RBRACKET, T.RBRACE -> { depth-- if (depth == 0) return i + 1 } else -> {} } i++ } return -1 } /** * Consumes a `{ … }` block as opaque tokens. * * Used for `#asm`, whose body is x86-64 assembly (`movd source:, byte;`) and not * Jai at all. Giving it structure is a separate job; tiling it keeps the * surrounding file parseable. */ @JvmStatic fun opaqueBlock( b: PsiBuilder, level: Int, ): Boolean { if (b.tokenType !== T.LBRACE) return false var depth = 0 while (!b.eof()) { val t = b.tokenType if (t === T.LBRACE) { depth++ } else if (t === T.RBRACE) { depth-- if (depth == 0) { b.advanceLexer() return true } } b.advanceLexer() } return false } /** `[$|$$] IDENT (',' [$|$$] IDENT)* (':' | '::' | ':=')` — a named parameter. */ @JvmStatic fun isParamNameAhead( b: PsiBuilder, level: Int, ): Boolean { var i = 0 while (true) { var t = b.lookAhead(i) if (t === T.DOLLAR || t === T.DOUBLE_DOLLAR) { i++ t = b.lookAhead(i) } if (t !== T.IDENT) return false i++ when (b.lookAhead(i)) { T.COLON, T.COLON_COLON, T.COLON_EQ -> return true T.COMMA -> i++ else -> return false } } } /** `IDENT (',' IDENT)* ':'` — the named-variable prefix of a `for` header. */ @JvmStatic fun isForNamesAhead( b: PsiBuilder, level: Int, ): Boolean { var i = 0 while (true) { if (b.lookAhead(i) === T.BACKTICK) i++ if (b.lookAhead(i) !== T.IDENT) return false i++ when (b.lookAhead(i)) { T.COLON -> return true T.COMMA -> i++ else -> return false } } } /** `IDENT ':'` — a loop label. */ @JvmStatic fun isLabelAhead( b: PsiBuilder, level: Int, ): Boolean = b.tokenType === T.IDENT && b.lookAhead(1) === T.COLON private const val MAX_LOOKAHEAD = 4096 /** Priority of `assignExpr` in the generated expression parser; see `Jai.bnf`. */ private const val ASSIGNMENT_PRIORITY = 0 }