2023-08-17 23:29:05 -03:00
|
|
|
|
/*
|
|
|
|
|
|
* Copyright (c) 2023, Dan Klishch <danilklishch@gmail.com>
|
|
|
|
|
|
*
|
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
#include <AK/NonnullOwnPtr.h>
|
2024-01-16 22:47:08 -03:00
|
|
|
|
#include <LibXML/Parser/Parser.h>
|
2023-08-17 23:29:05 -03:00
|
|
|
|
|
|
|
|
|
|
#include "Parser/Lexer.h"
|
2024-03-08 01:33:36 -03:00
|
|
|
|
#include "Parser/SpecificationParsing.h"
|
2023-08-17 23:29:05 -03:00
|
|
|
|
#include "Parser/XMLUtils.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace JSSpecCompiler {
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
2024-02-15 22:25:17 -03:00
|
|
|
|
Optional<Token> consume_number(LineTrackingLexer& lexer, Location& location)
|
2023-08-17 23:29:05 -03:00
|
|
|
|
{
|
|
|
|
|
|
u64 start = lexer.tell();
|
|
|
|
|
|
|
|
|
|
|
|
if (lexer.next_is('-'))
|
|
|
|
|
|
lexer.consume(1);
|
|
|
|
|
|
|
|
|
|
|
|
if (!lexer.next_is(is_ascii_digit)) {
|
|
|
|
|
|
lexer.retreat(lexer.tell() - start);
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lexer.consume_while(is_ascii_digit);
|
|
|
|
|
|
|
|
|
|
|
|
if (lexer.next_is('.')) {
|
|
|
|
|
|
lexer.consume(1);
|
|
|
|
|
|
if (lexer.consume_while(is_ascii_digit).length() == 0)
|
|
|
|
|
|
lexer.retreat(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto length = lexer.tell() - start;
|
|
|
|
|
|
lexer.retreat(length);
|
2024-01-20 22:58:43 -03:00
|
|
|
|
return { Token { TokenType::Number, lexer.consume(length), move(location) } };
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool can_end_word_token(char c)
|
|
|
|
|
|
{
|
|
|
|
|
|
return is_ascii_space(c) || ".,"sv.contains(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-20 21:47:02 -03:00
|
|
|
|
void tokenize_string(SpecificationParsingContext& ctx, XML::Node const* node, StringView view, Vector<Token>& tokens)
|
2023-08-17 23:29:05 -03:00
|
|
|
|
{
|
2024-01-16 22:47:08 -03:00
|
|
|
|
static constexpr struct {
|
|
|
|
|
|
StringView text_to_match;
|
|
|
|
|
|
TokenType token_type;
|
|
|
|
|
|
} choices[] = {
|
|
|
|
|
|
{ "-"sv, TokenType::AmbiguousMinus },
|
|
|
|
|
|
{ "}"sv, TokenType::BraceClose },
|
|
|
|
|
|
{ "{"sv, TokenType::BraceOpen },
|
|
|
|
|
|
{ ":"sv, TokenType::Colon },
|
|
|
|
|
|
{ ","sv, TokenType::Comma },
|
|
|
|
|
|
{ "/"sv, TokenType::Division },
|
|
|
|
|
|
{ ". "sv, TokenType::Dot },
|
|
|
|
|
|
{ ".\n"sv, TokenType::Dot },
|
|
|
|
|
|
{ "="sv, TokenType::Equals },
|
|
|
|
|
|
{ "is equal to"sv, TokenType::Equals },
|
|
|
|
|
|
{ "!"sv, TokenType::ExclamationMark },
|
|
|
|
|
|
{ ">"sv, TokenType::Greater },
|
|
|
|
|
|
{ "is"sv, TokenType::Is },
|
|
|
|
|
|
{ "<"sv, TokenType::Less },
|
2024-01-21 00:48:05 -03:00
|
|
|
|
{ "»"sv, TokenType::ListEnd },
|
|
|
|
|
|
{ "«"sv, TokenType::ListStart },
|
2024-01-16 22:47:08 -03:00
|
|
|
|
{ "."sv, TokenType::MemberAccess },
|
|
|
|
|
|
{ "×"sv, TokenType::Multiplication },
|
|
|
|
|
|
{ "is not equal to"sv, TokenType::NotEquals },
|
|
|
|
|
|
{ "≠"sv, TokenType::NotEquals },
|
|
|
|
|
|
{ ")"sv, TokenType::ParenClose },
|
|
|
|
|
|
{ "("sv, TokenType::ParenOpen },
|
|
|
|
|
|
{ "+"sv, TokenType::Plus },
|
2024-01-20 23:52:02 -03:00
|
|
|
|
{ "?"sv, TokenType::QuestionMark },
|
2024-01-21 16:46:33 -03:00
|
|
|
|
{ "]"sv, TokenType::SquareBracketClose },
|
|
|
|
|
|
{ "["sv, TokenType::SquareBracketOpen },
|
2024-01-21 17:27:23 -03:00
|
|
|
|
{ "NewTarget"sv, TokenType::WellKnownValue },
|
2024-01-16 22:47:08 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-02-15 22:25:17 -03:00
|
|
|
|
LineTrackingLexer lexer(view, node->offset);
|
2023-08-17 23:29:05 -03:00
|
|
|
|
|
|
|
|
|
|
while (!lexer.is_eof()) {
|
|
|
|
|
|
lexer.ignore_while(is_ascii_space);
|
|
|
|
|
|
|
2024-01-16 22:47:08 -03:00
|
|
|
|
// FIXME: This is incorrect since we count text offset after XML reference resolution. To do
|
|
|
|
|
|
// this properly, we need support from XML::Parser.
|
2024-02-15 22:25:17 -03:00
|
|
|
|
Location token_location = ctx.location_from_xml_offset(lexer.position_for(lexer.tell()));
|
2024-01-16 22:47:08 -03:00
|
|
|
|
|
2024-01-20 22:58:43 -03:00
|
|
|
|
if (auto result = consume_number(lexer, token_location); result.has_value()) {
|
2023-08-17 23:29:05 -03:00
|
|
|
|
tokens.append(result.release_value());
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-16 22:47:08 -03:00
|
|
|
|
bool matched = false;
|
|
|
|
|
|
for (auto const& [text_to_match, token_type] : choices) {
|
|
|
|
|
|
if (lexer.consume_specific(text_to_match)) {
|
2024-01-21 17:27:23 -03:00
|
|
|
|
tokens.append({ token_type, text_to_match, move(token_location) });
|
2024-01-16 22:47:08 -03:00
|
|
|
|
matched = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (matched)
|
|
|
|
|
|
continue;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
|
|
|
|
|
|
StringView word = lexer.consume_until(can_end_word_token);
|
|
|
|
|
|
if (word.length())
|
2024-01-20 22:58:43 -03:00
|
|
|
|
tokens.append({ TokenType::Word, word, move(token_location) });
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-20 21:47:02 -03:00
|
|
|
|
enum class TreeType {
|
|
|
|
|
|
AlgorithmStep,
|
2024-01-20 22:54:03 -03:00
|
|
|
|
NestedExpression,
|
2024-01-20 21:47:02 -03:00
|
|
|
|
Header,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct TokenizerState {
|
|
|
|
|
|
Vector<Token> tokens;
|
|
|
|
|
|
XML::Node const* substeps = nullptr;
|
|
|
|
|
|
bool has_errors = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void tokenize_tree(SpecificationParsingContext& ctx, TokenizerState& state, XML::Node const* node, TreeType tree_type)
|
2023-08-17 23:29:05 -03:00
|
|
|
|
{
|
2024-01-20 21:47:02 -03:00
|
|
|
|
// FIXME: Use structured binding once macOS Lagom CI updates to Clang >= 16.
|
|
|
|
|
|
auto& tokens = state.tokens;
|
|
|
|
|
|
auto& substeps = state.substeps;
|
|
|
|
|
|
auto& has_errors = state.has_errors;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
|
|
|
|
|
|
for (auto const& child : node->as_element().children) {
|
2024-01-20 21:47:02 -03:00
|
|
|
|
if (has_errors)
|
|
|
|
|
|
break;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
|
2024-01-20 21:47:02 -03:00
|
|
|
|
child->content.visit(
|
|
|
|
|
|
[&](XML::Node::Element const& element) -> void {
|
2024-01-16 22:47:08 -03:00
|
|
|
|
Location child_location = ctx.location_from_xml_offset(child->offset);
|
2024-01-20 21:47:02 -03:00
|
|
|
|
auto report_error = [&]<typename... Parameters>(AK::CheckedFormatString<Parameters...>&& fmt, Parameters const&... parameters) {
|
|
|
|
|
|
ctx.diag().error(child_location, move(fmt), parameters...);
|
|
|
|
|
|
has_errors = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (substeps) {
|
|
|
|
|
|
report_error("substeps list must be the last child of algorithm step");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-01-16 22:47:08 -03:00
|
|
|
|
|
2023-08-17 23:29:05 -03:00
|
|
|
|
if (element.name == tag_var) {
|
2024-01-20 21:47:02 -03:00
|
|
|
|
auto variable_name = get_text_contents(child);
|
|
|
|
|
|
if (!variable_name.has_value())
|
|
|
|
|
|
report_error("malformed <var> subtree, expected single text child node");
|
2023-08-17 23:29:05 -03:00
|
|
|
|
|
2024-01-20 22:58:43 -03:00
|
|
|
|
tokens.append({ TokenType::Identifier, variable_name.value_or(""sv), move(child_location) });
|
2024-01-20 21:47:02 -03:00
|
|
|
|
return;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (element.name == tag_emu_val) {
|
2024-01-20 21:47:02 -03:00
|
|
|
|
auto maybe_contents = get_text_contents(child);
|
|
|
|
|
|
if (!maybe_contents.has_value())
|
|
|
|
|
|
report_error("malformed <emu-val> subtree, expected single text child node");
|
|
|
|
|
|
|
|
|
|
|
|
auto contents = maybe_contents.value_or(""sv);
|
|
|
|
|
|
|
2023-08-17 23:29:05 -03:00
|
|
|
|
if (contents.length() >= 2 && contents.starts_with('"') && contents.ends_with('"'))
|
2024-01-20 22:58:43 -03:00
|
|
|
|
tokens.append({ TokenType::String, contents.substring_view(1, contents.length() - 2), move(child_location) });
|
2024-01-20 23:41:38 -03:00
|
|
|
|
else if (contents.is_one_of("undefined", "null", "this", "true", "false"))
|
|
|
|
|
|
tokens.append({ TokenType::WellKnownValue, contents, move(child_location) });
|
2023-08-17 23:29:05 -03:00
|
|
|
|
else
|
2024-01-20 22:58:43 -03:00
|
|
|
|
tokens.append({ TokenType::Identifier, contents, move(child_location) });
|
2024-01-20 21:47:02 -03:00
|
|
|
|
return;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (element.name == tag_emu_xref) {
|
2024-01-20 21:47:02 -03:00
|
|
|
|
auto identifier = get_single_child_with_tag(child, "a"sv).map([](XML::Node const* node) {
|
|
|
|
|
|
return get_text_contents(node).value_or(""sv);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!identifier.has_value() || identifier.value().is_empty())
|
|
|
|
|
|
report_error("malformed <emu-xref> subtree, expected <a> with nested single text node");
|
|
|
|
|
|
|
2024-01-20 22:58:43 -03:00
|
|
|
|
tokens.append({ TokenType::Identifier, identifier.value_or(""sv), move(child_location) });
|
2024-01-20 21:47:02 -03:00
|
|
|
|
return;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-20 22:54:03 -03:00
|
|
|
|
if (element.name == tag_sup) {
|
|
|
|
|
|
tokens.append({ TokenType::Superscript, ""sv, move(child_location) });
|
|
|
|
|
|
tokens.append({ TokenType::ParenOpen, ""sv, move(child_location) });
|
|
|
|
|
|
tokenize_tree(ctx, state, child, TreeType::NestedExpression);
|
|
|
|
|
|
tokens.append({ TokenType::ParenClose, ""sv, move(child_location) });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-21 00:10:07 -03:00
|
|
|
|
if (element.name == tag_emu_const) {
|
|
|
|
|
|
auto maybe_contents = get_text_contents(child);
|
|
|
|
|
|
if (!maybe_contents.has_value())
|
|
|
|
|
|
report_error("malformed <emu-const> subtree, expected single text child node");
|
|
|
|
|
|
|
|
|
|
|
|
tokens.append({ TokenType::Enumerator, maybe_contents.value_or(""sv), move(child_location) });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-20 21:47:02 -03:00
|
|
|
|
if (tree_type == TreeType::Header && element.name == tag_span) {
|
|
|
|
|
|
auto element_class = get_attribute_by_name(child, attribute_class);
|
|
|
|
|
|
if (element_class != class_secnum)
|
|
|
|
|
|
report_error("expected <span> to have class='secnum' attribute");
|
|
|
|
|
|
|
|
|
|
|
|
auto section_number = get_text_contents(child);
|
|
|
|
|
|
if (!section_number.has_value())
|
|
|
|
|
|
report_error("malformed section number span subtree, expected single text child node");
|
|
|
|
|
|
|
2024-01-20 22:58:43 -03:00
|
|
|
|
tokens.append({ TokenType::SectionNumber, section_number.value_or(""sv), move(child_location) });
|
2024-01-20 21:47:02 -03:00
|
|
|
|
return;
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-20 21:47:02 -03:00
|
|
|
|
if (tree_type == TreeType::AlgorithmStep && element.name == tag_ol) {
|
|
|
|
|
|
substeps = child;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
report_error("<{}> should not be a child of algorithm step", element.name);
|
2023-08-17 23:29:05 -03:00
|
|
|
|
},
|
2024-01-20 21:47:02 -03:00
|
|
|
|
[&](XML::Node::Text const& text) {
|
2023-08-17 23:29:05 -03:00
|
|
|
|
auto view = text.builder.string_view();
|
2024-01-20 21:47:02 -03:00
|
|
|
|
if (substeps != nullptr && !contains_empty_text(child)) {
|
|
|
|
|
|
ctx.diag().error(ctx.location_from_xml_offset(child->offset),
|
|
|
|
|
|
"substeps list must be the last child of algorithm step");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tokenize_string(ctx, child, view, tokens);
|
|
|
|
|
|
}
|
2023-08-17 23:29:05 -03:00
|
|
|
|
},
|
2024-01-20 21:47:02 -03:00
|
|
|
|
[&](auto const&) {});
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
2024-01-16 19:44:59 -03:00
|
|
|
|
|
2024-01-20 22:54:03 -03:00
|
|
|
|
if (tree_type == TreeType::AlgorithmStep && tokens.size() && tokens.last().type == TokenType::MemberAccess)
|
2024-01-16 19:44:59 -03:00
|
|
|
|
tokens.last().type = TokenType::Dot;
|
2024-01-20 21:47:02 -03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-01-16 19:44:59 -03:00
|
|
|
|
|
2024-01-20 21:47:02 -03:00
|
|
|
|
StepTokenizationResult tokenize_step(SpecificationParsingContext& ctx, XML::Node const* node)
|
|
|
|
|
|
{
|
|
|
|
|
|
TokenizerState state;
|
|
|
|
|
|
tokenize_tree(ctx, state, node, TreeType::AlgorithmStep);
|
|
|
|
|
|
return {
|
|
|
|
|
|
.tokens = state.has_errors ? OptionalNone {} : Optional<Vector<Token>> { move(state.tokens) },
|
|
|
|
|
|
.substeps = state.substeps,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Optional<Vector<Token>> tokenize_header(SpecificationParsingContext& ctx, XML::Node const* node)
|
|
|
|
|
|
{
|
|
|
|
|
|
TokenizerState state;
|
|
|
|
|
|
tokenize_tree(ctx, state, node, TreeType::Header);
|
|
|
|
|
|
return state.has_errors ? OptionalNone {} : Optional<Vector<Token>> { state.tokens };
|
2023-08-17 23:29:05 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|