Rework 'and' and 'or' to be "job decorators"

This promotes "and" and "or" from a type of statement to "job
decorators," as a possible prefix on a job. The point is to rationalize
how they interact with && and ||.

In the new world 'and' and 'or' apply to a entire job conjunction, i.e.
they have "lower precedence." Example:

if [ $age -ge 0 ] && [ $age -le 18 ]
   or [ $age -ge 75 ] && [ $age -le 100 ]
   echo "Child or senior"
end
This commit is contained in:
ridiculousfish 2018-03-02 18:09:16 -08:00
parent e1dafeab01
commit 357d3b8c6d
12 changed files with 151 additions and 148 deletions

View File

@ -1033,7 +1033,7 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
case symbol_if_clause:
case symbol_else_clause:
case symbol_case_item:
case symbol_boolean_statement:
case symbol_not_statement:
case symbol_decorated_statement:
case symbol_if_statement: {
this->color_children(node, parse_token_type_string, highlight_spec_command);

View File

@ -19,6 +19,7 @@ enum parse_token_type_t {
symbol_job_list,
symbol_job_conjunction,
symbol_job_conjunction_continuation,
symbol_job_decorator,
symbol_job,
symbol_job_continuation,
symbol_statement,
@ -35,7 +36,7 @@ enum parse_token_type_t {
symbol_switch_statement,
symbol_case_item_list,
symbol_case_item,
symbol_boolean_statement,
symbol_not_statement,
symbol_decorated_statement,
symbol_plain_statement,
symbol_arguments_or_redirections_list,
@ -84,36 +85,9 @@ const enum_map<parse_token_type_t> token_enum_map[] = {
{parse_token_type_andand, L"parse_token_type_andand"},
{parse_token_type_oror, L"parse_token_type_oror"},
{parse_token_type_terminate, L"parse_token_type_terminate"},
{symbol_andor_job_list, L"symbol_andor_job_list"},
{symbol_argument, L"symbol_argument"},
{symbol_argument_list, L"symbol_argument_list"},
{symbol_arguments_or_redirections_list, L"symbol_arguments_or_redirections_list"},
{symbol_begin_header, L"symbol_begin_header"},
{symbol_block_header, L"symbol_block_header"},
{symbol_block_statement, L"symbol_block_statement"},
{symbol_boolean_statement, L"symbol_boolean_statement"},
{symbol_case_item, L"symbol_case_item"},
{symbol_case_item_list, L"symbol_case_item_list"},
{symbol_decorated_statement, L"symbol_decorated_statement"},
{symbol_else_clause, L"symbol_else_clause"},
{symbol_else_continuation, L"symbol_else_continuation"},
{symbol_end_command, L"symbol_end_command"},
{symbol_for_header, L"symbol_for_header"},
{symbol_freestanding_argument_list, L"symbol_freestanding_argument_list"},
{symbol_function_header, L"symbol_function_header"},
{symbol_if_clause, L"symbol_if_clause"},
{symbol_if_statement, L"symbol_if_statement"},
{symbol_job, L"symbol_job"},
{symbol_job_conjunction, L"symbol_job_conjunction"},
{symbol_job_continuation, L"symbol_job_continuation"},
{symbol_job_list, L"symbol_job_list"},
{symbol_optional_newlines, L"symbol_optional_newlines"},
{symbol_optional_background, L"symbol_optional_background"},
{symbol_plain_statement, L"symbol_plain_statement"},
{symbol_redirection, L"symbol_redirection"},
{symbol_statement, L"symbol_statement"},
{symbol_switch_statement, L"symbol_switch_statement"},
{symbol_while_header, L"symbol_while_header"},
// Define all symbols
#define ELEM(sym) {symbol_##sym, L"symbol_" #sym},
#include "parse_grammar_elements.inc"
{token_type_invalid, L"token_type_invalid"},
{token_type_invalid, NULL}};
#define token_enum_map_len (sizeof token_enum_map / sizeof *token_enum_map)
@ -165,7 +139,7 @@ enum parse_statement_decoration_t {
};
// Boolean statement types, stored in node tag.
enum parse_bool_statement_type_t { parse_bool_and, parse_bool_or, parse_bool_not };
enum parse_bool_statement_type_t { parse_bool_none, parse_bool_and, parse_bool_or };
// Whether a statement is backgrounded.
enum parse_optional_background_t { parse_no_background, parse_background };

View File

@ -107,7 +107,7 @@ tnode_t<g::plain_statement> parse_execution_context_t::infinite_recursive_statem
const wcstring &forbidden_function_name = parser->forbidden_function.back();
// Get the first job in the job list.
tnode_t<g::job> first_job = job_list.try_get_child<g::job_conjunction, 0>().child<0>();
tnode_t<g::job> first_job = job_list.try_get_child<g::job_conjunction, 1>().child<0>();
if (!first_job) {
return {};
}
@ -215,7 +215,7 @@ bool parse_execution_context_t::job_is_simple_block(tnode_t<g::job> job_node) co
return is_empty(statement.require_get_child<g::switch_statement, 0>().child<5>());
case symbol_if_statement:
return is_empty(statement.require_get_child<g::if_statement, 0>().child<3>());
case symbol_boolean_statement:
case symbol_not_statement:
case symbol_decorated_statement:
// not block statements
return false;
@ -921,33 +921,10 @@ bool parse_execution_context_t::determine_io_chain(tnode_t<g::arguments_or_redir
return !errored;
}
parse_execution_result_t parse_execution_context_t::populate_boolean_process(
job_t *job, process_t *proc, tnode_t<g::boolean_statement> bool_statement) {
// Handle a boolean statement.
bool skip_job = false;
switch (bool_statement_type(bool_statement)) {
case parse_bool_and: {
// AND. Skip if the last job failed.
skip_job = (proc_get_last_status() != 0);
break;
}
case parse_bool_or: {
// OR. Skip if the last job succeeded.
skip_job = (proc_get_last_status() == 0);
break;
}
case parse_bool_not: {
// NOT. Negate it.
job->set_flag(JOB_NEGATE, !job->get_flag(JOB_NEGATE));
break;
}
}
if (skip_job) {
return parse_execution_skipped;
}
return this->populate_job_process(job, proc,
bool_statement.require_get_child<g::statement, 1>());
parse_execution_result_t parse_execution_context_t::populate_not_process(
job_t *job, process_t *proc, tnode_t<g::not_statement> not_statement) {
job->set_flag(JOB_NEGATE, !job->get_flag(JOB_NEGATE));
return this->populate_job_process(job, proc, not_statement.child<1>());
}
template <typename Type>
@ -985,8 +962,8 @@ parse_execution_result_t parse_execution_context_t::populate_job_process(
parse_execution_result_t result = parse_execution_success;
switch (specific_statement.type) {
case symbol_boolean_statement: {
result = this->populate_boolean_process(job, proc, {&tree(), &specific_statement});
case symbol_not_statement: {
result = this->populate_not_process(job, proc, {&tree(), &specific_statement});
break;
}
case symbol_block_statement:
@ -1224,6 +1201,7 @@ parse_execution_result_t parse_execution_context_t::run_job_conjunction(
tnode_t<grammar::job_conjunction> job_expr, const block_t *associated_block) {
parse_execution_result_t result = parse_execution_success;
tnode_t<g::job_conjunction> cursor = job_expr;
// continuation is the parent of the cursor
tnode_t<g::job_conjunction_continuation> continuation;
while (cursor) {
if (should_cancel_execution(associated_block)) break;
@ -1232,13 +1210,7 @@ parse_execution_result_t parse_execution_context_t::run_job_conjunction(
// Check the conjunction type.
parse_bool_statement_type_t conj = bool_statement_type(continuation);
assert((conj == parse_bool_and || conj == parse_bool_or) && "Unexpected conjunction");
if (conj == parse_bool_and) {
// Skip if last job failed.
skip = (proc_get_last_status() != 0);
} else if (conj == parse_bool_or) {
// Skip if last job succeeded.
skip = (proc_get_last_status() == 0);
}
skip = should_skip(conj);
}
if (! skip) {
result = run_1_job(cursor.child<0>(), associated_block);
@ -1249,20 +1221,40 @@ parse_execution_result_t parse_execution_context_t::run_job_conjunction(
return result;
}
bool parse_execution_context_t::should_skip(parse_bool_statement_type_t type) const {
switch (type) {
case parse_bool_and:
// AND. Skip if the last job failed.
return proc_get_last_status() != 0;
case parse_bool_or:
// OR. Skip if the last job succeeded.
return proc_get_last_status() == 0;
default:
return false;
}
}
template <typename Type>
parse_execution_result_t parse_execution_context_t::run_job_list(tnode_t<Type> 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");
parse_execution_result_t result = parse_execution_success;
while (tnode_t<g::job_conjunction> job_expr =
job_list.template next_in_list<g::job_conjunction>()) {
while (auto job_conj = job_list.template next_in_list<g::job_conjunction>()) {
if (should_cancel_execution(associated_block)) break;
result = this->run_job_conjunction(job_expr, associated_block);
// Maybe skip the job if it has a leading and/or.
// Skipping is treated as success.
if (should_skip(get_decorator(job_conj))) {
result = parse_execution_success;
} else {
result = this->run_job_conjunction(job_conj, associated_block);
}
}
// Returns the last job executed.
// Returns the result of the last job executed or skipped.
return result;
}

View File

@ -72,6 +72,9 @@ class parse_execution_context_t {
tnode_t<grammar::job_list> job_list, wcstring *out_func_name) const;
bool is_function_context() const;
/// Return whether we should skip a job with the given bool statement type.
bool should_skip(parse_bool_statement_type_t type) const;
/// Indicates whether a job is a simple block (one block, no redirections).
bool job_is_simple_block(tnode_t<grammar::job> job) const;
@ -81,8 +84,8 @@ class parse_execution_context_t {
// These create process_t structures from statements.
parse_execution_result_t populate_job_process(job_t *job, process_t *proc,
tnode_t<grammar::statement> statement);
parse_execution_result_t populate_boolean_process(
job_t *job, process_t *proc, tnode_t<grammar::boolean_statement> bool_statement);
parse_execution_result_t populate_not_process(job_t *job, process_t *proc,
tnode_t<grammar::not_statement> not_statement);
parse_execution_result_t populate_plain_process(job_t *job, process_t *proc,
tnode_t<grammar::plain_statement> statement);

View File

@ -199,12 +199,20 @@ struct alternative {};
// A job_list is a list of job_conjunctions, separated by semicolons or newlines
DEF_ALT(job_list) {
using normal = seq<job_conjunction, job_list>;
using normal = seq<job_decorator, job_conjunction, job_list>;
using empty_line = seq<tok_end, job_list>;
using empty = grammar::empty;
ALT_BODY(job_list, normal, empty_line, empty);
};
// Job decorators are 'and' and 'or'. These apply to the whole job.
DEF_ALT(job_decorator) {
using ands = single<keyword<parse_keyword_and>>;
using ors = single<keyword<parse_keyword_or>>;
using empty = grammar::empty;
ALT_BODY(job_decorator, ands, ors, empty);
};
// A job_conjunction is a job followed by a continuation.
DEF(job_conjunction) produces_sequence<job, job_conjunction_continuation> {
BODY(job_conjunction);
@ -231,12 +239,12 @@ DEF_ALT(job_continuation) {
// A statement is a normal command, or an if / while / and etc
DEF_ALT(statement) {
using boolean = single<boolean_statement>;
using nots = single<not_statement>;
using block = single<block_statement>;
using ifs = single<if_statement>;
using switchs = single<switch_statement>;
using decorated = single<decorated_statement>;
ALT_BODY(statement, boolean, block, ifs, switchs, decorated);
ALT_BODY(statement, nots, block, ifs, switchs, decorated);
};
// A block is a conditional, loop, or begin/end
@ -304,19 +312,15 @@ DEF(function_header)
produces_sequence<keyword<parse_keyword_function>, argument, argument_list, tok_end>{
BODY(function_header)};
// A boolean statement is AND or OR or NOT
DEF_ALT(boolean_statement) {
using ands = seq<keyword<parse_keyword_and>, statement>; // foo ; and bar
using ors = seq<keyword<parse_keyword_or>, statement>; // foo ; or bar
using nots = seq<keyword<parse_keyword_not>, statement>; // not foo
ALT_BODY(boolean_statement, ands, ors, nots);
DEF(not_statement) produces_sequence<keyword<parse_keyword_not>, statement> {
BODY(not_statement);
};
// An andor_job_list is zero or more job lists, where each starts with an `and` or `or` boolean
// statement.
DEF_ALT(andor_job_list) {
using empty = grammar::empty;
using andor_job = seq<job_conjunction, andor_job_list>;
using andor_job = seq<job_decorator, job_conjunction, andor_job_list>;
using empty_line = seq<tok_end, andor_job_list>;
ALT_BODY(andor_job_list, empty, andor_job, empty_line);
};

View File

@ -1,6 +1,7 @@
// Define ELEM before including this file.
ELEM(job_list)
ELEM(job)
ELEM(job_decorator)
ELEM(job_conjunction)
ELEM(job_conjunction_continuation)
ELEM(job_continuation)
@ -18,7 +19,7 @@ ELEM(for_header)
ELEM(while_header)
ELEM(begin_header)
ELEM(function_header)
ELEM(boolean_statement)
ELEM(not_statement)
ELEM(andor_job_list)
ELEM(decorated_statement)
ELEM(plain_statement)

View File

@ -61,6 +61,26 @@ RESOLVE(job_list) {
}
}
// A job decorator is AND or OR
RESOLVE(job_decorator) {
UNUSED(token2);
switch (token1.keyword) {
case parse_keyword_and: {
*out_tag = parse_bool_and;
return production_for<ands>();
}
case parse_keyword_or: {
*out_tag = parse_bool_or;
return production_for<ors>();
}
default: {
*out_tag = parse_bool_none;
return production_for<empty>();
}
}
}
RESOLVE(job_conjunction_continuation) {
UNUSED(token2);
UNUSED(out_tag);
@ -123,10 +143,8 @@ RESOLVE(statement) {
switch (token1.type) {
case parse_token_type_string: {
switch (token1.keyword) {
case parse_keyword_and:
case parse_keyword_or:
case parse_keyword_not: {
return production_for<boolean>();
return production_for<nots>();
}
case parse_keyword_for:
case parse_keyword_while:
@ -260,27 +278,6 @@ RESOLVE(block_header) {
}
}
// A boolean statement is AND or OR or NOT.
RESOLVE(boolean_statement) {
UNUSED(token2);
switch (token1.keyword) {
case parse_keyword_and: {
*out_tag = parse_bool_and;
return production_for<ands>();
}
case parse_keyword_or: {
*out_tag = parse_bool_or;
return production_for<ors>();
}
case parse_keyword_not: {
*out_tag = parse_bool_not;
return production_for<nots>();
}
default: { return NO_PRODUCTION; }
}
}
RESOLVE(decorated_statement) {
// If this is e.g. 'command --help' then the command is 'command' and not a decoration. If the

View File

@ -1072,17 +1072,15 @@ static bool detect_errors_in_backgrounded_job(tnode_t<grammar::job> job,
"Expected first job to be the node we found");
(void)first_jconj;
// Try getting the next job as a boolean statement.
tnode_t<g::job> next_job = jlist.next_in_list<g::job_conjunction>().child<0>();
tnode_t<g::statement> next_stmt = next_job.child<0>();
if (auto bool_stmt = next_stmt.try_get_child<g::boolean_statement, 0>()) {
// Try getting the next job's decorator.
if (auto next_job_dec = jlist.next_in_list<g::job_decorator>()) {
// The next job is indeed a boolean statement.
parse_bool_statement_type_t bool_type = bool_statement_type(bool_stmt);
if (bool_type == parse_bool_and) { // this is not allowed
errored = append_syntax_error(parse_errors, bool_stmt.source_range()->start,
parse_bool_statement_type_t bool_type = bool_statement_type(next_job_dec);
if (bool_type == parse_bool_and) {
errored = append_syntax_error(parse_errors, next_job_dec.source_range()->start,
BOOL_AFTER_BACKGROUND_ERROR_MSG, L"and");
} else if (bool_type == parse_bool_or) { // this is not allowed
errored = append_syntax_error(parse_errors, bool_stmt.source_range()->start,
} else if (bool_type == parse_bool_or) {
errored = append_syntax_error(parse_errors, next_job_dec.source_range()->start,
BOOL_AFTER_BACKGROUND_ERROR_MSG, L"or");
}
}
@ -1100,7 +1098,8 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src,
// In a few places below, we want to know if we are in a pipeline.
tnode_t<statement> st = pst.try_get_parent<decorated_statement>().try_get_parent<statement>();
const bool is_in_pipeline = statement_is_in_pipeline(st, true /* count first */);
pipeline_position_t pipe_pos = get_pipeline_position(st);
bool is_in_pipeline = (pipe_pos != pipeline_position_t::none);
// We need to know the decoration.
const enum parse_statement_decoration_t decoration = get_decoration(pst);
@ -1110,6 +1109,19 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src,
errored = append_syntax_error(parse_errors, source_start, EXEC_ERR_MSG, L"exec");
}
// This is a somewhat stale check that 'and' and 'or' are not in pipelines, except at the
// beginning. We can't disallow them as commands entirely because we need to support 'and
// --help', etc.
if (pipe_pos == pipeline_position_t::subsequent) {
// check if our command is 'and' or 'or'. This is very clumsy; we don't catch e.g. quoted
// commands.
wcstring command = pst.child<0>().get_source(buff_src);
if (command == L"and" || command == L"or") {
errored =
append_syntax_error(parse_errors, source_start, EXEC_ERR_MSG, command.c_str());
}
}
if (maybe_t<wcstring> mcommand = command_for_plain_statement(pst, buff_src)) {
wcstring command = std::move(*mcommand);
// Check that we can expand the command.
@ -1254,16 +1266,9 @@ parser_test_error_bits_t parse_util_detect_errors(const wcstring &buff_src,
has_unclosed_block = true;
} else if (node.type == symbol_statement && !node.has_source()) {
// Check for a statement without source in a pipeline, i.e. unterminated pipeline.
has_unclosed_pipe |= statement_is_in_pipeline({&node_tree, &node}, false);
} else if (node.type == symbol_boolean_statement) {
// 'or' and 'and' can be in a pipeline, as long as they're first.
tnode_t<g::boolean_statement> gbs{&node_tree, &node};
parse_bool_statement_type_t type = bool_statement_type(gbs);
if ((type == parse_bool_and || type == parse_bool_or) &&
statement_is_in_pipeline(gbs.try_get_parent<g::statement>(),
false /* don't count first */)) {
errored = append_syntax_error(&parse_errors, node.source_start, EXEC_ERR_MSG,
(type == parse_bool_and) ? L"and" : L"or");
auto pipe_pos = get_pipeline_position({&node_tree, &node});
if (pipe_pos != pipeline_position_t::none) {
has_unclosed_pipe = true;
}
} else if (node.type == symbol_argument) {
tnode_t<g::argument> arg{&node_tree, &node};

View File

@ -46,7 +46,7 @@ enum parse_statement_decoration_t get_decoration(tnode_t<grammar::plain_statemen
return decoration;
}
enum parse_bool_statement_type_t bool_statement_type(tnode_t<grammar::boolean_statement> stmt) {
enum parse_bool_statement_type_t bool_statement_type(tnode_t<grammar::job_decorator> stmt) {
return static_cast<parse_bool_statement_type_t>(stmt.tag());
}
@ -111,24 +111,35 @@ bool job_node_is_background(tnode_t<grammar::job> job) {
return bg.tag() == parse_background;
}
bool statement_is_in_pipeline(tnode_t<grammar::statement> st, bool include_first) {
parse_bool_statement_type_t get_decorator(tnode_t<grammar::job_conjunction> conj) {
using namespace grammar;
tnode_t<job_decorator> dec;
// We have two possible parents: job_list and andor_job_list.
if (auto p = conj.try_get_parent<job_list>()) {
dec = p.require_get_child<job_decorator, 0>();
} else if (auto p = conj.try_get_parent<andor_job_list>()) {
dec = p.require_get_child<job_decorator, 0>();
}
// note this returns 0 (none) if dec is empty.
return bool_statement_type(dec);
}
pipeline_position_t get_pipeline_position(tnode_t<grammar::statement> st) {
using namespace grammar;
if (!st) {
return false;
return pipeline_position_t::none;
}
// If we're part of a job continuation, we're definitely in a pipeline.
if (st.try_get_parent<job_continuation>()) {
return true;
return pipeline_position_t::subsequent;
}
// If include_first is set, check if we're the beginning of a job, and if so, whether that job
// Check if we're the beginning of a job, and if so, whether that job
// has a non-empty continuation.
if (include_first) {
tnode_t<job_continuation> jc = st.try_get_parent<job>().child<1>();
if (jc.try_get_child<statement, 2>()) {
return true;
}
tnode_t<job_continuation> jc = st.try_get_parent<job>().child<1>();
if (jc.try_get_child<statement, 2>()) {
return pipeline_position_t::first;
}
return false;
return pipeline_position_t::none;
}

View File

@ -234,7 +234,7 @@ maybe_t<wcstring> command_for_plain_statement(tnode_t<grammar::plain_statement>
parse_statement_decoration_t get_decoration(tnode_t<grammar::plain_statement> stmt);
/// Return the type for a boolean statement.
enum parse_bool_statement_type_t bool_statement_type(tnode_t<grammar::boolean_statement> stmt);
enum parse_bool_statement_type_t bool_statement_type(tnode_t<grammar::job_decorator> stmt);
enum parse_bool_statement_type_t bool_statement_type(tnode_t<grammar::job_conjunction_continuation> stmt);
@ -253,9 +253,19 @@ arguments_node_list_t get_argument_nodes(tnode_t<grammar::arguments_or_redirecti
/// Return whether the given job is background because it has a & symbol.
bool job_node_is_background(tnode_t<grammar::job>);
/// Return whether the statement is part of a pipeline. If include_first is set, the first command
/// in a pipeline is considered part of it; otherwise only the second or additional commands are.
bool statement_is_in_pipeline(tnode_t<grammar::statement> st, bool include_first);
/// If the conjunction is has a decorator (and/or), return it; otherwise return none. This only
/// considers the leading conjunction, e.g. in `and true || false` only the 'true' conjunction will
/// return 'and'.
parse_bool_statement_type_t get_decorator(tnode_t<grammar::job_conjunction>);
/// Return whether the statement is part of a pipeline.
/// This doesn't detect e.g. pipelines involving our parent's block statements.
enum class pipeline_position_t {
none, // not part of a pipeline
first, // first command in a pipeline
subsequent // second or further command in a pipeline
};
pipeline_position_t get_pipeline_position(tnode_t<grammar::statement> st);
/// Check whether an argument_list is a root list.
inline bool argument_list_is_root(tnode_t<grammar::argument_list> list) {

View File

@ -20,7 +20,7 @@ set beta 0
set gamma 0
set delta 0
while [ $alpha -lt 2 ] && [ $beta -lt 3 ]
and [ $gamma -lt 4 ] || [ $delta -lt 5 ]
or [ $gamma -lt 4 ] || [ $delta -lt 5 ]
echo $alpha $beta $gamma
set alpha ( math $alpha + 1 )
set beta ( math $beta + 1 )
@ -31,3 +31,8 @@ end
logmsg "Complex scenarios"
begin; echo 1 ; false ; end || begin ; echo 2 && echo 3 ; end
if false && true
or not false
echo 4
end

View File

@ -28,3 +28,4 @@ if test 4 ok
1
2
3
4