mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 03:37:43 +00:00
LibCpp: Parse templatized types
We can now parse things like Vector<int>
This commit is contained in:
parent
9954a1837f
commit
29b6915db9
4 changed files with 156 additions and 41 deletions
|
@ -54,7 +54,7 @@ void FunctionDeclaration::dump(size_t indent) const
|
||||||
|
|
||||||
String qualifiers_string;
|
String qualifiers_string;
|
||||||
if (!m_qualifiers.is_empty()) {
|
if (!m_qualifiers.is_empty()) {
|
||||||
print_indent(indent+1);
|
print_indent(indent + 1);
|
||||||
outln("[{}]", String::join(" ", m_qualifiers));
|
outln("[{}]", String::join(" ", m_qualifiers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ void StringLiteral::dump(size_t indent) const
|
||||||
void ReturnStatement::dump(size_t indent) const
|
void ReturnStatement::dump(size_t indent) const
|
||||||
{
|
{
|
||||||
ASTNode::dump(indent);
|
ASTNode::dump(indent);
|
||||||
if(m_value)
|
if (m_value)
|
||||||
m_value->dump(indent + 1);
|
m_value->dump(indent + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,4 +442,24 @@ void NullPointerLiteral::dump(size_t indent) const
|
||||||
ASTNode::dump(indent);
|
ASTNode::dump(indent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TemplatizedType::dump(size_t indent) const
|
||||||
|
{
|
||||||
|
ASTNode::dump(indent);
|
||||||
|
|
||||||
|
String qualifiers_string;
|
||||||
|
if (!m_qualifiers.is_empty())
|
||||||
|
qualifiers_string = String::formatted("[{}] ", String::join(" ", m_qualifiers));
|
||||||
|
|
||||||
|
print_indent(indent + 1);
|
||||||
|
outln("{}{}", qualifiers_string, m_name);
|
||||||
|
|
||||||
|
print_indent(indent + 1);
|
||||||
|
outln("<");
|
||||||
|
for (auto& arg : m_template_arguments) {
|
||||||
|
arg.dump(indent + 1);
|
||||||
|
}
|
||||||
|
print_indent(indent + 1);
|
||||||
|
outln(">");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,10 +215,10 @@ public:
|
||||||
const StringView& name() const { return m_name; }
|
const StringView& name() const { return m_name; }
|
||||||
virtual void dump(size_t indent) const override;
|
virtual void dump(size_t indent) const override;
|
||||||
virtual bool is_type() const override { return true; }
|
virtual bool is_type() const override { return true; }
|
||||||
|
virtual bool is_templatized() const { return false; }
|
||||||
|
|
||||||
Type(ASTNode* parent, Optional<Position> start, Optional<Position> end, const String& filename, StringView name)
|
Type(ASTNode* parent, Optional<Position> start, Optional<Position> end, const String& filename)
|
||||||
: ASTNode(parent, start, end, filename)
|
: ASTNode(parent, start, end, filename)
|
||||||
, m_name(name)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +226,21 @@ public:
|
||||||
Vector<StringView> m_qualifiers;
|
Vector<StringView> m_qualifiers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TemplatizedType : public Type {
|
||||||
|
public:
|
||||||
|
virtual ~TemplatizedType() override = default;
|
||||||
|
virtual const char* class_name() const override { return "TemplatizedType"; }
|
||||||
|
virtual void dump(size_t indent) const override;
|
||||||
|
virtual bool is_templatized() const override { return true; }
|
||||||
|
|
||||||
|
TemplatizedType(ASTNode* parent, Optional<Position> start, Optional<Position> end, const String& filename)
|
||||||
|
: Type(parent, start, end, filename)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
NonnullRefPtrVector<Type> m_template_arguments;
|
||||||
|
};
|
||||||
|
|
||||||
class Pointer : public Type {
|
class Pointer : public Type {
|
||||||
public:
|
public:
|
||||||
virtual ~Pointer() override = default;
|
virtual ~Pointer() override = default;
|
||||||
|
@ -233,7 +248,7 @@ public:
|
||||||
virtual void dump(size_t indent) const override;
|
virtual void dump(size_t indent) const override;
|
||||||
|
|
||||||
Pointer(ASTNode* parent, Optional<Position> start, Optional<Position> end, const String& filename)
|
Pointer(ASTNode* parent, Optional<Position> start, Optional<Position> end, const String& filename)
|
||||||
: Type(parent, start, end, filename, {})
|
: Type(parent, start, end, filename)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -189,16 +189,16 @@ NonnullRefPtr<FunctionDefinition> Parser::parse_function_definition(ASTNode& par
|
||||||
NonnullRefPtr<Statement> Parser::parse_statement(ASTNode& parent)
|
NonnullRefPtr<Statement> Parser::parse_statement(ASTNode& parent)
|
||||||
{
|
{
|
||||||
SCOPE_LOGGER();
|
SCOPE_LOGGER();
|
||||||
ArmedScopeGuard consume_semicolumn([this]() {
|
ArmedScopeGuard consume_semicolon([this]() {
|
||||||
consume(Token::Type::Semicolon);
|
consume(Token::Type::Semicolon);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (match_block_statement()) {
|
if (match_block_statement()) {
|
||||||
consume_semicolumn.disarm();
|
consume_semicolon.disarm();
|
||||||
return parse_block_statement(parent);
|
return parse_block_statement(parent);
|
||||||
}
|
}
|
||||||
if (match_comment()) {
|
if (match_comment()) {
|
||||||
consume_semicolumn.disarm();
|
consume_semicolon.disarm();
|
||||||
return parse_comment(parent);
|
return parse_comment(parent);
|
||||||
}
|
}
|
||||||
if (match_variable_declaration()) {
|
if (match_variable_declaration()) {
|
||||||
|
@ -211,15 +211,15 @@ NonnullRefPtr<Statement> Parser::parse_statement(ASTNode& parent)
|
||||||
return parse_return_statement(parent);
|
return parse_return_statement(parent);
|
||||||
}
|
}
|
||||||
if (match_keyword("for")) {
|
if (match_keyword("for")) {
|
||||||
consume_semicolumn.disarm();
|
consume_semicolon.disarm();
|
||||||
return parse_for_statement(parent);
|
return parse_for_statement(parent);
|
||||||
}
|
}
|
||||||
if (match_keyword("if")) {
|
if (match_keyword("if")) {
|
||||||
consume_semicolumn.disarm();
|
consume_semicolon.disarm();
|
||||||
return parse_if_statement(parent);
|
return parse_if_statement(parent);
|
||||||
} else {
|
} else {
|
||||||
error("unexpected statement type");
|
error("unexpected statement type");
|
||||||
consume_semicolumn.disarm();
|
consume_semicolon.disarm();
|
||||||
consume();
|
consume();
|
||||||
return create_ast_node<InvalidStatement>(parent, position(), position());
|
return create_ast_node<InvalidStatement>(parent, position(), position());
|
||||||
}
|
}
|
||||||
|
@ -251,16 +251,60 @@ NonnullRefPtr<BlockStatement> Parser::parse_block_statement(ASTNode& parent)
|
||||||
return block_statement;
|
return block_statement;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::match_type()
|
Parser::MatchTypeResult Parser::match_type()
|
||||||
{
|
{
|
||||||
save_state();
|
save_state();
|
||||||
ScopeGuard state_guard = [this] { load_state(); };
|
ScopeGuard state_guard = [this] { load_state(); };
|
||||||
|
|
||||||
parse_type_qualifiers();
|
parse_type_qualifiers();
|
||||||
// Type
|
|
||||||
if (!peek(Token::Type::KnownType).has_value() && !peek(Token::Type::Identifier).has_value())
|
if (!peek(Token::Type::KnownType).has_value() && !peek(Token::Type::Identifier).has_value())
|
||||||
|
return MatchTypeResult::NoMatch;
|
||||||
|
|
||||||
|
consume();
|
||||||
|
|
||||||
|
if (peek(Token::Type::Less).has_value()) {
|
||||||
|
if (match_template_arguments()) {
|
||||||
|
return MatchTypeResult::Templatized;
|
||||||
|
}
|
||||||
|
return MatchTypeResult::NoMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatchTypeResult::Regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Parser::match_template_arguments()
|
||||||
|
{
|
||||||
|
save_state();
|
||||||
|
ScopeGuard state_guard = [this] { load_state(); };
|
||||||
|
|
||||||
|
if (!peek(Token::Type::Less).has_value())
|
||||||
return false;
|
return false;
|
||||||
return true;
|
consume();
|
||||||
|
|
||||||
|
while (!eof() && peek().type() != Token::Type::Greater) {
|
||||||
|
if (match_type() == MatchTypeResult::NoMatch)
|
||||||
|
return false;
|
||||||
|
parse_type(*m_root_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return peek().type() == Token::Type::Greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
NonnullRefPtrVector<Type> Parser::parse_template_arguments(ASTNode& parent)
|
||||||
|
{
|
||||||
|
SCOPE_LOGGER();
|
||||||
|
|
||||||
|
consume(Token::Type::Less);
|
||||||
|
|
||||||
|
NonnullRefPtrVector<Type> template_arguments;
|
||||||
|
while (!eof() && peek().type() != Token::Type::Greater) {
|
||||||
|
template_arguments.append(parse_type(parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
consume(Token::Type::Greater);
|
||||||
|
|
||||||
|
return template_arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::match_variable_declaration()
|
bool Parser::match_variable_declaration()
|
||||||
|
@ -269,14 +313,17 @@ bool Parser::match_variable_declaration()
|
||||||
save_state();
|
save_state();
|
||||||
ScopeGuard state_guard = [this] { load_state(); };
|
ScopeGuard state_guard = [this] { load_state(); };
|
||||||
|
|
||||||
if (!match_type())
|
if (match_type() == MatchTypeResult::NoMatch) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
VERIFY(m_root_node);
|
VERIFY(m_root_node);
|
||||||
parse_type(*m_root_node);
|
parse_type(*m_root_node);
|
||||||
|
|
||||||
// Identifier
|
// Identifier
|
||||||
if (!peek(Token::Type::Identifier).has_value())
|
if (!peek(Token::Type::Identifier).has_value()) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
consume();
|
consume();
|
||||||
|
|
||||||
if (match(Token::Type::Equals)) {
|
if (match(Token::Type::Equals)) {
|
||||||
|
@ -586,7 +633,7 @@ bool Parser::match_function_declaration()
|
||||||
|
|
||||||
parse_function_qualifiers();
|
parse_function_qualifiers();
|
||||||
|
|
||||||
if (!match_type())
|
if (match_type() == MatchTypeResult::NoMatch)
|
||||||
return false;
|
return false;
|
||||||
VERIFY(m_root_node);
|
VERIFY(m_root_node);
|
||||||
parse_type(*m_root_node);
|
parse_type(*m_root_node);
|
||||||
|
@ -770,8 +817,8 @@ void Parser::error(StringView message)
|
||||||
m_tokens[m_state.token_index].start().line,
|
m_tokens[m_state.token_index].start().line,
|
||||||
m_tokens[m_state.token_index].start().column);
|
m_tokens[m_state.token_index].start().column);
|
||||||
}
|
}
|
||||||
m_errors.append(formatted_message);
|
|
||||||
dbgln_if(CPP_DEBUG, "{}", formatted_message);
|
m_state.errors.append(formatted_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::match_expression()
|
bool Parser::match_expression()
|
||||||
|
@ -925,7 +972,7 @@ NonnullRefPtr<ReturnStatement> Parser::parse_return_statement(ASTNode& parent)
|
||||||
SCOPE_LOGGER();
|
SCOPE_LOGGER();
|
||||||
auto return_statement = create_ast_node<ReturnStatement>(parent, position(), {});
|
auto return_statement = create_ast_node<ReturnStatement>(parent, position(), {});
|
||||||
consume(Token::Type::Keyword);
|
consume(Token::Type::Keyword);
|
||||||
if(!peek(Token::Type::Semicolon).has_value()) {
|
if (!peek(Token::Type::Semicolon).has_value()) {
|
||||||
auto expression = parse_expression(*return_statement);
|
auto expression = parse_expression(*return_statement);
|
||||||
return_statement->m_value = expression;
|
return_statement->m_value = expression;
|
||||||
}
|
}
|
||||||
|
@ -1011,19 +1058,19 @@ NonnullRefPtr<MemberDeclaration> Parser::parse_member_declaration(ASTNode& paren
|
||||||
{
|
{
|
||||||
SCOPE_LOGGER();
|
SCOPE_LOGGER();
|
||||||
auto member_decl = create_ast_node<MemberDeclaration>(parent, position(), {});
|
auto member_decl = create_ast_node<MemberDeclaration>(parent, position(), {});
|
||||||
auto type_token = consume();
|
member_decl->m_type = parse_type(*member_decl);
|
||||||
auto identifier_token = consume(Token::Type::Identifier);
|
|
||||||
RefPtr<Expression> initial_value;
|
|
||||||
|
|
||||||
|
auto identifier_token = consume(Token::Type::Identifier);
|
||||||
|
member_decl->m_name = text_of_token(identifier_token);
|
||||||
|
|
||||||
|
RefPtr<Expression> initial_value;
|
||||||
if (match(Token::Type::LeftCurly)) {
|
if (match(Token::Type::LeftCurly)) {
|
||||||
consume(Token::Type::LeftCurly);
|
consume(Token::Type::LeftCurly);
|
||||||
initial_value = parse_expression(*member_decl);
|
initial_value = parse_expression(*member_decl);
|
||||||
consume(Token::Type::RightCurly);
|
consume(Token::Type::RightCurly);
|
||||||
}
|
}
|
||||||
|
|
||||||
member_decl->m_type = create_ast_node<Type>(*member_decl, type_token.start(), type_token.end(), text_of_token(type_token));
|
|
||||||
member_decl->m_name = text_of_token(identifier_token);
|
|
||||||
member_decl->m_initial_value = move(initial_value);
|
member_decl->m_initial_value = move(initial_value);
|
||||||
|
|
||||||
consume(Token::Type::Semicolon);
|
consume(Token::Type::Semicolon);
|
||||||
member_decl->set_end(position());
|
member_decl->set_end(position());
|
||||||
|
|
||||||
|
@ -1052,21 +1099,47 @@ bool Parser::match_boolean_literal()
|
||||||
NonnullRefPtr<Type> Parser::parse_type(ASTNode& parent)
|
NonnullRefPtr<Type> Parser::parse_type(ASTNode& parent)
|
||||||
{
|
{
|
||||||
SCOPE_LOGGER();
|
SCOPE_LOGGER();
|
||||||
auto qualifiers = parse_type_qualifiers();
|
|
||||||
auto token = consume();
|
auto match_result = match_type();
|
||||||
auto type = create_ast_node<Type>(parent, token.start(), token.end(), text_of_token(token));
|
if (match_result == TemplatizedMatchResult::NoMatch) {
|
||||||
type->m_qualifiers = move(qualifiers);
|
auto token = consume();
|
||||||
if (token.type() != Token::Type::KnownType && token.type() != Token::Type::Identifier) {
|
return create_ast_node<Type>(parent, token.start(), token.end());
|
||||||
error(String::formatted("unexpected token for type: {}", token.to_string()));
|
|
||||||
return type;
|
|
||||||
}
|
}
|
||||||
while (peek().type() == Token::Type::Asterisk) {
|
bool is_templatized = match_result == TemplatizedMatchResult::Templatized;
|
||||||
|
|
||||||
|
RefPtr<Type> type;
|
||||||
|
if (is_templatized) {
|
||||||
|
type = create_ast_node<TemplatizedType>(parent, position(), {});
|
||||||
|
} else {
|
||||||
|
type = create_ast_node<Type>(parent, position(), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto qualifiers = parse_type_qualifiers();
|
||||||
|
auto type_name_token = consume();
|
||||||
|
type->m_qualifiers = move(qualifiers);
|
||||||
|
type->m_name = text_of_token(type_name_token);
|
||||||
|
|
||||||
|
if (type_name_token.type() != Token::Type::KnownType && type_name_token.type() != Token::Type::Identifier) {
|
||||||
|
type->set_end(position());
|
||||||
|
error(String::formatted("unexpected type_name_token for type: {}", type_name_token.to_string()));
|
||||||
|
return type.release_nonnull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_templatized) {
|
||||||
|
static_cast<TemplatizedType&>(*type).m_template_arguments = parse_template_arguments(*type);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!eof() && peek().type() == Token::Type::Asterisk) {
|
||||||
|
type->set_end(position());
|
||||||
auto asterisk = consume();
|
auto asterisk = consume();
|
||||||
auto ptr = create_ast_node<Pointer>(type, asterisk.start(), asterisk.end());
|
auto ptr = create_ast_node<Pointer>(parent, asterisk.start(), asterisk.end());
|
||||||
|
type->set_parent(*ptr);
|
||||||
ptr->m_pointee = type;
|
ptr->m_pointee = type;
|
||||||
type = ptr;
|
type = ptr;
|
||||||
}
|
}
|
||||||
return type;
|
|
||||||
|
type->set_end(position());
|
||||||
|
return type.release_nonnull();
|
||||||
}
|
}
|
||||||
|
|
||||||
NonnullRefPtr<ForStatement> Parser::parse_for_statement(ASTNode& parent)
|
NonnullRefPtr<ForStatement> Parser::parse_for_statement(ASTNode& parent)
|
||||||
|
@ -1142,7 +1215,6 @@ Vector<StringView> Parser::parse_function_qualifiers()
|
||||||
return qualifiers;
|
return qualifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool Parser::match_attribute_specification()
|
bool Parser::match_attribute_specification()
|
||||||
{
|
{
|
||||||
return text_of_token(peek()) == "__attribute__";
|
return text_of_token(peek()) == "__attribute__";
|
||||||
|
|
|
@ -53,7 +53,7 @@ public:
|
||||||
String text_of_node(const ASTNode&) const;
|
String text_of_node(const ASTNode&) const;
|
||||||
StringView text_of_token(const Cpp::Token& token) const;
|
StringView text_of_token(const Cpp::Token& token) const;
|
||||||
void print_tokens() const;
|
void print_tokens() const;
|
||||||
Vector<String> errors() const { return m_errors; }
|
const Vector<String>& errors() const { return m_state.errors; }
|
||||||
const Preprocessor::Definitions& definitions() const { return m_definitions; }
|
const Preprocessor::Definitions& definitions() const { return m_definitions; }
|
||||||
|
|
||||||
struct TokenAndPreprocessorDefinition {
|
struct TokenAndPreprocessorDefinition {
|
||||||
|
@ -88,7 +88,14 @@ private:
|
||||||
bool match_keyword(const String&);
|
bool match_keyword(const String&);
|
||||||
bool match_block_statement();
|
bool match_block_statement();
|
||||||
bool match_namespace_declaration();
|
bool match_namespace_declaration();
|
||||||
bool match_type();
|
bool match_template_arguments();
|
||||||
|
|
||||||
|
enum class MatchTypeResult {
|
||||||
|
NoMatch,
|
||||||
|
Regular,
|
||||||
|
Templatized,
|
||||||
|
};
|
||||||
|
MatchTypeResult match_type();
|
||||||
|
|
||||||
Optional<NonnullRefPtrVector<Parameter>> parse_parameter_list(ASTNode& parent);
|
Optional<NonnullRefPtrVector<Parameter>> parse_parameter_list(ASTNode& parent);
|
||||||
Optional<Token> consume_whitespace();
|
Optional<Token> consume_whitespace();
|
||||||
|
@ -98,7 +105,7 @@ private:
|
||||||
NonnullRefPtr<FunctionDeclaration> parse_function_declaration(ASTNode& parent);
|
NonnullRefPtr<FunctionDeclaration> parse_function_declaration(ASTNode& parent);
|
||||||
NonnullRefPtr<FunctionDefinition> parse_function_definition(ASTNode& parent);
|
NonnullRefPtr<FunctionDefinition> parse_function_definition(ASTNode& parent);
|
||||||
NonnullRefPtr<Statement> parse_statement(ASTNode& parent);
|
NonnullRefPtr<Statement> parse_statement(ASTNode& parent);
|
||||||
NonnullRefPtr<VariableDeclaration> parse_variable_declaration(ASTNode& parent, bool expect_semicolon=true);
|
NonnullRefPtr<VariableDeclaration> parse_variable_declaration(ASTNode& parent, bool expect_semicolon = true);
|
||||||
NonnullRefPtr<Expression> parse_expression(ASTNode& parent);
|
NonnullRefPtr<Expression> parse_expression(ASTNode& parent);
|
||||||
NonnullRefPtr<Expression> parse_primary_expression(ASTNode& parent);
|
NonnullRefPtr<Expression> parse_primary_expression(ASTNode& parent);
|
||||||
NonnullRefPtr<Expression> parse_secondary_expression(ASTNode& parent, NonnullRefPtr<Expression> lhs);
|
NonnullRefPtr<Expression> parse_secondary_expression(ASTNode& parent, NonnullRefPtr<Expression> lhs);
|
||||||
|
@ -121,6 +128,7 @@ private:
|
||||||
NonnullRefPtr<NamespaceDeclaration> parse_namespace_declaration(ASTNode& parent, bool is_nested_namespace = false);
|
NonnullRefPtr<NamespaceDeclaration> parse_namespace_declaration(ASTNode& parent, bool is_nested_namespace = false);
|
||||||
NonnullRefPtrVector<Declaration> parse_declarations_in_translation_unit(ASTNode& parent);
|
NonnullRefPtrVector<Declaration> parse_declarations_in_translation_unit(ASTNode& parent);
|
||||||
RefPtr<Declaration> parse_single_declaration_in_translation_unit(ASTNode& parent);
|
RefPtr<Declaration> parse_single_declaration_in_translation_unit(ASTNode& parent);
|
||||||
|
NonnullRefPtrVector<Type> parse_template_arguments(ASTNode& parent);
|
||||||
|
|
||||||
bool match(Token::Type);
|
bool match(Token::Type);
|
||||||
Token consume(Token::Type);
|
Token consume(Token::Type);
|
||||||
|
@ -136,6 +144,7 @@ private:
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
size_t token_index { 0 };
|
size_t token_index { 0 };
|
||||||
|
Vector<String> errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
void error(StringView message = {});
|
void error(StringView message = {});
|
||||||
|
@ -173,7 +182,6 @@ private:
|
||||||
Vector<State> m_saved_states;
|
Vector<State> m_saved_states;
|
||||||
RefPtr<TranslationUnit> m_root_node;
|
RefPtr<TranslationUnit> m_root_node;
|
||||||
NonnullRefPtrVector<ASTNode> m_nodes;
|
NonnullRefPtrVector<ASTNode> m_nodes;
|
||||||
Vector<String> m_errors;
|
|
||||||
|
|
||||||
Vector<TokenAndPreprocessorDefinition> m_replaced_preprocessor_tokens;
|
Vector<TokenAndPreprocessorDefinition> m_replaced_preprocessor_tokens;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue