Skip to content

stage2: remove operand from return instruction #5951

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

Vexu
Copy link
Member

@Vexu Vexu commented Jul 29, 2020

Removing the operand from the return instruction has multiple benefits:

Copy link
Member

@andrewrk andrewrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the goal of removing the operand from the return IR instruction but the semantics need a bit of work here, hopefully the comments should make it clear how to proceed.

.register => {
return self.fail(inst.base.src, "TODO implement storing to MCValue.register", .{});
.register => |reg| {
try self.setRegOrMem(inst.base.src, elem_ty, .{ .register = reg }, value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantics here are not quite right- this should behave as if the register has a pointer address in it, and store to that address. This is why I think to accomplish removing the operand from the return statement we need a "Set the return value" instruction, which would not necessarily operate on a pointer.

Comment on lines -460 to -470
if (nodeMayNeedMemoryLocation(rhs_node)) {
const ret_ptr = try addZIRNoOp(mod, scope, src, .ret_ptr);
const operand = try expr(mod, scope, .{ .ptr = ret_ptr }, rhs_node);
return addZIRUnOp(mod, scope, src, .@"return", operand);
} else {
const fn_ret_ty = try addZIRNoOp(mod, scope, src, .ret_type);
const operand = try expr(mod, scope, .{ .ty = fn_ret_ty }, rhs_node);
return addZIRUnOp(mod, scope, src, .@"return", operand);
}
} else {
return addZIRNoOp(mod, scope, src, .returnvoid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're going for here, and I like it. I was thinking along the same lines of removing the operand from the return instruction. However I think we still need a "Set the return value" instruction which will happen in the else branch here. For an explanation of why see the review comments in codegen.zig on the store instruction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider:

fn hello(cond: bool) T {
    return if (cond) foo() else bar();
}

Intended semantics are: the result location of hello is passed directly as the result location of foo and bar. In master branch, this is the case. However with these changes, foo and bar will each be given a temporary result location, which is then copied to hello's result location. This copy is incompatible with pinned memory, which is something we plan to support soon with #3803 and #2765.

I think the if (nodeMayNeedMemoryLocation logic should remain.

Comment on lines 117 to 119
const result_type = body.instructions[body.instructions.len - 1];
const val = try mod.resolveConstValue(&block_scope.base, result_type.analyzed_inst.?);
return val.toType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "set return value" instruction would be less brittle than looking at the last instruction too, I think.

Keeping the return instruction as simple as possible has multiple benefits:
* more accurately models machine code
* removes separate handling of less than pointer sized values
* makes it easier to generate defers without code duplication
Copy link
Member

@andrewrk andrewrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put some thought into this, and I have a vision for how this can be done cleanly, which takes into account defers. Let me know if you want to go for it, or if my comments are too confusing I would be happy to do a hand-off here.

Comment on lines -460 to -470
if (nodeMayNeedMemoryLocation(rhs_node)) {
const ret_ptr = try addZIRNoOp(mod, scope, src, .ret_ptr);
const operand = try expr(mod, scope, .{ .ptr = ret_ptr }, rhs_node);
return addZIRUnOp(mod, scope, src, .@"return", operand);
} else {
const fn_ret_ty = try addZIRNoOp(mod, scope, src, .ret_type);
const operand = try expr(mod, scope, .{ .ty = fn_ret_ty }, rhs_node);
return addZIRUnOp(mod, scope, src, .@"return", operand);
}
} else {
return addZIRNoOp(mod, scope, src, .returnvoid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider:

fn hello(cond: bool) T {
    return if (cond) foo() else bar();
}

Intended semantics are: the result location of hello is passed directly as the result location of foo and bar. In master branch, this is the case. However with these changes, foo and bar will each be given a temporary result location, which is then copied to hello's result location. This copy is incompatible with pinned memory, which is something we plan to support soon with #3803 and #2765.

I think the if (nodeMayNeedMemoryLocation logic should remain.

@@ -1309,7 +1309,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool {
!gen_scope.instructions.items[gen_scope.instructions.items.len - 1].tag.isNoReturn()))
{
const src = tree.token_locs[body_block.rbrace].start;
_ = try astgen.addZIRNoOp(self, &gen_scope.base, src, .returnvoid);
_ = try astgen.addZIRNoOp(self, &gen_scope.base, src, .@"return");
Copy link
Member

@andrewrk andrewrk Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing a "set return value to void" instruction, which would be necessary for the "expected T, found void" compile error (when one forgets to return a value). I think we should keep the existing returnvoid instruction as well as the "return with an operand" instruction, and what this branch can do is add an additional "set the return value" instruction and "return control flow assuming the return value has already been set".

Idea being that we can have IR instructions with overlapping responsibilities; it's easy to have them call the appropriate functions during semantic analysis. This is in the effort of lowering memory usage; adding another enum tag to ir.Inst.Tag is free, so we may as well minimize the data we are allocating for these instructions.

However another observation is that once we do defers, it would make sense to have one canonical "exit the function" path from any scope. I think we should actually remove the return control flow instruction.

So, here's my proposal:

  • Add set_ret_val instruction, which does no control flow.
  • Add set_ret_void instruction. Same as set_ret_val but the operand is assumed to be the void value. (This is simply to reduce memory usage)
  • Remove return and returnvoid instructions.

There would be no more return control flow logic. It would be implied at the end of the main outer block of a function, and early-return control flow would be managed with breaks. I think this would work well with how to codegen defer expressions. Note, however, that the return value would either be set with ret_ptr and writing through the pointer, or set_ret_val directly.

The astgen for return syntax will need to change, to use a combination of the "break" and "set_ret_val" instructions rather than emitting the now-deleted "return" control flow instruction.

@Vexu
Copy link
Member Author

Vexu commented Jul 30, 2020

You seem to have a clear idea of where you want to take this so I'd be happy to hand it off. I just want to split the value and the control flow so that I can try implementing defers.

@pixelherodev
Copy link
Contributor

pixelherodev commented Jul 30, 2020 via email

@andrewrk
Copy link
Member

OK I'm going to close this. I wrote up a little bit here: #283 (comment)
This is going to depend on finishing Register Allocation and Stack Allocation Across Conditional Branches, so I'm going to hold off on trying to implement it just yet.

@andrewrk andrewrk closed this Jul 30, 2020
@Vexu Vexu deleted the stage2-ret branch June 13, 2021 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants