See More

package processing.mode.cpp; import processing.mode.cpp.FunctionPointerType; import processing.mode.cpp.FunctionSignatureType; import processing.mode.cpp.NamedType; import processing.mode.cpp.Param; import processing.mode.cpp.TypeRef; import java.util.ArrayList; import java.util.List; /** * Recursive-descent parser for CppMode's Java-flavored-C++ grammar subset. * * Design notes: * - Comments are consumed by skipComments() and attached as leadingComments * on the next real node produced, per the Lexer's comment-as-first-class- * token design (see Lexer notes). This is the direct replacement for the * old blankCommentsAndLiterals regex pass. * - Fail-fast: the first parse error throws ParseException immediately. * No error recovery is attempted (see ParseException's notes for why). * - Multi-declarator statements ("int rectX, rectY;") are desugared into * multiple single-name nodes at this layer -- see parseVariableDeclTail. */ public final class Parser { private final List tokens; private int pos = 0; public Parser(List tokens) { this.tokens = tokens; } public static CompilationUnit parse(String source) { List tokens = new CppLexer(source).tokenize(); return new Parser(tokens).parseCompilationUnit(); } /** Test-only entry point: parses a single TypeRef from source text in isolation. */ static TypeRef parseTypeRefFromString(String source) { List tokens = new CppLexer(source).tokenize(); return new Parser(tokens).parseTypeRef(); } // ----- Core cursor helpers ----------------------------------------- // // peek()/peek(ahead)/advance() transparently skip over comment tokens. // This is deliberate and fixes a real bug class found via real-corpus // testing: a line comment can legally appear in the MIDDLE of a // multi-line expression (e.g. a function call whose arguments are each // commented, confirmed real by Move_Eye.pde's // "camera(x, y, z, // eyeX, eyeY, eyeZ \n ...")). The original design // only collected leading comments at statement/declaration boundaries // via consumeLeadingComments(), which left every OTHER parse point // (inside parseArgList, parsePostfix, anywhere mid-expression) just as // comment-blind as the regex passes this parser exists to replace. // Routing comment-skipping through the cursor primitives themselves // fixes every call site at once instead of needing a fix at each one. // // consumeLeadingComments() still exists and is still called explicitly // at statement/declaration start -- it now serves only to COLLECT the // comment tokens for attachment to the next node's leadingComments // field, not to make parsing work correctly (that's now peek/advance's // job). It works correctly alongside this since skipCommentsAt() is // idempotent -- calling it twice in a row from the same position is a // no-op the second time. private void skipCommentsAt() { while (pos < tokens.size() && (tokens.get(pos).type() == CppLexerTokenType.LINE_COMMENT || tokens.get(pos).type() == CppLexerTokenType.BLOCK_COMMENT)) { pos++; } } private CppLexerToken peek() { int p = pos; while (p < tokens.size() && (tokens.get(p).type() == CppLexerTokenType.LINE_COMMENT || tokens.get(p).type() == CppLexerTokenType.BLOCK_COMMENT)) p++; return p < tokens.size() ? tokens.get(p) : tokens.get(tokens.size() - 1); } private CppLexerToken peek(int ahead) { // Walk forward from pos, skipping comment tokens, to find the // "ahead"-th real (non-comment) token after the current position. skipCommentsAt(); int p = pos; int remaining = ahead; while (remaining > 0 && p < tokens.size() - 1) { p++; while (p < tokens.size() && (tokens.get(p).type() == CppLexerTokenType.LINE_COMMENT || tokens.get(p).type() == CppLexerTokenType.BLOCK_COMMENT)) { p++; } remaining--; } return p < tokens.size() ? tokens.get(p) : tokens.get(tokens.size() - 1); } private CppLexerToken advance() { skipCommentsAt(); CppLexerToken t = tokens.get(pos); if (t.type() != CppLexerTokenType.EOF) pos++; return t; } private boolean isAtEnd() { return peek().type() == CppLexerTokenType.EOF; } private boolean check(CppLexerTokenType type) { return peek().type() == type; } private boolean checkKeyword(String kw) { return peek().isKeyword(kw); } private boolean checkPunct(String p) { return peek().isPunct(p); } private boolean checkOp(String op) { return peek().isOp(op); } /** True if the current token is the integer literal "0" specifically * (not any other literal or expression) -- used to recognize the * pure-virtual specifier "= 0" precisely, without accidentally * matching some other "= " shape. */ private boolean checkLiteralZero() { CppLexerToken t = peek(); return t.type() == CppLexerTokenType.INT_LITERAL && t.text().equals("0"); } private boolean matchKeyword(String kw) { if (checkKeyword(kw)) { advance(); return true; } return false; } private boolean matchPunct(String p) { if (checkPunct(p)) { advance(); return true; } return false; } private boolean matchOp(String op) { if (checkOp(op)) { advance(); return true; } return false; } private CppLexerToken expectPunct(String p) { if (checkPunct(p)) return advance(); throw error("expected '" + p + "' but found '" + peek().text() + "'"); } private CppLexerToken expectOp(String op) { if (checkOp(op)) return advance(); throw error("expected '" + op + "' but found '" + peek().text() + "'"); } private CppLexerToken expectKeyword(String kw) { if (checkKeyword(kw)) return advance(); throw error("expected '" + kw + "' but found '" + peek().text() + "'"); } /** * Set of KEYWORD-classified tokens that are NOT actually reserved * words in real Processing/Java/C++ -- they're lexed as keywords * purely so they can be recognized as TYPE names in a type-name * position (see Lexer's KEYWORDS set and its own comment on this), * but that classification incorrectly also blocked them from ever * being used as a declarator/variable NAME, which real Processing * code is free to do. * * Found via a real sketch (RayTracer.pde's "Vec3 color;" struct * field -- "color" the Processing pseudo-type, used as an ordinary * field name, which is completely legal Processing/C++ and not * something the language itself reserves) hitting * "expected identifier but found 'color'" outright. * * Deliberately a narrow, explicit allow-list -- NOT a general * relaxation letting any KEYWORD stand in for an identifier, which * would incorrectly let real reserved words ("int", "if", "class", * etc.) be used as names too. Only pseudo-type keywords that this * project itself introduced for type-name recognition belong here. */ private static final java.util.Set PSEUDO_TYPE_KEYWORDS_USABLE_AS_NAMES = java.util.Set.of( "color" ); private CppLexerToken expectIdentifier() { if (check(CppLexerTokenType.IDENTIFIER)) return advance(); if (check(CppLexerTokenType.KEYWORD) && PSEUDO_TYPE_KEYWORDS_USABLE_AS_NAMES.contains(peek().text())) { return advance(); } throw error("expected identifier but found '" + peek().text() + "'"); } private ParseException error(String message) { CppLexerToken t = peek(); return new ParseException(message, t.line(), t.col()); } /** * Consumes and discards any whitespace-adjacent comment tokens immediately * preceding the current position, returning them so the caller can attach * them as leadingComments on the node it's about to build. Must be called * at the start of every "parse one item/statement/declaration" entry point * so comments are never silently dropped nor mistaken for code. */ /** * Consumes and collects any comment tokens sitting at the current raw * position, for attachment to the next node's leadingComments field. * * Deliberately reads tokens.get(pos) directly rather than calling * peek()/check() -- those now transparently skip comment tokens (see * the cursor-helpers notes above, added after the Move_Eye.pde bug), * so they can never see a comment to report back here. This method is * the one place that still needs to look at raw, unfiltered token- * stream position. */ private List consumeLeadingComments() { List comments = new ArrayList<>(); while (pos < tokens.size() && (tokens.get(pos).type() == CppLexerTokenType.LINE_COMMENT || tokens.get(pos).type() == CppLexerTokenType.BLOCK_COMMENT)) { comments.add(tokens.get(pos)); pos++; } return comments; } // ----- Entry point --------------------------------------------------- public CompilationUnit parseCompilationUnit() { List items = new ArrayList<>(); consumeLeadingComments(); // leading file-level comments currently dropped // at EOF if nothing follows; fine for now while (!isAtEnd()) { List comments = consumeLeadingComments(); if (isAtEnd()) break; items.addAll(parseTopLevelItem(comments)); } return new CompilationUnit(items); } /** * Dispatches on lookahead to the appropriate top-level (or class-member, * since both positions share this same dispatch per TopLevelItem's design * notes) sub-parser. Returns a List since a single source statement can * desugar into multiple VariableDecl nodes (multi-declarator decls, * confirmed at both top-level and class-member scope by the Button and * Handles fixtures respectively) -- every other branch returns a * single-element list. */ private List parseTopLevelItem(List leadingComments) { consumeAttributes(); if (check(CppLexerTokenType.PREPROCESSOR_DIRECTIVE)) { CppLexerToken t = advance(); return List.of(new PreprocessorLine(t.text(), t.line(), t.col(), leadingComments)); } // C++20 concept definition: "concept Name = constraint-expr;" // "concept" is lexed as IDENTIFIER (not in the keyword set), so // use text comparison rather than checkKeyword. if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("concept")) { int startPos = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} matchPunct(";"); } // Reconstruct the raw text and emit as-is (concept defs are valid at // namespace scope and must be visible before use in template params). StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(' '); raw.append(tokens.get(i).text()); } raw.append(';'); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } List templateParams = List.of(); // extern "C" / extern "C++" linkage specification -- consume verbatim if (checkKeyword("extern") && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.STRING_LITERAL) { advance(); advance(); // consume extern "C++" if (checkPunct("{")) { // extern "C++" { ... } block -- parse contents as top-level items advance(); // consume { List result = new ArrayList<>(); while (!isAtEnd() && !checkPunct("}")) { List innerComments = consumeLeadingComments(); if (checkPunct("}")) break; result.addAll(parseTopLevelItem(innerComments)); } matchPunct("}"); return result; } else { // extern "C++" single-declaration return parseTopLevelItem(leadingComments); } } if (matchKeyword("template")) { // Explicit template instantiation: "template class Foo;" (no < after template) if (!checkOp("<")) { int startPos = pos - 1; while (!isAtEnd() && !checkPunct(";")) advance(); matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(" "); raw.append(tokens.get(i).text()); } raw.append(";"); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } templateParams = parseTemplateParamList(); consumeLeadingComments(); // C++20 concept definition: "template concept Foo = ..." if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("concept")) { int startPos = pos - templateParams.size() - 3; // approximate; use simpler reconstruction // Build the full concept declaration text StringBuilder raw = new StringBuilder("template<"); for (int i = 0; i < templateParams.size(); i++) { if (i > 0) raw.append(", "); raw.append(templateParams.get(i)); } raw.append("> "); // Capture from "concept" through ";" int conceptStart = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} } matchPunct(";"); for (int i = conceptStart; i < pos - 1; i++) { if (i > conceptStart) raw.append(' '); raw.append(tokens.get(i).text()); } raw.append(';'); return List.of(new PreprocessorLine(raw.toString(), tokens.get(conceptStart).line(), tokens.get(conceptStart).col(), leadingComments)); } // C++20 "requires" clause after template params. // "requires" is lexed as IDENTIFIER (not in the keyword set). if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("requires")) { advance(); // 'requires' int depth = 0; while (!isAtEnd()) { if (checkPunct("(") || checkPunct("[")) { depth++; advance(); continue; } if (checkPunct(")") || checkPunct("]")) { depth--; advance(); continue; } if (checkOp("<")) { depth++; advance(); continue; } if (checkOp(">")) { depth--; advance(); continue; } if (checkOp(">>")) { depth -= 2; advance(); continue; } if (checkPunct(";")) break; if (checkPunct("{")) { // Braced block in requires: consume it entirely int _bd = 0; while (!isAtEnd()) { if (checkPunct("{")) _bd++; else if (checkPunct("}")) { _bd--; if (_bd == 0) { advance(); break; } } advance(); } continue; } if (depth == 0) { if (checkKeyword("void") || checkKeyword("auto") || checkKeyword("int") || checkKeyword("float") || checkKeyword("bool") || checkKeyword("char") || checkKeyword("double")|| checkKeyword("long") || checkKeyword("class") || checkKeyword("struct") || checkKeyword("const") || checkKeyword("static") || checkKeyword("inline")|| checkKeyword("virtual") || checkKeyword("explicit")|| checkKeyword("friend")) { break; } if (check(CppLexerTokenType.IDENTIFIER) && pos + 1 < tokens.size()) { CppLexerToken next = tokens.get(pos + 1); if (next.type() == CppLexerTokenType.IDENTIFIER) break; } } advance(); } } // C++20 concept used as a constraint directly in the template head: // "template" -- Concept is an identifier, not typename/class. // This is handled in parseTemplateParamName already. } if (checkKeyword("class") || checkKeyword("struct")) { // Partial specialization: "template struct Foo { ... }" // After parsing the class/struct, check for a specialization arg list. // BUT: "struct TypeName funcName(...)" is a function with elaborated return type if (pos + 2 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && tokens.get(pos + 2).type() == CppLexerTokenType.IDENTIFIER && !tokens.get(pos + 1).isKeyword("class") && !tokens.get(pos + 1).isKeyword("struct")) { return parseFunctionOrVariable(leadingComments, templateParams, true); } return List.of(parseTypeDef(leadingComments, templateParams)); } if (checkKeyword("enum")) { return List.of(parseEnumDecl(leadingComments)); } if (checkKeyword("namespace") || (checkKeyword("inline") && pos + 1 < tokens.size() && tokens.get(pos + 1).isKeyword("namespace"))) { return List.of(parseNamespaceDecl(leadingComments)); } // typedef -- emit verbatim as-is (covers function-pointer typedefs, etc.) if (checkKeyword("typedef")) { int startPos = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} matchPunct(";"); } StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(' '); raw.append(tokens.get(i).text()); } raw.append(';'); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } if (checkKeyword("typedef")) { int startPos = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} } matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(' '); raw.append(tokens.get(i).text()); } raw.append(';'); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } if (checkKeyword("typedef")) { int startPos = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} } matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(' '); raw.append(tokens.get(i).text()); } raw.append(';'); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } // static_assert at top level: emit verbatim as PreprocessorLine if (checkKeyword("static_assert")) { int startPos = pos; advance(); expectPunct("("); int _d=1; while (!isAtEnd() && _d > 0) { if (checkPunct("(")) _d++; else if (checkPunct(")")) _d--; advance(); } matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos; i++) { if (i > startPos) raw.append(" "); raw.append(tokens.get(i).text()); } if (!raw.toString().endsWith(";")) raw.append(";"); CppLexerToken t0 = tokens.get(startPos); return List.of(new TopLevelStatement( new ExprStatement(new Identifier(raw.toString(), t0.line(), t0.col(), List.of()), t0.line(), t0.col(), List.of()), t0.line(), t0.col(), leadingComments)); } if (checkKeyword("using") && tokens.get(pos + 1).isKeyword("namespace")) { return List.of(parseUsingNamespaceDecl(leadingComments)); } // using T = Type; -- type alias. Consume to ';' and emit as-is. // template using Alias = ...; -- template alias, prepend template params. if (checkKeyword("using")) { int startPos = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} matchPunct(";"); } StringBuilder raw = new StringBuilder(); // Prepend template params if this is a template alias if (!templateParams.isEmpty()) { raw.append("template<"); for (int ti = 0; ti < templateParams.size(); ti++) { if (ti > 0) raw.append(", "); raw.append(templateParams.get(ti)); } raw.append("> "); } for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(' '); raw.append(tokens.get(i).text()); } raw.append(';'); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } if (checkOp("~") || (checkKeyword("virtual") && tokens.get(pos + 1).isOp("~"))) { return List.of(parseFunctionOrConstructorOrDestructor(leadingComments, templateParams)); } // Global structured binding: "auto [a, b] = expr;" if (isStructuredBindingStart()) { Statement sb2 = parseStructuredBinding(leadingComments); return List.of(new TopLevelStatement(sb2, sb2.line(), sb2.col(), leadingComments)); } // Deduction guide: "Wrapper(const char*) -> Wrapper<:string>;" if (check(CppLexerTokenType.IDENTIFIER) && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("(")) { int scan = pos + 2; int depth = 1; while (scan < tokens.size() && depth > 0) { if (tokens.get(scan).isPunct("(")) depth++; else if (tokens.get(scan).isPunct(")")) depth--; scan++; } if (scan < tokens.size() && tokens.get(scan).isPunct("->")) { int startPos = pos; while (!isAtEnd() && !checkPunct(";")) advance(); matchPunct(";"); // Reconstruct: include template params prefix if present StringBuilder raw = new StringBuilder(); if (!templateParams.isEmpty()) { raw.append("template<"); for (int i = 0; i < templateParams.size(); i++) { if (i > 0) raw.append(", "); raw.append(templateParams.get(i)); } raw.append("> "); } for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(" "); raw.append(tokens.get(i).text()); } raw.append(";"); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } } return parseFunctionOrVariable(leadingComments, templateParams, true); } /** "template" -- returns just the param names. */ private List parseTemplateParamList() { // "template" already consumed by the caller; just consume "<...>" if (checkKeyword("template")) advance(); // in case called with template still present expectOp("<"); List params = new ArrayList<>(); // template<> is a valid explicit full specialization marker -- emit as empty list if (!checkOp(">")) { params.add(parseTemplateParamText()); while (matchPunct(",")) { params.add(parseTemplateParamText()); } } if (checkOp(">>")) { splitTrailingShiftIntoTwoCloseAngles(); } expectOp(">"); return params; } /** * Parses one template parameter and returns its FULL TEXT as a String, * preserving everything needed for CodeGen to emit it verbatim: * "typename T" * "typename... Args" * "class T" * "template class Container" * "auto V" * "int N = sizeof(T)" * "typename std::enable_if<:is_integral>::value, int>::type = 0" * "Numeric T" (concept-constrained) * * The AST stores List for template params. Previously it stored only * the param name, which lost all type/variadic/default information. Now it * stores the full text so CodeGen can round-trip it as "template". */ private String parseTemplateParamText() { int startPos = pos; // We'll capture tokens until we hit ',' or '>' or '>>' at depth 0. // Complex cases (template-template, SFINAE) are handled by parseTemplateParamName // for semantic purposes, but here we just need the text. parseTemplateParamName(); // advance pos past the parameter int endPos = pos; // Reconstruct the text from the tokens we consumed StringBuilder sb = new StringBuilder(); for (int i = startPos; i < endPos; i++) { if (i > startPos) { CppLexerToken prev = tokens.get(i - 1); CppLexerToken cur = tokens.get(i); // Add spacing between tokens where needed if (needsSpace(prev, cur)) sb.append(' '); } sb.append(tokens.get(i).text()); } return sb.toString(); } /** Whether two adjacent tokens need a space between them in output. */ private boolean needsSpace(CppLexerToken prev, CppLexerToken cur) { String p = prev.text(), c = cur.text(); // No space needed before/after punctuation pairs if (p.equals("<") || p.equals(">") || p.equals(">>") || p.equals("*") || p.equals("&") || p.equals("&&") || p.equals("::")) return false; if (c.equals(">") || c.equals(">>") || c.equals("<") || c.equals("::") || c.equals("(") || c.equals(")") || c.equals(",")) return false; // Space between two word tokens (keywords, identifiers) if ((prev.type() == CppLexerTokenType.KEYWORD || prev.type() == CppLexerTokenType.IDENTIFIER) && (cur.type() == CppLexerTokenType.KEYWORD || cur.type() == CppLexerTokenType.IDENTIFIER)) return true; // Space before/after ellipsis in most contexts if (p.equals("...") || c.equals("...")) return true; // Space after "=" for defaults if (p.equals("=") || c.equals("=")) return true; return false; } /** * Parses one template parameter in any of the C++ forms: * * typename T -- type param * class T -- type param (alternate spelling) * typename... Args -- variadic type param pack * class... Args -- variadic type param pack * template class C -- template-template param * template class... C -- variadic template-template param * int N -- non-type param * auto V -- non-type param (C++17 auto NTTP) * typename T = Default -- type param with default * int N = 42 -- non-type param with default * typename std::enable_if<...>::type = 0 -- SFINAE sink param * Concept T -- constrained type param (C++20) * * Returns just the name string (or a descriptive placeholder for complex * params the AST doesn't fully model -- consistent with existing practice * where template params are stored as strings, not as typed AST nodes). */ private String parseTemplateParamName() { // --- template-template parameter: "template<...> class Name" --- if (checkKeyword("template")) { parseTemplateParamList(); // consume the nested <...> (recursive) matchKeyword("class"); matchKeyword("typename"); // variadic template-template: "template class... C" if (checkPunct("...")) advance(); if (check(CppLexerTokenType.IDENTIFIER)) return advance().text(); return ""; } // --- concept-constrained type parameter: "Numeric T" --- if (check(CppLexerTokenType.IDENTIFIER) && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && pos + 2 < tokens.size() && (tokens.get(pos + 2).isOp(">") || tokens.get(pos + 2).isOp(">>") || tokens.get(pos + 2).isPunct(",") || tokens.get(pos + 2).isPunct("...") || tokens.get(pos + 2).isOp("="))) { advance(); if (checkPunct("...")) advance(); String name = advance().text(); if (matchOp("=")) parseTemplateDefaultValue(); return name; } // --- typename / class type parameter --- if (checkKeyword("typename") || checkKeyword("class")) { advance(); // consume typename/class // variadic pack: "typename... Args" if (checkPunct("...")) advance(); // SFINAE sink: "typename std::enable_if::type = 0" // After typename, if what follows is a qualified name (has :: or // template args), it's a non-type SFINAE param, not a plain // type-param name. Parse it as a full type and grab any default. if (check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD)) { boolean isSfinaeType = checkPunct("::") || (pos + 1 < tokens.size() && ( tokens.get(pos + 1).isPunct("::") || tokens.get(pos + 1).isOp("<"))); if (isSfinaeType) { try { parseTypeRef(); } catch (ParseException ignored) {} if (matchOp("=")) parseTemplateDefaultValue(); return ""; } String name = advance().text(); if (matchOp("=")) parseTemplateDefaultValue(); return name; } // Anonymous typename with default: "typename = std::enable_if" if (matchOp("=")) parseTemplateDefaultValue(); return ""; } // --- auto NTTP: "auto V" or "auto... Vs" --- if (checkKeyword("auto")) { advance(); if (checkPunct("...")) advance(); String name = check(CppLexerTokenType.IDENTIFIER) ? advance().text() : ""; if (matchOp("=")) parseTemplateDefaultValue(); return name; } // --- Non-type parameter: "int N", "std::size_t N", "const char* S", // or member-pointer NTTP: "int T::* Field" TypeRef type = parseTypeRef(); if (checkPunct("...")) advance(); String name; if (check(CppLexerTokenType.IDENTIFIER)) { name = advance().text(); // Member pointer NTTP: "int T::* Field" -- consume ::* and actual name if (checkPunct("::") && pos + 1 < tokens.size() && tokens.get(pos + 1).isOp("*")) { advance(); advance(); // :: and * if (check(CppLexerTokenType.IDENTIFIER)) name = advance().text(); } } else if (checkPunct("(")) { // Member function pointer NTTP: "int (T::* Method)(int) const" // or pointer-to-array NTTP: "int (*Arr)[N]" advance(); // consume ( // consume optional Class:: qualifier if (check(CppLexerTokenType.IDENTIFIER) && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("::")) { advance(); advance(); // Class :: } matchOp("*"); matchOp("&"); // * or & name = check(CppLexerTokenType.IDENTIFIER) ? advance().text() : ""; expectPunct(")"); // consume trailing (params) and optional const if (checkPunct("(")) { advance(); int d = 1; while (!isAtEnd() && d > 0) { if (checkPunct("(")) d++; else if (checkPunct(")")) d--; advance(); } matchKeyword("const"); } // consume trailing [dims] for pointer-to-array while (checkPunct("[")) { advance(); if (!checkPunct("]")) parseExpr(); expectPunct("]"); } } else { name = type instanceof NamedType nt ? nt.baseName() : ""; } // optional default value: "int N = 42" or "::type = 0" if (matchOp("=")) parseTemplateDefaultValue(); return name; } /** * Parses a class/struct declaration: "class Name [: public Base1, ...] { members };". * "public"/"private"/"protected" access specifiers within the body are * consumed and discarded (not modeled in the AST -- not yet confirmed * necessary by any semantic pass; if member visibility ever matters, * this is the place to start tracking it). */ private TypeDef parseTypeDef(List leadingComments, List templateParams) { CppLexerToken start = peek(); String kind = checkKeyword("class") ? "class" : "struct"; advance(); // alignas specifier: "struct alignas(64) CacheLine" if (checkKeyword("alignas") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("(")) { advance(); advance(); int _ad=1; while (!isAtEnd() && _ad > 0) { if (checkPunct("(")) _ad++; else if (checkPunct(")")) _ad--; advance(); } } // Also consume [[attributes]] before the name consumeAttributes(); String name = expectIdentifier().text(); // Partial or explicit specialization: "template struct Foo" // or "template<> struct TypeName" -- capture the specialization arg // text and fold it into the name so CodeGen emits "TypeName" correctly. if (checkOp("<")) { // Specialization args: "Foo" -- scan to matching > manually int argStart = pos; advance(); // consume < int depth = 1; while (!isAtEnd() && depth > 0) { if (checkOp("<")) { depth++; advance(); } else if (checkOp(">")) { depth--; advance(); } else if (checkOp(">>")) { depth -= 2; splitTrailingShiftIntoTwoCloseAngles(); advance(); // consume first > if (depth <= 0) { advance(); break; } // consume second > and exit advance(); // consume second > when still inside } else advance(); } int argEnd = pos; // Reconstruct the specialization suffix verbatim from tokens StringBuilder spec = new StringBuilder(); for (int i = argStart; i < argEnd; i++) spec.append(tokens.get(i).text()); name = name + spec.toString(); } // Forward declaration: "struct Node;" or "class Container;" with no body. // Also handles "template class Container;" with default args. if (checkPunct(";")) { advance(); return new TypeDef(kind, name, templateParams, List.of(), List.of(), start.line(), start.col(), leadingComments); } List baseClasses = new ArrayList<>(); if (matchPunct(":")) { baseClasses.add(parseBaseClassEntry()); while (matchPunct(",")) { baseClasses.add(parseBaseClassEntry()); } } expectPunct("{"); List members = new ArrayList<>(); while (!checkPunct("}")) { if (isAtEnd()) throw error("unexpected end of input inside class/struct body for '" + name + "'"); List comments = consumeLeadingComments(); if (checkPunct("}")) break; if (matchKeyword("public") || matchKeyword("private") || matchKeyword("protected")) { expectPunct(":"); continue; } members.addAll(parseClassMember(comments, name)); } expectPunct("}"); // The trailing ';' after a class/struct body is standard C++, but // confirmed OPTIONAL in real .pde input -- Scrollbar.pde's // "class HScrollbar { ... }" has no trailing ';' at all. Processing's // own preprocessing tolerates this; this parser does too rather than // hard-requiring strict C++ grammar here. matchPunct(";"); return new TypeDef(kind, name, templateParams, baseClasses, members, start.line(), start.col(), leadingComments); } /** "public BaseName" / "private BaseName" / "protected BaseName" / bare "BaseName". */ private String parseBaseClassEntry() { boolean isVirtualBase = matchKeyword("virtual"); matchKeyword("public"); if (!checkKeyword("public")) { matchKeyword("private"); matchKeyword("protected"); } if (!isVirtualBase) isVirtualBase = matchKeyword("virtual"); String name = parseQualifiedTypeName(); // Consume template args on base class name: "Base" (CRTP), // "std::enable_shared_from_this", etc. if (checkOp("<") && looksLikeTemplateArgList()) { StringBuilder sb = new StringBuilder(name); sb.append('<'); advance(); // '<' int depth = 1; while (!isAtEnd() && depth > 0) { if (checkOp("<")) depth++; else if (checkOp(">")) { depth--; if (depth == 0) break; } else if (checkOp(">>")) { depth -= 2; if (depth <= 0) { splitTrailingShiftIntoTwoCloseAngles(); break; } } sb.append(peek().text()); advance(); } expectOp(">"); sb.append('>'); name = sb.toString(); } boolean isPackExpansion = matchPunct("..."); // pack expansion in base class list: "Bases..." String suffix = isPackExpansion ? "..." : ""; return isVirtualBase ? "virtual " + name + suffix : name + suffix; } /** * A class member is almost always parseTopLevelItem's domain (field decl, * method, nested type) -- but constructors/destructors named after the * enclosing class need special handling parseTopLevelItem's generic * function/variable dispatch can't do alone (no return type at all, * possible initializer list), so this wraps that in enclosing-class * context. */ private List parseClassMember(List leadingComments, String enclosingClassName) { if (check(CppLexerTokenType.PREPROCESSOR_DIRECTIVE)) { CppLexerToken t = advance(); return List.of(new PreprocessorLine(t.text(), t.line(), t.col(), leadingComments)); } consumeAttributes(); // friend declarations: "friend class Foo;" or "friend void func(...);" // Consume entirely -- friend declarations don't produce AST members we need. if (checkKeyword("friend")) { advance(); // consume 'friend' // Friend function with a body: "friend T operator*(...) { ... }" // Must be parsed as a real function, not consumed to ';'. // Detect by scanning ahead: if we find '{' before ';', it has a body. int scan = pos; int depth = 0; boolean hasBody = false; while (scan < tokens.size()) { String txt = tokens.get(scan).text(); if (txt.equals("{")) { if (depth++ == 0) { hasBody = true; break; } } else if (txt.equals("}")) { if (--depth == 0) break; } else if (txt.equals(";") && depth == 0) break; scan++; } if (hasBody) { // Friend function WITH a body is a free function defined in-class // but semantically at namespace scope. Return it as a TopLevelItem // to be hoisted out of the class body. // We must NOT emit it as a class member since free functions with 2+ // parameters can't be member functions. List items = parseFunctionOrVariable(leadingComments, List.of(), false); return items; // will be added to class members and hoisted by ClassHoister } // Friend declaration (no body) -- consume to ';' { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} matchPunct(";"); } return List.of(); } List templateParams = List.of(); if (checkKeyword("template")) { templateParams = parseTemplateParamList(); consumeLeadingComments(); // Trailing requires clause on template member: "template\n requires Concept" if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("requires")) { advance(); // consume requires if (checkPunct("(")) { advance(); int _d=1; while(!isAtEnd()&&_d>0){if(checkPunct("("))_d++;else if(checkPunct(")"))_d--;advance();} } else { int _d=0; while(!isAtEnd()){if(checkOp("<"))_d++;else if(checkOp(">")&&_d>0)_d--;else if(_d==0&&(checkPunct("{;")||checkPunct(";")|| checkKeyword("auto")||checkKeyword("void")||checkKeyword("bool")||checkKeyword("int")||checkKeyword("float")||checkKeyword("const")||checkKeyword("inline")||checkKeyword("static")||checkKeyword("virtual")||checkKeyword("constexpr")||checkKeyword("explicit")||checkKeyword("operator")))break;advance();} } consumeLeadingComments(); } } if (checkKeyword("class") || checkKeyword("struct")) { // Anonymous struct: "struct { float x, y; } position;" if (pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("{")) { int anonStart = pos; advance(); // consume struct/class int bd = 0; while (!isAtEnd()) { if (checkPunct("{")) { bd++; advance(); } else if (checkPunct("}")) { bd--; advance(); if (bd == 0) break; } else advance(); } // pos is now after }, next is memberName then ; int beforeMember = pos; String memberName = check(CppLexerTokenType.IDENTIFIER) ? advance().text() : ""; matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = anonStart; i < beforeMember; i++) { if (i > anonStart) raw.append(" "); raw.append(tokens.get(i).text()); } if (!memberName.isEmpty()) raw.append(" ").append(memberName); raw.append(";"); return List.of(new PreprocessorLine(raw.toString(), tokens.get(anonStart).line(), tokens.get(anonStart).col(), leadingComments)); } // "struct/class Name funcName(" -- elaborated return type, not a definition if (pos + 2 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && tokens.get(pos + 2).type() == CppLexerTokenType.IDENTIFIER) { return parseFunctionOrVariable(leadingComments, templateParams, false); } return List.of(parseTypeDef(leadingComments, templateParams)); } if (checkKeyword("enum")) { return List.of(parseEnumDecl(leadingComments)); } // static_assert inside class body if (checkKeyword("static_assert")) { int startPos = pos; advance(); expectPunct("("); int _d=1; while (!isAtEnd() && _d > 0) { if (checkPunct("(")) _d++; else if (checkPunct(")")) _d--; advance(); } matchPunct(";"); return List.of(new PreprocessorLine("// static_assert", tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } // using X = Type; inside a struct/class body -- consume verbatim if (checkKeyword("using") && !tokens.get(pos + 1).isKeyword("namespace")) { int startPos = pos; { int _d=0; while(!isAtEnd()){if(checkPunct("{"))_d++;else if(checkPunct("}")){if(_d==0){advance();break;}_d--;}else if(checkPunct(";")&&_d==0)break;advance();} matchPunct(";"); } StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(" "); raw.append(tokens.get(i).text()); } raw.append(";"); return List.of(new PreprocessorLine(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } // Destructor dispatch: "~Name() {...}" or "virtual ~Name() {...}" -- // must look one token past an optional leading "virtual", since that // keyword (confirmed common on destructors, e.g. "virtual ~LSystem()") // precedes the '~' that would otherwise be the dispatch signal. if (checkOp("~") || (checkKeyword("virtual") && tokens.get(pos + 1).isOp("~"))) { return List.of(parseFunctionOrConstructorOrDestructor(leadingComments, templateParams)); } // Constructor dispatch: identifier matching the enclosing class name, // directly followed by '(' with no return type preceding it. // Also handle "constexpr ClassName(...)" -- consume constexpr first. // friend declaration: "friend class Foo;" or "friend Box makeBox(U v);" if (checkKeyword("friend")) { int startPos = pos; advance(); // consume friend // "friend class/struct Foo" -- forward decl, consume verbatim if (checkKeyword("class") || checkKeyword("struct")) { while (!isAtEnd() && !checkPunct(";")) advance(); matchPunct(";"); return List.of(new PreprocessorLine("// friend class", tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments)); } // "friend RetType funcName(...)" or "friend RetType operator...()" -- parse as function // Consume template params if present; also use outer templateParams if already consumed List friendTemplateParams = templateParams.isEmpty() ? List.of() : templateParams; if (matchKeyword("template")) friendTemplateParams = parseTemplateParamList(); return parseFunctionOrVariable(leadingComments, friendTemplateParams, false); } // explicit(...): conditional explicit specifier (C++20) if (checkKeyword("explicit") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("(")) { advance(); // consume explicit advance(); int _ed=1; // consume ( while (!isAtEnd() && _ed > 0) { if (checkPunct("(")) _ed++; else if (checkPunct(")")) _ed--; advance(); } } else { matchKeyword("explicit"); // plain explicit } boolean isConstexprCtor = false; if ((checkKeyword("constexpr") || checkKeyword("consteval")) && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && tokens.get(pos + 1).text().equals(enclosingClassName) && pos + 2 < tokens.size() && tokens.get(pos + 2).isPunct("(")) { advance(); // consume constexpr/consteval isConstexprCtor = true; } // Constructor: match enclosingClassName OR just its base name (for specializations like Grid) String enclosingBase = enclosingClassName.contains("<") ? enclosingClassName.substring(0, enclosingClassName.indexOf("<")) : enclosingClassName; if (check(CppLexerTokenType.IDENTIFIER) && (peek().text().equals(enclosingClassName) || peek().text().equals(enclosingBase)) && tokens.get(pos + 1).isPunct("(")) { FunctionDecl ctorFd = parseFunctionOrConstructorOrDestructor(leadingComments, templateParams); if (isConstexprCtor && !ctorFd.isConstexpr()) { // constexpr was consumed before dispatch; rebuild FunctionDecl with isConstexpr=true ctorFd = new FunctionDecl(ctorFd.returnType(), ctorFd.name(), ctorFd.templateParams(), ctorFd.params(), ctorFd.initializerList(), ctorFd.body(), ctorFd.isConstructor(), ctorFd.isDestructor(), ctorFd.isVirtual(), ctorFd.isOverride(), ctorFd.isConst(), true, ctorFd.isStatic(), ctorFd.isPureVirtual(), ctorFd.isDefault(), ctorFd.isDelete(), ctorFd.line(), ctorFd.col(), ctorFd.leadingComments()); } return List.of(ctorFd); } return parseFunctionOrVariable(leadingComments, templateParams, false); } /** * Parses a destructor ("~Name() { ... }") or a constructor (handled here * too since both share the "no return type, name/~name immediately * followed by '('" shape, including the optional virtual/override/const * qualifiers and, for constructors, an initializer list). */ private FunctionDecl parseFunctionOrConstructorOrDestructor(List leadingComments, List templateParams) { CppLexerToken start = peek(); boolean isVirtual = matchKeyword("virtual"); boolean isConstexprCtorMethod = matchKeyword("constexpr") || matchKeyword("consteval"); boolean isDestructor = matchOp("~"); String name = expectIdentifier().text(); if (isDestructor) name = "~" + name; List params = parseParamList(); boolean isConst = matchKeyword("const"); boolean isOverride = matchKeyword("override"); // order-flexible: also accept override before const, just in case if (!isConst) isConst = matchKeyword("const"); // Consume trailing qualifiers: noexcept, noexcept(expr), override, final, requires while (true) { if (checkKeyword("noexcept")) { advance(); if (checkPunct("(")) { advance(); int d=1; while(!isAtEnd()&&d>0){if(checkPunct("("))d++;else if(checkPunct(")"))d--;advance();} } } else if (checkKeyword("override")) { advance(); } else if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("final")) { advance(); } else if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("requires")) { advance(); if (checkPunct("(")) { advance(); int d=1; while(!isAtEnd()&&d>0){if(checkPunct("("))d++;else if(checkPunct(")"))d--;advance();} } else { while (!isAtEnd() && !checkPunct("{") && !checkPunct(";") && !checkOp("=")) { if (checkOp("<")) { advance(); int d=1; while(!isAtEnd()&&d>0){if(checkOp("<"))d++;else if(checkOp(">"))d--;advance();} } else advance(); } } } else break; } List initList = new ArrayList<>(); if (!isDestructor && matchPunct(":")) { FunctionDecl.ConstructorInit ci1 = parseConstructorInitEntry(); if (matchPunct("...")) ci1 = new FunctionDecl.ConstructorInit(ci1.memberName() + "...", ci1.args()); initList.add(ci1); while (matchPunct(",")) { FunctionDecl.ConstructorInit ci = parseConstructorInitEntry(); if (matchPunct("...")) ci = new FunctionDecl.ConstructorInit(ci.memberName() + "...", ci.args()); initList.add(ci); } } // Pure-virtual specifier ("= 0"), e.g. "virtual ~A() = 0;" -- a // real, valid C++ idiom for ensuring polymorphic deletion through // an abstract base. Found via a realistic builder-pattern sketch // using "virtual void draw() = 0;" on an abstract base class -- // confirmed real, ordinary OOP, not exotic. Must be checked // BEFORE the body/";" check below, since "= 0;" replaces both. boolean isPureVirtual = false, isDefault = false, isDelete = false; if (checkOp("=")) { int save = pos; advance(); if (checkKeyword("default")) { advance(); isDefault = true; } else if (checkKeyword("delete")) { advance(); isDelete = true; } else if (checkLiteralZero()) { advance(); isPureVirtual = true; } else { pos = save; } } Block body = checkPunct("{") ? parseBlock() : null; if (body == null && !isPureVirtual && !isDefault && !isDelete) expectPunct(";"); else if (isPureVirtual || isDefault || isDelete) expectPunct(";"); return new FunctionDecl(null, name, templateParams, params, initList, body, !isDestructor, isDestructor, isVirtual, isOverride, isConst, false, isConstexprCtorMethod, isPureVirtual, isDefault, isDelete, start.line(), start.col(), leadingComments); } private FunctionDecl.ConstructorInit parseConstructorInitEntry() { // Base class names can be qualified: "std::runtime_error(msg)" String memberName = parseQualifiedTypeName(); List args = new ArrayList<>(); if (checkPunct("{")) { // Brace initialization: "m{{1,0},{0,1}}" args.add(parseInitializerList()); } else { expectPunct("("); if (!checkPunct(")")) { Expr a0 = parseExpr(); if (matchPunct("...")) a0 = new PostfixExpr("...", a0, a0.line(), a0.col(), List.of()); args.add(a0); while (matchPunct(",")) { if (checkPunct(")")) break; Expr ai = parseExpr(); if (matchPunct("...")) ai = new PostfixExpr("...", ai, ai.line(), ai.col(), List.of()); args.add(ai); } } expectPunct(")"); } return new FunctionDecl.ConstructorInit(memberName, args); } /** * Handles every remaining top-level/class-member form: an ordinary * function declaration/definition, an operator overload, or a variable * declaration (possibly multi-declarator, desugared into multiple * VariableDecl nodes sharing one TypeRef). * * Disambiguation: parse [virtual] [static] [const-is-not-valid-here] * returnType, then a name (ordinary identifier OR "operator" followed by * an operator token, per the operator-overload fixture). If '(' follows * the name, it's a function; otherwise it's a variable declaration * (with possible comma-separated additional declarators). * * @param isTopLevel true when called from true file scope * (parseTopLevelItem), false when called from inside * a class/struct body (parseClassMember). Gates the * bare-statement (Processing static-mode) fallback, * which only makes sense at file scope -- a class * member is always a declaration, never a bare * statement. Without this gate, the fallback was * reachable from inside class bodies too, which * caused a real bug: "bool operator==(...) const {...}" * as a class member was misjudged as "not a * declaration" by the fallback's lookahead and * incorrectly routed into statement parsing instead * (found via the oop_features.cpp fixture). */ private List parseFunctionOrVariable(List leadingComments, List templateParams, boolean isTopLevel) { CppLexerToken start = peek(); // Bare top-level statement fallback (Processing "static mode" sketches, // confirmed real by the example corpus's Coordinates.pde -- a flat // sequence of statements like "size(640, 360);" with no enclosing // setup()/draw() at all). Distinguished from a real declaration by // lookahead: a declaration is "TypeName Identifier ...", whereas a // bare call statement is "identifier(" with nothing in between. This // check must run before any of virtual/static/const are consumed, // since none of those prefix a bare statement. Only ever applies at // true top level -- see isTopLevel notes above. if (isTopLevel && !looksLikeTopLevelDeclarationOrFunction()) { Statement stmt = parseStatement(List.of()); return List.of(new TopLevelStatement(stmt, start.line(), start.col(), leadingComments)); } boolean isVirtual = matchKeyword("virtual"); boolean isStatic = matchKeyword("static"); matchKeyword("inline"); matchKeyword("volatile"); // consume volatile qualifier // alignas(expr): consume alignment specifier before type if (checkKeyword("alignas") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("(")) { advance(); advance(); int _aad=1; while (!isAtEnd() && _aad > 0) { if (checkPunct("(")) _aad++; else if (checkPunct(")")) _aad--; advance(); } } boolean isConst = matchKeyword("const"); boolean isConstexprFn = matchKeyword("constexpr") || matchKeyword("consteval"); if (isConstexprFn && !isConst) isConst = true; matchKeyword("constinit"); if (!isVirtual) isVirtual = matchKeyword("virtual"); // constexpr virtual if (!isStatic) isStatic = matchKeyword("static"); if (!isConst) isConst = matchKeyword("const"); matchKeyword("volatile"); // volatile may appear after const // Raw function-pointer variable declaration: "Type (*name)(Params) = init;" if (looksLikeFunctionPointerVariable()) { TypeRef returnType = parseTypeRef(); NameAndFunctionPointerType decl = parseFunctionPointerDeclaratorTail(returnType); Expr initializer = null; if (matchOp("=")) { initializer = parseExpr(); } expectPunct(";"); return List.of(new VariableDecl(decl.type(), decl.name(), List.of(), initializer, isConst, isStatic, start.line(), start.col(), leadingComments)); } TypeRef type = parseTypeRef(isConstexprFn ? false : isConst); // constexpr should not mark return type as const // Constructor: if ( follows the type with no name AND we have leading constexpr/virtual, // the type name IS the function name (e.g. "constexpr RGBA(...)") String name; if (checkPunct("(") && type instanceof NamedType nt2 && !looksLikeFunctionPointerDeclarator() && (isConst || isVirtual || isStatic) // only when qualifier precedes && looksLikeParamList()) { name = nt2.baseName(); } else { name = parseFunctionOrVariableName(); } if (checkPunct("(") && !looksLikeFunctionPointerDeclarator() && looksLikeParamList()) { List params = parseParamList(); boolean isOverride = false; boolean isMethodConst = false; // trailing const/override may appear in either order for (int i = 0; i < 3; i++) { if (matchKeyword("const")) { isMethodConst = true; continue; } if (matchKeyword("volatile")) { continue; } // const volatile method if (matchKeyword("override")) { isOverride = true; continue; } } // Trailing return type: "auto f() -> ReturnType" // Capture it and use it as the actual return type (replacing "auto"). TypeRef trailingReturnType = null; if (checkPunct("->")) { advance(); trailingReturnType = parseTypeRef(); } TypeRef effectiveReturnType = (trailingReturnType != null) ? trailingReturnType : type; // Consume __attribute__((...)): GCC attribute syntax before trailing qualifiers if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("__attribute__")) { advance(); // __attribute__ if (checkPunct("(")) { advance(); int _ad=1; while(!isAtEnd()&&_ad>0){if(checkPunct("("))_ad++;else if(checkPunct(")"))_ad--;advance();} } } // Consume trailing qualifiers: noexcept, noexcept(expr), override, final, requires, &/&& while (true) { if (checkOp("&") || checkOp("&&")) { // Ref-qualifier on member function: "T f() &" or "T f() &&" // Consume and discard -- not represented in the AST currently. advance(); } else if (checkKeyword("noexcept")) { advance(); if (checkPunct("(")) { advance(); int d=1; while(!isAtEnd()&&d>0){if(checkPunct("("))d++;else if(checkPunct(")"))d--;advance();} } } else if (checkKeyword("override")) { isOverride = true; advance(); } else if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("final")) { advance(); } else if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("requires")) { // trailing requires clause: "requires expr" -- consume to { or ; advance(); if (checkPunct("(")) { advance(); int d=1; while(!isAtEnd()&&d>0){if(checkPunct("("))d++;else if(checkPunct(")"))d--;advance();} } else if (checkPunct("{")) { // requires { expr } body -- consume the whole braced block advance(); int d=1; while (!isAtEnd() && d > 0) { if (checkPunct("{")) d++; else if (checkPunct("}")) { if (--d == 0) { advance(); break; } } advance(); } } else { // bare expression like "requires std::is_arithmetic_v" // Also handle "requires requires { ... }" (nested) if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("requires")) { advance(); // consume inner requires } if (checkPunct("{")) { advance(); int d=1; while (!isAtEnd() && d > 0) { if (checkPunct("{")) d++; else if (checkPunct("}")) { if (--d == 0) { advance(); break; } } advance(); } } else { while (!isAtEnd() && !checkPunct("{") && !checkPunct(";") && !checkOp("=")) { if (checkOp("<")) { advance(); int d=1; while(!isAtEnd()&&d>0){if(checkOp("<"))d++;else if(checkOp(">"))d--;advance();} } else advance(); } } } } else break; } // Pure-virtual specifier ("= 0") or = default / = delete boolean isPureVirtual = false, isDefault = false, isDelete = false; if (checkOp("=")) { int save = pos; advance(); if (checkLiteralZero()) { advance(); isPureVirtual = true; } else if (checkKeyword("default")) { advance(); isDefault = true; } else if (checkKeyword("delete")) { advance(); isDelete = true; } else { pos = save; } } // Constructor initializer list: ": mem(val), mem2(val2)" if (matchPunct(":") && !checkPunct(":")) { // consume initializer list entries until { or ; int _d = 0; while (!isAtEnd()) { if (checkPunct("{") && _d == 0) break; if (checkPunct("(") || checkPunct("{")) _d++; else if (checkPunct(")") || checkPunct("}")) _d--; advance(); } } Block body = checkPunct("{") ? parseBlock() : null; if (body == null) expectPunct(";"); FunctionDecl fn = new FunctionDecl(effectiveReturnType, name, templateParams, params, List.of(), body, false, false, isVirtual, isOverride, isMethodConst, isConstexprFn, isStatic, isPureVirtual, isDefault, isDelete, start.line(), start.col(), leadingComments); return List.of(fn); } // Variable declaration, possibly multi-declarator. // Bit field: "unsigned int active : 1" or unnamed "unsigned int : 2" // Consume ": width" specifier. Width may be a literal or constexpr identifier. if (checkPunct(":") && pos + 1 < tokens.size() && (tokens.get(pos + 1).type() == CppLexerTokenType.INT_LITERAL || tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER)) { advance(); // consume ":" parseExpr(); // consume width expression } // GCC __attribute__((...)): may appear after variable name if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("__attribute__")) { advance(); if (checkPunct("(")) { advance(); int _ad=1; while(!isAtEnd()&&_ad>0){if(checkPunct("("))_ad++;else if(checkPunct(")"))_ad--;advance();} } } List result = new ArrayList<>(); result.add(parseOneTopLevelDeclarator(type, name, isConst, isStatic, templateParams, start, leadingComments)); while (matchPunct(",")) { // Each declarator after the first comma can carry its OWN // leading "*"/"&" markers, independent of every other // declarator on the line -- real, standard C++ syntax // ("int* a, b;" -- only a is a pointer; "int* a, *b;" -- // both are pointers, the "*" repeated per declarator). // Confirmed real and necessary by Scrollbar.pde's own // corrected declaration ("HScrollbar* hs1, * hs2;"). // // IMPORTANT: build each declarator's type from the base // type's NAME/TEMPLATE-ARGS/CONST ONLY -- never reuse // "type"'s own pointerDepth/isReference, which already // belongs EXCLUSIVELY to the first declarator (parseTypeRef // consumes a "*"/"&" immediately after the base type name, // before the first declarator's name is even parsed). A // first attempt at this fix added each subsequent // declarator's marker count ON TOP of the shared type's // existing pointerDepth, which produced "HScrollbar**" for // the second declarator in "HScrollbar* hs1, * hs2;" // instead of the correct "HScrollbar*" -- caught by // checking the RENDERED OUTPUT, not just parse success; // "does this parse" and "is the resulting AST actually // correct" are different questions, the same lesson from // the return-statement misparse bug found much earlier in // this project. int extraPointerDepth = 0; boolean extraIsReference = false; while (checkOp("*") || checkOp("&")) { if (matchOp("*")) extraPointerDepth++; else { matchOp("&"); extraIsReference = true; } } String nextName = expectIdentifier().text(); TypeRef declaratorType = type; if (type instanceof NamedType nt) { declaratorType = new NamedType(nt.baseName(), nt.templateArgs(), extraPointerDepth, extraIsReference, nt.isConst(), false); } result.add(parseOneTopLevelDeclarator(declaratorType, nextName, isConst, isStatic, List.of(), start, List.of())); } expectPunct(";"); return result; } /** * Lookahead-only: determines whether the upcoming tokens form a real * top-level declaration or function definition (possibly preceded by * virtual/static/const) rather than a bare statement. A declaration has * the shape "[virtual] [static] [const] TypeName Identifier ..."; a bare * statement -- the Processing static-mode case -- starts with an * identifier immediately followed by something that isn't another * identifier (most commonly '(' for a call, or an assignment operator * for a plain assignment to an already-declared global). */ private boolean looksLikeTopLevelDeclarationOrFunction() { int save = pos; try { matchKeyword("virtual"); matchKeyword("static"); matchKeyword("volatile"); matchKeyword("const"); matchKeyword("constexpr"); matchKeyword("consteval"); matchKeyword("constinit"); matchKeyword("inline"); matchKeyword("static"); // tolerate either order, mirroring the real parse path if (looksLikeFunctionPointerVariable()) return true; if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; TypeRef type; try { type = parseTypeRef(); } catch (ParseException e) { return false; } // "operator==" style names: 'operator' is lexed as a KEYWORD. // Two cases reach here: // (a) "operator" itself was parsed as the bare "type" (when // there's no separate return type before it -- not // actually valid C++ for operator overloads but tolerated // defensively), OR // (b) more commonly, a real return type was already consumed // ("bool", "Handle", etc.) and the CURRENT token is now // "operator" itself, e.g. "bool operator==(...)" -- // confirmed real and previously missed by the // oop_features.cpp fixture's Handle::operator==. // Either way, this is unambiguously a declaration. if ((type instanceof NamedType nt && nt.baseName().equals("operator")) || checkKeyword("operator")) { return true; } // Constructor: type name followed by ( -- "RGBA(...)" inside struct RGBA if (checkPunct("(")) return true; return check(CppLexerTokenType.IDENTIFIER); } finally { pos = save; } } /** * Parses an ordinary name, "operator" followed by an operator/comparison * token (e.g. "operator=="), or a "::"-qualified name for out-of-class * static member definitions (e.g. "Counter::count" in * "int Counter::count = 0;", confirmed real by the kitchen-sink fixture). */ private String parseFunctionOrVariableName() { if (checkKeyword("operator")) { CppLexerToken opKw = advance(); // Special multi-token operator names: // operator[] -- subscript // operator() -- function call // operator new / operator delete // operator== / operator+ / etc. -- single-token symbols if (checkPunct("[")) { advance(); // '[' expectPunct("]"); return opKw.text() + "[]"; } if (checkPunct("(")) { advance(); // '(' expectPunct(")"); return opKw.text() + "()"; } if (checkKeyword("new") || checkKeyword("delete")) { CppLexerToken kw = advance(); String opName = opKw.text() + " " + kw.text(); // operator new[] / operator delete[] if (checkPunct("[")) { advance(); expectPunct("]"); opName += "[]"; } return opName; } // Cast operator: "operator int", "operator float", "operator bool", etc. // ONLY fire for actual C++ primitive type keywords, not arbitrary // keywords that happen to follow "operator" in other contexts. // Using a small explicit set prevents keywords like "color", "auto", // "return", etc. from being misidentified as cast targets. if (check(CppLexerTokenType.KEYWORD) && CAST_OPERATOR_TYPE_KEYWORDS.contains(peek().text())) { CppLexerToken castTok = advance(); return opKw.text() + " " + castTok.text(); } // Also handle identifier type names used as cast targets (e.g. // "operator MyType()" where MyType is a user-defined class). // Only safe when the next token is an identifier (not a keyword), // so we don't misidentify keyword-named things like "color". if (check(CppLexerTokenType.IDENTIFIER)) { CppLexerToken castTok = advance(); return opKw.text() + " " + castTok.text(); } // User-defined literal operator: "operator"" _suffix" // The "" is a STRING_LITERAL token; the suffix is an IDENTIFIER. if (check(CppLexerTokenType.STRING_LITERAL) && peek().text().equals("\"\"")) { advance(); // consume "" // Optional suffix identifier (e.g. _deg, _kb, _v) if (check(CppLexerTokenType.IDENTIFIER) || (check(CppLexerTokenType.KEYWORD) && peek().text().startsWith("_"))) { CppLexerToken suffix = advance(); return opKw.text() + "\"\"" + suffix.text(); } return opKw.text() + "\"\""; } // Ordinary single-token operator, e.g. "==", "+", "<<". // Special case: spaceship operator "<=>" is lexed as "<=" then ">". CppLexerToken opTok = advance(); if (opTok.text().equals("<=") && checkOp(">")) { advance(); // consume the ">" return opKw.text() + "<=>"; } return opKw.text() + opTok.text(); } // Cast-operator case: "operator" was already consumed by parseTypeRef() // as NamedType("operator"), leaving a keyword type like "bool"/"int" as // the current token. "operator bool()" and "operator int()" reach here. // Guard against mistakenly eating real qualifiers, storage specifiers, OR // Processing-specific keywords like "color" that are legitimately used as // method names (e.g. "Builder& color(int r, int g, int b)" in the // builder pattern) -- these must fall through to expectIdentifier() below. if (check(CppLexerTokenType.KEYWORD) && !checkKeyword("const") && !checkKeyword("override") && !checkKeyword("virtual") && !checkKeyword("static") && !checkKeyword("inline") && !checkKeyword("explicit") && !checkKeyword("operator") && !PSEUDO_TYPE_KEYWORDS_USABLE_AS_NAMES.contains(peek().text())) { CppLexerToken castTok = advance(); String suffix = ""; while (checkOp("*") || checkOp("&")) suffix += advance().text(); return "operator " + castTok.text() + suffix; } // Unnamed bitfield: "unsigned int : 2" -- no identifier before ":" // Return empty name; the caller's bitfield-width consumer will handle ":". if (checkPunct(":") && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.INT_LITERAL) { return ""; } String name = expectIdentifier().text(); // Member data pointer: "ClassName::* varName" -- e.g. "int Point::* ptr" // After consuming "Point" as the name, if :: follows and then * (not another identifier), // this is a pointer-to-member-data declarator, not a qualified name. if (checkPunct("::") && pos + 1 < tokens.size() && tokens.get(pos + 1).isOp("*")) { advance(); // "::" advance(); // "*" String memberName = expectIdentifier().text(); return name + "::* " + memberName; } while (checkPunct("::")) { advance(); name += "::" + expectIdentifier().text(); } // Variable template specialization: "zero" -- consume if (checkOp("<") && looksLikeTemplateArgList()) { StringBuilder sb = new StringBuilder(name); sb.append("<"); advance(); // consume < int depth = 1; while (!isAtEnd() && depth > 0) { if (checkOp("<")) { depth++; sb.append(advance().text()); } else if (checkOp(">")) { depth--; if (depth > 0) sb.append(advance().text()); else advance(); } else if (checkOp(">>")) { depth -= 2; splitTrailingShiftIntoTwoCloseAngles(); if (depth > 0) sb.append(advance().text()); else advance(); } else sb.append(advance().text()); } sb.append(">"); name = sb.toString(); } return name; } private VariableDecl parseOneTopLevelDeclarator(TypeRef type, String name, boolean isConst, boolean isStatic, List templateParams, CppLexerToken start, List leadingComments) { List dims = parseOptionalArrayDims(); // Bitfield declarator: "unsigned int active : 1" -- consume ": width" and discard. // The ":" is a PUNCT token; must be distinguished from inheritance ":" (which // appears at class scope before a type name) and ternary ":" (inside expressions). // Here we're inside a declarator so ":" followed by INT_LITERAL or IDENTIFIER // is unambiguously a bitfield width specifier. if (checkPunct(":") && pos + 1 < tokens.size() && (tokens.get(pos + 1).type() == CppLexerTokenType.INT_LITERAL || tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER)) { advance(); // consume ":" parseExpr(); // consume width expression (usually a literal, may be constexpr) } Expr initializer = null; if (matchOp("=")) { initializer = checkPunct("{") ? parseInitializerList() : parseExpr(); } else if (checkPunct("(")) { initializer = parseDirectInitAsCall(name); } else if (checkPunct("{")) { // Brace-direct-init: "Type name{args};" -- the unambiguous, // most-vexing-parse-proof sibling of paren-direct-init // ("Type name(args);"). Confirmed necessary by this project's // own CodeGen, which deliberately renders the self-named- // CallExpr direct-init shape using braces rather than parens // (see CodeGen.emitDeclaratorTail's notes on why) -- without // this branch, the parser couldn't read its own generated // output back, which a round-trip test caught immediately. // Represented with the exact same CallExpr shape as the paren // form (callee = an Identifier matching the declarator's own // name), since semantically they mean the same thing and // CodeGen already knows how to render that shape as braces. initializer = parseDirectInitAsCall(name); } return new VariableDecl(type, name, dims, initializer, isConst, isStatic, templateParams, start.line(), start.col(), leadingComments); } /** * Lookahead-only: from the "(" that follows a name (in * parseFunctionOrVariable, deciding between "this is a function decl" * and "this is direct-init construction syntax"), determines whether the * parenthesized content actually looks like a parameter list rather than * a constructor-call argument list. * * Confirmed necessary by Arctangent.pde's "Eye e1(250, 16, 120);" -- * direct-init with literal constructor arguments. A real parameter list * is either empty ("()") or starts with something type-shaped followed * by a parameter name; an argument list starting with a literal, * a unary operator, or anything else that can't start a TypeRef is * unambiguously NOT a parameter list. */ private boolean looksLikeParamList() { int save = pos; try { expectPunct("("); if (checkPunct(")")) return true; if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; // Skip explicit object param keyword: "this" if (checkKeyword("this")) advance(); if (checkPunct(")")) return true; if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; try { parseTypeRef(); } catch (ParseException e) { return false; } // Accept: named param, variadic pack "...", anonymous param ")", or // reference/pointer-to-array: "int (&arr)[10]" -- ( follows the type return check(CppLexerTokenType.IDENTIFIER) || checkPunct("...") // variadic: "Args... args" or "T..." || checkPunct(")") // anonymous param: "void f(int)" || checkPunct(",") // next param after anonymous || checkPunct("("); // reference/pointer-to-array param: "int (&arr)[10]" } finally { pos = save; } } private List parseParamList() { expectPunct("("); List params = new ArrayList<>(); if (!checkPunct(")")) { params.add(parseParam()); while (matchPunct(",")) { params.add(parseParam()); } } expectPunct(")"); return params; } /** * Lookahead for the raw function-pointer VARIABLE form specifically * (distinct from looksLikeFunctionPointerDeclarator, which checks * starting from the "(" itself once a return type has already been * consumed) -- this version starts from before the return type, so it * must first skip over a tentative TypeRef. */ private boolean looksLikeFunctionPointerVariable() { int save = pos; try { if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; try { parseTypeRef(); } catch (ParseException e) { return false; } return looksLikeFunctionPointerDeclarator(); } finally { pos = save; } } private EnumDecl parseEnumDecl(List leadingComments) { CppLexerToken start = expectKeyword("enum"); boolean isScoped = matchKeyword("class"); if (!isScoped) matchKeyword("struct"); // "enum struct" is also valid String name = expectIdentifier().text(); // Optional underlying type: "enum class Color : int" if (matchPunct(":")) { parseTypeRef(); // consume the underlying type, discard } expectPunct("{"); List values = new ArrayList<>(); if (!checkPunct("}")) { values.add(expectIdentifier().text()); if (matchOp("=")) parseExpr(); // optional initializer: "Red = 1" while (matchPunct(",")) { if (checkPunct("}")) break; // tolerate a trailing comma values.add(expectIdentifier().text()); if (matchOp("=")) parseExpr(); // optional initializer } } expectPunct("}"); expectPunct(";"); return new EnumDecl(name, isScoped, values, start.line(), start.col(), leadingComments); } private NamespaceDecl parseNamespaceDecl(List leadingComments) { CppLexerToken start = peek(); boolean isInlineNs = matchKeyword("inline"); // "inline namespace" expectKeyword("namespace"); StringBuilder nameBuilder = new StringBuilder(expectIdentifier().text()); while (checkPunct("::")) { advance(); nameBuilder.append("::").append(expectIdentifier().text()); } String name = nameBuilder.toString(); if (matchOp("=")) { StringBuilder alias = new StringBuilder(); while (!isAtEnd() && !checkPunct(";")) { alias.append(peek().text()); advance(); } matchPunct(";"); return new NamespaceDecl(name, false, List.of(), start.line(), start.col(), leadingComments); } expectPunct("{"); List items = new ArrayList<>(); while (!checkPunct("}")) { if (isAtEnd()) throw error("unexpected end of input inside namespace '" + name + "'"); List comments = consumeLeadingComments(); if (checkPunct("}")) break; items.addAll(parseTopLevelItem(comments)); } expectPunct("}"); return new NamespaceDecl(name, isInlineNs, items, start.line(), start.col(), leadingComments); } private UsingNamespaceDecl parseUsingNamespaceDecl(List leadingComments) { CppLexerToken start = expectKeyword("using"); expectKeyword("namespace"); String name = parseQualifiedTypeName(); expectPunct(";"); return new UsingNamespaceDecl(name, start.line(), start.col(), leadingComments); } // ----- Type references ------------------------------------------------ /** * Parses a TypeRef in any position: variable type, return type, param * type, template argument, cast target type, etc. * * Handles, in order: * 1. An optional leading "const". * 2. The raw C-style function-pointer special case: "Type (*)(Params)". * Detected by lookahead -- after parsing the return type, if we see * "(" "*" we know this is a function pointer declarator, not an * ordinary parenthesized expression (callers parsing a VariableDecl * pass the name through separately -- see parseFunctionPointerDecl * for the full "Type (*name)(Params)" variable-declaration form). * 3. An ordinary dotted/"::"-qualified base name (e.g. "std::string"). * 4. An optional "<...>" template argument list, where each argument * is itself parsed as either a TypeRef or -- specifically inside * std::function's argument list -- a bare function signature * "ReturnType(ParamType, ...)" with no name and no "(*)" (see * tryParseFunctionSignatureArg). * 5. Any number of trailing '*' (pointerDepth) and at most one trailing * '&' (isReference). */ private TypeRef parseTypeRef() { boolean isConst = matchKeyword("const"); return parseTypeRefAfterConst(isConst); } private TypeRef parseTypeRef(boolean leadingConst) { // Always consume "const" if present. If leadingConst=true, isConst is already // set; we still MUST consume the token so parseQualifiedTypeName doesn't see // "const" as the type name (e.g. "constexpr const char* p" -- constexpr sets // leadingConst=true, then "const" must still be consumed before "char"). // If const is explicitly present, always mark type as const regardless of leadingConst. boolean hasExplicitConst = matchKeyword("const"); return parseTypeRefAfterConst(leadingConst || hasExplicitConst); } private TypeRef parseTypeRefAfterConst(boolean isConst) { // typename/struct/class/enum X -- type elaboration or dependent type, consume prefix if (checkKeyword("typename")) advance(); else if (checkKeyword("struct") || checkKeyword("class") || checkKeyword("enum")) advance(); // decltype(expr) -- C++11 computed type if (checkKeyword("decltype")) { advance(); expectPunct("("); int d = 1; StringBuilder dtExpr = new StringBuilder("decltype("); while (!isAtEnd() && d > 0) { if (checkPunct("(")) { d++; dtExpr.append("("); advance(); } else if (checkPunct(")")) { d--; if (d > 0) dtExpr.append(")"); advance(); } else { dtExpr.append(peek().text()); advance(); } } dtExpr.append(")"); return new NamedType(dtExpr.toString(), List.of(), 0, false, isConst, false); } String baseName = parseQualifiedTypeName(); if (checkKeyword("auto")) { advance(); baseName = "auto"; } // C++20 concept-constrained auto if (matchKeyword("const")) isConst = true; // east-const: "int const" == "const int" matchKeyword("volatile"); // consume optional volatile after type name List templateArgs = List.of(); if (checkOp("<") && !checkOp("<>")) { if (pos + 1 < tokens.size() && tokens.get(pos + 1).isOp(">")) { // Explicit empty "<>" -- e.g. "Ec06Buf<> ec06_b" or "new Foo<>()". // Store sentinel so CodeGen emits "<>" rather than bare name. // (List.of() is indistinguishable from "no args" -- sentinel needed.) advance(); advance(); // consume < and > templateArgs = List.of(new NamedType("<>", List.of(), 0, false, false, false)); } else { templateArgs = parseTemplateArgList(); } // After template args, may have "::member" -- e.g. // "std::enable_if::type" or "std::is_pointer::value". // Fold these into the baseName as a qualified suffix so the full // type name round-trips correctly. while (checkPunct("::")) { advance(); // '::' if (check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD)) { baseName = baseName + "<...>::" + advance().text(); // May have further template args after the member if (checkOp("<")) { try { parseTemplateArgList(); } catch (ParseException ignored) {} } } } } int pointerDepth = 0; while (matchOp("*")) { pointerDepth++; matchKeyword("const"); // east-const pointer: "int* const" -- consume trailing const matchKeyword("volatile"); // similarly for volatile } boolean isReference = matchOp("&"); // Only consume && as rvalue-ref if followed by a name or * (not = or binary operator context) boolean isRvalueRef = !isReference && checkOp("&&") && pos + 1 < tokens.size() && (tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER || tokens.get(pos + 1).type() == CppLexerTokenType.KEYWORD || tokens.get(pos + 1).isOp("*") || tokens.get(pos + 1).isPunct(")") || tokens.get(pos + 1).isPunct(",") || tokens.get(pos + 1).isPunct(">") || tokens.get(pos + 1).isPunct("...")); if (isRvalueRef) advance(); return new NamedType(baseName, templateArgs, pointerDepth, isReference, isConst, isRvalueRef); } /** * Parses a "::"-joined sequence of identifiers/keywords-used-as-typenames, * e.g. "std::string", "MyNamespace::Handle", or a single plain name like * "int" or "Handle". Accepts KEYWORD tokens too (not just IDENTIFIER) * since C++ builtin type keywords ("int", "float", "bool", "void", "auto", * "char", etc.) are lexed as KEYWORD, and "color" (a Processing-flavored * pseudo-keyword, see Lexer notes) needs the same treatment. */ /** * Parses a "::"-joined sequence of identifiers/keywords-used-as-typenames, * e.g. "std::string", "MyNamespace::Handle", or a single plain name like * "int" or "Handle". Also handles compound built-in integer type * keywords -- "signed"/"unsigned"/"long"/"short" composing with a * following base type keyword, e.g. "signed char", "unsigned int", * "long long", "unsigned long long" -- confirmed real and previously * missed entirely by the original corpus walk (found via real-corpus * testing on Datatype_Conversion.pde's "signed char b;"). */ private String parseQualifiedTypeName() { StringBuilder sb = new StringBuilder(); sb.append(parseTypeNameSegment()); // Compound integer-type keyword combinations: greedily consume any // run of signed/unsigned/long/short/int/char that follows, since // these only ever combine with each other (never with a separate // user type name in the same position) -- e.g. "unsigned long long // int" is valid C++, "unsigned long long ArrayList" never occurs. while ((check(CppLexerTokenType.KEYWORD)) && isIntegerTypeModifierOrBase(peek().text()) && isIntegerTypeModifierOrBase(sb.toString())) { sb.append(' ').append(parseTypeNameSegment()); } while (checkPunct("::")) { advance(); sb.append("::").append(parseTypeNameSegment()); } return sb.toString(); } private static final java.util.Set INTEGER_TYPE_WORDS = java.util.Set.of( "signed", "unsigned", "long", "short", "int", "char", "double", "float" ); /** True if every space-separated word in s is one of the integer-type-modifier/base words. */ private boolean isIntegerTypeModifierOrBase(String s) { for (String word : s.split(" ")) { if (!INTEGER_TYPE_WORDS.contains(word)) return false; } return true; } /** * Consumes tokens for a default template argument value or type expression, * stopping when it hits an unbalanced '>' or ',' or ';' at depth 0. * This is necessary because parseExpr() would consume '>' as a comparison * operator, swallowing the closing '>' of the template parameter list. */ private void parseTemplateDefaultValue() { // parenDepth: tracks () and [] nesting -- a > inside parens is a // comparison operator, not a template closer. e.g. (sizeof(T) > 2 ? 8 : 4) // angleDepth: tracks <> nesting from template args inside the default value // braceDepth: tracks {} so < inside lambdas/init-lists doesn't affect angle depth int parenDepth = 0; int angleDepth = 0; int braceDepth = 0; while (!isAtEnd()) { if (checkPunct("{")) { braceDepth++; advance(); continue; } if (checkPunct("}")) { if (braceDepth > 0) { braceDepth--; advance(); continue; } break; } if (checkPunct("(") || checkPunct("[")) { parenDepth++; advance(); continue; } if (checkPunct(")") || checkPunct("]")) { if (parenDepth == 0) break; // closing the outer param list parenDepth--; advance(); continue; } // < and > are only angle brackets when not inside parens/braces if (checkOp("<") && parenDepth == 0 && braceDepth == 0) { angleDepth++; advance(); continue; } if (checkOp(">") && parenDepth == 0 && braceDepth == 0) { if (angleDepth == 0) break; // closes the outer template param list angleDepth--; advance(); continue; } if (checkOp(">>") && parenDepth == 0 && braceDepth == 0) { if (angleDepth == 1) { // Split ">>" -- first closes inner angle, second closes outer splitTrailingShiftIntoTwoCloseAngles(); angleDepth--; advance(); // consume first ">" break; // second ">" left for outer list } if (angleDepth == 0) break; angleDepth -= 2; advance(); continue; } if (parenDepth == 0 && angleDepth == 0 && checkPunct(",")) break; if (parenDepth == 0 && angleDepth == 0 && checkPunct(";")) break; advance(); } } private String parseTypeNameSegment() { CppLexerToken t = peek(); if (t.type() == CppLexerTokenType.IDENTIFIER || t.type() == CppLexerTokenType.KEYWORD) { advance(); return t.text(); } throw error("expected a type name but found '" + t.text() + "'"); } /** * Parses "<Arg1, Arg2, ...>" where each Arg is either an ordinary * TypeRef or a bare function-signature (for std::function<Ret(Params)>). */ private List parseTemplateArgList() { expectOp("<"); List args = new ArrayList<>(); // Explicit empty "<>": store a sentinel so CodeGen emits "<>" not bare name if (checkOp(">") || checkOp(">>")) { if (checkOp(">>")) splitTrailingShiftIntoTwoCloseAngles(); expectOp(">"); args.add(new NamedType("<>", List.of(), 0, false, false, false)); return args; } if (!checkOp(">")) { TypeRef _a0 = parseTemplateArg(); if (matchPunct("...") && _a0 instanceof NamedType _nt0) _a0 = new NamedType(_nt0.baseName() + "...", _nt0.templateArgs(), _nt0.pointerDepth(), _nt0.isReference(), _nt0.isConst(), _nt0.isRvalueRef()); args.add(_a0); while (matchPunct(",")) { if (checkOp(">") || checkOp(">>")) break; TypeRef _ai = parseTemplateArg(); if (matchPunct("...") && _ai instanceof NamedType _nti) _ai = new NamedType(_nti.baseName() + "...", _nti.templateArgs(), _nti.pointerDepth(), _nti.isReference(), _nti.isConst(), _nti.isRvalueRef()); args.add(_ai); } } // Note: ">>" closing two nested template lists at once (e.g. // "Pair>") is lexed as a single ">>" OPERATOR token // by the lexer's longest-match rule, not as two separate '>' tokens -- // the corpus's "template angle brackets vs shift/comparison ambiguity" // smoke test confirmed ">>" lexes as one token. We must special-case // that here: a closing ">>" needs to close *this* list and leave a // single '>' behind for the enclosing list to consume. if (checkOp(">>")) { splitTrailingShiftIntoTwoCloseAngles(); } expectOp(">"); return args; } /** * Rewrites the current ">>" token into two consecutive ">" tokens in the * token stream, so nested template arg lists each consume exactly one * '>' as their closer. This mutates the token list in place rather than * re-lexing, which is simpler than threading "are we inside a template * list" state back into the lexer (the lexer has no such context, and * shouldn't need one -- this is a parser-level concern only). */ private void splitTrailingShiftIntoTwoCloseAngles() { CppLexerToken shift = tokens.get(pos); CppLexerToken first = new CppLexerToken(CppLexerTokenType.OPERATOR, ">", shift.line(), shift.col()); CppLexerToken second = new CppLexerToken(CppLexerTokenType.OPERATOR, ">", shift.line(), shift.col() + 1); tokens.set(pos, first); tokens.add(pos + 1, second); } /** Render a NamedType back to its source string including template args. */ private static String renderNamedTypeAsString(NamedType nt) { StringBuilder sb = new StringBuilder(nt.baseName()); if (!nt.templateArgs().isEmpty()) { sb.append("<"); for (int i = 0; i < nt.templateArgs().size(); i++) { if (i > 0) sb.append(", "); TypeRef a = nt.templateArgs().get(i); if (a instanceof NamedType na) sb.append(renderNamedTypeAsString(na)); else sb.append(a.toString()); } sb.append(">"); } if (nt.pointerDepth() > 0) sb.append("*".repeat(nt.pointerDepth())); if (nt.isReference()) sb.append("&"); return sb.toString(); } private TypeRef parseTemplateArg() { if (checkKeyword("true") || checkKeyword("false") || check(CppLexerTokenType.BOOL_LITERAL)) { String val = advance().text(); return new NamedType(val, List.of(), 0, false, false, false); } if (check(CppLexerTokenType.INT_LITERAL) || check(CppLexerTokenType.FLOAT_LITERAL) || check(CppLexerTokenType.CHAR_LITERAL)) { String val = advance().text(); return new NamedType(val, List.of(), 0, false, false, false); } if (checkOp("-") && pos + 1 < tokens.size() && (tokens.get(pos + 1).type() == CppLexerTokenType.INT_LITERAL || tokens.get(pos + 1).type() == CppLexerTokenType.FLOAT_LITERAL)) { advance(); String val = "-" + advance().text(); return new NamedType(val, List.of(), 0, false, false, false); } // Address-of non-type template argument: "&Widget::value", "&Class::method" if (checkOp("&")) { advance(); // consume & String name = parseQualifiedTypeName(); // Widget::value return new NamedType("&" + name, List.of(), 0, false, false, false); } // Logical negation: "!std::is_integral::value" in enable_if<!...> if (checkOp("!")) { advance(); // consume '!' parseTemplateDefaultValue(); // consume the rest of the expression return new NamedType("!...", List.of(), 0, false, false, false); } // decltype as template argument: "Pair" // Must be handled before parseTypeRef() which would consume "decltype" as a type. if (checkKeyword("decltype")) { advance(); // consume "decltype" expectPunct("("); int d = 1; StringBuilder dt = new StringBuilder("decltype("); while (!isAtEnd() && d > 0) { if (checkPunct("(")) { d++; dt.append("("); advance(); } else if (checkPunct(")")) { d--; if (d > 0) dt.append(")"); advance(); } else { dt.append(peek().text()); advance(); } } dt.append(")"); return new NamedType(dt.toString(), List.of(), 0, false, false, false); } // Pack expansion "..." trailing a type: "Ts..." in template args // Also handles standalone "..." as a variadic marker if (checkPunct("...")) { advance(); return new NamedType("...", List.of(), 0, false, false, false); } // sizeof expression: sizeof(T) or sizeof...(Args) if (checkKeyword("sizeof")) { int sStart = pos; advance(); StringBuilder sof = new StringBuilder("sizeof"); if (checkPunct("...")) { sof.append("..."); advance(); } if (checkPunct("(")) { sof.append("("); advance(); int d = 1; while (!isAtEnd() && d > 0) { if (checkPunct("(")) { d++; sof.append("("); advance(); } else if (checkPunct(")")) { d--; if (d > 0) sof.append(")"); advance(); } else { sof.append(peek().text()); advance(); } } sof.append(")"); } return new NamedType(sof.toString(), List.of(), 0, false, false, false); } TypeRef maybeReturnType = parseTypeRef(); // NTTP brace-init: "Point19{0.0f, 0.0f}" as template argument if (checkPunct("{") && maybeReturnType instanceof NamedType _nttp) { StringBuilder _nb = new StringBuilder(_nttp.baseName()).append("{"); int _nd = 1; advance(); while (!isAtEnd() && _nd > 0) { if (checkPunct("{")) { _nd++; _nb.append("{"); advance(); } else if (checkPunct("}")) { _nd--; if (_nd == 0) { _nb.append("}"); advance(); break; } _nb.append("}"); advance(); } else { _nb.append(peek().text()); advance(); } } return new NamedType(_nb.toString(), List.of(), 0, false, false, false); } // Compound boolean expression in template arg: "is_arithmetic_v && !is_same_v" // The && was consumed as rvalue-ref by parseTypeRef; check if ! or || follows // The && was already consumed by parseTypeRef as rvalue-ref boolean trailingRvalueRef = maybeReturnType instanceof NamedType ntrr && ntrr.isRvalueRef(); if (trailingRvalueRef || checkOp("||") || checkOp("!")) { // Strip the falsely-consumed && from the type if (trailingRvalueRef && maybeReturnType instanceof NamedType ntrr2) { maybeReturnType = new NamedType(ntrr2.baseName(), ntrr2.templateArgs(), ntrr2.pointerDepth(), ntrr2.isReference(), ntrr2.isConst(), false); } StringBuilder expr = new StringBuilder( maybeReturnType instanceof NamedType nt ? renderNamedTypeAsString(nt) : ""); if (trailingRvalueRef) expr.append(" && "); while (!isAtEnd()) { if (checkOp("&&")) { expr.append(" && "); advance(); } else if (checkOp("||")) { expr.append(" || "); advance(); } else if (checkOp("!")) { expr.append("!"); advance(); } else if (checkOp(">") || checkOp(">>") || checkPunct(",")) break; else if (checkPunct("(") || check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD)) { TypeRef sub = parseTypeRef(); if (sub instanceof NamedType nts) expr.append(renderNamedTypeAsString(nts)); } else break; } return new NamedType(expr.toString(), List.of(), 0, false, false, false); } if (checkPunct("(")) { return parseFunctionSignatureTail(maybeReturnType); } // NTTP arithmetic expression: "N-1", "N*2", "N+1" etc. // After parsing the base type/value, consume arithmetic ops and operands if (maybeReturnType instanceof NamedType nt && !nt.baseName().equals("void")) { StringBuilder expr = new StringBuilder(nt.baseName()); while (!isAtEnd() && (checkOp("+") || checkOp("-") || checkOp("*") || checkOp("/") || checkOp("%"))) { expr.append(advance().text()); if (!isAtEnd() && (check(CppLexerTokenType.INT_LITERAL) || check(CppLexerTokenType.IDENTIFIER))) { expr.append(advance().text()); } } if (expr.length() > nt.baseName().length()) { return new NamedType(expr.toString(), List.of(), 0, false, false, false); } } return maybeReturnType; } /** Parses "(ParamType, ...)" given an already-parsed return type, producing a FunctionSignatureType. */ private FunctionSignatureType parseFunctionSignatureTail(TypeRef returnType) { expectPunct("("); List paramTypes = new ArrayList<>(); if (!checkPunct(")")) { paramTypes.add(parseTypeRef()); while (matchPunct(",")) { paramTypes.add(parseTypeRef()); } } expectPunct(")"); return new FunctionSignatureType(returnType, paramTypes); } /** * Detects whether the upcoming tokens form a raw C-style function pointer * declarator: "(" "*" IDENTIFIER ")" "(" -- used by declaration parsing to * decide whether a "(" right after a type belongs to this special form * rather than being parsed as part of an ordinary VariableDecl. Lookahead * only; does not consume. */ private boolean looksLikeFunctionPointerDeclarator() { if (!checkPunct("(")) return false; int i = 1; // Consume optional Class:: qualifier for member function pointers while (pos + i + 1 < tokens.size() && tokens.get(pos + i).type() == CppLexerTokenType.IDENTIFIER && tokens.get(pos + i + 1).isPunct("::")) { i += 2; } if (pos + i >= tokens.size()) return false; if (!tokens.get(pos + i).isOp("*") && !tokens.get(pos + i).isOp("&")) return false; i++; // past * or & if (pos + i >= tokens.size()) return false; // name (possibly followed by [N] for array-of-function-pointers) if (tokens.get(pos + i).type() != CppLexerTokenType.IDENTIFIER) return false; i++; // past name if (pos + i >= tokens.size()) return false; // Either ) directly followed by ( for params, or [N] then ) // A bare "(*name)" MUST be followed by "(" (param list) to be a function pointer. // "shader(*noiseShader);" is a call expression, not a fn ptr -- no "(" after ")". if (tokens.get(pos + i).isPunct(")")) { int j = i + 1; if (j >= tokens.size()) return false; // Function pointer: "void (*fp)(int)" -- "(" must follow // Array of fn ptrs: "void (*fp)[N]" -- "[" may follow return tokens.get(pos + j).isPunct("(") || tokens.get(pos + j).isPunct("["); } if (tokens.get(pos + i).isPunct("[")) return true; // array of fn ptrs return false; } /** * Parses the remainder of "Type (*name)(ParamTypes...)" given an * already-parsed return type, returning the declared variable's name and * its FunctionPointerType. Confirmed necessary by the corpus's * useRawFunctionPointer fixture. */ private NameAndFunctionPointerType parseFunctionPointerDeclaratorTail(TypeRef returnType) { expectPunct("("); // Consume optional Class:: qualifier for member function pointers: (Widget::*name) // Encode Class:: and & vs * into name so CodeGen can render correctly. StringBuilder classQual = new StringBuilder(); while (check(CppLexerTokenType.IDENTIFIER) && tokens.get(pos + 1).isPunct("::")) { classQual.append(advance().text()).append("::"); advance(); // consume "::" } boolean isRef = matchOp("&"); if (!isRef) matchOp("*"); // (&name) or (*name) String name = expectIdentifier().text(); // Encoded name: "Point::*memberFuncPtr" or "&refToRow" etc. String encodedName = classQual.length() > 0 ? classQual.toString() + (isRef ? "&" : "*") + name : (isRef ? "&" : "") + name; // Array-of-function-pointers: (*arr[5]) -- consume the [N] subscript // Encode dims into name now so CodeGen can emit "void (*arr[5])(int)" correctly. // parseOptionalArrayDims consumes them; we re-encode as "[N]" string suffix. List arrayDims = parseOptionalArrayDims(); StringBuilder arrayDimStr = new StringBuilder(); for (Expr dim : arrayDims) { arrayDimStr.append("["); if (dim != null) arrayDimStr.append(CodeGen.renderExpr(dim)); arrayDimStr.append("]"); } encodedName = encodedName + arrayDimStr.toString(); expectPunct(")"); // Pointer-to-array or reference-to-array: "int (*p)[20]" / "int (&r)[20]" // Encode array dims into name for CodeGen: "&refToRow[20]" if (checkPunct("[")) { StringBuilder dimStr = new StringBuilder(); while (checkPunct("[")) { advance(); dimStr.append("["); if (!checkPunct("]")) { dimStr.append(peek().text()); parseExpr(); } expectPunct("]"); dimStr.append("]"); } return new NameAndFunctionPointerType(encodedName + dimStr.toString(), new FunctionPointerType(returnType, List.of())); } expectPunct("("); List paramTypes = new ArrayList<>(); if (!checkPunct(")")) { paramTypes.add(parseTypeRef()); while (matchPunct(",")) { paramTypes.add(parseTypeRef()); } } expectPunct(")"); boolean isConstMethod = matchKeyword("const"); // trailing const on member function pointer // Encode const into name with sentinel __const__ for CodeGen if (isConstMethod) encodedName = encodedName + "__const__"; return new NameAndFunctionPointerType(encodedName, new FunctionPointerType(returnType, paramTypes)); } private record NameAndFunctionPointerType(String name, FunctionPointerType type) { } // ----- Expressions (precedence climbing, low to high) ----------------- // // assignment -> ternary (("=") assignment)? [right-assoc] // ternary -> logicalOr ("?" expr ":" ternary)? // logicalOr -> logicalAnd ("||" logicalAnd)* // logicalAnd -> bitOr ("&&" bitOr)* // bitOr -> bitXor ("|" bitXor)* // bitXor -> bitAnd ("^" bitAnd)* // bitAnd -> equality ("&" equality)* // equality -> relational (("=="|"!=") relational)* // relational -> shift (("<"|">"|"<="|">=") shift)* // shift -> additive (("<<"|">>") additive)* // additive -> multiplicative (("+"|"-") multiplicative)* // multiplicative -> unary (("*"|"/"|"%") unary)* // unary -> ("-"|"!"|"&"|"~"|"++"|"--") unary | postfix // postfix -> primary (call | member | index | "++" | "--")* // primary -> literal | identifier | scopedName | "(" expr ")" // | lambda | new | cast | initializerList private static final java.util.Set COMPOUND_ASSIGN_OPS = java.util.Set.of( "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=" ); Expr parseExpr() { return parseAssignment(); } private Expr parseAssignment() { Expr left = parseTernary(); if (checkOp("=")) { CppLexerToken t = advance(); Expr right = parseAssignment(); // right-associative return new AssignExpr(left, right, t.line(), t.col(), List.of()); } if (peek().type() == CppLexerTokenType.OPERATOR && COMPOUND_ASSIGN_OPS.contains(peek().text())) { CppLexerToken t = advance(); Expr right = parseAssignment(); return new BinaryExpr(t.text(), left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseTernary() { // throw-expression: "throw expr" is valid as an expression in C++ // e.g. "cond ? val : throw std::runtime_error(...)" if (checkKeyword("throw")) { CppLexerToken t = advance(); if (checkPunct(";") || checkPunct(")") || checkPunct(":") || checkPunct(",")) { // bare throw (rethrow) with no operand return new UnaryExpr("throw ", new Literal(Literal.Kind.INT, "0", t.line(), t.col(), List.of()), t.line(), t.col(), List.of()); } Expr val = parseTernary(); return new UnaryExpr("throw ", val, t.line(), t.col(), List.of()); } Expr cond = parseLogicalOr(); if (checkPunct("?")) { CppLexerToken t = advance(); Expr thenExpr = parseExpr(); expectPunct(":"); Expr elseExpr = parseTernary(); return new TernaryExpr(cond, thenExpr, elseExpr, t.line(), t.col(), List.of()); } return cond; } private Expr parseLogicalOr() { Expr left = parseLogicalAnd(); while (checkOp("||")) { CppLexerToken t = advance(); Expr right = parseLogicalAnd(); left = new BinaryExpr("||", left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseLogicalAnd() { Expr left = parseBitOr(); while (checkOp("&&")) { CppLexerToken t = advance(); Expr right = parseBitOr(); left = new BinaryExpr("&&", left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseBitOr() { Expr left = parseBitXor(); while (checkOp("|")) { CppLexerToken t = advance(); Expr right = parseBitXor(); left = new BinaryExpr("|", left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseBitXor() { Expr left = parseBitAnd(); while (checkOp("^")) { CppLexerToken t = advance(); Expr right = parseBitAnd(); left = new BinaryExpr("^", left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseBitAnd() { Expr left = parseEquality(); while (checkOp("&")) { CppLexerToken t = advance(); Expr right = parseEquality(); left = new BinaryExpr("&", left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseEquality() { Expr left = parseRelational(); while (checkOp("==") || checkOp("!=")) { CppLexerToken t = advance(); Expr right = parseRelational(); left = new BinaryExpr(t.text(), left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseRelational() { Expr left = parseShift(); while (checkOp("<") || checkOp(">") || checkOp("<=") || checkOp(">=")) { // Spaceship operator <=> is lexed as <= then > -- detect and merge if (checkOp("<=") && pos + 1 < tokens.size() && tokens.get(pos + 1).isOp(">")) { CppLexerToken t = advance(); advance(); // consume <= and > Expr right = parseShift(); left = new BinaryExpr("<=>", left, right, t.line(), t.col(), List.of()); continue; } CppLexerToken t = advance(); Expr right = parseShift(); left = new BinaryExpr(t.text(), left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseShift() { Expr left = parseAdditive(); while (checkOp("<<") || checkOp(">>")) { CppLexerToken t = advance(); Expr right = parseAdditive(); left = new BinaryExpr(t.text(), left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseAdditive() { Expr left = parseMultiplicative(); while (checkOp("+") || checkOp("-")) { CppLexerToken t = advance(); Expr right = parseMultiplicative(); left = new BinaryExpr(t.text(), left, right, t.line(), t.col(), List.of()); } return left; } private Expr parseMultiplicative() { Expr left = parseUnary(); while (checkOp("*") || checkOp("/") || checkOp("%")) { CppLexerToken t = advance(); Expr right = parseUnary(); left = new BinaryExpr(t.text(), left, right, t.line(), t.col(), List.of()); } return left; } private static final java.util.Set UNARY_OPS = java.util.Set.of("-", "!", "&", "~", "+", "*"); private Expr parseUnary() { if (checkOp("++") || checkOp("--")) { CppLexerToken t = advance(); Expr operand = parseUnary(); return new UnaryExpr(t.text(), operand, t.line(), t.col(), List.of()); } if (peek().type() == CppLexerTokenType.OPERATOR && UNARY_OPS.contains(peek().text())) { CppLexerToken t = advance(); Expr operand = parseUnary(); return new UnaryExpr(t.text(), operand, t.line(), t.col(), List.of()); } if (checkKeyword("new")) { return parseNew(); } if (checkKeyword("delete")) { // delete is modeled as a statement (DeleteStatement) per the AST // design notes -- it should never be reached from expression // context. If it is, that's a real grammar gap, not something to // silently paper over. throw error("'delete' is only valid as a statement, not inside an expression"); } // C-style cast: "(" TypeName ")" expr -- only recognized when the // parenthesized content is unambiguously a type (an identifier/keyword // optionally followed by '*'/'&'/template-args, then immediately ')'), // and is followed by something that can start an expression. This // disambiguates from a plain parenthesized expression like "(a + b)". if (checkPunct("(") && looksLikeCast()) { return parseCast(); } return parsePostfix(); } /** * Lookahead-only check for whether the upcoming "(...)" is a C-style cast * rather than a parenthesized expression. We tentatively try parsing a * TypeRef starting just after "(" and require it to be immediately * followed by ")" and then something that looks like the start of a unary * expression (identifier, literal, "(", unary operator). This is a * backtracking lookahead -- cheap here since types are short and this * only runs when we've already seen "(". */ private boolean looksLikeCast() { int save = pos; try { if (!matchPunct("(")) return false; // Must look like a type: starts with an identifier or type-keyword. if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; TypeRef ignored; try { ignored = parseTypeRef(); } catch (ParseException e) { return false; } if (!checkPunct(")")) return false; advance(); // consume ')' // What follows a real cast must be able to start a unary expression. return canStartExpression(peek()); } finally { pos = save; } } private boolean canStartExpression(CppLexerToken t) { if (t.type() == CppLexerTokenType.IDENTIFIER || t.type() == CppLexerTokenType.INT_LITERAL || t.type() == CppLexerTokenType.FLOAT_LITERAL || t.type() == CppLexerTokenType.STRING_LITERAL || t.type() == CppLexerTokenType.CHAR_LITERAL || t.type() == CppLexerTokenType.BOOL_LITERAL) { return true; } if (t.isPunct("(")) return true; if (t.isOp("-") || t.isOp("!") || t.isOp("&") || t.isOp("~") || t.isOp("++") || t.isOp("--")) return true; if (t.isKeyword("new")) return true; return false; } private Expr parseCast() { CppLexerToken start = peek(); expectPunct("("); TypeRef targetType = parseTypeRef(); expectPunct(")"); Expr expr = parseUnary(); return new CastExpr(targetType, expr, start.line(), start.col(), List.of()); } private Expr parseNew() { CppLexerToken start = expectKeyword("new"); // Placement new: "new (buf) Type(args)" -- consume placement args first if (checkPunct("(")) { advance(); int _pd=1; while (!isAtEnd() && _pd > 0) { if (checkPunct("(")) _pd++; else if (checkPunct(")")) _pd--; advance(); } } TypeRef type = parseTypeRef(); if (matchPunct("[")) { Expr sizeExpr = parseExpr(); expectPunct("]"); // Consume optional value-initializer on array-new: "new float[n]()" // This is valid C++ (zero-initializes the array) and must be consumed // here or the "()" is left in the stream, breaking the enclosing // constructor init-list parser. Args inside are allowed but rare. if (checkPunct("(")) { advance(); int _d = 1; while (!isAtEnd() && _d > 0) { if (checkPunct("(")) _d++; else if (checkPunct(")")) _d--; advance(); } } return new ArrayNewExpr(type, sizeExpr, start.line(), start.col(), List.of()); } List args = new ArrayList<>(); if (matchPunct("(")) { if (!checkPunct(")")) { Expr a0 = parseExpr(); if (matchPunct("...")) a0 = new PostfixExpr("...", a0, a0.line(), a0.col(), List.of()); args.add(a0); while (matchPunct(",")) { Expr ai = parseExpr(); if (matchPunct("...")) ai = new PostfixExpr("...", ai, ai.line(), ai.col(), List.of()); args.add(ai); } } expectPunct(")"); } return new NewExpr(type, args, start.line(), start.col(), List.of()); } private Expr parsePostfix() { Expr expr = parsePrimary(); if (checkPunct("...") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("[")) { advance(); // pack indexing: args...[0] } while (true) { if (checkPunct("::")) { // Static member / scope resolution in expression context: // "std::is_pointer::value", "MyClass::staticMethod()", etc. // Fold the "::" and member name into the existing identifier string. advance(); // '::' String member = check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD) ? advance().text() : ""; if (expr instanceof Identifier id) { expr = new Identifier(id.name() + "::" + member, id.line(), id.col(), List.of()); } else { expr = new Identifier("::" + member, peek().line(), peek().col(), List.of()); } // Template args in expression: "AutoParam<42>::value" } else if (checkOp("<") && expr instanceof Identifier eid && looksLikeTemplateArgList()) { int argStart = pos; advance(); // consume < int depth = 1, pd = 0, bd = 0; while (!isAtEnd() && depth > 0) { if (checkPunct("(") || checkPunct("[")) pd++; else if (checkPunct(")") || checkPunct("]")) pd--; else if (checkPunct("{")) bd++; else if (checkPunct("}")) bd--; else if (pd == 0 && bd == 0) { if (checkOp("<")) depth++; else if (checkOp(">")) { depth--; if (depth == 0) { advance(); break; } } else if (checkOp(">>")) { depth -= 2; splitTrailingShiftIntoTwoCloseAngles(); if (depth <= 0) { advance(); break; } } } if (depth > 0) advance(); } StringBuilder targs = new StringBuilder(eid.name()).append("<"); for (int i = argStart + 1; i < pos - 1; i++) targs.append(tokens.get(i).text()); targs.append(">"); expr = new Identifier(targs.toString(), eid.line(), eid.col(), List.of()); } else if (checkOp("->*")) { // "->*" lexed as a single token CppLexerToken t = advance(); Expr rhs = parseUnary(); expr = new BinaryExpr("->*", expr, rhs, t.line(), t.col(), List.of()); continue; } else if (checkPunct(".") || checkPunct("->")) { boolean isArrow = checkPunct("->"); CppLexerToken t = advance(); // Pointer-to-member dereference: "obj.*ptr" or "obj->*ptr" // After consuming "." or "->", if "*" follows, this is ".*" / "->*" if (checkOp("*")) { advance(); // consume the "*" Expr rhs = parseUnary(); // the member pointer expression expr = new BinaryExpr(isArrow ? "->*" : ".*", expr, rhs, t.line(), t.col(), List.of()); continue; } // Explicit destructor call: "p->~PoolObj()" String member; if (checkOp("~")) { advance(); // consume ~ member = "~" + expectIdentifier().text(); } else { member = expectIdentifierOrKeywordName(); } // Sibling fix to the std::vector(...) bug found via // RealHeaderStressTest: a member name can ALSO be // followed by explicit template arguments before the // call parens ("obj.method(5)" -- calling a // template member function with an explicit type // argument, since the argument types alone aren't always // enough for the compiler to deduce it). Without this // check, "obj.method(5)" misparses the same way // "std::vector(rows)" did before that fix: as // "obj.method" followed by an unrelated // "< int > (5)" comparison chain. Folded the member name // and its template args into one combined member-name // string, consistent with how a templated callee // Identifier already folds its name and args together // elsewhere in this parser. if (checkOp("<") && looksLikeTemplatedConstructionCallee()) { List templateArgs = parseTemplateArgList(); member = member + "<" + renderTemplateArgs(templateArgs) + ">"; } expr = new MemberAccessExpr(expr, member, isArrow, t.line(), t.col(), List.of()); } else if (checkPunct("(")) { CppLexerToken t = peek(); List args = parseArgList(); expr = new CallExpr(expr, args, t.line(), t.col(), List.of()); } else if (checkPunct("{") && expr instanceof Identifier id) { // Brace-init of a named type: "Inner{1, 2}", "Rect{x, y}" // Any identifier (with or without template args) can be // brace-initialized this way in C++. The guard "expr instanceof // Identifier" prevents misfiring on expressions like "x{..." which // are never valid C++ (x would be a function call target, not a type). CppLexerToken t = peek(); InitializerListExpr init = (InitializerListExpr) parseInitializerList(); expr = new CallExpr(expr, init.elements(), true, t.line(), t.col(), List.of()); } else if (checkPunct("[")) { CppLexerToken t = advance(); Expr index = parseExpr(); while (matchPunct(",")) parseExpr(); // multi-dim subscript C++23 expectPunct("]"); expr = new IndexExpr(expr, index, t.line(), t.col(), List.of()); } else if (checkOp("++") || checkOp("--")) { CppLexerToken t = advance(); expr = new PostfixExpr(t.text(), expr, t.line(), t.col(), List.of()); } else if (checkPunct("{") && (expr instanceof Identifier || expr instanceof ScopedName)) { // Brace-init after identifier: "std::pair{1, 1.0f}" or "Type{args}" int _bl = expr.line(); int _bc = expr.col(); Expr braceInit = parseInitializerList(); Identifier _callee = new Identifier( expr instanceof Identifier _eid ? _eid.name() : String.join("::", ((ScopedName)expr).parts()), _bl, _bc, List.of()); expr = new CallExpr(_callee, ((InitializerListExpr)braceInit).elements(), true, _bl, _bc, List.of()); } else { break; } } return expr; } /** * Member names are usually identifiers but may be a few keyword-shaped * tokens too in practice (e.g. nothing in-scope right now requires this, * but kept permissive since C++ member names are never actual keywords * in valid code -- this mainly just defers to expectIdentifier). */ private String expectIdentifierOrKeywordName() { return expectIdentifier().text(); } private List parseArgList() { expectPunct("("); List args = new ArrayList<>(); if (!checkPunct(")")) { Expr a0 = parseExpr(); if (matchPunct("...")) a0 = new PostfixExpr("...", a0, a0.line(), a0.col(), List.of()); args.add(a0); while (matchPunct(",")) { if (checkPunct(")")) break; Expr ai = parseExpr(); if (matchPunct("...")) ai = new PostfixExpr("...", ai, ai.line(), ai.col(), List.of()); args.add(ai); } } expectPunct(")"); return args; } private Expr parsePrimary() { CppLexerToken t = peek(); switch (t.type()) { case INT_LITERAL -> { String txt = advance().text(); if (txt.equals("0") && !isAtEnd() && check(CppLexerTokenType.IDENTIFIER)) { String nx=peek().text(); if(nx.startsWith("b")||nx.startsWith("B")||nx.startsWith("x")||nx.startsWith("X")) txt+=advance().text(); } while (!isAtEnd() && check(CppLexerTokenType.CHAR_LITERAL)) { String chunk=peek().text(); if(!chunk.startsWith("'"))break; String inner=chunk.substring(1); boolean wf=inner.endsWith("'"); if(wf)inner=inner.substring(0,inner.length()-1); if(!inner.matches("[0-9a-fA-F]+"))break; advance();txt+=inner; if(wf&&!isAtEnd()&&check(CppLexerTokenType.INT_LITERAL))txt+=advance().text(); if(!wf)break; } txt += consumeUdlSuffix(); return new Literal(Literal.Kind.INT, txt, t.line(), t.col(), List.of()); } case FLOAT_LITERAL -> { String txt = advance().text(); while (!isAtEnd() && check(CppLexerTokenType.CHAR_LITERAL)) { String chunk=peek().text(); if(!chunk.startsWith("'"))break; String inner=chunk.substring(1); boolean wf=inner.endsWith("'"); if(wf)inner=inner.substring(0,inner.length()-1); if(!inner.matches("[0-9a-fA-F]+"))break; advance();txt+=inner; if(wf&&!isAtEnd()&&(check(CppLexerTokenType.INT_LITERAL)||check(CppLexerTokenType.FLOAT_LITERAL)))txt+=advance().text(); if(!wf)break; } txt += consumeUdlSuffix(); return new Literal(Literal.Kind.FLOAT, txt, t.line(), t.col(), List.of()); } case STRING_LITERAL -> { String txt = advance().text(); txt += consumeUdlSuffix(); // Adjacent string literal concatenation: "hello" " " "world" while (check(CppLexerTokenType.STRING_LITERAL)) { txt += advance().text(); txt += consumeUdlSuffix(); } return new Literal(Literal.Kind.STRING, txt, t.line(), t.col(), List.of()); } case CHAR_LITERAL -> { advance(); return new Literal(Literal.Kind.CHAR, t.text(), t.line(), t.col(), List.of()); } case BOOL_LITERAL -> { advance(); return new Literal(Literal.Kind.BOOL, t.text(), t.line(), t.col(), List.of()); } default -> { /* fall through below */ } } if (checkPunct("[")) { // lambda capture-list start return parseLambda(); } if (checkPunct("(")) { advance(); Expr inner = parseExpr(); // Fold expression or comma operator inside parens. // Binary fold: "(init op ... op pack)" -- op and ... already in inner as BinaryExpr // Unary fold: "(pack op ...)" -- already in inner // Comma fold: "((void)(expr), ...)" -- comma then ... before ")" if (!checkPunct(")")) { int depth = 0; boolean seenCommaFold = false; while (!isAtEnd()) { if (checkPunct("(")) { depth++; advance(); } else if (checkPunct(")")) { if (depth == 0) break; depth--; advance(); } else if (checkPunct(",") && depth == 0) { advance(); if (checkPunct("...")) { advance(); seenCommaFold = true; break; } // Regular comma -- consume the next expression parseExpr(); } else advance(); } if (seenCommaFold) { // Comma fold: "(expr, ...)" -- wrap inner in BinaryExpr("," , ...) CppLexerToken fakeToken = peek(); inner = new BinaryExpr(",", inner, new Identifier("...", fakeToken.line(), fakeToken.col(), List.of()), fakeToken.line(), fakeToken.col(), List.of()); } } expectPunct(")"); return inner; } if (checkPunct("{")) { return parseInitializerList(); } // Global scope resolution: "::operator new(...)" or "::SomeFunc()" if (checkPunct("::")) { advance(); // consume :: // Parse what follows as an identifier/operator name if (checkKeyword("operator")) { String opName = "::" + parseFunctionOrVariableName(); return new Identifier(opName, t.line(), t.col(), List.of()); } if (check(CppLexerTokenType.IDENTIFIER)) { String name = advance().text(); return new Identifier("::" + name, t.line(), t.col(), List.of()); } } // Wide/unicode string prefix: L"..." u"..." U"..." u8"..." if (t.type() == CppLexerTokenType.IDENTIFIER && (t.text().equals("L") || t.text().equals("u") || t.text().equals("U") || t.text().equals("u8")) && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.STRING_LITERAL) { String prefix = advance().text(); String txt = prefix + advance().text(); while (check(CppLexerTokenType.STRING_LITERAL)) txt += advance().text(); return new Literal(Literal.Kind.STRING, txt, t.line(), t.col(), List.of()); } // sizeof expression: "sizeof(T)" or "sizeof expr" (no parens) if (checkKeyword("sizeof")) { int sizeofStart = pos; advance(); // consume "sizeof" StringBuilder sizeofText = new StringBuilder("sizeof"); if (checkPunct("...")) { advance(); sizeofText.append("..."); } // sizeof... if (checkPunct("(")) { sizeofText.append("("); advance(); int d=1; while(!isAtEnd()&&d>0){ if(checkPunct("(")){ d++; sizeofText.append("("); advance(); } else if(checkPunct(")")){ d--; if(d>0)sizeofText.append(")"); advance(); } else { // Add space before identifier/keyword tokens to avoid merging String _st = peek().text(); if (sizeofText.length() > 0) { char _last = sizeofText.charAt(sizeofText.length()-1); if (Character.isLetterOrDigit(_last) || _last == '_') sizeofText.append(" "); } sizeofText.append(_st); advance(); } } sizeofText.append(")"); } else { // sizeof without parens: consume one unary expression sizeofText.append(" "); int before = pos; parseUnary(); for (int si = before; si < pos; si++) { if (si > before) sizeofText.append(" "); sizeofText.append(tokens.get(si).text()); } } return new Identifier(sizeofText.toString(), t.line(), t.col(), List.of()); } // alignof expression if (checkKeyword("alignof") || (t.type() == CppLexerTokenType.IDENTIFIER && t.text().equals("alignof"))) { advance(); expectPunct("("); int d=1; while(!isAtEnd()&&d>0){if(checkPunct("("))d++;else if(checkPunct(")"))d--;advance();} return new Literal(Literal.Kind.INT, "alignof(...)", t.line(), t.col(), List.of()); } if (t.type() == CppLexerTokenType.IDENTIFIER || t.type() == CppLexerTokenType.KEYWORD) { // "typename T::member" in expression context -- fold typename into the following qualified name if (t.text().equals("typename")) { advance(); // consume typename // Parse the following type and prepend "typename " CppLexerToken next = peek(); if (next.type() == CppLexerTokenType.IDENTIFIER || next.type() == CppLexerTokenType.KEYWORD) { String typeName = advance().text(); // Consume :: qualified parts while (checkPunct("::")) { advance(); if (check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD)) typeName += "::" + advance().text(); } // Consume template args if present if (checkOp("<") && looksLikeTemplateArgList()) { int _as = pos; advance(); int _d=1; while (!isAtEnd() && _d > 0) { if (checkOp("<")) { _d++; advance(); } else if (checkOp(">")) { _d--; advance(); } else if (checkOp(">>")) { _d-=2; splitTrailingShiftIntoTwoCloseAngles(); advance(); } else advance(); } StringBuilder _ta = new StringBuilder(typeName).append("<"); for (int _i=_as+1; _i"); typeName = _ta.toString(); } // Consume trailing :: member if (checkPunct("::")) { advance(); if (check(CppLexerTokenType.IDENTIFIER)||check(CppLexerTokenType.KEYWORD)) typeName += "::" + advance().text(); } return new Identifier("typename " + typeName, t.line(), t.col(), List.of()); } return new Identifier("typename", t.line(), t.col(), List.of()); } if (t.text().equals("requires")) { advance(); if(checkPunct("(")){ advance();int d=1;while(!isAtEnd()&&d>0){if(checkPunct("("))d++;else if(checkPunct(")"))d--;advance();}} if(checkPunct("{")){ advance();int d=1;while(!isAtEnd()&&d>0){if(checkPunct("{"))d++;else if(checkPunct("}"))d--;advance();}} return new Literal(Literal.Kind.BOOL,"true",t.line(),t.col(),List.of()); } if (t.text().equals("R") && !isAtEnd() && check(CppLexerTokenType.STRING_LITERAL)) { advance(); String raw=advance().text(); return new Literal(Literal.Kind.STRING,"R"+raw,t.line(),t.col(),List.of()); } // Could be a plain identifier, the start of a "::"-qualified // ScopedName, or a bare templated-construction-call callee like // "ArrayList()" / "PenroseSnowflakeLSystem()" (the latter // with no template args at all). Confirmed by the corpus as the // no-"new" construction idiom (see CallExpr's design notes). // // The tricky case is the templated form: "ArrayList()" // is syntactically ambiguous with "a < b > ()" (comparisons // chained with an empty-paren expression, which isn't valid // anyway, but the grammar can't assume that). We resolve it with // a backtracking lookahead: if a '<' follows the identifier AND // the content up to a matching '>' parses as a template-arg-like // list AND is immediately followed by '(', treat the whole // "Name" as one callee identifier whose text includes the // template args verbatim -- semantic resolution (is this really // a constructor call) is deferred to a later pass either way, // consistent with how the untemplated bare-call case already // works. String first = t.text(); advance(); if (checkPunct("::")) { List parts = new ArrayList<>(); parts.add(first); while (matchPunct("::")) { parts.add(parseTypeNameSegment()); } // BUG FIX, found via RealHeaderStressTest against the real // engine headers (Game_Of_Life.pde's real // "cells.resize(cols, std::vector(rows));"): this // branch used to return immediately as a plain ScopedName // the moment it saw "::", WITHOUT ever checking for a // following "(" templated-construction-call suffix // -- unlike the bare-identifier case just below (line // ~1342 in this method), which already had this check. // This meant "std::vector(rows)" was parsed as // "std::vector" (a ScopedName) followed by a SEPARATE, // unrelated "< int > (rows)" comparison-chain expression // -- confirmed directly: g++ rejected the resulting // "((std::vector < int) > rows)" with "template argument // 1 is invalid". A bare bare "Foo(...)" (no "::") // already worked correctly; only the "::"-qualified form // (which is exactly what every "std::"-prefixed standard // container construction looks like) had this gap. String joined = String.join("::", parts); if (checkOp("<") && looksLikeTemplatedConstructionCallee()) { List templateArgs = parseTemplateArgList(); String rendered = joined + "<" + renderTemplateArgs(templateArgs) + ">"; return new Identifier(rendered, t.line(), t.col(), List.of()); } // Non-call template use: "std::is_arithmetic_v" (no "(" after ">") if (checkOp("<") && looksLikeTemplateArgList()) { int argStart = pos; advance(); int _d=1, _pd=0, _bd=0; while (!isAtEnd() && _d > 0) { if (checkPunct("(")||checkPunct("[")) _pd++; else if (checkPunct(")")||checkPunct("]")) _pd--; else if (checkPunct("{")) _bd++; else if (checkPunct("}")) _bd--; else if (_pd==0&&_bd==0) { if (checkOp("<")) _d++; else if (checkOp(">")) { _d--; if(_d==0){advance();break;} } else if (checkOp(">>")) { _d-=2; splitTrailingShiftIntoTwoCloseAngles(); if(_d<=0){advance();break;} } } if (_d>0) advance(); } StringBuilder _tb = new StringBuilder(joined).append("<"); for (int _i=argStart+1; _i"); return new Identifier(_tb.toString(), t.line(), t.col(), List.of()); } return new ScopedName(parts, t.line(), t.col(), List.of()); } if (checkOp("<") && looksLikeTemplatedConstructionCallee()) { List templateArgs = parseTemplateArgList(); String rendered = first + "<" + renderTemplateArgs(templateArgs) + ">"; return new Identifier(rendered, t.line(), t.col(), List.of()); } return new Identifier(first, t.line(), t.col(), List.of()); } // Fold expression pack expansion: "..." -- appears in "(args + ...)" and // "((void)(std::cout << ts), ...)" as the RHS of a fold operator. // Produce a placeholder identifier so the enclosing expression completes. if (checkPunct("...")) { CppLexerToken t2 = advance(); return new Identifier("...", t2.line(), t2.col(), List.of()); } throw error("unexpected token '" + t.text() + "' while parsing an expression"); } private String renderTemplateArgs(List args) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < args.size(); i++) { if (i > 0) sb.append(", "); sb.append(args.get(i).describe()); } return sb.toString(); } /** * Lookahead-only: from just before the '<' following an identifier, * determines whether this is a templated-construction-call callee * ("Name(" ) rather than a less-than comparison. Tries to parse a * template-arg list and checks that '(' immediately follows. Restores * position regardless. */ /** True if current position looks like a template arg list: '<' followed eventually by '>', * used to detect "Base" in base class context. Lighter than parseTemplateArgList. */ private boolean looksLikeTemplateArgList() { // Peek ahead to see if we can find a matching '>' without hitting ';' or '{' // Track () and {} depth so { inside decltype(...) doesn't abort int save = pos; try { if (!checkOp("<")) return false; advance(); int depth = 1; int parenDepth = 0; int braceDepth = 0; int steps = 0; while (!isAtEnd() && depth > 0 && steps < 120) { if (checkPunct("(")) parenDepth++; else if (checkPunct(")")) { parenDepth--; if (parenDepth < 0) return false; } else if (checkPunct("{")) braceDepth++; else if (checkPunct("}")) braceDepth--; else if (parenDepth == 0 && braceDepth == 0) { if (checkOp("<")) depth++; else if (checkOp(">")) depth--; else if (checkOp(">>")) depth -= 2; else if (checkPunct(";")) return false; // A bare "?" at depth 0 means ternary operator -- not a template arg list. // Template args never contain unparenthesized ternary operators. // e.g. "v < lo ? lo : v > hi" -- the "?" rules out template args. else if (checkPunct("?") && depth == 1) return false; // A bare "+" "-" "*" "/" "%" at depth 0 after a non-type token // strongly suggests arithmetic expression, not template args. // But these can appear in NTTPs so only reject "?" which is unambiguous. } advance(); steps++; } return depth <= 0; } finally { pos = save; } } /** Consumes a user-defined literal suffix (_deg, _kb, _v, etc.) and returns it (or empty string). */ private String consumeUdlSuffix() { if (check(CppLexerTokenType.IDENTIFIER) && peek().text().startsWith("_")) { return advance().text(); } return ""; } private void consumeAttributes() { while (checkPunct("[") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("[")) { advance(); advance(); int depth = 2; while (!isAtEnd() && depth > 0) { if (checkPunct("[")) depth++; else if (checkPunct("]")) depth--; advance(); } } } private boolean isStructuredBindingStart() { int i = pos; if (i < tokens.size() && tokens.get(i).isKeyword("const")) i++; if (i >= tokens.size() || !tokens.get(i).isKeyword("auto")) return false; i++; if (i < tokens.size() && (tokens.get(i).isOp("&") || tokens.get(i).isOp("&&"))) i++; return i < tokens.size() && tokens.get(i).isPunct("["); } private Statement parseStructuredBinding(List leadingComments) { CppLexerToken start = peek(); matchKeyword("const"); expectKeyword("auto"); matchOp("&"); matchOp("&&"); expectPunct("["); List names = new ArrayList<>(); names.add(expectIdentifier().text()); while (matchPunct(",")) names.add(expectIdentifier().text()); expectPunct("]"); expectOp("="); Expr initializer = parseExpr(); expectPunct(";"); String bindingName = "[" + String.join(", ", names) + "]"; return new DeclStatement(new NamedType("auto", List.of(), 0, false, false, false), bindingName, List.of(), initializer, false, false, start.line(), start.col(), leadingComments); } private boolean looksLikeTemplatedConstructionCallee() { int save = pos; try { try { parseTemplateArgList(); } catch (ParseException e) { return false; } // Paren-init / brace-init (construction call) if (checkPunct("(") || checkPunct("{")) return true; // Static member access: "std::is_pointer::value" if (checkPunct("::")) return true; // Variable template: "pi" followed by ; , ) = etc. if (checkPunct(";") || checkPunct(",") || checkPunct(")") || checkPunct("]") || checkOp("=") || isAtEnd()) return true; return false; } finally { pos = save; } } private Expr parseInitializerList() { CppLexerToken start = expectPunct("{"); List elements = new ArrayList<>(); if (!checkPunct("}")) { elements.add(parseInitializerElement()); while (matchPunct(",")) { if (checkPunct("}")) break; // trailing comma elements.add(parseInitializerElement()); } } expectPunct("}"); return new InitializerListExpr(elements, start.line(), start.col(), List.of()); } /** * One element of a brace-init list. Can be: * - A nested brace-init: { 1, 2 } * - A named brace-init: Inner{1, 2} (parsed as expr then brace-init) * - Any other expression: a + b, func(), etc. */ private Expr parseInitializerElement() { if (checkPunct("{")) { return parseInitializerList(); // anonymous nested brace-init } // Designated initializer: ".field = expr" (C++20) if (checkPunct(".") && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && pos + 2 < tokens.size() && tokens.get(pos + 2).isOp("=")) { advance(); // consume "." advance(); // consume field name advance(); // consume "=" } Expr e = parseExpr(); if (matchPunct("...")) // pack expansion in brace-init: "{args...}" e = new PostfixExpr("...", e, e.line(), e.col(), List.of()); return e; } /** * Parses a lambda: "[captures](params) [-> ReturnType] { body }". * Capture forms confirmed by the corpus: "[]" (none), "[x]" (by value), * "[&x]" (by reference). */ private Expr parseLambda() { CppLexerToken start = expectPunct("["); List captures = new ArrayList<>(); if (!checkPunct("]")) { captures.add(parseCapture()); while (matchPunct(",")) { captures.add(parseCapture()); } } expectPunct("]"); // C++20 template lambda: [](T val) { ... } if (checkOp("<")) { StringBuilder _tp = new StringBuilder(); int _td = 1; advance(); while (!isAtEnd() && _td > 0) { if (checkOp("<")) { _td++; _tp.append(peek().text()); advance(); } else if (checkOp(">>")) { _td-=2; if(_td<=0)break; _tp.append(peek().text()); advance(); } else if (checkOp(">")) { _td--; if (_td==0) break; _tp.append(peek().text()); advance(); } else { if(_tp.length()>0 && !_tp.toString().endsWith("<") && !peek().text().equals(">") && !peek().text().equals(",")) _tp.append(" "); _tp.append(peek().text()); advance(); } } if (!isAtEnd()) advance(); // consume final > captures.add(0, new Capture("__tmpl__<" + _tp.toString() + ">", false)); } List params = new ArrayList<>(); if (checkPunct("(")) { advance(); if (!checkPunct(")")) { params.add(parseParam()); while (matchPunct(",")) { params.add(parseParam()); } } expectPunct(")"); } // mutable, noexcept, attributes boolean isMutable = matchKeyword("mutable"); matchKeyword("constexpr"); matchKeyword("consteval"); if (checkKeyword("noexcept")) { advance(); if(checkPunct("(")){advance();int _d=1;while(!isAtEnd()&&_d>0){if(checkPunct("("))_d++;else if(checkPunct(")"))_d--;advance();}} } consumeAttributes(); TypeRef returnType = null; if (matchPunct("->")) { returnType = parseTypeRef(); } if (check(CppLexerTokenType.IDENTIFIER) && peek().text().equals("requires")) { advance(); int _rd = 0; while (!isAtEnd()) { if (checkPunct("(") || checkOp("<")) _rd++; else if (checkPunct(")") || checkOp(">")) _rd--; else if (checkOp(">>")) _rd -= 2; else if (checkPunct("{") && _rd == 0) break; advance(); } } Block body = parseBlock(); return new LambdaExpr(captures, params, returnType, isMutable, body, start.line(), start.col(), List.of()); } private Capture parseCapture() { // Capture-ALL forms ("[=]" -- everything by value, "[&]" -- // everything by reference) are real, common, idiomatic lambda // syntax that this method never recognized at all -- it // unconditionally called expectIdentifier(), assuming every // capture is a named identifier, with no check for a bare "=" // or a lone "&" (not followed by a name) first. // // Represented using the EXISTING Capture(name, byRef) shape, no // AST change needed: "[&]" is byRef=true with an empty name // (CodeGen's existing "if (byRef) append('&'); append(name);" // rendering already produces exactly "&" for this with zero // changes); "[=]" is byRef=false with name="=" (renders the // literal "=" token, matching real C++ syntax exactly, since // CodeGen always appends cap.name() verbatim). if (checkOp("=")) { advance(); return new Capture("=", false); } if (checkOp("&") && tokens.get(pos + 1).text().equals("]")) { advance(); return new Capture("", true); } boolean byRef = matchOp("&"); // "this" is a keyword capture: [this] or [&this] if (checkKeyword("this")) { advance(); return new Capture("this", byRef); } // C++20 pack init-capture: "[...vals = expr]" -- "..." precedes the name boolean packPrefix = checkPunct("..."); if (packPrefix) advance(); String name = expectIdentifier().text(); // Pack expansion in capture: "[args...]" if (checkPunct("...")) { advance(); name = name + "..."; } if (packPrefix) name = "..." + name; // Init-capture: "z = z * 2" or "w = x + y" -- encode into name string if (checkOp("=")) { advance(); StringBuilder _ie = new StringBuilder(); int _d = 0; while (!isAtEnd()) { if (checkPunct("(") || checkPunct("[")) { _d++; _ie.append(peek().text()); advance(); } else if (checkPunct(")") || checkPunct("]")) { if (_d == 0) break; _d--; _ie.append(peek().text()); advance(); } else if (checkPunct(",") && _d == 0) break; else { _ie.append(" ").append(peek().text()); advance(); } } name = name + " = " + _ie.toString().trim(); } return new Capture(name, byRef); } /** Parses a single "Type name [= default]" parameter, shared by function decls and lambdas. */ /** * Parses a single "Type name [= default]" parameter, shared by function * decls and lambdas. Also handles the C-style array-parameter form * ("int data[]"), confirmed real by Pie_Chart.pde's * "void pieChart(float diameter, int data[], int length)" -- per real * C++ semantics, an array parameter decays to a pointer, so this is * represented by bumping the TypeRef's pointerDepth by one rather than * adding a separate array-dims field to Param (which would need to mean * something different from -- and easily confusable with -- the fixed- * size array dims already tracked on VariableDecl/DeclStatement). */ private Param parseParam() { // C-style variadic: bare "..." as a parameter (e.g. "void f(int, ...)") if (checkPunct("...")) { advance(); return new Param(new NamedType("...", List.of(), 0, false, false, false), "...", null, List.of(), false); } if (checkKeyword("this")) advance(); // explicit object param (C++23) TypeRef type = parseTypeRef(); boolean isTypePackExpansion = matchPunct("..."); // pack expansion after type: "Rest... rest" // Member pointer parameter: "int Widget::* dp" or "int (Widget::* mp)(int) const" if (check(CppLexerTokenType.IDENTIFIER) && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("::") && pos + 2 < tokens.size() && tokens.get(pos + 2).isOp("*")) { String className = advance().text(); // Class name advance(); // :: advance(); // * String pname = expectIdentifier().text(); // variable name // Member function pointer param: "int (Widget::* mp)(int) const" -- consume parens if (checkPunct("(")) { advance(); // ( int depth = 1; while (!isAtEnd() && depth > 0) { if (checkPunct("(")) depth++; else if (checkPunct(")")) depth--; advance(); } matchKeyword("const"); } return new Param(type, className + "::*" + pname, null, List.of(), false); } // Member function pointer param: "int (Widget::* mp)(int) const" // Detected by: ( IDENTIFIER :: * name ) ( params ) optional-const if (checkPunct("(") && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && pos + 2 < tokens.size() && tokens.get(pos + 2).isPunct("::") && pos + 3 < tokens.size() && tokens.get(pos + 3).isOp("*")) { advance(); // ( String className = advance().text(); // Widget advance(); // :: advance(); // * String pname = expectIdentifier().text(); // mp expectPunct(")"); // Capture (params) text for encoding StringBuilder paramsSig = new StringBuilder("("); if (checkPunct("(")) { advance(); int depth = 1; List paramTokens = new ArrayList<>(); while (!isAtEnd() && depth > 0) { if (checkPunct("(")) { depth++; paramTokens.add("("); advance(); } else if (checkPunct(")")) { depth--; if (depth > 0) { paramTokens.add(")"); advance(); } else advance(); } else { paramTokens.add(peek().text()); advance(); } } paramsSig.append(String.join(" ", paramTokens)).append(")"); } else { paramsSig.append(")"); } boolean isConstMethod = matchKeyword("const"); String encoded = className + "::*" + pname + paramsSig + (isConstMethod ? "__const__" : ""); return new Param(type, encoded, null, List.of(), false); } // Reference-to-array, pointer-to-array, or function-pointer parameter: // "int (&arr)[10]" -- ref-to-array // "int (*arr)[10]" -- ptr-to-array // "float (*fn)(float)" -- function pointer // "float (*fn)(float) = nullptr" -- function pointer with default // Discriminator: after consuming (*name), if "(" follows it is a function // pointer param list; if "[" follows it is an array dimension. if (checkPunct("(") && pos + 1 < tokens.size() && (tokens.get(pos + 1).isOp("&") || tokens.get(pos + 1).isOp("*")) && pos + 2 < tokens.size() && tokens.get(pos + 2).type() == CppLexerTokenType.IDENTIFIER) { advance(); // consume "(" boolean isRef = matchOp("&"); if (!isRef) matchOp("*"); String pname = expectIdentifier().text(); expectPunct(")"); // Function pointer param: "float (*fn)(float) = nullptr" // "(" immediately after ")" means this is a fn-ptr, not an array. if (checkPunct("(")) { // Capture the param list tokens verbatim for CodeGen encoding. // CodeGen detects "name__fnptr__(sig)" and emits "rettype (*name)(sig)". int sigStart = pos; advance(); int d = 1; List sigToks = new ArrayList<>(); while (!isAtEnd() && d > 0) { if (checkPunct("(")) { d++; sigToks.add("("); advance(); } else if (checkPunct(")")) { d--; if (d > 0) { sigToks.add(")"); advance(); } else advance(); } else { sigToks.add(peek().text()); advance(); } } String paramSig = "(" + String.join(", ", sigToks) + ")"; // Consume optional trailing qualifiers matchKeyword("const"); matchKeyword("noexcept"); // Consume optional default value: "= nullptr", "= identity", etc. Expr defaultValue = null; if (matchOp("=")) defaultValue = parseExpr(); // Encode as "name__fnptr__(sig)" -- CodeGen decodes this in emitParamList. // Type carries the return type; pointerDepth bump marks it as fn-ptr. if (type instanceof NamedType nt) { type = new NamedType(nt.baseName(), nt.templateArgs(), nt.pointerDepth() + 1, false, nt.isConst(), false); } return new Param(type, pname + "__fnptr__" + paramSig, defaultValue, List.of(), false); } // Ref-to-array or ptr-to-array: encode dims into name. // CodeGen uses the "&"/"*" prefix to emit "int (&arr)[10]". StringBuilder dimEnc = new StringBuilder(); while (checkPunct("[")) { int _ds = pos; advance(); dimEnc.append("["); if (!checkPunct("]")) { int _de = pos; parseExpr(); // consume dimension expression for (int _di = _de; _di < pos; _di++) dimEnc.append(tokens.get(_di).text()); } expectPunct("]"); dimEnc.append("]"); } return new Param(type, (isRef ? "&" : "*") + pname + dimEnc.toString(), null, List.of(), false); } // Variadic pack expansion after the type: "Args... args" or "Ts&&... ts" boolean isVariadic = isTypePackExpansion; if (checkPunct("...")) { advance(); isVariadic = true; } String name; if (check(CppLexerTokenType.IDENTIFIER) || (check(CppLexerTokenType.KEYWORD) && PSEUDO_TYPE_KEYWORDS_USABLE_AS_NAMES.contains(peek().text()))) { name = advance().text(); if (checkPunct("...")) { advance(); isVariadic = true; } } else { name = ""; } List innerDims = new ArrayList<>(); if (checkPunct("[")) { boolean first = true; while (checkPunct("[")) { advance(); Integer dimSize = null; if (!checkPunct("]")) { if (peek().type() == CppLexerTokenType.INT_LITERAL) { try { dimSize = Integer.parseInt(peek().text()); } catch (NumberFormatException ignored) {} } parseExpr(); } expectPunct("]"); if (!first) innerDims.add(dimSize != null ? dimSize : 0); first = false; } type = bumpPointerDepth(type); } // Function type parameter: "Widget()" or "int(*)(int)" -- function pointer param // If name is empty and "(" follows, this is a function-type or function-pointer param. // Consume the "(params)" signature and treat as pointer-to-function type. if (name.isEmpty() && checkPunct("(")) { advance(); // consume "(" // Consume optional * for explicit function pointer: "int(*)(int)" matchOp("*"); // Consume optional name inside parens: "int(*name)(int)" if (check(CppLexerTokenType.IDENTIFIER)) advance(); expectPunct(")"); // Consume the actual param list of the function type: "(int, float)" if (checkPunct("(")) { advance(); int d = 1; while (!isAtEnd() && d > 0) { if (checkPunct("(")) d++; else if (checkPunct(")")) d--; advance(); } } // Represent as a pointer type if (type instanceof NamedType nt) { type = new NamedType(nt.baseName(), nt.templateArgs(), nt.pointerDepth() + 1, false, nt.isConst(), false); } } Expr defaultValue = null; if (matchOp("=")) { defaultValue = parseExpr(); } return new Param(type, name, defaultValue, innerDims, isVariadic); } private TypeRef bumpPointerDepth(TypeRef type) { if (type instanceof NamedType nt) { return new NamedType(nt.baseName(), nt.templateArgs(), nt.pointerDepth() + 1, nt.isReference(), nt.isConst(), false); } // Function-pointer/function-signature types can't sensibly gain a // C-style array-parameter pointer bump this way; not encountered by // any corpus fixture in this position, so left as-is rather than // guessing at a shape nothing has confirmed yet. return type; } // ----- Statements ------------------------------------------------------- /** * Parses a brace-delimited block. Handles multi-declarator desugaring * directly here (e.g. "int boxx, boxy;" inside a method body, confirmed * present in the Handles fixture): a single source statement with N * comma-separated declarators becomes N consecutive DeclStatement nodes * in this block's statement list. This is the right place to do the * splicing since Block is the only structure that owns a List * it can freely expand -- parseStatement itself can only return one node. */ private Block parseBlock() { CppLexerToken start = expectPunct("{"); List statements = new ArrayList<>(); while (!checkPunct("}")) { if (isAtEnd()) { throw error("unexpected end of input while looking for closing '}'"); } List comments = consumeLeadingComments(); if (checkPunct("}")) break; statements.addAll(parseStatementOrMultiDecl(comments)); } expectPunct("}"); return new Block(statements, start.line(), start.col(), List.of()); } /** * Returns one or more statements: more than one only when this was a * multi-declarator local declaration ("int x, y;"), which desugars into * one DeclStatement per declarator, all sharing the same TypeRef. */ private List parseStatementOrMultiDecl(List leadingComments) { if (isStructuredBindingStart()) return List.of(parseStructuredBinding(leadingComments)); // Local struct/class definition: "struct Point { float x, y; }; Point p{...}" // Also handle forward decl: "struct LocalFwd;" -- consume and skip if ((checkKeyword("struct") || checkKeyword("class")) && pos + 1 < tokens.size() && (tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER || tokens.get(pos + 1).isPunct("{"))) { // Forward declaration: "struct Foo;" -- just consume and emit empty if (tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && pos + 2 < tokens.size() && tokens.get(pos + 2).isPunct(";")) { CppLexerToken sk = peek(); advance(); advance(); advance(); // struct Name ; return List.of(new ExprStatement( new Identifier("", sk.line(), sk.col(), List.of()), sk.line(), sk.col(), leadingComments)); } TypeDef td = parseTypeDef(leadingComments, List.of()); matchPunct(";"); // Emit struct as verbatim text via CodeGen String structCode = processing.mode.cpp.CodeGen.generateNode(td, 0).stripTrailing(); ExprStatement structStmt = new ExprStatement( new Identifier(structCode, td.line(), td.col(), List.of()), td.line(), td.col(), leadingComments); // If a variable declaration follows, parse it too if (looksLikeDeclaration()) { List result = new java.util.ArrayList<>(); result.add(structStmt); result.addAll(parseDeclStatementsDesugared(List.of())); return result; } return List.of(structStmt); } // "using T = Type;" local alias -- consume verbatim as ExprStatement if (checkKeyword("using") && pos + 1 < tokens.size() && tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER && pos + 2 < tokens.size() && tokens.get(pos + 2).isOp("=")) { int _us = pos; while (!isAtEnd() && !checkPunct(";")) advance(); matchPunct(";"); StringBuilder _ur = new StringBuilder(); for (int _i = _us; _i < pos - 1; _i++) { if (_i > _us) _ur.append(" "); _ur.append(tokens.get(_i).text()); } _ur.append(";"); return List.of(new ExprStatement(new Identifier(_ur.toString(), tokens.get(_us).line(), tokens.get(_us).col(), List.of()), tokens.get(_us).line(), tokens.get(_us).col(), leadingComments)); } if (looksLikeDeclaration()) { return new ArrayList(parseDeclStatementsDesugared(leadingComments)); } return List.of(parseStatement(leadingComments)); } /** * Dispatches on lookahead to the correct single-statement parser. * Declarations are handled by the caller (parseStatementOrMultiDecl) * before this is reached in the common case, but this method still * handles the declaration form too for callers that only ever expect * exactly one declarator (if/for/while bodies that are a single * statement with no braces, e.g. "if (x) int y = 1;" -- unusual but * grammatically legal, and not distinguishable from the multi-decl case * until we've already looked). */ private Statement parseStatement(List leadingComments) { consumeAttributes(); if (checkPunct(";")) { CppLexerToken t = advance(); return new ExprStatement(new Literal(Literal.Kind.INT, "0", t.line(), t.col(), List.of()), t.line(), t.col(), leadingComments); } if (checkPunct("{")) { Block b = parseBlock(); return new Block(b.statements(), b.line(), b.col(), leadingComments); } // "if constexpr" -- consume constexpr before dispatching to parseIf if (checkKeyword("constexpr") && pos + 1 < tokens.size() && tokens.get(pos + 1).isKeyword("if")) { advance(); // consume constexpr, leave "if" for parseIf } // "typename T::member;" inside requires body -- type validity assertion if (checkKeyword("typename")) { CppLexerToken _t0 = peek(); advance(); try { parseTypeRef(); } catch (ParseException _e) {} matchPunct(";"); return new ExprStatement(new Identifier("typename", _t0.line(), _t0.col(), List.of()), _t0.line(), _t0.col(), leadingComments); } // static_assert: consume entirely and emit as-is if (checkKeyword("static_assert")) { int startPos = pos; advance(); // consume static_assert expectPunct("("); int d=1; while (!isAtEnd() && d > 0) { if (checkPunct("(")) d++; else if (checkPunct(")")) d--; advance(); } matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos; i++) { if (i > startPos) raw.append(" "); raw.append(tokens.get(i).text()); } if (!raw.toString().endsWith(";")) raw.append(";"); CppLexerToken t0 = tokens.get(startPos); return new ExprStatement(new Identifier(raw.toString(), t0.line(), t0.col(), List.of()), t0.line(), t0.col(), leadingComments); } // throw statement: "throw expr;" or bare "throw;" if (checkKeyword("throw")) { CppLexerToken t0 = advance(); if (checkPunct(";")) { advance(); return new ReturnStatement(null, t0.line(), t0.col(), leadingComments); } Expr val = parseExpr(); expectPunct(";"); // Encode as ReturnStatement with a UnaryExpr("throw", val) -- CodeGen emits via renderExpr return new ExprStatement(new UnaryExpr("throw ", val, t0.line(), t0.col(), List.of()), t0.line(), t0.col(), leadingComments); } // Local struct/class definition inside a function body if ((checkKeyword("struct") || checkKeyword("class")) && pos + 1 < tokens.size() && (tokens.get(pos + 1).type() == CppLexerTokenType.IDENTIFIER || tokens.get(pos + 1).isPunct("{"))) { TypeDef td = parseTypeDef(leadingComments, List.of()); matchPunct(";"); return new ExprStatement(new Identifier("", td.line(), td.col(), List.of()), td.line(), td.col(), leadingComments); } if (checkKeyword("if")) return parseIf(leadingComments); if (checkKeyword("for")) return parseForOrRangeFor(leadingComments); if (checkKeyword("while")) return parseWhile(leadingComments); if (checkKeyword("do")) return parseDoWhile(leadingComments); if (checkKeyword("switch")) return parseSwitch(leadingComments); if (checkKeyword("using") && pos + 1 < tokens.size() && tokens.get(pos + 1).isKeyword("enum")) { int startPos = pos; while (!isAtEnd() && !checkPunct(";")) advance(); matchPunct(";"); StringBuilder raw = new StringBuilder(); for (int i = startPos; i < pos - 1; i++) { if (i > startPos) raw.append(" "); raw.append(tokens.get(i).text()); } raw.append(";"); return new ExprStatement(new Identifier(raw.toString(), tokens.get(startPos).line(), tokens.get(startPos).col(), List.of()), tokens.get(startPos).line(), tokens.get(startPos).col(), leadingComments); } if (checkKeyword("return")) return parseReturn(leadingComments); if (checkKeyword("break")) return parseBreak(leadingComments); if (checkKeyword("continue")) return parseContinue(leadingComments); if (checkKeyword("try")) return parseTry(leadingComments); if (checkKeyword("delete")) return parseDelete(leadingComments); if (looksLikeDeclaration()) { List decls = parseDeclStatementsDesugared(leadingComments); if (decls.size() > 1) { throw error("multiple comma-separated declarators are not supported in a single-statement context (e.g. directly inside an 'if'/'for'/'while' with no braces)"); } return decls.get(0); } return parseExprStatement(leadingComments); } private Statement parseIf(List leadingComments) { CppLexerToken start = expectKeyword("if"); boolean isConstexpr = matchKeyword("constexpr"); // C++17 if constexpr if (checkKeyword("consteval") || (checkOp("!") && pos + 1 < tokens.size() && tokens.get(pos+1).isKeyword("consteval"))) { if (checkOp("!")) advance(); advance(); // consume consteval Statement thenBr = parseStatement(consumeLeadingComments()); Statement elseBr = null; consumeLeadingComments(); if (checkKeyword("else")) { advance(); elseBr = parseStatement(consumeLeadingComments()); } return new IfStatement(new Identifier("true", start.line(), start.col(), List.of()), thenBr, elseBr, false, start.line(), start.col(), leadingComments); } expectPunct("("); // C++17 if-init-statement: if (init; cond) -- collect init decls, // wrap the IfStatement in a Block so the init vars are in scope. java.util.List initStmts = new ArrayList<>(); Expr cond; { int scan=pos,depth=0; boolean hasInit=false; while(scan leadingComments) { CppLexerToken start = expectKeyword("for"); expectPunct("("); // Structured binding range-for: "for (const auto& [k, v] : m)" if (isStructuredBindingStart()) { matchKeyword("const"); expectKeyword("auto"); boolean isRef = matchOp("&") || matchOp("&&"); expectPunct("["); List sbNames = new ArrayList<>(); sbNames.add(expectIdentifier().text()); while (matchPunct(",")) sbNames.add(expectIdentifier().text()); expectPunct("]"); expectPunct(":"); Expr sbIterable = parseExpr(); expectPunct(")"); Statement sbBody = parseStatement(consumeLeadingComments()); String sbName = "[" + String.join(", ", sbNames) + "]"; return new RangeForStatement(new NamedType("auto",List.of(),0,false,false, false), sbName, isRef, sbIterable, sbBody, start.line(), start.col(), leadingComments); } if (looksLikeRangeFor()) { TypeRef declType = parseTypeRef(); boolean isReference = false; if (declType instanceof NamedType nt && nt.isReference()) { isReference = true; declType = new NamedType(nt.baseName(), nt.templateArgs(), nt.pointerDepth(), false, nt.isConst(), false); } String declName = expectIdentifier().text(); expectPunct(":"); Expr iterable = parseExpr(); expectPunct(")"); Statement body = parseStatement(consumeLeadingComments()); return new RangeForStatement(declType, declName, isReference, iterable, body, start.line(), start.col(), leadingComments); } Statement init = null; if (!checkPunct(";")) { if (looksLikeDeclaration()) { List decls = parseDeclStatementsDesugared(List.of()); // Wrap multiple declarators ("int i=0, j=10") in a block for the init slot if (decls.size() == 1) { init = decls.get(0); } else { // Emit as a sequence of decl statements wrapped in a synthetic block List stmts = new ArrayList<>(decls); init = new Block(stmts, decls.get(0).line(), decls.get(0).col(), List.of()); } } else { CppLexerToken t = peek(); Expr e = parseExpr(); expectPunct(";"); init = new ExprStatement(e, t.line(), t.col(), List.of()); } } else { advance(); // consume the bare ';' } Expr cond = checkPunct(";") ? null : parseExpr(); expectPunct(";"); // For-loop update may be comma-separated: "i++, j--" Expr update = null; if (!checkPunct(")")) { update = parseExpr(); while (matchPunct(",")) { CppLexerToken ct = peek(); Expr next = parseExpr(); update = new BinaryExpr(",", update, next, ct.line(), ct.col(), List.of()); } } expectPunct(")"); Statement body = parseStatement(consumeLeadingComments()); return new ForStatement(init, cond, update, body, start.line(), start.col(), leadingComments); } /** * Lookahead-only: from just after "for (", determine whether this is a * range-for by tentatively parsing a TypeRef + identifier and checking * whether ':' (not ';') follows. Restores position regardless. */ private boolean looksLikeRangeFor() { int save = pos; try { if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; try { parseTypeRef(); } catch (ParseException e) { return false; } if (!check(CppLexerTokenType.IDENTIFIER) && !(check(CppLexerTokenType.KEYWORD) && PSEUDO_TYPE_KEYWORDS_USABLE_AS_NAMES.contains(peek().text()))) { return false; } advance(); return checkPunct(":"); } finally { pos = save; } } private Statement parseWhile(List leadingComments) { CppLexerToken start = expectKeyword("while"); expectPunct("("); // C++17 while with condition declaration: "while (auto val = expr)" Expr cond; if (looksLikeDeclaration()) { // Scan to find the = and parse the initializer expression while (!isAtEnd() && !checkOp("=") && !checkPunct(")")) advance(); if (matchOp("=")) cond = parseExpr(); else cond = new Identifier("true", start.line(), start.col(), List.of()); } else { cond = parseExpr(); } expectPunct(")"); Statement body = parseStatement(consumeLeadingComments()); return new WhileStatement(cond, body, start.line(), start.col(), leadingComments); } private Statement parseDoWhile(List leadingComments) { CppLexerToken start = expectKeyword("do"); Statement body = parseStatement(consumeLeadingComments()); expectKeyword("while"); expectPunct("("); Expr cond = parseExpr(); expectPunct(")"); expectPunct(";"); return new DoWhileStatement(body, cond, start.line(), start.col(), leadingComments); } private Statement parseSwitch(List leadingComments) { CppLexerToken start = expectKeyword("switch"); expectPunct("("); java.util.List swInitStmts = new ArrayList<>(); {int scan=pos,depth=0;boolean hasInit=false;while(scan cases = new ArrayList<>(); while (!checkPunct("}")) { consumeLeadingComments(); if (checkPunct("}")) break; Expr matchValue; if (matchKeyword("case")) { matchValue = parseExpr(); } else { expectKeyword("default"); matchValue = null; } expectPunct(":"); List body = new ArrayList<>(); while (!checkKeyword("case") && !checkKeyword("default") && !checkPunct("}")) { List comments = consumeLeadingComments(); if (checkKeyword("case") || checkKeyword("default") || checkPunct("}")) break; body.addAll(parseStatementOrMultiDecl(comments)); } cases.add(new SwitchCase(matchValue, body)); } expectPunct("}"); Statement swStmt = new SwitchStatement(subject, cases, start.line(), start.col(), leadingComments); if (!swInitStmts.isEmpty()) { swInitStmts.add(swStmt); return new Block(swInitStmts, start.line(), start.col(), leadingComments); } return swStmt; } private Statement parseReturn(List leadingComments) { CppLexerToken start = expectKeyword("return"); Expr value = checkPunct(";") ? null : parseExpr(); expectPunct(";"); return new ReturnStatement(value, start.line(), start.col(), leadingComments); } private Statement parseBreak(List leadingComments) { CppLexerToken start = expectKeyword("break"); expectPunct(";"); return new BreakStatement(start.line(), start.col(), leadingComments); } private Statement parseContinue(List leadingComments) { CppLexerToken start = expectKeyword("continue"); expectPunct(";"); return new ContinueStatement(start.line(), start.col(), leadingComments); } private Statement parseTry(List leadingComments) { CppLexerToken start = expectKeyword("try"); Block tryBlock = parseBlock(); List clauses = new ArrayList<>(); while (matchKeyword("catch")) { expectPunct("("); CatchClause clause; if (matchPunct("...")) { expectPunct(")"); Block body = parseBlock(); clause = new CatchClause(null, null, body, true); } else { TypeRef exType = parseTypeRef(); String varName = check(CppLexerTokenType.IDENTIFIER) ? expectIdentifier().text() : null; expectPunct(")"); Block body = parseBlock(); clause = new CatchClause(exType, varName, body, false); } clauses.add(clause); } return new TryStatement(tryBlock, clauses, start.line(), start.col(), leadingComments); } private Statement parseDelete(List leadingComments) { CppLexerToken start = expectKeyword("delete"); boolean isArray = false; if (checkPunct("[")) { advance(); expectPunct("]"); isArray = true; } Expr target = parseExpr(); expectPunct(";"); return new DeleteStatement(target, isArray, start.line(), start.col(), leadingComments); } private Statement parseExprStatement(List leadingComments) { CppLexerToken start = peek(); Expr expr = parseExpr(); expectPunct(";"); return new ExprStatement(expr, start.line(), start.col(), leadingComments); } /** * Lookahead-only: determines whether the upcoming tokens form a variable * declaration ("Type name ...") rather than an expression-statement. * Tries to parse a TypeRef followed by an identifier; if that succeeds * and is followed by '=', ';', '[', ',', or '(' (direct-init), it's * treated as a declaration. Restores position regardless of outcome. * * This disambiguation is necessary because both "int x = 5;" * (declaration) and "x = 5;" (plain assignment expression statement) * start with an identifier-shaped token; the grammar must decide which * without a symbol table -- same category of ambiguity documented on * CallExpr for bare construction calls. */ /** Keywords that introduce a statement form and can NEVER be the start * of a type name. Checked first and unconditionally in * looksLikeDeclaration, because parseTypeRef/parseQualifiedTypeName * have no concept of "which keywords are valid type names" -- they * accept ANY KEYWORD-or-IDENTIFIER token as a type name segment (this * is intentional and correct for real type keywords like "int", * "auto", "color", etc., see parseTypeNameSegment's notes -- the bug * is that nothing ever excluded the OTHER kind of keyword, the ones * that start an entirely different statement form). * * Found via PipelineCompositionTest's real g++ check, not by the * parser's own corpus sweep: "return foo(1, 2);" was being parsed as * a DeclStatement (treating "return" as a bogus type name, "foo" as * the declared name, and "(1, 2)" as a direct-init argument list), * not a ReturnStatement -- producing a syntactically-different-but- * still-valid-shaped AST that round-tripped through codegen as * "return foo{1, 2};" with no parse exception anywhere, so the * existing "did this fail to parse" corpus sweep never caught it. * Confirmed this affected real corpus files too (Handles.pde, * Scrollbar.pde) once specifically checked for, not just the * synthetic case that surfaced it. */ private static final java.util.Set STATEMENT_KEYWORDS = java.util.Set.of( "return", "if", "for", "while", "do", "switch", "break", "continue", "try", "delete", "case", "default", "else", "throw" ); /** Keyword type names that are valid cast-operator targets ("operator int()", * "operator bool()", etc.). Kept explicit and narrow to prevent arbitrary * keywords (like Processing's "color", or "auto", "return", etc.) from * being misidentified as cast-operator type names in parseFunctionOrVariableName. */ private static final java.util.Set CAST_OPERATOR_TYPE_KEYWORDS = java.util.Set.of( "int", "float", "double", "bool", "char", "long", "short", "unsigned", "signed", "void", "size_t" ); private boolean looksLikeDeclaration() { int save = pos; try { if (check(CppLexerTokenType.KEYWORD) && STATEMENT_KEYWORDS.contains(peek().text())) return false; // Tolerate a leading "static"/"const" (in either order) before // attempting to parse a type -- mirrors the actual parse path // in parseDeclStatementsDesugared, which consumes these same // qualifiers before calling parseTypeRef(). Without this, // "static int x = 5;" as a LOCAL variable was rejected right // here (parseTypeRef has no concept of consuming a leading // qualifier itself, so it choked on the literal token // "static"), before ever reaching parseDeclStatementsDesugared's // own (correct, but unreachable without this fix) handling of // that same qualifier. if (checkKeyword("static")) advance(); if (checkKeyword("volatile")) advance(); if (checkKeyword("const")) advance(); if (checkKeyword("volatile")) advance(); // vol // alignas specifier: "alignas(T) type name" if (checkKeyword("alignas") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("(")) { advance(); advance(); int _ad=1; while (!isAtEnd() && _ad > 0) { if (checkPunct("(")) _ad++; else if (checkPunct(")")) _ad--; advance(); } } // volatile after const if (checkKeyword("constexpr")) advance(); if (checkKeyword("inline")) advance(); if (checkKeyword("static")) advance(); // tolerate either order if (!(check(CppLexerTokenType.IDENTIFIER) || check(CppLexerTokenType.KEYWORD))) return false; // Function-pointer-variable form: "Type (*name)(Params) = init;" // Must be checked before the ordinary TypeRef+identifier path, // since after the return type this shape has '(' where an // ordinary declarator would have an identifier. if (looksLikeFunctionPointerVariable()) return true; TypeRef type; try { type = parseTypeRef(); } catch (ParseException e) { return false; } if (!check(CppLexerTokenType.IDENTIFIER) && !(check(CppLexerTokenType.KEYWORD) && PSEUDO_TYPE_KEYWORDS_USABLE_AS_NAMES.contains(peek().text()))) { return false; } advance(); // tentatively consume the name // Member data pointer: "int Point::* dp" -- ::* follows the class name if (checkPunct("::") && pos + 1 < tokens.size() && tokens.get(pos + 1).isOp("*")) return true; return checkOp("=") || checkPunct(";") || checkPunct("[") || checkPunct(",") || checkPunct("(") || checkPunct("{"); } finally { pos = save; } } /** * Parses "Type name1 [=init1|dims1], name2 [=init2|dims2], ...;" and * desugars it into one DeclStatement per declarator, all sharing the * same TypeRef instance. This is the single implementation backing * every declaration-statement call site (block-level, for-init, * single-statement contexts) -- callers that require exactly one * declarator check decls.size() themselves and raise a clear error, * rather than this method silently dropping extras. */ private List parseDeclStatementsDesugared(List leadingComments) { CppLexerToken start = peek(); // BUG FIX, found via adversarial stress-case probing: "static" // (and "const") were never consumed here at all -- only the // TOP-LEVEL declaration path consumed them. "static int x = 5;" // as a LOCAL variable failed to parse outright ("expected ';' // but found 'int'", since "static" was left sitting in the // token stream as if it were the start of an unrelated // statement, then "int" showed up where a ";" was expected). // Tolerate either order ("static const" or "const static"), // mirroring the top-level path's own tolerance. // alignas specifier: "alignas(T) type name" if (checkKeyword("alignas") && pos + 1 < tokens.size() && tokens.get(pos + 1).isPunct("(")) { advance(); advance(); int _ad=1; while (!isAtEnd() && _ad > 0) { if (checkPunct("(")) _ad++; else if (checkPunct(")")) _ad--; advance(); } } boolean isStatic = matchKeyword("static"); matchKeyword("volatile"); // consume volatile qualifier at statement scope boolean isConst = matchKeyword("const"); if (matchKeyword("constexpr")) isConst = true; // constexpr implies const if (!isStatic) isStatic = matchKeyword("static"); if (!isConst) isConst = matchKeyword("const"); matchKeyword("volatile"); // volatile may follow const if (looksLikeFunctionPointerVariable()) { TypeRef returnType = parseTypeRef(); NameAndFunctionPointerType decl = parseFunctionPointerDeclaratorTail(returnType); Expr initializer = null; if (matchOp("=")) { initializer = parseExpr(); } expectPunct(";"); return List.of(new DeclStatement(decl.type(), decl.name(), List.of(), initializer, isStatic, isConst, start.line(), start.col(), leadingComments)); } TypeRef type = parseTypeRef(); List result = new ArrayList<>(); result.add(parseOneDeclarator(type, isStatic, isConst, start, leadingComments)); while (matchPunct(",")) { // Same gap, same fix, as parseOneTopLevelDeclarator's comma- // continuation loop -- see its notes for the full rationale, // including why each declarator's type must be built from // the base type's name/templateArgs/const ONLY, never // reusing "type"'s own pointerDepth/isReference (which // belongs exclusively to the first declarator). int extraPointerDepth = 0; boolean extraIsReference = false; while (checkOp("*") || checkOp("&")) { if (matchOp("*")) extraPointerDepth++; else { matchOp("&"); extraIsReference = true; } } TypeRef declaratorType = type; if (type instanceof NamedType nt) { declaratorType = new NamedType(nt.baseName(), nt.templateArgs(), extraPointerDepth, extraIsReference, nt.isConst(), false); } result.add(parseOneDeclarator(declaratorType, isStatic, isConst, start, List.of())); } expectPunct(";"); return result; } private DeclStatement parseOneDeclarator(TypeRef type, boolean isStatic, boolean isConst, CppLexerToken start, List leadingComments) { String name = expectIdentifier().text(); if (checkPunct("::") && pos + 1 < tokens.size() && tokens.get(pos + 1).isOp("*")) { advance(); advance(); name = name + "::* " + expectIdentifier().text(); } List dims = parseOptionalArrayDims(); Expr initializer = null; if (matchOp("=")) { if (checkPunct("{")) { initializer = parseInitializerList(); } else { initializer = parseExpr(); } } else if (checkPunct("(")) { initializer = parseDirectInitAsCall(name); } else if (checkPunct("{")) { // Brace-direct-init at statement scope, same rationale as the // top-level declarator case -- see parseOneTopLevelDeclarator's // notes. initializer = parseDirectInitAsCall(name); } return new DeclStatement(type, name, dims, initializer, isStatic, isConst, start.line(), start.col(), leadingComments); } /** * Parses zero or more "[expr]" / "[]" array-dimension suffixes after * a declarator name. * * BUG FIX, found via RealHeaderStressTest against the real engine * headers (Pie_Chart.pde's real "int angles[] = { 30, 10, 45, ... };"): * an EMPTY bracket pair (no size expression, size inferred from the * initializer -- valid, common C++/Java-array syntax) must still be * recorded as a real dimension, just with a null size expression -- * NOT silently dropped as if no brackets were present at all. The * previous version of this method only ever called dims.add(...) * inside the "if (!checkPunct(\"]\"))" branch, meaning an empty "[]" * added NOTHING to the dims list at all, making * "int angles[] = {...}" structurally indistinguishable from * "int angles = {...}" (a scalar) once parsed -- confirmed directly: * vd.arrayDims() was an empty list, not a list containing one null * entry, for "int angles[] = { 30, 10, 45 };". This silently turned * a real top-level array declaration into a scalar declaration with * a brace-init initializer, which g++ correctly rejects ("cannot * convert '' to 'int'"). * * CodeGen.emitArrayDims already handled a null dim entry correctly * (rendering bare "[]" when dim is null) -- this representation was * already anticipated and supported on the rendering side; the * parser simply never produced it. */ private List parseOptionalArrayDims() { List dims = new ArrayList<>(); while (checkPunct("[")) { advance(); if (!checkPunct("]")) { dims.add(parseExpr()); } else { dims.add(null); // empty "[]" -- still a real dimension, size inferred from initializer } expectPunct("]"); } return dims; } /** * Handles C++'s "most vexing parse" direct-init form, "Type name(args);", * confirmed real by CppBuild.java's existing OBJECT_DIRECT_INIT handling * in classifyTopLevelDecls. Represented as a CallExpr initializer whose * callee is the variable's own name, consistent with how bare- * construction-call ("Type()") is already represented elsewhere. * * Handles both "Type name(args);" (paren-direct-init) and * "Type name{args};" (brace-direct-init) -- the latter confirmed * necessary so the parser can read back CodeGen's own brace-rendered * output (see CodeGen.emitDeclaratorTail's most-vexing-parse notes). * Both forms produce the identical CallExpr shape; which punctuation * was used in the source is not preserved on the AST, since nothing * downstream has needed that distinction so far -- if it ever does, * this is the place to add it. */ private Expr parseDirectInitAsCall(String varName) { CppLexerToken t = peek(); boolean isBrace = checkPunct("{"); List args = isBrace ? parseBraceArgList() : parseArgList(); return new CallExpr(new Identifier(varName, t.line(), t.col(), List.of()), args, isBrace, t.line(), t.col(), List.of()); } private List parseBraceArgList() { expectPunct("{"); List args = new ArrayList<>(); if (!checkPunct("}")) { args.add(parseExpr()); while (matchPunct(",")) { args.add(parseExpr()); } } expectPunct("}"); return args; } }