Skip to content

Commit

Permalink
Write to constant path, call, and index in rescue
Browse files Browse the repository at this point in the history
When compiling a rescue reference, you can write to a constant
path, a call, or an index. In these cases we need to compile the
prefix expression first, then handle actually doing the write/call.
  • Loading branch information
kddnewton committed Jan 12, 2024
1 parent 16624ef commit 2d9db72
Showing 1 changed file with 265 additions and 31 deletions.
296 changes: 265 additions & 31 deletions prism_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -207,21 +207,21 @@ parse_string_encoded(const pm_node_t *node, const pm_string_t *string, const pm_
}

static inline ID
parse_symbol(const uint8_t *start, const uint8_t *end, pm_parser_t *parser)
parse_symbol(const uint8_t *start, const uint8_t *end, const pm_parser_t *parser)
{
rb_encoding *enc = rb_enc_from_index(rb_enc_find_index(parser->encoding->name));
return rb_intern3((const char *) start, end - start, enc);
}

static inline ID
parse_string_symbol(const pm_string_t *string, pm_parser_t *parser)
parse_string_symbol(const pm_string_t *string, const pm_parser_t *parser)
{
const uint8_t *start = pm_string_source(string);
return parse_symbol(start, start + pm_string_length(string), parser);
}

static inline ID
parse_location_symbol(pm_location_t *location, pm_parser_t *parser)
parse_location_symbol(const pm_location_t *location, const pm_parser_t *parser)
{
return parse_symbol(location->start, location->end, parser);
}
Expand Down Expand Up @@ -326,7 +326,7 @@ pm_new_regex(pm_regular_expression_node_t * cast, const pm_parser_t * parser) {
* literal values can be compiled into a literal array.
*/
static inline VALUE
pm_static_literal_value(const pm_node_t *node, pm_scope_node_t *scope_node, pm_parser_t *parser)
pm_static_literal_value(const pm_node_t *node, const pm_scope_node_t *scope_node, const pm_parser_t *parser)
{
// Every node that comes into this function should already be marked as
// static literal. If it's not, then we have a bug somewhere.
Expand Down Expand Up @@ -1147,6 +1147,118 @@ pm_setup_args(pm_arguments_node_t *arguments_node, int *flags, struct rb_callinf
return orig_argc;
}

/**
* A callinfo struct basically mirrors the information that is going to be
* passed to a callinfo object that will be used on a send instruction. We use
* it to communicate the information between the function that derives it and
* the function that uses it.
*/
typedef struct {
int argc;
int flags;
struct rb_callinfo_kwarg *kwargs;
} pm_callinfo_t;

/**
* Derive the callinfo from the given arguments node. It assumes the pointer to
* the callinfo struct is zeroed memory.
*/
static void
pm_arguments_node_callinfo(pm_callinfo_t *callinfo, const pm_arguments_node_t *node, const pm_scope_node_t *scope_node, const pm_parser_t *parser)
{
if (node == NULL) {
if (callinfo->flags & VM_CALL_FCALL) {
callinfo->flags |= VM_CALL_VCALL;
}
} else {
const pm_node_list_t *arguments = &node->arguments;
bool has_splat = false;

for (size_t argument_index = 0; argument_index < arguments->size; argument_index++) {
const pm_node_t *argument = arguments->nodes[argument_index];

switch (PM_NODE_TYPE(argument)) {
case PM_KEYWORD_HASH_NODE: {
pm_keyword_hash_node_t *keyword_hash = (pm_keyword_hash_node_t *) argument;
size_t elements_size = keyword_hash->elements.size;

if (PM_NODE_FLAG_P(node, PM_ARGUMENTS_NODE_FLAGS_CONTAINS_KEYWORD_SPLAT)) {
for (size_t element_index = 0; element_index < elements_size; element_index++) {
const pm_node_t *element = keyword_hash->elements.nodes[element_index];

switch (PM_NODE_TYPE(element)) {
case PM_ASSOC_NODE:
callinfo->argc++;
break;
case PM_ASSOC_SPLAT_NODE:
if (elements_size > 1) callinfo->flags |= VM_CALL_KW_SPLAT_MUT;
callinfo->flags |= VM_CALL_KW_SPLAT;
break;
default:
rb_bug("Unknown type in keyword argument %s\n", pm_node_type_to_str(PM_NODE_TYPE(element)));
}
}
break;
} else if (PM_NODE_FLAG_P(keyword_hash, PM_KEYWORD_HASH_NODE_FLAGS_SYMBOL_KEYS)) {
// We need to first figure out if all elements of the
// KeywordHashNode are AssocNode nodes with symbol keys. If
// they are all symbol keys then we can pass them as keyword
// arguments.
callinfo->flags |= VM_CALL_KWARG;

callinfo->kwargs = rb_xmalloc_mul_add(elements_size, sizeof(VALUE), sizeof(struct rb_callinfo_kwarg));
callinfo->kwargs->references = 0;
callinfo->kwargs->keyword_len = (int) elements_size;

VALUE *keywords = callinfo->kwargs->keywords;
for (size_t element_index = 0; element_index < elements_size; element_index++) {
pm_assoc_node_t *assoc = (pm_assoc_node_t *) keyword_hash->elements.nodes[element_index];
keywords[element_index] = pm_static_literal_value(assoc->key, scope_node, parser);
}
} else {
// If they aren't all symbol keys then we need to construct
// a new hash and pass that as an argument.
callinfo->argc++;
callinfo->flags |= VM_CALL_KW_SPLAT;

if (elements_size > 1) {
// A new hash will be created for the keyword arguments
// in this case, so mark the method as passing mutable
// keyword splat.
callinfo->flags |= VM_CALL_KW_SPLAT_MUT;
}
}
break;
}
case PM_SPLAT_NODE: {
// Splat nodes add a splat flag and can change the way the
// arguments are loaded by combining them into a single array.
callinfo->flags |= VM_CALL_ARGS_SPLAT;
if (((pm_splat_node_t *) argument)->expression != NULL) callinfo->argc++;
has_splat = true;
break;
}
case PM_FORWARDING_ARGUMENTS_NODE: {
// Forwarding arguments indicate that a splat and a block are
// present, and increase the argument count by one.
callinfo->flags |= VM_CALL_ARGS_BLOCKARG | VM_CALL_ARGS_SPLAT;
callinfo->argc++;
break;
}
default: {
// A regular argument increases the argument count by one. If
// there is a splat and this is the last argument, then the
// argument count becomes 1 because it gets grouped into a
// single array.
callinfo->argc++;
if (has_splat && (argument_index == arguments->size - 1)) callinfo->argc = 1;
break;
}
}
}
}
}

static void
pm_compile_index_and_or_write_node(bool and_node, pm_node_t *receiver, pm_node_t *value, pm_arguments_node_t *arguments, pm_node_t *block, LINK_ANCHOR *const ret, rb_iseq_t *iseq, int lineno, const uint8_t * src, bool popped, pm_scope_node_t *scope_node, pm_parser_t *parser)
{
Expand Down Expand Up @@ -3234,6 +3346,20 @@ pm_compile_node(rb_iseq_t *iseq, const pm_node_t *node, LINK_ANCHOR *const ret,

return;
}
case PM_CALL_TARGET_NODE: {
// Call targets can be used to indirectly call a method in places like
// rescue references, for loops, and multiple assignment. In those
// circumstances, it's necessary to first compile the receiver, then to
// compile the method call itself.
//
// Therefore in the main switch case here where we're compiling a call
// target, we're only going to compile the receiver. Then wherever
// we've called into pm_compile_node when we're compiling call targets,
// we'll need to make sure we compile the method call as well.
pm_call_target_node_t *cast = (pm_call_target_node_t*) node;
PM_COMPILE_NOT_POPPED(cast->receiver);
return;
}
case PM_CASE_NODE: {
pm_case_node_t *case_node = (pm_case_node_t *)node;
bool has_predicate = case_node->predicate;
Expand Down Expand Up @@ -3731,13 +3857,23 @@ pm_compile_node(rb_iseq_t *iseq, const pm_node_t *node, LINK_ANCHOR *const ret,
}

ADD_INSN1(ret, &dummy_line_node, setconstant, child_name);
return ;
return;
}
case PM_CONSTANT_PATH_TARGET_NODE: {
pm_constant_path_target_node_t *cast = (pm_constant_path_target_node_t *)node;
// Constant path targets can be used to indirectly write a constant in
// places like rescue references, for loops, and multiple assignment. In
// those circumstances, it's necessary to first compile the parent, then
// to compile the child.
//
// Therefore in the main switch case here where we're compiling a
// constant path target, we're only going to compile the parent. Then
// wherever we've called into pm_compile_node when we're compiling
// constant path targets, we'll need to make sure we compile the child
// as well.
pm_constant_path_target_node_t *cast = (pm_constant_path_target_node_t *) node;

if (cast->parent) {
PM_COMPILE(cast->parent);
PM_COMPILE_NOT_POPPED(cast->parent);
}

return;
Expand Down Expand Up @@ -4379,6 +4515,33 @@ pm_compile_node(rb_iseq_t *iseq, const pm_node_t *node, LINK_ANCHOR *const ret,

return;
}
case PM_INDEX_TARGET_NODE: {
// Index targets can be used to indirectly call a method in places like
// rescue references, for loops, and multiple assignment. In those
// circumstances, it's necessary to first compile the receiver and
// arguments, then to compile the method call itself.
//
// Therefore in the main switch case here where we're compiling a index
// target, we're only going to compile the receiver and arguments. Then
// wherever we've called into pm_compile_node when we're compiling index
// targets, we'll need to make sure we compile the method call as well.
//
// Note that index target nodes can have blocks attached to them in the
// form of the & operator. These blocks should almost always be compiled
// _after_ the value that is being written is added to the argument
// list, so we don't compile them here. Therefore at the places where
// these nodes are handled, blocks also need to be handled.
pm_index_target_node_t *cast = (pm_index_target_node_t*) node;
PM_COMPILE_NOT_POPPED(cast->receiver);

if (cast->arguments != NULL) {
int flags;
struct rb_callinfo_kwarg *keywords = NULL;
pm_setup_args(cast->arguments, &flags, &keywords, iseq, ret, src, false, scope_node, dummy_line_node, parser);
}

return;
}
case PM_INSTANCE_VARIABLE_AND_WRITE_NODE: {
pm_instance_variable_and_write_node_t *instance_variable_and_write_node = (pm_instance_variable_and_write_node_t*) node;

Expand Down Expand Up @@ -5371,49 +5534,120 @@ pm_compile_node(rb_iseq_t *iseq, const pm_node_t *node, LINK_ANCHOR *const ret,
return;
}
case PM_RESCUE_NODE: {
LABEL *excep_match = NEW_LABEL(lineno);
LABEL *rescue_end = NEW_LABEL(lineno);

ISEQ_COMPILE_DATA(iseq)->end_label = rescue_end;

pm_rescue_node_t *rescue_node = (pm_rescue_node_t *)node;
pm_rescue_node_t *cast = (pm_rescue_node_t *) node;
iseq_set_exception_local_table(iseq);

pm_node_list_t exception_list = rescue_node->exceptions;
if (exception_list.size > 0) {
for (size_t i = 0; i < exception_list.size; i++) {
// First, establish the labels that we need to be able to jump to within
// this compilation block.
LABEL *exception_match_label = NEW_LABEL(lineno);
LABEL *rescue_end_label = NEW_LABEL(lineno);
ISEQ_COMPILE_DATA(iseq)->end_label = rescue_end_label;

// Next, compile each of the exceptions that we're going to be
// handling. For each one, we'll add instructions to check if the
// exception matches the raised one, and if it does then jump to the
// exception_match_label label. Otherwise it will fall through to the
// subsequent check. If there are no exceptions, we'll only check
// StandardError.
pm_node_list_t *exceptions = &cast->exceptions;

if (exceptions->size > 0) {
for (size_t index = 0; index < exceptions->size; index++) {
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);
PM_COMPILE(exception_list.nodes[i]);
PM_COMPILE(exceptions->nodes[index]);
ADD_INSN1(ret, &dummy_line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_RESCUE));
ADD_INSN1(ret, &dummy_line_node, branchif, excep_match);
ADD_INSN1(ret, &dummy_line_node, branchif, exception_match_label);
}
} else {
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);
ADD_INSN1(ret, &dummy_line_node, putobject, rb_eStandardError);
ADD_INSN1(ret, &dummy_line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_RESCUE));
ADD_INSN1(ret, &dummy_line_node, branchif, excep_match);
ADD_INSN1(ret, &dummy_line_node, branchif, exception_match_label);
}
ADD_INSN1(ret, &dummy_line_node, jump, rescue_end);

ADD_LABEL(ret, excep_match);
// If none of the exceptions that we are matching against matched, then
// we'll jump straight to the rescue_end_label label.
ADD_INSN1(ret, &dummy_line_node, jump, rescue_end_label);

// Here we have the exception_match_label, which is where the
// control-flow goes in the case that one of the exceptions matched.
// Here we will compile the instructions to handle the exception.
ADD_LABEL(ret, exception_match_label);
ADD_TRACE(ret, RUBY_EVENT_RESCUE);
if (rescue_node->reference) {
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);
PM_COMPILE((pm_node_t *)rescue_node->reference);
}

if (rescue_node->statements) {
PM_COMPILE((pm_node_t *)rescue_node->statements);
// If we have a reference to the exception, then we'll compile the write
// into the instruction sequence. This can look quite different
// depending on the kind of write being performed.
if (cast->reference) {
switch (PM_NODE_TYPE(cast->reference)) {
case PM_CALL_TARGET_NODE: {
// begin; rescue => Foo.bar; end
const pm_call_target_node_t *reference = (const pm_call_target_node_t *) cast->reference;
ID method_id = pm_constant_id_lookup(scope_node, reference->name);

PM_COMPILE((pm_node_t *) reference);
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);

ADD_SEND(ret, &dummy_line_node, method_id, INT2NUM(1));
ADD_INSN(ret, &dummy_line_node, pop);
break;
}
case PM_CONSTANT_PATH_TARGET_NODE: {
// begin; rescue => Foo::Bar; end
const pm_constant_path_target_node_t *reference = (const pm_constant_path_target_node_t *) cast->reference;
const pm_constant_read_node_t *constant = (const pm_constant_read_node_t *) reference->child;

PM_COMPILE((pm_node_t *) reference);
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);

ADD_INSN(ret, &dummy_line_node, swap);
ADD_INSN1(ret, &dummy_line_node, setconstant, ID2SYM(pm_constant_id_lookup(scope_node, constant->name)));
break;
}
case PM_INDEX_TARGET_NODE: {
// begin; rescue => foo[:bar]; end
const pm_index_target_node_t *reference = (const pm_index_target_node_t *) cast->reference;

pm_callinfo_t callinfo = { 0 };
pm_arguments_node_callinfo(&callinfo, reference->arguments, scope_node, parser);

PM_COMPILE((pm_node_t *) reference);
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);

if (reference->block != NULL) {
callinfo.flags |= VM_CALL_ARGS_BLOCKARG;
PM_COMPILE_NOT_POPPED((pm_node_t *) reference->block);
}

ADD_SEND_R(ret, &dummy_line_node, idASET, INT2FIX(callinfo.argc + 1), NULL, INT2FIX(callinfo.flags), callinfo.kwargs);
ADD_INSN(ret, &dummy_line_node, pop);
break;
}
default:
// Indirectly writing to a variable or constant.
ADD_GETLOCAL(ret, &dummy_line_node, LVAR_ERRINFO, 0);
PM_COMPILE((pm_node_t *) cast->reference);
break;
}
}
else {

// If we have statements to execute, we'll compile them here. Otherwise
// we'll push nil onto the stack.
if (cast->statements) {
PM_COMPILE((pm_node_t *) cast->statements);
} else {
PM_PUTNIL;
}

ADD_INSN(ret, &dummy_line_node, leave);
ADD_LABEL(ret, rescue_end);

if (rescue_node->consequent) {
PM_COMPILE((pm_node_t *)rescue_node->consequent);
// Here we'll insert the rescue_end_label label, which is jumped to if
// none of the exceptions matched. It will cause the control-flow to
// either jump to the next rescue clause or it will fall through to the
// subsequent instruction returning the raised error.
ADD_LABEL(ret, rescue_end_label);
if (cast->consequent) {
PM_COMPILE((pm_node_t *) cast->consequent);
} else {
ADD_GETLOCAL(ret, &dummy_line_node, 1, 0);
}
Expand Down

0 comments on commit 2d9db72

Please sign in to comment.