Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable milestones for the nera project are documented in this file.

## Receives nested in after bodies resume at their own wait

- A `receive` directly inside another receive's `after` body now waits for
its own timeout and receives messages sent while it waits, as on BEAM.
- Values evaluated earlier in the `after` body survive suspension. Previously
the enclosing receive could resume inside the inner receive before those
values or the inner timeout had been initialized.
- The `receive_nested_after` regression runs unchanged natively and on BEAM,
covering message and timeout wakes, tail position, deeper nesting and
successive inner receives.

## Oversized binary copies raise Erlang errors

- `binary:copy/2` checks result-size multiplication and the binary header's
Expand Down
42 changes: 22 additions & 20 deletions crates/nera-ir/src/lower/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,26 +849,11 @@ impl IrBuilder {
let before = self.collect_all_bindings();
let mut exits = Vec::new();

// --- After body block (shared target for timeout paths) ---
let after_block = if let Some(after_clause) = after {
let ab = self.fresh_block_with_id();
// Like a clause body, the after body binds in its own scope.
self.push_scope();
self.start_block(ab);
let after_result = self.lower_body(&after_clause.body, is_tail)?;
if !is_tail {
self.emit(Instruction::Move(result_var, after_result));
self.set_terminator(Terminator::Jump(join_block, vec![]));
exits.extend(self.branch_exit(&before));
} else if self.current_terminator.is_none() {
self.set_terminator(Terminator::Return(after_result));
}
self.close_current_block();
self.pop_scope();
Some(ab)
} else {
None
};
// Allocate the after target now, but emit its body after the wait.
// Codegen associates receive_wait with the last receive_peek in block
// order. A nested receive in this body must not take ownership of the
// enclosing receive's wait (or its saved live variables).
let after_block = after.map(|_| self.fresh_block_with_id());

// --- Empty block: no messages (or all scanned, none matched) ---
self.start_block(empty_block);
Expand Down Expand Up @@ -913,6 +898,23 @@ impl IrBuilder {
self.close_current_block();
}

// --- After body block (shared target for timeout paths) ---
if let (Some(after_clause), Some(ab)) = (after, after_block) {
// Like a clause body, the after body binds in its own scope.
self.push_scope();
self.start_block(ab);
let after_result = self.lower_body(&after_clause.body, is_tail)?;
if !is_tail {
self.emit(Instruction::Move(result_var, after_result));
self.set_terminator(Terminator::Jump(join_block, vec![]));
exits.extend(self.branch_exit(&before));
} else if self.current_terminator.is_none() {
self.set_terminator(Terminator::Return(after_result));
}
self.close_current_block();
self.pop_scope();
}

// --- Build match clauses ---
// Each body block calls receive_accept() to remove the matched message
// before executing the clause body.
Expand Down
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ These are automatically skipped by the runner and show up as SKIP in output.
| `selective_receive.erl` | Selective receive (mailbox scan) | ~6 |
| `receive_after.erl` | Receive with timeout | ~4 |
| `receive_timeout_domain.erl` | `after` values outside `infinity` and 0..4294967295: a matching queued message is received first, including selective receive and inside `try`; with no match, `error:timeout_value` at once, queued messages kept, no wait for a later sender, and as an uncaught exit; an error in the `after` expression or earlier in a `try` body comes first; valid boundaries; also run on BEAM | Table-driven |
| `receive_nested_after.erl` | Receive directly in an after body: delayed messages, own timeout, earlier values, tail position, deeper nesting and successive inner receives; also run on BEAM | 5 scenarios |
| `receive_nil_message.erl` | The message `[]` is received like any other: first, middle and last in the mailbox, `[]` patterns and guards, `after 0` and no `after`, selective receive skipping it, and a waiting process woken by it; later messages still arrive; also run on BEAM | ~21 |
| `send_order_between_processes.erl` | Messages one process sends another arrive in the order they were sent: four senders each stream 100,000 numbers to their own receiver, and 2,000 bursts of three come back from an echo process in order; also run on BEAM | 3 |
| `process_dict.erl` | put/get/erase/get_keys | ~10 |
Expand Down
75 changes: 75 additions & 0 deletions tests/receive_nested_after.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
%% EXPECT: receive_nested_after_ok
%% A receive directly inside an after body must resume itself, not its
%% enclosing receive, and preserve values evaluated before the inner wait.
%% Run this fixture unchanged natively and on BEAM.
-module(receive_nested_after).
-export([test/0]).

-define(WAIT_MS, 120).

test() ->
{aft, {late, x}} = delayed_message(),
io:format("delayed message and earlier atom ok~n"),
{{kept, [1, 2], <<3, 4>>}, nolate} = own_timeout(),
io:format("inner timeout and earlier value ok~n"),
{tail, x} = tail_receive(),
io:format("tail receive without timeout ok~n"),
{outer, {middle, {late, x}}} = three_levels(),
io:format("three nested receives ok~n"),
{first, x, second, y} = successive_receives(),
io:format("successive inner receives ok~n"),
io:format("receive_nested_after_ok~n"),
ok.

delayed_message() ->
Sender = sender(self(), x),
receive never -> unexpected after 10 ->
%% Start the sender only once the enclosing receive has timed out.
Sender ! go,
{aft, receive X -> {late, X} after 1000 -> nolate end}
end.

own_timeout() ->
receive never -> unexpected after 10 ->
Started = erlang:monotonic_time(millisecond),
Keep = {kept, [1, 2], <<3, 4>>},
R = {Keep, receive never -> unexpected after ?WAIT_MS -> nolate end},
Elapsed = erlang:monotonic_time(millisecond) - Started,
%% Allow clock quantization, not an early wake on the outer deadline.
true = Elapsed >= ?WAIT_MS - 2,
R
end.

tail_receive() ->
Sender = sender(self(), x),
receive never -> unexpected after 0 ->
Sender ! go,
receive X -> {tail, X} end
end.

three_levels() ->
Sender = sender(self(), x),
receive never -> unexpected after 10 ->
{outer, receive never -> unexpected after 10 ->
Sender ! go,
{middle, receive X -> {late, X} after 1000 -> nolate end}
end}
end.

successive_receives() ->
Sender1 = sender(self(), x),
Sender2 = sender(self(), y),
receive never -> unexpected after 0 ->
Sender1 ! go,
A = {first, receive X -> X after 1000 -> nolate end},
Sender2 ! go,
{first, V} = A,
{first, V, second, receive Y -> Y after 1000 -> nolate end}
end.

sender(To, Value) ->
spawn(fun() ->
receive go ->
receive after 30 -> To ! Value end
end
end).
Loading