diff --git a/Shell/AST.cpp b/Shell/AST.cpp index 00ac0cd1bf8..1568c2bf030 100644 --- a/Shell/AST.cpp +++ b/Shell/AST.cpp @@ -1347,6 +1347,149 @@ Join::~Join() { } +void MatchExpr::dump(int level) const +{ + Node::dump(level); + print_indented(String::format("(expression)", m_expr_name.characters()), level + 1); + m_matched_expr->dump(level + 2); + print_indented(String::format("(named: %s)", m_expr_name.characters()), level + 1); + print_indented("(entries)", level + 1); + for (auto& entry : m_entries) { + print_indented("(match)", level + 2); + for (auto& node : entry.options) + node.dump(level + 3); + print_indented("(execute)", level + 2); + if (entry.body) + entry.body->dump(level + 3); + else + print_indented("(nothing)", level + 3); + } +} + +RefPtr MatchExpr::run(RefPtr shell) +{ + auto value = m_matched_expr->run(shell)->resolve_without_cast(shell); + auto list = value->resolve_as_list(shell); + + auto list_matches = [&](auto&& pattern) { + if (pattern.size() != list.size()) + return false; + + for (size_t i = 0; i < pattern.size(); ++i) { + if (!list[i].matches(pattern[i])) + return false; + } + + return true; + }; + + auto resolve_pattern = [&](auto& option) { + Vector pattern; + if (option.is_glob()) { + pattern.append(static_cast(&option)->text()); + } else if (option.is_bareword()) { + pattern.append(static_cast(&option)->text()); + } else if (option.is_list()) { + auto list = option.run(shell); + option.for_each_entry(shell, [&](auto&& value) { + pattern.append(value->resolve_as_list(nullptr)); // Note: 'nullptr' incurs special behaviour, + // asking the node for a 'raw' value. + return IterationDecision::Continue; + }); + } + + return pattern; + }; + + auto frame = shell->push_frame(); + if (!m_expr_name.is_empty()) + shell->set_local_variable(m_expr_name, value); + + for (auto& entry : m_entries) { + for (auto& option : entry.options) { + if (list_matches(resolve_pattern(option))) { + if (entry.body) + return entry.body->run(shell); + else + return create({}); + } + } + } + + // FIXME: Somehow raise an error in the shell. + dbg() << "Non-exhaustive match rules!"; + return create({}); +} + +void MatchExpr::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) +{ + editor.stylize({ m_position.start_offset, m_position.start_offset + 5 }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); + if (m_as_position.has_value()) + editor.stylize({ m_as_position.value().start_offset, m_as_position.value().end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); + + metadata.is_first_in_list = false; + if (m_matched_expr) + m_matched_expr->highlight_in_editor(editor, shell, metadata); + + for (auto& entry : m_entries) { + metadata.is_first_in_list = false; + for (auto& option : entry.options) + option.highlight_in_editor(editor, shell, metadata); + + metadata.is_first_in_list = true; + if (entry.body) + entry.body->highlight_in_editor(editor, shell, metadata); + + for (auto& position : entry.pipe_positions) + editor.stylize({ position.start_offset, position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); + } +} + +HitTestResult MatchExpr::hit_test_position(size_t offset) +{ + if (!position().contains(offset)) + return {}; + + auto result = m_matched_expr->hit_test_position(offset); + if (result.matching_node) + return result; + + for (auto& entry : m_entries) { + if (!entry.body) + continue; + auto result = entry.body->hit_test_position(offset); + if (result.matching_node) + return result; + } + + return {}; +} + +MatchExpr::MatchExpr(Position position, RefPtr expr, String name, Optional as_position, Vector entries) + : Node(move(position)) + , m_matched_expr(move(expr)) + , m_expr_name(move(name)) + , m_as_position(move(as_position)) + , m_entries(move(entries)) +{ + if (m_matched_expr && m_matched_expr->is_syntax_error()) { + set_is_syntax_error(m_matched_expr->syntax_error_node()); + } else { + for (auto& entry : m_entries) { + if (!entry.body) + continue; + if (entry.body->is_syntax_error()) { + set_is_syntax_error(entry.body->syntax_error_node()); + break; + } + } + } +} + +MatchExpr::~MatchExpr() +{ +} + void Or::dump(int level) const { Node::dump(level); diff --git a/Shell/AST.h b/Shell/AST.h index 013d24e67a8..f632221d9cb 100644 --- a/Shell/AST.h +++ b/Shell/AST.h @@ -757,6 +757,31 @@ private: RefPtr m_right; }; +struct MatchEntry { + NonnullRefPtrVector options; + Vector pipe_positions; + RefPtr body; +}; + +class MatchExpr final : public Node { +public: + MatchExpr(Position, RefPtr expr, String name, Optional as_position, Vector entries); + virtual ~MatchExpr(); + +private: + virtual void dump(int level) const override; + virtual RefPtr run(RefPtr) override; + virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override; + virtual HitTestResult hit_test_position(size_t) override; + virtual String class_name() const override { return "MatchExpr"; } + virtual bool would_execute() const override { return true; } + + RefPtr m_matched_expr; + String m_expr_name; + Optional m_as_position; + Vector m_entries; +}; + class Or final : public Node { public: Or(Position, RefPtr, RefPtr); diff --git a/Shell/Parser.cpp b/Shell/Parser.cpp index ee9589b0a3b..f3ec66bd935 100644 --- a/Shell/Parser.cpp +++ b/Shell/Parser.cpp @@ -460,6 +460,9 @@ RefPtr Parser::parse_control_structure() if (auto subshell = parse_subshell()) return subshell; + if (auto match = parse_match_expr()) + return match; + return nullptr; } @@ -627,6 +630,142 @@ RefPtr Parser::parse_subshell() return create(move(body)); } +RefPtr Parser::parse_match_expr() +{ + auto rule_start = push_start(); + if (!expect("match")) + return nullptr; + + if (consume_while(is_whitespace).is_empty()) { + m_offset = rule_start->offset; + return nullptr; + } + + auto match_expression = parse_expression(); + if (!match_expression) { + return create( + create("Expected an expression after 'match'"), + String {}, Optional {}, Vector {}); + } + + consume_while(is_any_of(" \t\n")); + + String match_name; + Optional as_position; + auto as_start = m_offset; + if (expect("as")) { + as_position = AST::Position { as_start, m_offset }; + + if (consume_while(is_any_of(" \t\n")).is_empty()) { + auto node = create( + move(match_expression), + String {}, move(as_position), Vector {}); + node->set_is_syntax_error(create("Expected whitespace after 'as' in 'match'")); + return node; + } + + match_name = consume_while(is_word_character); + if (match_name.is_empty()) { + auto node = create( + move(match_expression), + String {}, move(as_position), Vector {}); + node->set_is_syntax_error(create("Expected an identifier after 'as' in 'match'")); + return node; + } + } + + consume_while(is_any_of(" \t\n")); + + if (!expect('{')) { + auto node = create( + move(match_expression), + move(match_name), move(as_position), Vector {}); + node->set_is_syntax_error(create("Expected an open brace '{' to start a 'match' entry list")); + return node; + } + + consume_while(is_any_of(" \t\n")); + + Vector entries; + for (;;) { + auto entry = parse_match_entry(); + consume_while(is_any_of(" \t\n")); + if (entry.options.is_empty()) + break; + + entries.append(entry); + } + + consume_while(is_any_of(" \t\n")); + + if (!expect('}')) { + auto node = create( + move(match_expression), + move(match_name), move(as_position), move(entries)); + node->set_is_syntax_error(create("Expected a close brace '}' to end a 'match' entry list")); + return node; + } + + return create(move(match_expression), move(match_name), move(as_position), move(entries)); +} + +AST::MatchEntry Parser::parse_match_entry() +{ + auto rule_start = push_start(); + + NonnullRefPtrVector patterns; + Vector pipe_positions; + + auto pattern = parse_match_pattern(); + if (!pattern) + return { {}, {}, create("Expected a pattern in 'match' body") }; + + patterns.append(pattern.release_nonnull()); + + consume_while(is_any_of(" \t\n")); + + auto previous_pipe_start_position = m_offset; + RefPtr error; + while (expect('|')) { + pipe_positions.append({ previous_pipe_start_position, m_offset }); + consume_while(is_any_of(" \t\n")); + auto pattern = parse_match_pattern(); + if (!pattern) { + error = create("Expected a pattern to follow '|' in 'match' body"); + break; + } + consume_while(is_any_of(" \t\n")); + + patterns.append(pattern.release_nonnull()); + } + + consume_while(is_any_of(" \t\n")); + + if (!expect('{')) { + if (!error) + error = create("Expected an open brace '{' to start a match entry body"); + } + + auto body = parse_toplevel(); + + if (!expect('}')) { + if (!error) + error = create("Expected a close brace '}' to end a match entry body"); + } + + if (body && error) + body->set_is_syntax_error(*error); + else if (error) + body = error; + + return { move(patterns), move(pipe_positions), move(body) }; +} + +RefPtr Parser::parse_match_pattern() +{ + return parse_expression(); +} + RefPtr Parser::parse_redirection() { auto rule_start = push_start(); diff --git a/Shell/Parser.h b/Shell/Parser.h index 6d6b789e4b6..36cce629918 100644 --- a/Shell/Parser.h +++ b/Shell/Parser.h @@ -55,6 +55,9 @@ private: RefPtr parse_for_loop(); RefPtr parse_if_expr(); RefPtr parse_subshell(); + RefPtr parse_match_expr(); + AST::MatchEntry parse_match_entry(); + RefPtr parse_match_pattern(); RefPtr parse_redirection(); RefPtr parse_list_expression(); RefPtr parse_expression(); @@ -135,6 +138,7 @@ pipe_sequence :: command '|' pipe_sequence control_structure :: for_expr | if_expr | subshell + | match_expr for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' toplevel '}' @@ -145,6 +149,12 @@ else_clause :: else '{' toplevel '}' subshell :: '{' toplevel '}' +match_expr :: 'match' ws+ expression ws* ('as' ws+ identifier)? '{' match_entry* '}' + +match_entry :: match_pattern ws* '{' toplevel '}' + +match_pattern :: expression (ws* '|' ws* expression)* + command :: redirection command | list_expression command?