diff --git a/src/builtin_function.cpp b/src/builtin_function.cpp index 28b40706d..5300b3e07 100644 --- a/src/builtin_function.cpp +++ b/src/builtin_function.cpp @@ -200,8 +200,7 @@ static int validate_function_name(int argc, const wchar_t *const *argv, wcstring /// Define a function. Calls into `function.cpp` to perform the heavy lifting of defining a /// function. int builtin_function(parser_t &parser, io_streams_t &streams, const wcstring_list_t &c_args, - const parsed_source_ref_t &source, - tnode_t func_node) { + const parsed_source_ref_t &source, const ast::block_statement_t &func_node) { assert(source && "Missing source in builtin_function"); // The wgetopt function expects 'function' as the first argument. Make a new wcstring_list with // that property. This is needed because this builtin has a different signature than the other @@ -252,7 +251,7 @@ int builtin_function(parser_t &parser, io_streams_t &streams, const wcstring_lis props->shadow_scope = opts.shadow_scope; props->named_arguments = std::move(opts.named_arguments); props->parsed_source = source; - props->func_node = func_node; + props->func_node = &func_node; // Populate inherit_vars. for (const wcstring &name : opts.inherit_vars) { diff --git a/src/builtin_function.h b/src/builtin_function.h index 9499a9a9f..4da1a378c 100644 --- a/src/builtin_function.h +++ b/src/builtin_function.h @@ -8,7 +8,10 @@ class parser_t; struct io_streams_t; +namespace ast { +struct block_statement_t; +} + int builtin_function(parser_t &parser, io_streams_t &streams, const wcstring_list_t &c_args, - const parsed_source_ref_t &source, - tnode_t func_node); + const parsed_source_ref_t &source, const ast::block_statement_t &func_node); #endif diff --git a/src/exec.cpp b/src/exec.cpp index bca3f90cd..45ce0cae1 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -623,10 +623,10 @@ static proc_performer_t get_performer_for_process(process_t *p, job_t *job, if (p->type == process_type_t::block_node) { const parsed_source_ref_t &source = p->block_node_source; - tnode_t node = p->internal_block_node; + const ast::statement_t *node = p->internal_block_node; assert(source && node && "Process is missing node info"); return [=](parser_t &parser) { - return parser.eval_node(source, node, io_chain, job_group).status; + return parser.eval_node(source, *node, io_chain, job_group).status; }; } else { assert(p->type == process_type_t::function); @@ -638,7 +638,7 @@ static proc_performer_t get_performer_for_process(process_t *p, job_t *job, auto argv = move_to_sharedptr(p->get_argv_array().to_list()); return [=](parser_t &parser) { // Pull out the job list from the function. - tnode_t body = props->func_node.child<1>(); + const ast::job_list_t &body = props->func_node->jobs; const block_t *fb = function_prepare_environment(parser, *argv, *props); auto res = parser.eval_node(props->parsed_source, body, io_chain, job_group); function_restore_environment(parser, fb); diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index e61046d5a..5aa171a8b 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -4567,6 +4567,8 @@ static void test_new_parser_errors() { {L"case", parse_error_unbalancing_case}, {L"if true ; case ; end", parse_error_generic}, + + {L"true | and", parse_error_andor_in_pipeline}, }; for (const auto &test : tests) { diff --git a/src/function.cpp b/src/function.cpp index e841d55fc..7ec28275e 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -224,17 +224,14 @@ bool function_get_definition(const wcstring &name, wcstring &out_definition) { const function_info_t *func = funcset->get_info(name); if (!func || !func->props) return false; // We want to preserve comments that the AST attaches to the header (#5285). - // Take everything from the end of the header to the end of the body. + // Take everything from the end of the header to the 'end' keyword. const auto &props = func->props; - namespace g = grammar; - tnode_t header = props->func_node.child<0>(); - tnode_t jobs = props->func_node.child<1>(); - auto header_src = header.source_range(); - auto jobs_src = jobs.source_range(); - if (header_src && jobs_src) { + auto header_src = props->func_node->header->try_source_range(); + auto end_kw_src = props->func_node->end.try_source_range(); + if (header_src && end_kw_src) { uint32_t body_start = header_src->start + header_src->length; - uint32_t body_end = jobs_src->start + jobs_src->length; - assert(body_start <= jobs_src->start && "job list must come after header"); + uint32_t body_end = end_kw_src->start; + assert(body_start <= body_end && "end keyword should come after header"); out_definition = wcstring(props->parsed_source->src, body_start, body_end - body_start); } return true; @@ -313,7 +310,7 @@ int function_get_definition_lineno(const wcstring &name) { // return one plus the number of newlines at offsets less than the start of our function's // statement (which includes the header). // TODO: merge with line_offset_of_character_at_offset? - auto source_range = func->props->func_node.source_range(); + auto source_range = func->props->func_node->try_source_range(); assert(source_range && "Function has no source range"); uint32_t func_start = source_range->start; const wcstring &source = func->props->parsed_source->src; diff --git a/src/function.h b/src/function.h index 3f612efab..2de5f3081 100644 --- a/src/function.h +++ b/src/function.h @@ -15,6 +15,10 @@ class parser_t; +namespace ast { +struct block_statement_t; +} + /// A function's constant properties. These do not change once initialized. struct function_properties_t { /// Parsed source containing the function. @@ -23,7 +27,7 @@ struct function_properties_t { /// Node containing the function statement, pointing into parsed_source. /// We store block_statement, not job_list, so that comments attached to the header are /// preserved. - tnode_t func_node; + const ast::block_statement_t *func_node; /// List of all named arguments for this function. wcstring_list_t named_arguments; diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index ef3b77f6b..22f4669e4 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -25,6 +25,7 @@ #include #include +#include "ast.h" #include "builtin.h" #include "builtin_function.h" #include "common.h" @@ -44,40 +45,74 @@ #include "proc.h" #include "reader.h" #include "timer.h" -#include "tnode.h" #include "tokenizer.h" #include "trace.h" #include "util.h" #include "wildcard.h" #include "wutil.h" -namespace g = grammar; - /// These are the specific statement types that support redirections. -static constexpr bool type_is_redirectable_block(parse_token_type_t type) { - return type == symbol_block_statement || type == symbol_if_statement || - type == symbol_switch_statement; +static constexpr bool type_is_redirectable_block(ast::type_t type) { + using t = ast::type_t; + return type == t::block_statement || type == t::if_statement || type == t::switch_statement; } -static bool specific_statement_type_is_redirectable_block(const parse_node_t &node) { +static bool specific_statement_type_is_redirectable_block(const ast::node_t &node) { return type_is_redirectable_block(node.type); } /// Get the name of a redirectable block, for profiling purposes. -static wcstring profiling_cmd_name_for_redirectable_block(const parse_node_t &node, - const parse_node_tree_t &tree, - const wcstring &src) { +static wcstring profiling_cmd_name_for_redirectable_block(const ast::node_t &node, + const parsed_source_t &pstree) { + using namespace ast; assert(specific_statement_type_is_redirectable_block(node)); - assert(node.has_source()); + + auto source_range = node.try_source_range(); + assert(source_range.has_value() && "No source range for block"); + + size_t src_end = 0; + switch (node.type) { + case type_t::block_statement: { + const node_t *block_header = node.as()->header.get(); + switch (block_header->type) { + case type_t::for_header: + src_end = block_header->as()->semi_nl.source_range().start; + break; + + case type_t::while_header: + src_end = block_header->as()->condition.source_range().end(); + break; + + case type_t::function_header: + src_end = block_header->as()->semi_nl.source_range().start; + break; + + case type_t::begin_header: + src_end = block_header->as()->kw_begin.source_range().end(); + break; + + default: + DIE("Unexpected block header type"); + } + } break; + + case type_t::if_statement: + src_end = node.as()->if_clause.condition.source_range().end(); + break; + + case type_t::switch_statement: + src_end = node.as()->semi_nl.source_range().start; + break; + + default: + DIE("Not a redirectable block type"); + break; + } + + assert(src_end >= source_range->start && "Invalid source end"); // Get the source for the block, and cut it at the next statement terminator. - const size_t src_start = node.source_start; - - auto term = tree.find_child(node); - assert(term.has_source() && term.source_range()->start >= src_start); - size_t src_len = term.source_range()->start - src_start; - - wcstring result = wcstring(src, src_start, src_len); + wcstring result = pstree.src.substr(source_range->start, src_end - source_range->start); result.append(L"..."); return result; } @@ -98,12 +133,13 @@ parse_execution_context_t::parse_execution_context_t(parsed_source_ref_t pstree, // Utilities -wcstring parse_execution_context_t::get_source(const parse_node_t &node) const { - return node.get_source(pstree->src); +wcstring parse_execution_context_t::get_source(const ast::node_t &node) const { + return node.source(pstree->src); } -tnode_t parse_execution_context_t::infinite_recursive_statement_in_job_list( - tnode_t job_list, wcstring *out_func_name) const { +const ast::decorated_statement_t * +parse_execution_context_t::infinite_recursive_statement_in_job_list(const ast::job_list_t &jobs, + wcstring *out_func_name) const { // This is a bit fragile. It is a test to see if we are inside of function call, but not inside // a block in that function call. If, in the future, the rules for what block scopes are pushed // on function invocation changes, then this check will break. @@ -111,60 +147,67 @@ tnode_t parse_execution_context_t::infinite_recursive_statem bool is_within_function_call = (current && parent && current->type() == block_type_t::top && parent->is_function_call()); if (!is_within_function_call) { - return {}; + return nullptr; } // Get the function name of the immediate block. const wcstring &forbidden_function_name = parent->function_name; // Get the first job in the job list. - tnode_t first_job = job_list.try_get_child().child<0>(); - if (!first_job) { - return {}; - } + const ast::job_conjunction_t *jc = jobs.at(0); + if (!jc) return nullptr; + const ast::job_t *job = &jc->job; - // Here's the statement node we find that's infinite recursive. - tnode_t infinite_recursive_statement; + // Helper to return if a statement is infinitely recursive in this function. + auto statement_recurses = + [&](const ast::statement_t &stat) -> const ast::decorated_statement_t * { + // Ignore non-decorated statements like `if`, etc. + const ast::decorated_statement_t *dc = + stat.contents.contents->try_as(); + if (!dc) return nullptr; - // Ignore the jobs variable assigment and "time" prefixes. - tnode_t statement = first_job.child<2>(); - tnode_t continuation = first_job.child<3>(); - const null_environment_t nullenv{}; - while (statement) { - // Get the list of plain statements. // Ignore statements with decorations like 'builtin' or 'command', since those // are not infinite recursion. In particular that is what enables 'wrapper functions'. - tnode_t plain_statement = - statement.try_get_child() - .try_get_child(); - if (plain_statement) { - maybe_t cmd = command_for_plain_statement(plain_statement, pstree->src); - if (cmd && - expand_one(*cmd, {expand_flag::skip_cmdsubst, expand_flag::skip_variables}, ctx) && - cmd == forbidden_function_name) { - // This is it. - infinite_recursive_statement = plain_statement; - if (out_func_name != nullptr) { - *out_func_name = forbidden_function_name; - } + if (dc->decoration() != parse_statement_decoration_none) return nullptr; + + // Check the command. + wcstring cmd = dc->command.source(pstree->src); + bool forbidden = + !cmd.empty() && + expand_one(cmd, {expand_flag::skip_cmdsubst, expand_flag::skip_variables}, ctx) && + cmd == forbidden_function_name; + return forbidden ? dc : nullptr; + }; + + const ast::decorated_statement_t *infinite_recursive_statement = nullptr; + + // Check main statement. + infinite_recursive_statement = statement_recurses(jc->job.statement); + + // Check piped remainder. + if (!infinite_recursive_statement) { + for (const ast::job_continuation_t &c : job->continuation) { + if (const auto *s = statement_recurses(c.statement)) { + infinite_recursive_statement = s; break; } } - statement = continuation.next_in_list(); } + if (infinite_recursive_statement && out_func_name) { + *out_func_name = forbidden_function_name; + } + // may be null return infinite_recursive_statement; } process_type_t parse_execution_context_t::process_type_for_command( - tnode_t statement, const wcstring &cmd) const { + const ast::decorated_statement_t &statement, const wcstring &cmd) const { enum process_type_t process_type = process_type_t::external; // Determine the process type, which depends on the statement decoration (command, builtin, // etc). - enum parse_statement_decoration_t decoration = get_decoration(statement); - - switch (decoration) { + switch (statement.decoration()) { case parse_statement_decoration_exec: process_type = process_type_t::exec; break; @@ -209,31 +252,33 @@ maybe_t parse_execution_context_t::check_end_execution() } /// Return whether the job contains a single statement, of block type, with no redirections. -bool parse_execution_context_t::job_is_simple_block(tnode_t job_node) const { - tnode_t statement = job_node.child<2>(); - +bool parse_execution_context_t::job_is_simple_block(const ast::job_t &job) const { + using namespace ast; // Must be no pipes. - if (job_node.child<3>().try_get_child()) { + if (!job.continuation.empty()) { return false; } - // Helper to check if an argument or redirection list has no redirections. - auto is_empty = [](tnode_t lst) -> bool { - return !lst.next_in_list(); + // Helper to check if an argument_or_redirection_list_t has no redirections. + auto no_redirs = [](const argument_or_redirection_list_t &list) -> bool { + for (const argument_or_redirection_t &val : list) { + if (val.is_redirection()) return false; + } + return true; }; // Check if we're a block statement with redirections. We do it this obnoxious way to preserve // type safety (in case we add more specific statement types). - const parse_node_t &specific_statement = statement.get_child_node<0>(); - switch (specific_statement.type) { - case symbol_block_statement: - return is_empty(statement.require_get_child().child<3>()); - case symbol_switch_statement: - return is_empty(statement.require_get_child().child<5>()); - case symbol_if_statement: - return is_empty(statement.require_get_child().child<3>()); - case symbol_not_statement: - case symbol_decorated_statement: + const node_t &ss = *job.statement.contents.contents; + switch (ss.type) { + case type_t::block_statement: + return no_redirs(ss.as()->args_or_redirs); + case type_t::switch_statement: + return no_redirs(ss.as()->args_or_redirs); + case type_t::if_statement: + return no_redirs(ss.as()->args_or_redirs); + case type_t::not_statement: + case type_t::decorated_statement: // not block statements return false; default: @@ -243,14 +288,19 @@ bool parse_execution_context_t::job_is_simple_block(tnode_t job_node) co } end_execution_reason_t parse_execution_context_t::run_if_statement( - tnode_t statement, const block_t *associated_block) { + const ast::if_statement_t &statement, const block_t *associated_block) { + using namespace ast; + using job_list_t = ast::job_list_t; end_execution_reason_t result = end_execution_reason_t::ok; // We have a sequence of if clauses, with a final else, resulting in a single job list that we // execute. - tnode_t job_list_to_execute; - tnode_t if_clause = statement.child<0>(); - tnode_t else_clause = statement.child<1>(); + const job_list_t *job_list_to_execute = nullptr; + const if_clause_t *if_clause = &statement.if_clause; + + // Index of the *next* elseif_clause to test. + const elseif_clause_list_t &elseif_clauses = statement.elseif_clauses; + size_t next_elseif_idx = 0; // We start with the 'if'. trace_if_enabled(*parser, L"if"); @@ -262,59 +312,54 @@ end_execution_reason_t parse_execution_context_t::run_if_statement( } // An if condition has a job and a "tail" of andor jobs, e.g. "foo ; and bar; or baz". - tnode_t condition_head = if_clause.child<1>(); - tnode_t condition_boolean_tail = if_clause.child<3>(); - // Check the condition and the tail. We treat end_execution_reason_t::error here as failure, // in accordance with historic behavior. - end_execution_reason_t cond_ret = run_job_conjunction(condition_head, associated_block); + end_execution_reason_t cond_ret = + run_job_conjunction(if_clause->condition, associated_block); if (cond_ret == end_execution_reason_t::ok) { - cond_ret = run_job_list(condition_boolean_tail, associated_block); + cond_ret = run_job_list(if_clause->andor_tail, associated_block); } const bool take_branch = (cond_ret == end_execution_reason_t::ok) && parser->get_last_status() == EXIT_SUCCESS; if (take_branch) { // Condition succeeded. - job_list_to_execute = if_clause.child<4>(); + job_list_to_execute = &if_clause->body; break; } - auto else_cont = else_clause.try_get_child(); - if (!else_cont) { - // 'if' condition failed, no else clause, return 0, we're done. - parser->set_last_statuses(statuses_t::just(STATUS_CMD_OK)); - break; + + // See if we have an elseif. + const auto *elseif_clause = elseif_clauses.at(next_elseif_idx++); + if (elseif_clause) { + trace_if_enabled(*parser, L"else if"); + if_clause = &elseif_clause->if_clause; } else { - // We have an 'else continuation' (either else-if or else). - if (auto maybe_if_clause = else_cont.try_get_child()) { - // it's an 'else if', go to the next one. - if_clause = maybe_if_clause; - else_clause = else_cont.try_get_child(); - assert(else_clause && "Expected to have an else clause"); - trace_if_enabled(*parser, L"else if"); - } else { - // It's the final 'else', we're done. - job_list_to_execute = else_cont.try_get_child(); - assert(job_list_to_execute && "Should have a job list"); - trace_if_enabled(*parser, L"else"); - break; - } + break; } } - // Execute any job list we got. - if (job_list_to_execute) { + if (!job_list_to_execute) { + // our ifs and elseifs failed. + // Check our else body. + if (statement.else_clause) { + trace_if_enabled(*parser, L"else"); + job_list_to_execute = &statement.else_clause->body; + } + } + + if (!job_list_to_execute) { + // 'if' condition failed, no else clause, return 0, we're done. + // No job list means no successful conditions, so return 0 (issue #1443). + parser->set_last_statuses(statuses_t::just(STATUS_CMD_OK)); + } else { + // Execute the job list we got. block_t *ib = parser->push_block(block_t::if_block()); - run_job_list(job_list_to_execute, ib); + run_job_list(*job_list_to_execute, ib); if (auto ret = check_end_execution()) { result = *ret; } parser->pop_block(ib); - } else { - // No job list means no successful conditions, so return 0 (issue #1443). - parser->set_last_statuses(statuses_t::just(STATUS_CMD_OK)); } - trace_if_enabled(*parser, L"end if"); // It's possible there's a last-minute cancellation (issue #1297). @@ -327,7 +372,7 @@ end_execution_reason_t parse_execution_context_t::run_if_statement( } end_execution_reason_t parse_execution_context_t::run_begin_statement( - tnode_t contents) { + const ast::job_list_t &contents) { // Basic begin/end block. Push a scope block, run jobs, pop it trace_if_enabled(*parser, L"begin"); block_t *sb = parser->push_block(block_t::scope_block(block_type_t::begin)); @@ -339,10 +384,12 @@ end_execution_reason_t parse_execution_context_t::run_begin_statement( // Define a function. end_execution_reason_t parse_execution_context_t::run_function_statement( - tnode_t statement, tnode_t header) { + const ast::block_statement_t &statement, const ast::function_header_t &header) { + using namespace ast; // Get arguments. wcstring_list_t arguments; - argument_node_list_t arg_nodes = header.descendants(); + ast_args_list_t arg_nodes = get_argument_nodes(header.args); + arg_nodes.insert(arg_nodes.begin(), &header.first_arg); end_execution_reason_t result = this->expand_arguments_from_nodes(arg_nodes, &arguments, failglob); @@ -362,48 +409,46 @@ end_execution_reason_t parse_execution_context_t::run_function_statement( } end_execution_reason_t parse_execution_context_t::run_block_statement( - tnode_t statement, const block_t *associated_block) { - tnode_t bheader = statement.child<0>(); - tnode_t contents = statement.child<1>(); - + const ast::block_statement_t &statement, const block_t *associated_block) { + const ast::node_t &bh = *statement.header.contents; + const ast::job_list_t &contents = statement.jobs; end_execution_reason_t ret = end_execution_reason_t::ok; - if (auto header = bheader.try_get_child()) { - ret = run_for_statement(header, contents); - } else if (auto header = bheader.try_get_child()) { - ret = run_while_statement(header, contents, associated_block); - } else if (auto header = bheader.try_get_child()) { - ret = run_function_statement(statement, header); - } else if (auto header = bheader.try_get_child()) { + if (const auto *fh = bh.try_as()) { + ret = run_for_statement(*fh, contents); + } else if (const auto *wh = bh.try_as()) { + ret = run_while_statement(*wh, contents, associated_block); + } else if (const auto *fh = bh.try_as()) { + ret = run_function_statement(statement, *fh); + } else if (bh.try_as()) { ret = run_begin_statement(contents); } else { - FLOGF(error, L"Unexpected block header: %ls\n", bheader.node()->describe().c_str()); + FLOGF(error, L"Unexpected block header: %ls\n", bh.describe().c_str()); PARSER_DIE(); } return ret; } end_execution_reason_t parse_execution_context_t::run_for_statement( - tnode_t header, tnode_t block_contents) { + const ast::for_header_t &header, const ast::job_list_t &block_contents) { // Get the variable name: `for var_name in ...`. We expand the variable name. It better result // in just one. - tnode_t var_name_node = header.child<1>(); - wcstring for_var_name = get_source(var_name_node); + wcstring for_var_name = header.var_name.source(get_source()); if (!expand_one(for_var_name, expand_flags_t{}, ctx)) { - return report_error(STATUS_EXPAND_ERROR, var_name_node, + return report_error(STATUS_EXPAND_ERROR, header.var_name, FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, for_var_name.c_str()); } // Get the contents to iterate over. wcstring_list_t arguments; - end_execution_reason_t ret = this->expand_arguments_from_nodes( - get_argument_nodes(header.child<3>()), &arguments, nullglob); + ast_args_list_t arg_nodes = get_argument_nodes(header.args); + end_execution_reason_t ret = this->expand_arguments_from_nodes(arg_nodes, &arguments, nullglob); if (ret != end_execution_reason_t::ok) { return ret; } auto var = parser->vars().get(for_var_name, ENV_DEFAULT); if (var && var->read_only()) { - return report_error(STATUS_INVALID_ARGS, var_name_node, + return report_error(STATUS_INVALID_ARGS, header.var_name, L"You cannot use read-only variable '%ls' in a for loop", for_var_name.c_str()); } @@ -416,7 +461,7 @@ end_execution_reason_t parse_execution_context_t::run_for_statement( assert(retval == ENV_OK); if (!valid_var_name(for_var_name)) { - return report_error(STATUS_INVALID_ARGS, var_name_node, BUILTIN_ERR_VARNAME, L"for", + return report_error(STATUS_INVALID_ARGS, header.var_name, BUILTIN_ERR_VARNAME, L"for", for_var_name.c_str()); } @@ -454,17 +499,16 @@ end_execution_reason_t parse_execution_context_t::run_for_statement( } end_execution_reason_t parse_execution_context_t::run_switch_statement( - tnode_t statement) { + const ast::switch_statement_t &statement) { // Get the switch variable. - tnode_t switch_value_n = statement.child<1>(); - const wcstring switch_value = get_source(switch_value_n); + const wcstring switch_value = get_source(statement.argument); // Expand it. We need to offset any errors by the position of the string. completion_list_t switch_values_expanded; parse_error_list_t errors; auto expand_ret = expand_string(switch_value, &switch_values_expanded, expand_flag::no_descriptions, ctx, &errors); - parse_error_offset_source_start(&errors, switch_value_n.source_range()->start); + parse_error_offset_source_start(&errors, statement.argument.range.start); switch (expand_ret.result) { case expand_result_t::error: @@ -474,12 +518,12 @@ end_execution_reason_t parse_execution_context_t::run_switch_statement( return end_execution_reason_t::cancelled; case expand_result_t::wildcard_no_match: - return report_error(STATUS_UNMATCHED_WILDCARD, switch_value_n, WILDCARD_ERR_MSG, - get_source(switch_value_n).c_str()); + return report_error(STATUS_UNMATCHED_WILDCARD, statement.argument, WILDCARD_ERR_MSG, + get_source(statement.argument).c_str()); case expand_result_t::ok: if (switch_values_expanded.size() > 1) { - return report_error(STATUS_INVALID_ARGS, switch_value_n, + return report_error(STATUS_INVALID_ARGS, statement.argument, _(L"switch: Expected at most one argument, got %lu\n"), switch_values_expanded.size()); } @@ -497,9 +541,8 @@ end_execution_reason_t parse_execution_context_t::run_switch_statement( block_t *sb = parser->push_block(block_t::switch_block()); // Expand case statements. - tnode_t case_item_list = statement.child<3>(); - tnode_t matching_case_item{}; - while (auto case_item = case_item_list.next_in_list()) { + const ast::case_item_t *matching_case_item = nullptr; + for (const ast::case_item_t &case_item : statement.cases) { if (auto ret = check_end_execution()) { result = *ret; break; @@ -508,7 +551,7 @@ end_execution_reason_t parse_execution_context_t::run_switch_statement( // Expand arguments. A case item list may have a wildcard that fails to expand to // anything. We also report case errors, but don't stop execution; i.e. a case item that // contains an unexpandable process will report and then fail to match. - auto arg_nodes = get_argument_nodes(case_item.child<1>()); + ast_args_list_t arg_nodes = get_argument_nodes(case_item.arguments); wcstring_list_t case_args; end_execution_reason_t case_result = this->expand_arguments_from_nodes(arg_nodes, &case_args, failglob); @@ -520,7 +563,7 @@ end_execution_reason_t parse_execution_context_t::run_switch_statement( // If this matched, we're done. if (match) { - matching_case_item = case_item; + matching_case_item = &case_item; break; } } @@ -531,8 +574,7 @@ end_execution_reason_t parse_execution_context_t::run_switch_statement( if (matching_case_item) { // Success, evaluate the job list. assert(result == end_execution_reason_t::ok && "Expected success"); - auto job_list = matching_case_item.child<3>(); - result = this->run_job_list(job_list, sb); + result = this->run_job_list(matching_case_item->body, sb); } parser->pop_block(sb); @@ -540,7 +582,7 @@ end_execution_reason_t parse_execution_context_t::run_switch_statement( } end_execution_reason_t parse_execution_context_t::run_while_statement( - tnode_t header, tnode_t contents, + const ast::while_header_t &header, const ast::job_list_t &contents, const block_t *associated_block) { end_execution_reason_t ret = end_execution_reason_t::ok; @@ -555,10 +597,6 @@ end_execution_reason_t parse_execution_context_t::run_while_statement( // affordance for the first condition. bool first_cond_check = true; - // The conditions of the while loop. - tnode_t condition_head = header.child<1>(); - tnode_t condition_boolean_tail = header.child<3>(); - trace_if_enabled(*parser, L"while"); // Run while the condition is true. @@ -571,9 +609,9 @@ end_execution_reason_t parse_execution_context_t::run_while_statement( // Check the condition. end_execution_reason_t cond_ret = - this->run_job_conjunction(condition_head, associated_block); + this->run_job_conjunction(header.condition, associated_block); if (cond_ret == end_execution_reason_t::ok) { - cond_ret = run_job_list(condition_boolean_tail, associated_block); + cond_ret = run_job_list(header.andor_tail, associated_block); } // If the loop condition failed to execute, then exit the loop without modifying the exit @@ -623,13 +661,15 @@ end_execution_reason_t parse_execution_context_t::run_while_statement( } // Reports an error. Always returns end_execution_reason_t::error. -end_execution_reason_t parse_execution_context_t::report_error(int status, const parse_node_t &node, +end_execution_reason_t parse_execution_context_t::report_error(int status, const ast::node_t &node, const wchar_t *fmt, ...) const { + auto r = node.source_range(); + // Create an error. parse_error_list_t error_list = parse_error_list_t(1); parse_error_t *error = &error_list.at(0); - error->source_start = node.source_start; - error->source_length = node.source_length; + error->source_start = r.start; + error->source_length = r.length; error->code = parse_error_syntax; // hackish va_list va; @@ -662,9 +702,27 @@ end_execution_reason_t parse_execution_context_t::report_errors( return end_execution_reason_t::error; } +// static +parse_execution_context_t::ast_args_list_t parse_execution_context_t::get_argument_nodes( + const ast::argument_list_t &args) { + ast_args_list_t result; + for (const ast::argument_t &arg : args) result.push_back(&arg); + return result; +} + +// static +parse_execution_context_t::ast_args_list_t parse_execution_context_t::get_argument_nodes( + const ast::argument_or_redirection_list_t &args) { + ast_args_list_t result; + for (const ast::argument_or_redirection_t &v : args) { + if (v.is_argument()) result.push_back(&v.argument()); + } + return result; +} + /// Handle the case of command not found. end_execution_reason_t parse_execution_context_t::handle_command_not_found( - const wcstring &cmd_str, tnode_t statement, int err_code) { + const wcstring &cmd_str, const ast::decorated_statement_t &statement, int err_code) { // We couldn't find the specified command. This is a non-fatal error. We want to set the exit // status to 127, which is the standard number used by other shells like bash and zsh. @@ -677,7 +735,7 @@ end_execution_reason_t parse_execution_context_t::handle_command_not_found( // error messages. wcstring_list_t event_args; { - auto args = get_argument_nodes(statement.child<1>()); + ast_args_list_t args = get_argument_nodes(statement.args_or_redirs); end_execution_reason_t arg_result = this->expand_arguments_from_nodes(args, &event_args, failglob); @@ -696,7 +754,7 @@ end_execution_reason_t parse_execution_context_t::handle_command_not_found( } end_execution_reason_t parse_execution_context_t::expand_command( - tnode_t statement, wcstring *out_cmd, + const ast::decorated_statement_t &statement, wcstring *out_cmd, wcstring_list_t *out_args) const { // Here we're expanding a command, for example $HOME/bin/stuff or $randomthing. The first // completion becomes the command itself, everything after becomes arguments. Command @@ -704,8 +762,8 @@ end_execution_reason_t parse_execution_context_t::expand_command( parse_error_list_t errors; // Get the unexpanded command string. We expect to always get it here. - wcstring unexp_cmd = *command_for_plain_statement(statement, pstree->src); - size_t pos_of_command_token = statement.child<0>().source_range()->start; + wcstring unexp_cmd = get_source(statement.command); + size_t pos_of_command_token = statement.command.range.start; // Expand the string to produce completions, and report errors. expand_result_t expand_err = @@ -715,7 +773,7 @@ end_execution_reason_t parse_execution_context_t::expand_command( // excluding prefixes such as " " or "if ". // This means that the error positions are relative to the beginning // of the token; we need to make them relative to the original source. - for (auto &error : errors) error.source_start += pos_of_command_token; + parse_error_offset_source_start(&errors, pos_of_command_token); return report_errors(STATUS_ILLEGAL_CMD, errors); } else if (expand_err == expand_result_t::wildcard_no_match) { return report_error(STATUS_UNMATCHED_WILDCARD, statement, WILDCARD_ERR_MSG, @@ -734,7 +792,7 @@ end_execution_reason_t parse_execution_context_t::expand_command( /// Creates a 'normal' (non-block) process. end_execution_reason_t parse_execution_context_t::populate_plain_process( - job_t *job, process_t *proc, tnode_t statement) { + job_t *job, process_t *proc, const ast::decorated_statement_t &statement) { assert(job != nullptr); assert(proc != nullptr); @@ -765,11 +823,9 @@ end_execution_reason_t parse_execution_context_t::populate_plain_process( const int no_cmd_err_code = errno; // If the specified command does not exist, and is undecorated, try using an implicit cd. - if (!has_command && get_decoration(statement) == parse_statement_decoration_none) { + if (!has_command && statement.decoration() == parse_statement_decoration_none) { // Implicit cd requires an empty argument and redirection list. - tnode_t args = statement.child<1>(); - if (args_from_cmd_expansion.empty() && !args.try_get_child() && - !args.try_get_child()) { + if (statement.args_or_redirs.empty()) { // Ok, no arguments or redirections; check to see if the command is a directory. use_implicit_cd = path_as_implicit_cd(cmd, parser->vars().get_pwd_slash(), parser->vars()) @@ -804,7 +860,8 @@ end_execution_reason_t parse_execution_context_t::populate_plain_process( cmd_args.push_back(cmd); cmd_args.insert(cmd_args.end(), args_from_cmd_expansion.begin(), args_from_cmd_expansion.end()); - argument_node_list_t arg_nodes = statement.descendants(); + + ast_args_list_t arg_nodes = get_argument_nodes(statement.args_or_redirs); end_execution_reason_t arg_result = this->expand_arguments_from_nodes(arg_nodes, &cmd_args, glob_behavior); if (arg_result != end_execution_reason_t::ok) { @@ -812,7 +869,7 @@ end_execution_reason_t parse_execution_context_t::populate_plain_process( } // The set of IO redirections that we construct for the process. - auto reason = this->determine_redirections(statement.child<1>(), &redirections); + auto reason = this->determine_redirections(statement.args_or_redirs, &redirections); if (reason != end_execution_reason_t::ok) { return reason; } @@ -832,23 +889,23 @@ end_execution_reason_t parse_execution_context_t::populate_plain_process( // Determine the list of arguments, expanding stuff. Reports any errors caused by expansion. If we // have a wildcard that could not be expanded, report the error and continue. end_execution_reason_t parse_execution_context_t::expand_arguments_from_nodes( - const argument_node_list_t &argument_nodes, wcstring_list_t *out_arguments, + const ast_args_list_t &argument_nodes, wcstring_list_t *out_arguments, globspec_t glob_behavior) { // Get all argument nodes underneath the statement. We guess we'll have that many arguments (but // may have more or fewer, if there are wildcards involved). out_arguments->reserve(out_arguments->size() + argument_nodes.size()); completion_list_t arg_expanded; - for (const auto &arg_node : argument_nodes) { + for (const ast::argument_t *arg_node : argument_nodes) { // Expect all arguments to have source. - assert(arg_node.has_source()); - const wcstring arg_str = arg_node.get_source(pstree->src); + assert(arg_node->has_source()); + const wcstring arg_str = get_source(*arg_node); // Expand this string. parse_error_list_t errors; arg_expanded.clear(); auto expand_ret = expand_string(arg_str, &arg_expanded, expand_flag::no_descriptions, ctx, &errors); - parse_error_offset_source_start(&errors, arg_node.source_range()->start); + parse_error_offset_source_start(&errors, arg_node->range.start); switch (expand_ret.result) { case expand_result_t::error: { return this->report_errors(expand_ret.status, errors); @@ -862,8 +919,8 @@ end_execution_reason_t parse_execution_context_t::expand_arguments_from_nodes( // For no_exec, ignore the error - this might work at runtime. if (no_exec()) return end_execution_reason_t::ok; // Report the unmatched wildcard error and stop processing. - return report_error(STATUS_UNMATCHED_WILDCARD, arg_node, WILDCARD_ERR_MSG, - get_source(arg_node).c_str()); + return report_error(STATUS_UNMATCHED_WILDCARD, *arg_node, WILDCARD_ERR_MSG, + get_source(*arg_node).c_str()); } break; } @@ -892,41 +949,42 @@ end_execution_reason_t parse_execution_context_t::expand_arguments_from_nodes( } end_execution_reason_t parse_execution_context_t::determine_redirections( - tnode_t node, redirection_spec_list_t *out_redirections) { + const ast::argument_or_redirection_list_t &list, redirection_spec_list_t *out_redirections) { // Get all redirection nodes underneath the statement. - while (auto redirect_node = node.next_in_list()) { - wcstring target; // file path or target fd - auto redirect = redirection_for_node(redirect_node, pstree->src, &target); + for (const ast::argument_or_redirection_t &arg_or_redir : list) { + if (!arg_or_redir.is_redirection()) continue; + const ast::redirection_t &redir_node = arg_or_redir.redirection(); - if (!redirect || !redirect->is_valid()) { + maybe_t oper = pipe_or_redir_t::from_string(get_source(redir_node.oper)); + if (!oper || !oper->is_valid()) { // TODO: figure out if this can ever happen. If so, improve this error message. - return report_error(STATUS_INVALID_ARGS, redirect_node, _(L"Invalid redirection: %ls"), - redirect_node.get_source(pstree->src).c_str()); + return report_error(STATUS_INVALID_ARGS, redir_node, _(L"Invalid redirection: %ls"), + get_source(redir_node).c_str()); } // PCA: I can't justify this skip_variables flag. It was like this when I got here. + wcstring target = get_source(redir_node.target); bool target_expanded = expand_one(target, no_exec() ? expand_flag::skip_variables : expand_flags_t{}, ctx); if (!target_expanded || target.empty()) { // TODO: Improve this error message. - return report_error(STATUS_INVALID_ARGS, redirect_node, + return report_error(STATUS_INVALID_ARGS, redir_node, _(L"Invalid redirection target: %ls"), target.c_str()); } // Make a redirection spec from the redirect token. - assert(redirect && redirect->is_valid() && "expected to have a valid redirection"); - - redirection_spec_t spec{redirect->fd, redirect->mode, std::move(target)}; + assert(oper && oper->is_valid() && "expected to have a valid redirection"); + redirection_spec_t spec{oper->fd, oper->mode, std::move(target)}; // Validate this spec. if (spec.mode == redirection_mode_t::fd && !spec.is_close() && !spec.get_target_as_fd()) { const wchar_t *fmt = _(L"Requested redirection to '%ls', which is not a valid file descriptor"); - return report_error(STATUS_INVALID_ARGS, redirect_node, fmt, spec.target.c_str()); + return report_error(STATUS_INVALID_ARGS, redir_node, fmt, spec.target.c_str()); } out_redirections->push_back(std::move(spec)); - if (redirect->stderr_merge) { + if (oper->stderr_merge) { // This was a redirect like &> which also modifies stderr. // Also redirect stderr to stdout. out_redirections->push_back(get_stderr_merge()); @@ -936,57 +994,70 @@ end_execution_reason_t parse_execution_context_t::determine_redirections( } end_execution_reason_t parse_execution_context_t::populate_not_process( - job_t *job, process_t *proc, tnode_t not_statement) { + job_t *job, process_t *proc, const ast::not_statement_t ¬_statement) { auto &flags = job->mut_flags(); flags.negate = !flags.negate; - auto optional_time = not_statement.require_get_child(); - if (optional_time.tag() == parse_optional_time_time) { + if (not_statement.time) { flags.has_time_prefix = true; if (!job->mut_flags().foreground) { return this->report_error(STATUS_INVALID_ARGS, not_statement, ERROR_TIME_BACKGROUND); } } - return this->populate_job_process( - job, proc, not_statement.require_get_child(), - not_statement.require_get_child()); + return this->populate_job_process(job, proc, not_statement.contents, not_statement.variables); } template end_execution_reason_t parse_execution_context_t::populate_block_process( - job_t *job, process_t *proc, tnode_t statement, - tnode_t specific_statement) { + job_t *job, process_t *proc, const ast::statement_t &statement, + const Type &specific_statement) { + using namespace ast; // We handle block statements by creating process_type_t::block_node, that will bounce back to // us when it's time to execute them. UNUSED(job); - static_assert(Type::token == symbol_block_statement || Type::token == symbol_if_statement || - Type::token == symbol_switch_statement, + static_assert(Type::AstType == type_t::block_statement || + Type::AstType == type_t::if_statement || + Type::AstType == type_t::switch_statement, "Invalid block process"); - assert(statement && "statement missing"); - assert(specific_statement && "specific_statement missing"); - // The set of IO redirections that we construct for the process. - // TODO: fix this ugly find_child. - auto arguments = specific_statement.template find_child(); + // Get the argument or redirections list. + // TODO: args_or_redirs should be available without resolving the statement type. + const argument_or_redirection_list_t *args_or_redirs = nullptr; + + // Upcast to permit dropping the 'template' keyword. + const node_t &ss = specific_statement; + switch (Type::AstType) { + case type_t::block_statement: + args_or_redirs = &ss.as()->args_or_redirs; + break; + case type_t::if_statement: + args_or_redirs = &ss.as()->args_or_redirs; + break; + case type_t::switch_statement: + args_or_redirs = &ss.as()->args_or_redirs; + break; + default: + DIE("Unexpected block node type"); + } + assert(args_or_redirs && "Should have args_or_redirs"); + redirection_spec_list_t redirections; - auto reason = this->determine_redirections(arguments, &redirections); + auto reason = this->determine_redirections(*args_or_redirs, &redirections); if (reason == end_execution_reason_t::ok) { proc->type = process_type_t::block_node; proc->block_node_source = pstree; - proc->internal_block_node = statement; + proc->internal_block_node = &statement; proc->set_redirection_specs(std::move(redirections)); } return reason; } end_execution_reason_t parse_execution_context_t::apply_variable_assignments( - process_t *proc, tnode_t variable_assignments, + process_t *proc, const ast::variable_assignment_list_t &variable_assignment_list, const block_t **block) { - variable_assignment_node_list_t assignment_list = - get_variable_assignment_nodes(variable_assignments); - if (assignment_list.empty()) return end_execution_reason_t::ok; + if (variable_assignment_list.empty()) return end_execution_reason_t::ok; *block = parser->push_block(block_t::variable_assignment_block()); - for (const auto &variable_assignment : assignment_list) { - const wcstring &source = variable_assignment.get_source(pstree->src); + for (const ast::variable_assignment_t &variable_assignment : variable_assignment_list) { + const wcstring &source = get_source(variable_assignment); auto equals_pos = variable_assignment_equals_pos(source); assert(equals_pos); const wcstring variable_name = source.substr(0, *equals_pos); @@ -996,8 +1067,7 @@ end_execution_reason_t parse_execution_context_t::apply_variable_assignments( // TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function auto expand_ret = expand_string(expression, &expression_expanded, expand_flag::no_descriptions, ctx, &errors); - parse_error_offset_source_start( - &errors, variable_assignment.source_range()->start + *equals_pos + 1); + parse_error_offset_source_start(&errors, variable_assignment.range.start + *equals_pos + 1); switch (expand_ret.result) { case expand_result_t::error: return this->report_errors(expand_ret.status, errors); @@ -1024,10 +1094,11 @@ end_execution_reason_t parse_execution_context_t::apply_variable_assignments( } end_execution_reason_t parse_execution_context_t::populate_job_process( - job_t *job, process_t *proc, tnode_t statement, - tnode_t variable_assignments) { + job_t *job, process_t *proc, const ast::statement_t &statement, + const ast::variable_assignment_list_t &variable_assignments) { + using namespace ast; // Get the "specific statement" which is boolean / block / if / switch / decorated. - const parse_node_t &specific_statement = statement.get_child_node<0>(); + const node_t &specific_statement = *statement.contents.contents; const block_t *block = nullptr; end_execution_reason_t result = @@ -1038,27 +1109,26 @@ end_execution_reason_t parse_execution_context_t::populate_job_process( if (result != end_execution_reason_t::ok) return result; switch (specific_statement.type) { - case symbol_not_statement: { - result = this->populate_not_process(job, proc, {&tree(), &specific_statement}); + case type_t::not_statement: { + result = + this->populate_not_process(job, proc, *specific_statement.as()); break; } - case symbol_block_statement: - result = this->populate_block_process( - job, proc, statement, tnode_t(&tree(), &specific_statement)); + case type_t::block_statement: + result = this->populate_block_process(job, proc, statement, + *specific_statement.as()); break; - case symbol_if_statement: - result = this->populate_block_process( - job, proc, statement, tnode_t(&tree(), &specific_statement)); + case type_t::if_statement: + result = this->populate_block_process(job, proc, statement, + *specific_statement.as()); break; - case symbol_switch_statement: - result = this->populate_block_process( - job, proc, statement, tnode_t(&tree(), &specific_statement)); + case type_t::switch_statement: + result = this->populate_block_process(job, proc, statement, + *specific_statement.as()); break; - case symbol_decorated_statement: { - // Get the plain statement. It will pull out the decoration itself. - tnode_t dec_stat{&tree(), &specific_statement}; - auto plain_statement = dec_stat.find_child(); - result = this->populate_plain_process(job, proc, plain_statement); + case type_t::decorated_statement: { + result = this->populate_plain_process(job, proc, + *specific_statement.as()); break; } default: { @@ -1073,47 +1143,36 @@ end_execution_reason_t parse_execution_context_t::populate_job_process( } end_execution_reason_t parse_execution_context_t::populate_job_from_job_node( - job_t *j, tnode_t job_node, const block_t *associated_block) { + job_t *j, const ast::job_t &job_node, const block_t *associated_block) { UNUSED(associated_block); // Tell the job what its command is. j->set_command(get_source(job_node)); - // We are going to construct process_t structures for every statement in the job. Get the first - // statement. - tnode_t optional_time = job_node.child<0>(); - tnode_t variable_assignments = job_node.child<1>(); - tnode_t statement = job_node.child<2>(); - + // We are going to construct process_t structures for every statement in the job. // Create processes. Each one may fail. process_list_t processes; processes.emplace_back(new process_t()); - if (optional_time.tag() == parse_optional_time_time) { + if (job_node.time) { j->mut_flags().has_time_prefix = true; - if (job_node_is_background(job_node)) { + if (job_node.bg) { return this->report_error(STATUS_INVALID_ARGS, job_node, ERROR_TIME_BACKGROUND); } } - end_execution_reason_t result = - this->populate_job_process(j, processes.back().get(), statement, variable_assignments); + end_execution_reason_t result = this->populate_job_process( + j, processes.back().get(), job_node.statement, job_node.variables); - // Construct process_ts for job continuations (pipelines), by walking the list until we hit the - // terminal (empty) job continuation. - tnode_t job_cont = job_node.child<3>(); - assert(job_cont); - while (auto pipe = job_cont.try_get_child()) { + // Construct process_ts for job continuations (pipelines). + for (const ast::job_continuation_t &jc : job_node.continuation) { if (result != end_execution_reason_t::ok) { break; } - auto variable_assignments = job_cont.require_get_child(); - auto statement = job_cont.require_get_child(); - // Handle the pipe, whose fd may not be the obvious stdout. - auto parsed_pipe = pipe_or_redir_t::from_string(get_source(pipe)); + auto parsed_pipe = pipe_or_redir_t::from_string(get_source(jc.pipe)); assert(parsed_pipe.has_value() && parsed_pipe->is_pipe && "Failed to parse valid pipe"); if (!parsed_pipe->is_valid()) { - result = report_error(STATUS_INVALID_ARGS, pipe, ILLEGAL_FD_ERR_MSG, - get_source(pipe).c_str()); + result = report_error(STATUS_INVALID_ARGS, jc.pipe, ILLEGAL_FD_ERR_MSG, + get_source(jc.pipe).c_str()); break; } processes.back()->pipe_write_fd = parsed_pipe->fd; @@ -1127,12 +1186,7 @@ end_execution_reason_t parse_execution_context_t::populate_job_from_job_node( // Store the new process (and maybe with an error). processes.emplace_back(new process_t()); - result = - this->populate_job_process(j, processes.back().get(), statement, variable_assignments); - - // Get the next continuation. - job_cont = job_cont.require_get_child(); - assert(job_cont); + result = this->populate_job_process(j, processes.back().get(), jc.statement, jc.variables); } // Inform our processes of who is first and last @@ -1158,7 +1212,7 @@ static bool remove_job(parser_t &parser, job_t *job) { return false; } -end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t job_node, +end_execution_reason_t parse_execution_context_t::run_1_job(const ast::job_t &job_node, const block_t *associated_block) { if (auto ret = check_end_execution()) { return *ret; @@ -1180,7 +1234,7 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t job_ scoped_push saved_eval_level(&parser->eval_level, parser->eval_level + 1); // Save the node index. - scoped_push> saved_node(&executing_job_node, job_node); + scoped_push saved_node(&executing_job_node, &job_node); // Profiling support. long long start_time = 0, parse_time = 0, exec_time = 0; @@ -1194,34 +1248,33 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t job_ // However, if there are no redirections, then we can just jump into the block directly, which // is significantly faster. if (job_is_simple_block(job_node)) { - tnode_t optional_time = job_node.child<0>(); + bool do_time = job_node.time.has_value(); // If no-exec has been given, there is nothing to time. - cleanup_t timer = push_timer(optional_time.tag() == parse_optional_time_time && !no_exec()); - tnode_t variable_assignments = job_node.child<1>(); + cleanup_t timer = push_timer(do_time && !no_exec()); const block_t *block = nullptr; end_execution_reason_t result = - this->apply_variable_assignments(nullptr, variable_assignments, &block); + this->apply_variable_assignments(nullptr, job_node.variables, &block); cleanup_t scope([&]() { if (block) parser->pop_block(block); }); - tnode_t statement = job_node.child<2>(); - const parse_node_t &specific_statement = statement.get_child_node<0>(); - assert(specific_statement_type_is_redirectable_block(specific_statement)); + const ast::node_t *specific_statement = job_node.statement.contents.get(); + assert(specific_statement_type_is_redirectable_block(*specific_statement)); if (result == end_execution_reason_t::ok) { - switch (specific_statement.type) { - case symbol_block_statement: { - result = - this->run_block_statement({&tree(), &specific_statement}, associated_block); + switch (specific_statement->type) { + case ast::type_t::block_statement: { + result = this->run_block_statement( + *specific_statement->as(), associated_block); break; } - case symbol_if_statement: { - result = - this->run_if_statement({&tree(), &specific_statement}, associated_block); + case ast::type_t::if_statement: { + result = this->run_if_statement(*specific_statement->as(), + associated_block); break; } - case symbol_switch_statement: { - result = this->run_switch_statement({&tree(), &specific_statement}); + case ast::type_t::switch_statement: { + result = this->run_switch_statement( + *specific_statement->as()); break; } default: { @@ -1240,8 +1293,8 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t job_ profile_item->level = parser->eval_level; profile_item->parse = 0; profile_item->exec = static_cast(exec_time - start_time); - profile_item->cmd = profiling_cmd_name_for_redirectable_block( - specific_statement, this->tree(), this->pstree->src); + profile_item->cmd = + profiling_cmd_name_for_redirectable_block(*specific_statement, *this->pstree); profile_item->skipped = false; } @@ -1258,7 +1311,7 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t job_ job_t::properties_t props{}; props.wants_terminal = wants_job_control && !ld.is_event; - props.initial_background = job_node_is_background(job_node); + props.initial_background = job_node.bg.has_value(); props.skip_notification = ld.is_subshell || ld.is_block || ld.is_event || !parser->is_interactive(); props.from_event_handler = ld.is_event; @@ -1329,29 +1382,36 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t job_ } end_execution_reason_t parse_execution_context_t::run_job_conjunction( - tnode_t job_expr, const block_t *associated_block) { - end_execution_reason_t result = end_execution_reason_t::ok; - tnode_t cursor = job_expr; - // continuation is the parent of the cursor - tnode_t continuation; - while (cursor) { - if (auto reason = check_end_execution()) { - result = *reason; - break; + const ast::job_conjunction_t &job_expr, const block_t *associated_block) { + if (auto reason = check_end_execution()) { + return *reason; + } + end_execution_reason_t result = run_1_job(job_expr.job, associated_block); + + for (const ast::job_conjunction_continuation_t &jc : job_expr.continuations) { + if (result != end_execution_reason_t::ok) { + return result; } + if (auto reason = check_end_execution()) { + return *reason; + } + // Check the conjunction type. bool skip = false; - if (continuation) { - // Check the conjunction type. - parse_job_decoration_t conj = bool_statement_type(continuation); - assert((conj == parse_job_decoration_and || conj == parse_job_decoration_or) && - "Unexpected conjunction"); - skip = should_skip(conj); + switch (jc.conjunction.type) { + case parse_token_type_andand: + // AND. Skip if the last job failed. + skip = parser->get_last_status() != 0; + break; + case parse_token_type_oror: + // OR. Skip if the last job succeeded. + skip = parser->get_last_status() == 0; + break; + default: + DIE("Unexpected job conjunction type"); } if (!skip) { - result = run_1_job(cursor.child<0>(), associated_block); + result = run_1_job(jc.job, associated_block); } - continuation = cursor.child<1>(); - cursor = continuation.try_get_child(); } return result; } @@ -1369,66 +1429,86 @@ bool parse_execution_context_t::should_skip(parse_job_decoration_t type) const { } } -template -end_execution_reason_t parse_execution_context_t::run_job_list(tnode_t job_list, - const block_t *associated_block) { - // We handle both job_list and andor_job_list uniformly. - static_assert(Type::token == symbol_job_list || Type::token == symbol_andor_job_list, - "Not a job list"); - - end_execution_reason_t result = end_execution_reason_t::ok; - while (auto job_conj = job_list.template next_in_list()) { - if (auto reason = check_end_execution()) { - result = *reason; - break; - } - - // Maybe skip the job if it has a leading and/or. - // Skipping is treated as success. - if (should_skip(get_decorator(job_conj))) { - result = end_execution_reason_t::ok; - } else { - result = this->run_job_conjunction(job_conj, associated_block); +end_execution_reason_t parse_execution_context_t::test_and_run_1_job_conjunction( + const ast::job_conjunction_t &jc, const block_t *associated_block) { + // Test this job conjunction if it has an 'and' or 'or' decorator. + // If it passes, then run it. + if (auto reason = check_end_execution()) { + return *reason; + } + // Maybe skip the job if it has a leading and/or. + bool skip = false; + if (jc.decorator.has_value()) { + switch (jc.decorator->kw) { + case parse_keyword_t::kw_and: + // AND. Skip if the last job failed. + skip = parser->get_last_status() != 0; + break; + case parse_keyword_t::kw_or: + // OR. Skip if the last job succeeded. + skip = parser->get_last_status() == 0; + break; + default: + DIE("Unexpected keyword"); } } + // Skipping is treated as success. + if (skip) { + return end_execution_reason_t::ok; + } else { + return this->run_job_conjunction(jc, associated_block); + } +} +end_execution_reason_t parse_execution_context_t::run_job_list(const ast::job_list_t &job_list_node, + const block_t *associated_block) { + auto result = end_execution_reason_t::ok; + for (const ast::job_conjunction_t &jc : job_list_node) { + result = test_and_run_1_job_conjunction(jc, associated_block); + } // Returns the result of the last job executed or skipped. return result; } -end_execution_reason_t parse_execution_context_t::eval_node(tnode_t statement, +end_execution_reason_t parse_execution_context_t::run_job_list( + const ast::andor_job_list_t &job_list_node, const block_t *associated_block) { + auto result = end_execution_reason_t::ok; + for (const ast::andor_job_t &aoj : job_list_node) { + result = test_and_run_1_job_conjunction(aoj.job, associated_block); + } + // Returns the result of the last job executed or skipped. + return result; +} + +end_execution_reason_t parse_execution_context_t::eval_node(const ast::statement_t &statement, const block_t *associated_block) { - assert(statement && "Empty node in eval_node"); - assert(statement.matches_node_tree(tree()) && "statement has unexpected tree"); + // Note we only expect block-style statements here. No not statements. enum end_execution_reason_t status = end_execution_reason_t::ok; - if (auto block = statement.try_get_child()) { - status = this->run_block_statement(block, associated_block); - } else if (auto ifstat = statement.try_get_child()) { - status = this->run_if_statement(ifstat, associated_block); - } else if (auto switchstat = statement.try_get_child()) { - status = this->run_switch_statement(switchstat); + const ast::node_t *contents = statement.contents.get(); + if (const auto *block = contents->try_as()) { + status = this->run_block_statement(*block, associated_block); + } else if (const auto *ifstat = contents->try_as()) { + status = this->run_if_statement(*ifstat, associated_block); + } else if (const auto *switchstat = contents->try_as()) { + status = this->run_switch_statement(*switchstat); } else { - FLOGF(error, L"Unexpected node %ls found in %s", statement.node()->describe().c_str(), + FLOGF(error, L"Unexpected node %ls found in %s", statement.describe().c_str(), __FUNCTION__); abort(); } return status; } -end_execution_reason_t parse_execution_context_t::eval_node(tnode_t job_list, +end_execution_reason_t parse_execution_context_t::eval_node(const ast::job_list_t &job_list, const block_t *associated_block) { - // Apply this block IO for the duration of this function. - assert(job_list && "Empty node in eval_node"); - assert(job_list.matches_node_tree(tree()) && "job_list has unexpected tree"); assert(associated_block && "Null block"); // Check for infinite recursion: a function which immediately calls itself.. wcstring func_name; - auto infinite_recursive_node = - this->infinite_recursive_statement_in_job_list(job_list, &func_name); - if (infinite_recursive_node) { + if (const auto *infinite_recursive_node = + this->infinite_recursive_statement_in_job_list(job_list, &func_name)) { // We have an infinite recursion. - return this->report_error(STATUS_CMD_ERROR, infinite_recursive_node, + return this->report_error(STATUS_CMD_ERROR, *infinite_recursive_node, INFINITE_FUNC_RECURSION_ERR_MSG, func_name.c_str()); } @@ -1439,14 +1519,14 @@ end_execution_reason_t parse_execution_context_t::eval_node(tnode_t return this->run_job_list(job_list, associated_block); } -int parse_execution_context_t::line_offset_of_node(tnode_t node) { +int parse_execution_context_t::line_offset_of_node(const ast::job_t *node) { // If we're not executing anything, return -1. if (!node) { return -1; } // If for some reason we're executing a node without source, return -1. - auto range = node.source_range(); + auto range = node->try_source_range(); if (!range) { return -1; } @@ -1501,7 +1581,7 @@ int parse_execution_context_t::get_current_line_number() { int parse_execution_context_t::get_current_source_offset() const { int result = -1; if (executing_job_node) { - if (auto range = executing_job_node.source_range()) { + if (auto range = executing_job_node->try_source_range()) { result = static_cast(range->start); } } diff --git a/src/parse_execution.h b/src/parse_execution.h index 8ac778a0f..fe91ba902 100644 --- a/src/parse_execution.h +++ b/src/parse_execution.h @@ -4,6 +4,7 @@ #include +#include "ast.h" #include "common.h" #include "io.h" #include "parse_constants.h" @@ -38,7 +39,7 @@ class parse_execution_context_t { const operation_context_t &ctx; // The currently executing job node, used to indicate the line number. - tnode_t executing_job_node{}; + const ast::job_t *executing_job_node{}; // Cached line number information. size_t cached_lineno_offset = 0; @@ -59,88 +60,94 @@ class parse_execution_context_t { // Report an error, setting $status to \p status. Always returns // 'end_execution_reason_t::error'. - end_execution_reason_t report_error(int status, const parse_node_t &node, const wchar_t *fmt, + end_execution_reason_t report_error(int status, const ast::node_t &node, const wchar_t *fmt, ...) const; end_execution_reason_t report_errors(int status, const parse_error_list_t &error_list) const; /// Command not found support. end_execution_reason_t handle_command_not_found(const wcstring &cmd, - tnode_t statement, + const ast::decorated_statement_t &statement, int err_code); // Utilities - wcstring get_source(const parse_node_t &node) const; - tnode_t infinite_recursive_statement_in_job_list( - tnode_t job_list, wcstring *out_func_name) const; + wcstring get_source(const ast::node_t &node) const; + const ast::decorated_statement_t *infinite_recursive_statement_in_job_list( + const ast::job_list_t &job_list, wcstring *out_func_name) const; // Expand a command which may contain variables, producing an expand command and possibly // arguments. Prints an error message on error. - end_execution_reason_t expand_command(tnode_t statement, + end_execution_reason_t expand_command(const ast::decorated_statement_t &statement, wcstring *out_cmd, wcstring_list_t *out_args) const; /// Return whether we should skip a job with the given bool statement type. bool should_skip(parse_job_decoration_t type) const; /// Indicates whether a job is a simple block (one block, no redirections). - bool job_is_simple_block(tnode_t job) const; + bool job_is_simple_block(const ast::job_t &job) const; - enum process_type_t process_type_for_command(tnode_t statement, + enum process_type_t process_type_for_command(const ast::decorated_statement_t &statement, const wcstring &cmd) const; end_execution_reason_t apply_variable_assignments( - process_t *proc, tnode_t variable_assignments, + process_t *proc, const ast::variable_assignment_list_t &variable_assignments, const block_t **block); // These create process_t structures from statements. end_execution_reason_t populate_job_process( - job_t *job, process_t *proc, tnode_t statement, - tnode_t variable_assignments); + job_t *job, process_t *proc, const ast::statement_t &statement, + const ast::variable_assignment_list_t &variable_assignments_list_t); end_execution_reason_t populate_not_process(job_t *job, process_t *proc, - tnode_t not_statement); + const ast::not_statement_t ¬_statement); end_execution_reason_t populate_plain_process(job_t *job, process_t *proc, - tnode_t statement); + const ast::decorated_statement_t &statement); template end_execution_reason_t populate_block_process(job_t *job, process_t *proc, - tnode_t statement, - tnode_t specific_statement); + const ast::statement_t &statement, + const Type &specific_statement); // These encapsulate the actual logic of various (block) statements. - end_execution_reason_t run_block_statement(tnode_t statement, + end_execution_reason_t run_block_statement(const ast::block_statement_t &statement, const block_t *associated_block); - end_execution_reason_t run_for_statement(tnode_t header, - tnode_t contents); - end_execution_reason_t run_if_statement(tnode_t statement, + end_execution_reason_t run_for_statement(const ast::for_header_t &header, + const ast::job_list_t &contents); + end_execution_reason_t run_if_statement(const ast::if_statement_t &statement, const block_t *associated_block); - end_execution_reason_t run_switch_statement(tnode_t statement); - end_execution_reason_t run_while_statement(tnode_t header, - tnode_t contents, + end_execution_reason_t run_switch_statement(const ast::switch_statement_t &statement); + end_execution_reason_t run_while_statement(const ast::while_header_t &header, + const ast::job_list_t &contents, const block_t *associated_block); - end_execution_reason_t run_function_statement(tnode_t statement, - tnode_t header); - end_execution_reason_t run_begin_statement(tnode_t contents); + end_execution_reason_t run_function_statement(const ast::block_statement_t &statement, + const ast::function_header_t &header); + end_execution_reason_t run_begin_statement(const ast::job_list_t &contents); enum globspec_t { failglob, nullglob }; - using argument_node_list_t = std::vector>; - end_execution_reason_t expand_arguments_from_nodes(const argument_node_list_t &argument_nodes, + using ast_args_list_t = std::vector; + + static ast_args_list_t get_argument_nodes(const ast::argument_list_t &args); + static ast_args_list_t get_argument_nodes(const ast::argument_or_redirection_list_t &args); + + end_execution_reason_t expand_arguments_from_nodes(const ast_args_list_t &argument_nodes, wcstring_list_t *out_arguments, globspec_t glob_behavior); // Determines the list of redirections for a node. - end_execution_reason_t determine_redirections( - tnode_t node, - redirection_spec_list_t *out_redirections); + end_execution_reason_t determine_redirections(const ast::argument_or_redirection_list_t &list, + redirection_spec_list_t *out_redirections); - end_execution_reason_t run_1_job(tnode_t job, const block_t *associated_block); - end_execution_reason_t run_job_conjunction(tnode_t job_expr, + end_execution_reason_t run_1_job(const ast::job_t &job, const block_t *associated_block); + end_execution_reason_t test_and_run_1_job_conjunction(const ast::job_conjunction_t &jc, + const block_t *associated_block); + end_execution_reason_t run_job_conjunction(const ast::job_conjunction_t &job_expr, const block_t *associated_block); - template - end_execution_reason_t run_job_list(tnode_t job_list_node, + end_execution_reason_t run_job_list(const ast::job_list_t &job_list_node, const block_t *associated_block); - end_execution_reason_t populate_job_from_job_node(job_t *j, tnode_t job_node, + end_execution_reason_t run_job_list(const ast::andor_job_list_t &job_list_node, + const block_t *associated_block); + end_execution_reason_t populate_job_from_job_node(job_t *j, const ast::job_t &job_node, const block_t *associated_block); // Returns the line number of the node. Not const since it touches cached_lineno_offset. - int line_offset_of_node(tnode_t node); + int line_offset_of_node(const ast::job_t *node); int line_offset_of_character_at_offset(size_t offset); public: @@ -159,14 +166,14 @@ class parse_execution_context_t { /// Returns the source string. const wcstring &get_source() const { return pstree->src; } - /// Return the parse tree. - const parse_node_tree_t &tree() const { return pstree->tree; } + /// Return the parsed ast. + const ast::ast_t &ast() const { return *pstree->ast; } /// Start executing at the given node. Returns 0 if there was no error, 1 if there was an /// error. - end_execution_reason_t eval_node(tnode_t statement, + end_execution_reason_t eval_node(const ast::statement_t &statement, const block_t *associated_block); - end_execution_reason_t eval_node(tnode_t job_list, + end_execution_reason_t eval_node(const ast::job_list_t &job_list, const block_t *associated_block); }; diff --git a/src/parse_tree.cpp b/src/parse_tree.cpp index b3eadcd8d..8337800b7 100644 --- a/src/parse_tree.cpp +++ b/src/parse_tree.cpp @@ -1214,11 +1214,19 @@ const parse_node_t *parse_node_tree_t::get_child(const parse_node_t &parent, nod return result; } +parsed_source_t::parsed_source_t(wcstring s, ast::ast_t &&ast) + : src(std::move(s)), ast(make_unique(std::move(ast))) {} + +parsed_source_t::~parsed_source_t() = default; + parsed_source_ref_t parse_source(wcstring src, parse_tree_flags_t flags, parse_error_list_t *errors) { - parse_node_tree_t tree; - if (!parse_tree_from_string(src, flags, &tree, errors, symbol_job_list)) return {}; - return std::make_shared(std::move(src), std::move(tree)); + using namespace ast; + ast_t ast = ast_t::parse(src, flags, errors); + if (ast.errored() && !(flags & parse_flag_continue_after_error)) { + return nullptr; + } + return std::make_shared(std::move(src), std::move(ast)); } const parse_node_t &parse_node_tree_t::find_child(const parse_node_t &parent, diff --git a/src/parse_tree.h b/src/parse_tree.h index 8f8d54f74..5e10d9e1a 100644 --- a/src/parse_tree.h +++ b/src/parse_tree.h @@ -206,19 +206,26 @@ bool parse_tree_from_string(const wcstring &str, parse_tree_flags_t flags, parse_node_tree_t *output, parse_error_list_t *errors, parse_token_type_t goal = symbol_job_list); +namespace ast { +class ast_t; +} + /// A type wrapping up a parse tree and the original source behind it. struct parsed_source_t { wcstring src; - parse_node_tree_t tree; + std::unique_ptr ast; - parsed_source_t(wcstring s, parse_node_tree_t t) : src(std::move(s)), tree(std::move(t)) {} + parsed_source_t(wcstring s, ast::ast_t &&ast); + ~parsed_source_t(); parsed_source_t(const parsed_source_t &) = delete; void operator=(const parsed_source_t &) = delete; - parsed_source_t(parsed_source_t &&) = default; - parsed_source_t &operator=(parsed_source_t &&) = default; + parsed_source_t(parsed_source_t &&) = delete; + parsed_source_t &operator=(parsed_source_t &&) = delete; }; + /// Return a shared pointer to parsed_source_t, or null on failure. +/// If parse_flag_continue_after_error is not set, this will return null on any error. using parsed_source_ref_t = std::shared_ptr; parsed_source_ref_t parse_source(wcstring src, parse_tree_flags_t flags, parse_error_list_t *errors); diff --git a/src/parse_util.cpp b/src/parse_util.cpp index 8a74c905c..4ee438946 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -1311,9 +1311,9 @@ parser_test_error_bits_t parse_util_detect_errors(const wcstring &buff_src, *out_errors = std::move(parse_errors); } + // \return the ast to our caller if requested. if (out_pstree != nullptr) { - // TODO: legacy - *out_pstree = parse_source(buff_src, parse_flags, nullptr); + *out_pstree = std::make_shared(buff_src, std::move(ast)); } return res; diff --git a/src/parser.cpp b/src/parser.cpp index c48ec3868..12f670548 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -656,10 +656,10 @@ eval_res_t parser_t::eval(const wcstring &cmd, const io_chain_t &io, eval_res_t parser_t::eval(const parsed_source_ref_t &ps, const io_chain_t &io, const job_group_ref_t &job_group, enum block_type_t block_type) { assert(block_type == block_type_t::top || block_type == block_type_t::subst); - if (!ps->tree.empty()) { - // Execute the first node. - tnode_t start{&ps->tree, &ps->tree.front()}; - return this->eval_node(ps, start, io, job_group, block_type); + const auto *job_list = ps->ast->top()->as(); + if (!job_list->empty()) { + // Execute the top job list. + return this->eval_node(ps, *job_list, io, job_group, block_type); } else { auto status = proc_status_t::from_exit_code(get_last_status()); bool break_expand = false; @@ -669,11 +669,11 @@ eval_res_t parser_t::eval(const parsed_source_ref_t &ps, const io_chain_t &io, } template -eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, tnode_t node, +eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node, const io_chain_t &block_io, const job_group_ref_t &job_group, block_type_t block_type) { static_assert( - std::is_same::value || std::is_same::value, + std::is_same::value || std::is_same::value, "Unexpected node type"); // Handle cancellation requests. If our block stack is currently empty, then we already did // successfully cancel (or there was nothing to cancel); clear the flag. If our block stack is @@ -725,9 +725,9 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, tnode_t node, } // Explicit instantiations. TODO: use overloads instead? -template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, tnode_t, +template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, const ast::statement_t &, const io_chain_t &, const job_group_ref_t &, block_type_t); -template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, tnode_t, +template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, const ast::job_list_t &, const io_chain_t &, const job_group_ref_t &, block_type_t); void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &errors, diff --git a/src/parser.h b/src/parser.h index be8b001aa..4987c706d 100644 --- a/src/parser.h +++ b/src/parser.h @@ -300,9 +300,9 @@ class parser_t : public std::enable_shared_from_this { block_type_t block_type = block_type_t::top); /// Evaluates a node. - /// The node type must be grammar::statement or grammar::job_list. + /// The node type must be ast_t::statement_t or ast::job_list_t. template - eval_res_t eval_node(const parsed_source_ref_t &ps, tnode_t node, const io_chain_t &block_io, + eval_res_t eval_node(const parsed_source_ref_t &ps, const T &node, const io_chain_t &block_io, const job_group_ref_t &job_group, block_type_t block_type = block_type_t::top); diff --git a/src/proc.h b/src/proc.h index eaae95e1c..c17dc216a 100644 --- a/src/proc.h +++ b/src/proc.h @@ -44,6 +44,10 @@ enum class job_control_t { none, }; +namespace ast { +struct statement_t; +} + /// A proc_status_t is a value type that encapsulates logic around exited vs stopped vs signaled, /// etc. class proc_status_t { @@ -261,10 +265,10 @@ class process_t { /// Type of process. process_type_t type{process_type_t::external}; - /// For internal block processes only, the node offset of the statement. + /// For internal block processes only, the node of the statement. /// This is always either block, ifs, or switchs, never boolean or decorated. parsed_source_ref_t block_node_source{}; - tnode_t internal_block_node{}; + const ast::statement_t *internal_block_node{}; struct concrete_assignment { wcstring variable_name;