From 3d7713b5af2e1229949b31dcce74c7aba1fe042a Mon Sep 17 00:00:00 2001
From: Kimplul <kimi.h.kuparinen@gmail.com>
Date: Tue, 18 Mar 2025 18:37:00 +0200
Subject: add move queues

+ Returning blocks don't want to show moves for subsequent statements,
  but do want to show them for possible closure callers above.
---
 src/move.c | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 82 insertions(+), 13 deletions(-)

(limited to 'src/move.c')

diff --git a/src/move.c b/src/move.c
index 6ed687b..e11c2be 100644
--- a/src/move.c
+++ b/src/move.c
@@ -12,6 +12,7 @@ struct ast_pair {
 
 struct state {
 	struct moved moved;
+	struct moved queued;
 	struct moved referenced;
 	struct state *parent;
 
@@ -35,6 +36,7 @@ static struct state create_state(struct state *parent)
 	struct state state = {};
 	state.parent = parent;
 	state.pure = false;
+	state.queued = moved_create();
 	state.moved = moved_create();
 	state.referenced = moved_create();
 	return state;
@@ -43,6 +45,7 @@ static struct state create_state(struct state *parent)
 static void destroy_state(struct state *state)
 {
 	moved_destroy(&state->moved);
+	moved_destroy(&state->queued);
 	moved_destroy(&state->referenced);
 }
 
@@ -123,6 +126,16 @@ static void merge_moves(struct state *to, struct state *from)
 	}
 }
 
+static void merge_queued(struct state *to, struct state *from)
+{
+	if (moved_len(&from->queued) == 0)
+		return;
+
+	foreach(moved, n, &from->queued) {
+		moved_insert(&to->queued, *n);
+	}
+}
+
 static void merge_references(struct state *to, struct state *from)
 {
 	if (moved_len(&from->referenced) == 0)
@@ -136,6 +149,7 @@ static void merge_references(struct state *to, struct state *from)
 static void merge(struct state *to, struct state *from)
 {
 	merge_moves(to, from);
+	merge_queued(to, from);
 	merge_references(to, from);
 }
 
@@ -148,9 +162,37 @@ static void push_up(struct state *state)
 	merge(parent, state);
 }
 
+static void mark_queued(struct state *state)
+{
+	if (moved_len(&state->moved) == 0)
+		return;
+
+	foreach(moved, m, &state->moved) {
+		moved_insert(&state->queued, *m);
+	}
+
+	/* empty out moves now that they're all queued */
+	moved_destroy(&state->moved);
+	state->moved = moved_create();
+}
+
+static void mark_unqueued(struct state *state)
+{
+	if (moved_len(&state->queued) == 0)
+		return;
+
+	foreach(moved, q, &state->queued) {
+		moved_insert(&state->moved, *q);
+	}
+
+	moved_destroy(&state->queued);
+	state->queued = moved_create();
+}
+
 static void forget_references(struct state *state)
 {
 	moved_destroy(&state->referenced);
+	state->referenced = moved_create();
 }
 
 static int mvcheck(struct state *state, struct ast *node);
@@ -270,12 +312,13 @@ static int mvcheck_block(struct state *state, struct ast *node)
 	}
 
 	if (block_error(node)) {
-		/* don't push state up since there's no way to continue
-		 * execution after an error, so whatever code follows this
-		 * didn't see any moves take place */
 		if (total_check_proc(&new_state, node->scope))
 			semantic_info(node->scope, node, "at end of block");
 
+		/* mark us queued so moves are not visible for the next scopes,
+		 * but appear to the caller function if we're in a closure */
+		mark_queued(&new_state);
+		push_up(&new_state);
 		destroy_state(&new_state);
 		return 0;
 	}
@@ -306,20 +349,17 @@ static int mvcheck_call(struct state *state, struct ast *node)
 			return -1;
 	}
 
-	/* check if there are any arguments that might not be moved if function
-	 * returns immediately */
-	if (!call_err(node) && total_check_proc(state, node->scope))
-		semantic_info(node->scope, node, "in implicit error branch");
-
 	/* check into closures */
 	int ret = 0;
-	struct state group_state = create_state(state);
+	struct state buffer_state = create_state(state);
+	struct state group_state = create_state(&buffer_state);
 	foreach_node(arg, call_args(node)) {
 		if (arg->k != AST_CLOSURE)
 			continue;
 
 		struct state arg_state = create_state(state);
 		ret = mvcheck(&arg_state, arg);
+		mark_unqueued(&arg_state);
 
 		merge(&group_state, &arg_state);
 		destroy_state(&arg_state);
@@ -329,7 +369,7 @@ static int mvcheck_call(struct state *state, struct ast *node)
 			/* this group ended, push state immediately */
 			push_up(&group_state);
 
-			/* something like reset_state would maybe me more clear? */
+			/* something like reset_state would maybe be more clear? */
 			destroy_state(&group_state);
 			group_state = create_state(state);
 		}
@@ -338,14 +378,43 @@ static int mvcheck_call(struct state *state, struct ast *node)
 			break;
 	}
 
-	if (!ret && call_err(node)) {
+	destroy_state(&group_state);
+
+	/* the next section looks a bit weird, but it kind of makes sense. We
+	 * don't know when the error branch might get taken, so we first check
+	 * it 'before' the moves from any possible closures take place to see if
+	 * there's a possibility that some variable gets leaked. The second time
+	 * around we take the closure moves into account to see if the error
+	 * branch might accidentally move an already moved variable, i.e. the
+	 * user would need to add `own` blocks. */
+
+	/** @todo this is more or less what needs to happen for correctness, but
+	 * might mean that warnings from leaks in the first check get reprinted
+	 * in the second round, should probably add in some mechanism to check
+	 * against that */
+	if (call_err(node)) {
+		struct state err_state = create_state(state);
+		ret |= mvcheck(&err_state, call_err(node));
+		destroy_state(&err_state);
+	}
+	else if (total_check_proc(state, node->scope)) {
+		semantic_info(node->scope, node, "in implicit err branch");
+	}
+
+	push_up(&buffer_state);
+	destroy_state(&buffer_state);
+
+	if (call_err(node)) {
 		struct state err_state = create_state(state);
-		ret = mvcheck(&err_state, call_err(node));
+		ret |= mvcheck(&err_state, call_err(node));
+
+		/* store results of this check */
 		push_up(&err_state);
 		destroy_state(&err_state);
 	}
+	/* no need to check implicit error branch since it by definition can't
+	 * move already moved variables */
 
-	destroy_state(&group_state);
 	return ret;
 }
 
-- 
cgit v1.2.3