From cd34414fd9f17ad7e2d2fc5c078175657f2b5966 Mon Sep 17 00:00:00 2001 From: Gabriel Ferreira Date: Fri, 23 Aug 2024 15:21:34 -0300 Subject: [PATCH 1/2] Making an API for using flow analysis --- src/pallene/coder.lua | 2 +- src/pallene/flow.lua | 313 ++++++++++++++++++++++++++++++++++ src/pallene/gc.lua | 220 +++++------------------- src/pallene/uninitialized.lua | 177 ++++--------------- 4 files changed, 392 insertions(+), 320 deletions(-) create mode 100644 src/pallene/flow.lua diff --git a/src/pallene/coder.lua b/src/pallene/coder.lua index 387a0d17..8b16ee84 100644 --- a/src/pallene/coder.lua +++ b/src/pallene/coder.lua @@ -958,7 +958,7 @@ end function Coder:init_gc() for _, func in ipairs(self.module.functions) do - self.gc[func] = gc.compute_stack_slots(func) + self.gc[func] = gc.compute_gc_info(func) end for _, func in ipairs(self.module.functions) do diff --git a/src/pallene/flow.lua b/src/pallene/flow.lua new file mode 100644 index 00000000..0b61a5c1 --- /dev/null +++ b/src/pallene/flow.lua @@ -0,0 +1,313 @@ +-- Copyright (c) 2020, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +-- Functions for doing flow analysis +-- +-- The flow.lua file is designed as an API that helps doing flow analysis. +-- +-- We give a brief introduction of flow analysis just to get the reader acquainted with the +-- terminology being used here. To better understand the code you should know how flow analys work +-- already. +-- +-- Flow Analysis introduction: +-- +-- Doing flow analysis on a function consists of tracking down properties of it's code along each +-- command. These properties are represented using a set. Each basic block has a "start" set and a +-- "finish" set. The "start" set is the set of values available right before we start to process the +-- block's commands and the "finish" set is the set of values available right after we finish +-- processing the commands. Each block also has a "kill" and a "gen" set that help transform the +-- "start" set into the "finish" set. The "kill" set contains the values that will be removed from +-- the running set while "gen" (as in "generate") contains the values that will be added to it. The +-- flow analysis algorithm's input is a pair of "kill" and "gen" sets for each block and the initial +-- values for the "start" sets of each block. During it's runtime, the algorithm updates the "start" +-- and "finish" sets in a loop until they all converge to some value. The algorithm requires a loop +-- because a block's "start" set depends on the "finish" set of it's predecessor or it's successors, +-- depending on what order the flow analysis is being done (some analyses require that we walk +-- through the code backwards). +-- +-- +-- API usage: +-- +-- When doing flow analysis, follow these steps. Also look at examples of usage inside the codebase, +-- as in "gc.lua". +-- +-- 1) Create a function of type "function (set, block id)" that will be used internally by the API +-- to initialize the "start" sets. The function is called once for each basic block. It's first +-- argument is the "start" set of the block and the second is the block's index. +-- +-- 2) Create function of type "function (FlowState, block id, command id)" that will be used for +-- updating the running set as we read the function's commands. The first argument is an object +-- of type FlowState, that stores various sets used internally; the second argument is the +-- block index and the third is the command's index inside the block. For removing/adding +-- elements from/to the set, use the API function "flow.kill_value" for removal +-- and "flow.gen_value" for insertion. Both functions are of type "function (FlowState, +-- element)", where the first argument is a FlowState object and the second is the element +-- that will be removed/inserted from/into the set. +-- +-- 3) Create a FlowInfo object +-- The object's constructor takes three parameters: +-- order: +-- The order in which commands and blocks are iterated during flow analysis. +-- "Order.Forwards" updates the running set by reading a block's commands in order and +-- builds the "start" set of a block from it's predecessors' "finish" sets, while +-- "Order.Backwards" updates the running set by reading a block's commands in backwards +-- order and builds the "start" set of a block from it's successors' "finish" sets. +-- process_cmd: +-- function used to update the running set, use the one you wrote in step 2 +-- init_start: +-- function used to initalize the "start" set of blocks, use the one you wrote in +-- step 1 +-- +-- 4) Call the function "flow.flow_analysis". It's parameters are: +-- func_block: +-- A list of the function's blocks +-- flow_info : +-- An object of type "FlowInfo". Use the one you created in step 3. +-- +-- "flow.flow_analysis" returns a list of objects of type "FlowState.Build". Each basic block has a +-- corresponding object on the list. +-- +-- 5) Having now the list of flow states, iterate through blocks and commands (if you used +-- Order.Backwards previously, in step 3, then you'll have to iterate through the commands of the +-- block backwards too). +-- +-- 5.1) Inside the loop that iterates over blocks and before entering the loop that iterates over +-- the commands of a block, call "flow.make_apply_state". The function receives one argument, +-- which will be the flow state of the current block that can retrieved from the list obtained in +-- step 4 (e.g. the flow state corresponding to the 3rd block will be flow_state_list[3]). +-- "flow.make_apply_state" returns an object of type "FlowState.Apply". This one will be used to +-- update the state of the flow analysis set as we iterate over the commands of the current +-- block. This set can be accesses through the "set" property of the "FlowState.Apply" object +-- returned by "flow.make_apply_state". Checking the contents of this set as you update it +-- throught the commands is essentialy the whole point of everything we're doing here, that's +-- what flow analysis is for. The "set" property of the "FlowState.Apply" object returned by +-- "flow.make_apply_state" is equal the "start" set of the current block. +-- +-- 5.2) Inside the loop that iterates over a block's commands, call "flow.update_set" to update +-- the set of the "FlowState.Apply" object. "flow.update_set"'s first argument is the +-- "FlowState.Apply" object; second argument is the FlowInfo object created in step 3; third +-- argument is the current block's index and the forth argument is the current command's index +-- inside the block. + +local flow = {} + +local ir = require "pallene.ir" +local tagged_union = require "pallene.tagged_union" +local define_union = tagged_union.in_namespace(flow, "flow") + +define_union("Order", { + Forward = {}, + Backwards = {}, +}) + +define_union("FlowState", { + Build = { + "start", -- set of values when we start analysing the block + "finish", -- set of values when we finish analysing the block, + "kill", -- set of values + "gen", -- set of values + }, + + Apply = { + "set", -- set of values + }, +}) + +function flow.FlowInfo(order, process_cmd, init_start) + return { + order = order, -- flow.Order + process_cmd = process_cmd, -- function + init_start = init_start, -- function + } +end + +local function copy_set(S) + local new_set = {} + for v,_ in pairs(S) do + new_set[v] = true + end + return new_set +end + +local function apply_gen_kill_sets(flow_state) + local start = flow_state.start + local finish = flow_state.finish + local gen = flow_state.gen + local kill = flow_state.kill + local in_changed = false + + for v, _ in pairs(start) do + local val = true + if kill[v] then + val = nil + end + local previous_val = finish[v] + local new_val = previous_val or val + finish[v] = new_val + in_changed = in_changed or (previous_val ~= new_val) + end + + for v, g in pairs(gen) do + assert(not (g and kill[v]), "gen and kill can't both be true") + local previous_val = finish[v] + local new_val = true + finish[v] = new_val + in_changed = in_changed or (previous_val ~= new_val) + end + + for v, _ in pairs(finish) do + if not start[v] and not gen[v] then + finish[v] = nil + in_changed = true + end + end + + return in_changed +end + +local function clear_set(S) + for v,_ in pairs(S) do + S[v] = nil + end +end + +local function merge_sets(blocks_states, src_indices, dest_index) + local dest = blocks_states[dest_index].start + clear_set(dest) + for _,src_i in ipairs(src_indices) do + local src = blocks_states[src_i].finish + for v, _ in pairs(src) do + dest[v] = true + end + end +end + +local function make_state_list(block_list, flow_info) + local blocks_states = {} + local order = flow_info.order._tag + for block_i, block in ipairs(block_list) do + local state = flow.FlowState.Build({},{},{},{}) + blocks_states[block_i] = state + flow_info.init_start(state.start, block_i) + if order == "flow.Order.Forward" then + for cmd_i = 1, #block.cmds do + flow_info.process_cmd(state, block_i, cmd_i) + end + elseif order == "flow.Order.Backwards" then + for cmd_i = #block.cmds, 1, -1 do + flow_info.process_cmd(state, block_i, cmd_i) + end + else + tagged_union.error(order) + end + end + return blocks_states +end + +function flow.flow_analysis(block_list, flow_info) + + local blocks_states = make_state_list(block_list, flow_info) + + local succ_list = ir.get_successor_list(block_list) + local pred_list = ir.get_predecessor_list(block_list) + + local block_order -- { block_id }, order in which blocks will be traversed + local merge_src_list -- { block_id => { block_id } }, maps a block to the blocks it uses + -- for assembling it's "start" set + local dirty_propagation_list -- { block_id => { block_id } }, maps a block to the blocks it + -- should propagate the dirty flag when it's "finish" set is + -- changed + local order = flow_info.order._tag + if order == "flow.Order.Forward" then + block_order = ir.get_successor_depth_search_topological_sort(succ_list) + merge_src_list = pred_list + dirty_propagation_list = succ_list + elseif order == "flow.Order.Backwards" then + block_order = ir.get_predecessor_depth_search_topological_sort(pred_list) + merge_src_list = succ_list + dirty_propagation_list = pred_list + else + tagged_union.error(order) + end + + local dirty_flag = {} -- { block_id -> bool } keeps track of modified blocks + for i = 1, #block_list do + dirty_flag[i] = true + end + + local first_block_i = block_order[1] + + local function update_block(block_i) + local state = blocks_states[block_i] + + -- first block's starting set is supposed to be constant + if block_i ~= first_block_i then + local src_indices = merge_src_list[block_i] + merge_sets(blocks_states, src_indices, block_i) + end + + local dirty_propagation = dirty_propagation_list[block_i] + local state_changed = apply_gen_kill_sets(state) + if state_changed then + for _, i in ipairs(dirty_propagation) do + dirty_flag[i] = true + end + end + end + + repeat + local found_dirty_block = false + for _,block_i in ipairs(block_order) do + if dirty_flag[block_i] then + found_dirty_block = true + -- CAREFUL: we have to clean the dirty flag BEFORE updating the block or else a + -- block that jumps to itself might set it's dirty flag to "true" during + -- "update_block" and we'll then wrongly set it to "false" in here. + dirty_flag[block_i] = false + update_block(block_i) + end + end + until not found_dirty_block + + return blocks_states +end + +function flow.kill_value(flow_state, value) + local tag = flow_state._tag + if tag == "flow.FlowState.Build" then + flow_state.kill[value] = true + flow_state.gen[value] = nil + elseif tag == "flow.FlowState.Apply" then + flow_state.set[value] = nil + else + tagged_union.error(tag) + end +end + +function flow.gen_value(flow_state, value) + local tag = flow_state._tag + if tag == "flow.FlowState.Build" then + flow_state.gen[value] = true + flow_state.kill[value] = nil + elseif tag == "flow.FlowState.Apply" then + flow_state.set[value] = true + else + tagged_union.error(tag) + end +end + +function flow.make_apply_state(flow_state) + assert(flow_state._tag == "flow.FlowState.Build") + local start = copy_set(flow_state.start) + local a_state = flow.FlowState.Apply(start) + return a_state +end + +function flow.update_set(flow_state, flow_info, block_i, cmd_i) + assert(flow_state._tag == "flow.FlowState.Apply") + flow_info.process_cmd(flow_state, block_i, cmd_i) +end + +return flow diff --git a/src/pallene/gc.lua b/src/pallene/gc.lua index 35b6a60d..44b3770d 100644 --- a/src/pallene/gc.lua +++ b/src/pallene/gc.lua @@ -4,6 +4,7 @@ -- SPDX-License-Identifier: MIT local ir = require "pallene.ir" +local flow = require "pallene.flow" local types = require "pallene.types" local tagged_union = require "pallene.tagged_union" @@ -35,146 +36,14 @@ local tagged_union = require "pallene.tagged_union" local gc = {} -local function FlowState() - return { - input = {}, -- set of var_id, live variables at block start - output = {}, -- set of var_id, live variables at block end - kill = {}, -- set of var_id, variables that are killed inside block - gen = {}, -- set of var_id, variables that become live inside block - } -end - -local function cmd_uses_gc(tag) +local function cmd_uses_gc(cmd) + local tag = cmd._tag assert(tagged_union.typename(tag) == "ir.Cmd") return tag == "ir.Cmd.CallStatic" or tag == "ir.Cmd.CallDyn" or tag == "ir.Cmd.CheckGC" end -local function copy_set(S) - local new_set = {} - for v,_ in pairs(S) do - new_set[v] = true - end - return new_set -end - -local function flow_analysis(block_list, state_list) - local function apply_gen_kill_sets(flow_state) - local input = flow_state.input - local output = flow_state.output - local gen = flow_state.gen - local kill = flow_state.kill - local in_changed = false - - for v, _ in pairs(output) do - local val = true - if kill[v] then - val = nil - end - local previous_val = input[v] - local new_val = previous_val or val - input[v] = new_val - in_changed = in_changed or (previous_val ~= new_val) - end - - for v, g in pairs(gen) do - assert(not (g and kill[v]), "gen and kill can't both be true") - local previous_val = input[v] - local new_val = true - input[v] = new_val - in_changed = in_changed or (previous_val ~= new_val) - end - - for v, _ in pairs(input) do - if not output[v] and not gen[v] then - input[v] = nil - in_changed = true - end - end - - return in_changed - end - - local function merge_live(input, output) - for v, _ in pairs(input) do - output[v] = true - end - end - - local function clear_set(S) - for v,_ in pairs(S) do - S[v] = nil - end - end - - local succ_list = ir.get_successor_list(block_list) - local pred_list = ir.get_predecessor_list(block_list) - local block_order = ir.get_predecessor_depth_search_topological_sort(pred_list) - - local dirty_flag = {} -- { block_id -> bool? } keeps track of modified blocks - for i = 1, #block_list do - dirty_flag[i] = true - end - - local function update_block(block_i) - local block_succs = succ_list[block_i] - local block_preds = pred_list[block_i] - local state = state_list[block_i] - - -- last block's output is supposed to be fixed - if block_i ~= #block_list then - clear_set(state.output) - for _,succ in ipairs(block_succs) do - local succ_in = state_list[succ].input - merge_live(succ_in, state.output) - end - end - - local in_changed = apply_gen_kill_sets(state) - if in_changed then - for _, pred in ipairs(block_preds) do - dirty_flag[pred] = true - end - end - end - - repeat - local found_dirty_block = false - for _,block_i in ipairs(block_order) do - if dirty_flag[block_i] then - found_dirty_block = true - -- CAREFUL: we have to clean the dirty flag BEFORE updating the block or else we - -- will do the wrong thing for auto-referencing blocks - dirty_flag[block_i] = false - update_block(block_i) - end - end - until not found_dirty_block -end - -local function mark_gen_kill(cmd, gen_set, kill_set) - assert(tagged_union.typename(cmd._tag) == "ir.Cmd") - for _, dst in ipairs(ir.get_dsts(cmd)) do - gen_set[dst] = nil - kill_set[dst] = true - end - - for _, src in ipairs(ir.get_srcs(cmd)) do - if src._tag == "ir.Value.LocalVar" then - gen_set[src.id] = true - kill_set[src.id] = nil - end - end -end - -local function make_gen_kill_sets(block, flow_state) - for i = #block.cmds, 1, -1 do - local cmd = block.cmds[i] - mark_gen_kill(cmd, flow_state.gen, flow_state.kill) - end -end - -- Returns information that is used for allocating variables into the Lua stack. -- The returned data is: -- * live_gc_vars: @@ -185,27 +54,40 @@ end -- * max_frame_size: -- what's the maximum number of slots of the Lua stack used for storing GC'd variables -- during the function. -function gc.compute_stack_slots(func) - - local state_list = {} -- { FlowState } - - -- initialize states - for block_i, block in ipairs(func.blocks) do - local fst = FlowState() - make_gen_kill_sets(block, fst) - state_list[block_i] = fst +local function compute_stack_slots(func) + + -- 1) Find live GC'd variables for each basic block + local function init_start(start_set, block_index) + -- set returned variables to "live" on exit block + if block_index == #func.blocks then + for _, var in ipairs(func.ret_vars) do + start_set[var] = true + end + end end - -- set returned variables to "live" on exit block - if #func.blocks > 0 then - local exit_output = state_list[#func.blocks].output - for _, var in ipairs(func.ret_vars) do - exit_output[var] = true + local function process_cmd(flow_state, block_i, cmd_i) + local cmd = func.blocks[block_i].cmds[cmd_i] + assert(tagged_union.typename(cmd._tag) == "ir.Cmd") + for _, dst in ipairs(ir.get_dsts(cmd)) do + local typ = func.vars[dst].typ + if types.is_gc(typ) then + flow.kill_value(flow_state, dst) + end + end + + for _, src in ipairs(ir.get_srcs(cmd)) do + if src._tag == "ir.Value.LocalVar" then + local typ = func.vars[src.id].typ + if types.is_gc(typ) then + flow.gen_value(flow_state, src.id) + end + end end end - -- 1) Find live variables at the end of each basic block - flow_analysis(func.blocks, state_list) + local flow_info = flow.FlowInfo(flow.Order.Backwards, process_cmd, init_start) + local blocks_flow_states = flow.flow_analysis(func.blocks, flow_info) -- 2) Find which GC'd variables are live at each GC spot in the program and -- which GC'd variables are live at the same time @@ -222,38 +104,18 @@ function gc.compute_stack_slots(func) end for block_i, block in ipairs(func.blocks) do - local lives_block = copy_set(state_list[block_i].output) - -- filter out non-GC'd variables from set - for var_i, _ in pairs(lives_block) do - local var = func.vars[var_i] - if not types.is_gc(var.typ) then - lives_block[var_i] = nil - end - end + local lives_block = flow.make_apply_state(blocks_flow_states[block_i]) for cmd_i = #block.cmds, 1, -1 do local cmd = block.cmds[cmd_i] - assert(tagged_union.typename(cmd._tag) == "ir.Cmd") - for _, dst in ipairs(ir.get_dsts(cmd)) do - lives_block[dst] = nil - end - for _, src in ipairs(ir.get_srcs(cmd)) do - if src._tag == "ir.Value.LocalVar" then - local typ = func.vars[src.id].typ - if types.is_gc(typ) then - lives_block[src.id] = true - end - end - end - - if cmd_uses_gc(cmd._tag) - then + flow.update_set(lives_block, flow_info, block_i, cmd_i) + if cmd_uses_gc(cmd) then local lives_cmd = {} - for var,_ in pairs(lives_block) do + for var,_ in pairs(lives_block.set) do table.insert(lives_cmd, var) end live_gc_vars[block_i][cmd_i] = lives_cmd - for var1,_ in pairs(lives_block) do - for var2,_ in pairs(lives_block) do + for var1,_ in pairs(lives_block.set) do + for var2,_ in pairs(lives_block.set) do if not live_at_same_time[var1] then live_at_same_time[var1] = {} end @@ -293,6 +155,11 @@ function gc.compute_stack_slots(func) assert(slot_of_variable[v1], "should always find a slot") end + return live_gc_vars, max_frame_size, slot_of_variable +end + +function gc.compute_gc_info(func) + local live_gc_vars, max_frame_size, slot_of_variable = compute_stack_slots(func) return { live_gc_vars = live_gc_vars, max_frame_size = max_frame_size, @@ -300,5 +167,4 @@ function gc.compute_stack_slots(func) } end - return gc diff --git a/src/pallene/uninitialized.lua b/src/pallene/uninitialized.lua index 7ede0165..189be72b 100644 --- a/src/pallene/uninitialized.lua +++ b/src/pallene/uninitialized.lua @@ -9,170 +9,63 @@ -- loops. local ir = require "pallene.ir" -local tagged_union = require "pallene.tagged_union" +local flow = require "pallene.flow" local uninitialized = {} -local function FlowState() - return { - input = {}, -- set of var_id, uninitialized variables at block start - output = {}, -- set of var_id, uninitialized variables at block end - kill = {}, -- set of var_id, variables that are initialized inside block - } -end +function uninitialized.verify_variables(module) -local function copy_set(S) - local new_set = {} - for v,_ in pairs(S) do - new_set[v] = true - end - return new_set -end + local errors = {} -local function fill_set(cmd, set, val) - assert(tagged_union.typename(cmd._tag) == "ir.Cmd") - for _, src in ipairs(ir.get_srcs(cmd)) do - if src._tag == "ir.Value.LocalVar" then - -- `SetField` instructions can count as initializers when the target is an - -- upvalue box. This is because upvalue boxes are allocated, but not initialized - -- upon declaration. - if cmd._tag == "ir.Cmd.SetField" and cmd.rec_typ.is_upvalue_box then - set[src.id] = val - end - end - end + for _, func in ipairs(module.functions) do - -- Artificial initializers introduced by the compilers do not count. - if not (cmd._tag == "ir.Cmd.NewRecord" and cmd.rec_typ.is_upvalue_box) then - for _, v_id in ipairs(ir.get_dsts(cmd)) do - set[v_id] = val - end - end -end + local nvars = #func.vars + local nargs = #func.typ.arg_types -local function flow_analysis(block_list, state_list) - local function apply_kill_set(flow_state) - local input = flow_state.input - local output = flow_state.output - local kill = flow_state.kill - local out_changed = false - for v, _ in pairs(input) do - if not kill[v] then - if not output[v] then - out_changed = true + -- solve flow equations + local function init_start(start_set, block_index) + if block_index == 1 then + for v_i = nargs+1, nvars do + start_set[v_i] = true end - output[v] = true - end - end - - for v, _ in pairs(output) do - if not input[v] then - output[v] = nil - out_changed = true - end - end - return out_changed - end - - local function merge_uninit(input, output) - for v, _ in pairs(output) do - input[v] = true - end - end - - local function clear_set(S) - for v,_ in pairs(S) do - S[v] = nil - end - end - - local succ_list = ir.get_successor_list(block_list) - local pred_list = ir.get_predecessor_list(block_list) - local block_order = ir.get_successor_depth_search_topological_sort(succ_list) - - local dirty_flag = {} -- { block_id -> bool? } keeps track of modified blocks - for i = 1, #block_list do - dirty_flag[i] = true - end - - local function update_block(block_i) - local block_succs = succ_list[block_i] - local block_preds = pred_list[block_i] - local state = state_list[block_i] - - -- first block's input is supposed to be fixed - if block_i ~= 1 then - clear_set(state.input) - for _,pred in ipairs(block_preds) do - local pred_out = state_list[pred].output - merge_uninit(state.input, pred_out) end end - local out_changed = apply_kill_set(state) - if out_changed then - for _, succ in ipairs(block_succs) do - dirty_flag[succ] = true + local function process_cmd(flow_state, block_i, cmd_i) + local cmd = func.blocks[block_i].cmds[cmd_i] + for _, src in ipairs(ir.get_srcs(cmd)) do + if src._tag == "ir.Value.LocalVar" then + -- `SetField` instructions can count as initializers when the target is an + -- upvalue box. This is because upvalue boxes are allocated, but not initialized + -- upon declaration. + if cmd._tag == "ir.Cmd.SetField" and cmd.rec_typ.is_upvalue_box then + flow.kill_value(flow_state, src.id) + end + end end - end - end - repeat - local found_dirty_block = false - for _,block_i in ipairs(block_order) do - if dirty_flag[block_i] then - found_dirty_block = true - -- CAREFUL: we have to clean the dirty flag BEFORE updating the block or else we - -- will do the wrong thing for auto-referencing blocks - dirty_flag[block_i] = false - update_block(block_i) + -- Artificial initializers introduced by the compilers do not count. + if not (cmd._tag == "ir.Cmd.NewRecord" and cmd.rec_typ.is_upvalue_box) then + for _, v_id in ipairs(ir.get_dsts(cmd)) do + flow.kill_value(flow_state, v_id) + end end end - until not found_dirty_block -end - -local function gen_kill_set(block) - local kill = {} - for _,cmd in ipairs(block.cmds) do - fill_set(cmd, kill, true) - end - return kill -end -function uninitialized.verify_variables(module) - - local errors = {} - - for _, func in ipairs(module.functions) do - - local nvars = #func.vars - local nargs = #func.typ.arg_types - - local state_list = {} -- { FlowState } - -- initialize states - for block_i,block in ipairs(func.blocks) do - local fst = FlowState() - fst.kill = gen_kill_set(block) - state_list[block_i] = fst - end - local entry_input = state_list[1].input - for v_i = nargs+1, nvars do - entry_input[v_i] = true - end - - -- solve flow equations - flow_analysis(func.blocks, state_list) + local flow_info = flow.FlowInfo(flow.Order.Forward, process_cmd, init_start) + local blocks_flow_states = flow.flow_analysis(func.blocks, flow_info) -- check for errors local reported_variables = {} -- (only one error message per variable) for block_i, block in ipairs(func.blocks) do - local uninit = copy_set(state_list[block_i].input) - for _, cmd in ipairs(block.cmds) do + local flow_state = blocks_flow_states[block_i] + local uninit = flow.make_apply_state(flow_state) + for cmd_i, cmd in ipairs(block.cmds) do local loc = cmd.loc - fill_set(cmd, uninit, nil) + flow.update_set(uninit, flow_info, block_i, cmd_i) for _, src in ipairs(ir.get_srcs(cmd)) do local v = src.id - if src._tag == "ir.Value.LocalVar" and uninit[v] then + if src._tag == "ir.Value.LocalVar" and uninit.set[v] then if not reported_variables[v] then reported_variables[v] = true local name = assert(func.vars[v].name) @@ -184,7 +77,7 @@ function uninitialized.verify_variables(module) end end - local exit_uninit = state_list[#func.blocks].output + local exit_uninit = blocks_flow_states[#func.blocks].finish if #func.ret_vars > 0 then local ret1 = func.ret_vars[1] if exit_uninit[ret1] then From e4b8a40b798e66254d1ad687b448f0bc3f3ac17f Mon Sep 17 00:00:00 2001 From: Gabriel Ferreira Date: Mon, 2 Sep 2024 21:14:12 -0300 Subject: [PATCH 2/2] Refactor of flow analysis API --- src/pallene/flow.lua | 251 ++++++++++++++++------------------ src/pallene/gc.lua | 20 +-- src/pallene/uninitialized.lua | 19 +-- 3 files changed, 136 insertions(+), 154 deletions(-) diff --git a/src/pallene/flow.lua b/src/pallene/flow.lua index 0b61a5c1..20a2f310 100644 --- a/src/pallene/flow.lua +++ b/src/pallene/flow.lua @@ -7,89 +7,50 @@ -- -- The flow.lua file is designed as an API that helps doing flow analysis. -- +-- Flow Analysis introduction +-- -- We give a brief introduction of flow analysis just to get the reader acquainted with the -- terminology being used here. To better understand the code you should know how flow analys work -- already. -- --- Flow Analysis introduction: --- -- Doing flow analysis on a function consists of tracking down properties of it's code along each -- command. These properties are represented using a set. Each basic block has a "start" set and a -- "finish" set. The "start" set is the set of values available right before we start to process the -- block's commands and the "finish" set is the set of values available right after we finish --- processing the commands. Each block also has a "kill" and a "gen" set that help transform the --- "start" set into the "finish" set. The "kill" set contains the values that will be removed from --- the running set while "gen" (as in "generate") contains the values that will be added to it. The --- flow analysis algorithm's input is a pair of "kill" and "gen" sets for each block and the initial --- values for the "start" sets of each block. During it's runtime, the algorithm updates the "start" --- and "finish" sets in a loop until they all converge to some value. The algorithm requires a loop --- because a block's "start" set depends on the "finish" set of it's predecessor or it's successors, --- depending on what order the flow analysis is being done (some analyses require that we walk --- through the code backwards). --- +-- processing the block's commands. Each block also has a "kill" and a "gen" set that help transform +-- the "start" set into the "finish" set. The "kill" set contains the values that will be removed +-- from the running set while "gen" (as in "generate") contains the values that will be added to it. +-- The flow analysis algorithm's input is a collection of "kill" and "gen" sets for each block and +-- the initial values for the "start" sets of each block. During it's runtime, the algorithm updates +-- the "start" and "finish" sets in a loop until they all converge to some value. The algorithm +-- requires a loop because a block's "start" set depends on the "finish" set of it's predecessors or +-- it's successors, depending on what order the flow analysis is being done (some analyses require +-- that we walk through the code backwards). -- -- API usage: -- -- When doing flow analysis, follow these steps. Also look at examples of usage inside the codebase, -- as in "gc.lua". -- --- 1) Create a function of type "function (set, block id)" that will be used internally by the API --- to initialize the "start" sets. The function is called once for each basic block. It's first --- argument is the "start" set of the block and the second is the block's index. --- --- 2) Create function of type "function (FlowState, block id, command id)" that will be used for --- updating the running set as we read the function's commands. The first argument is an object --- of type FlowState, that stores various sets used internally; the second argument is the --- block index and the third is the command's index inside the block. For removing/adding --- elements from/to the set, use the API function "flow.kill_value" for removal --- and "flow.gen_value" for insertion. Both functions are of type "function (FlowState, --- element)", where the first argument is a FlowState object and the second is the element --- that will be removed/inserted from/into the set. -- --- 3) Create a FlowInfo object --- The object's constructor takes three parameters: --- order: --- The order in which commands and blocks are iterated during flow analysis. --- "Order.Forwards" updates the running set by reading a block's commands in order and --- builds the "start" set of a block from it's predecessors' "finish" sets, while --- "Order.Backwards" updates the running set by reading a block's commands in backwards --- order and builds the "start" set of a block from it's successors' "finish" sets. --- process_cmd: --- function used to update the running set, use the one you wrote in step 2 --- init_start: --- function used to initalize the "start" set of blocks, use the one you wrote in --- step 1 +-- 1) Create a FlowInfo object and the functions it receives as arguments (see the flow.FlowInfo +-- constructor to learn what those functions are supposed to do). -- --- 4) Call the function "flow.flow_analysis". It's parameters are: --- func_block: --- A list of the function's blocks --- flow_info : --- An object of type "FlowInfo". Use the one you created in step 3. -- --- "flow.flow_analysis" returns a list of objects of type "FlowState.Build". Each basic block has a --- corresponding object on the list. +-- 2) Call the function "flow.flow_analysis" using as arguments the blocks of the functions you're +-- analysing and the "FlowInfo" object you created in step 3. "flow.flow_analysis" returns a list +-- that contains a set for each block in the function. The returned sets are the starting sets of +-- each corresponding block. To get the sets corresponding to the commands of a block, you'll have +-- to loop through them and update the set yourself. The next step teaches you how to do that. -- --- 5) Having now the list of flow states, iterate through blocks and commands (if you used --- Order.Backwards previously, in step 3, then you'll have to iterate through the commands of the --- block backwards too). -- --- 5.1) Inside the loop that iterates over blocks and before entering the loop that iterates over --- the commands of a block, call "flow.make_apply_state". The function receives one argument, --- which will be the flow state of the current block that can retrieved from the list obtained in --- step 4 (e.g. the flow state corresponding to the 3rd block will be flow_state_list[3]). --- "flow.make_apply_state" returns an object of type "FlowState.Apply". This one will be used to --- update the state of the flow analysis set as we iterate over the commands of the current --- block. This set can be accesses through the "set" property of the "FlowState.Apply" object --- returned by "flow.make_apply_state". Checking the contents of this set as you update it --- throught the commands is essentialy the whole point of everything we're doing here, that's --- what flow analysis is for. The "set" property of the "FlowState.Apply" object returned by --- "flow.make_apply_state" is equal the "start" set of the current block. +-- 3) Having now the list of sets, iterate through blocks and commands (if you used Order.Backwards +-- previously, in step 3, then you'll have to iterate through the commands of the block backwards +-- too). -- --- 5.2) Inside the loop that iterates over a block's commands, call "flow.update_set" to update --- the set of the "FlowState.Apply" object. "flow.update_set"'s first argument is the --- "FlowState.Apply" object; second argument is the FlowInfo object created in step 3; third --- argument is the current block's index and the forth argument is the current command's index --- inside the block. +-- 3.1) Inside the loop that iterates over a block's commands, call "flow.update_set" to update +-- the set. + local flow = {} @@ -102,40 +63,51 @@ define_union("Order", { Backwards = {}, }) -define_union("FlowState", { - Build = { - "start", -- set of values when we start analysing the block - "finish", -- set of values when we finish analysing the block, - "kill", -- set of values - "gen", -- set of values - }, - - Apply = { - "set", -- set of values - }, -}) +function flow.GenKill() + return { + kill = {}, -- set of values + gen = {}, -- set of values + } +end + -function flow.FlowInfo(order, process_cmd, init_start) +local function FlowState() return { - order = order, -- flow.Order - process_cmd = process_cmd, -- function - init_start = init_start, -- function + start = {}, -- set of values when we start analysing the block + finish = {}, -- set of values when we finish analysing the block, + gk = flow.GenKill(), } end -local function copy_set(S) - local new_set = {} - for v,_ in pairs(S) do - new_set[v] = true - end - return new_set +function flow.FlowInfo(order, compute_gen_kill, init_start) + return { + -- "order" is the order in which commands and blocks are iterated during flow analysis. + -- "Order.Forwards" updates the running set by reading a block's commands in order and + -- builds the "start" set of a block from it's predecessors' "finish" sets, while + -- "Order.Backwards" updates the running set by reading a block's commands in backwards + -- order and builds the "start" set of a block from it's successors' "finish" sets. + order = order, -- flow.Order + + -- "compute_gen_kill" is a function that will be used for updating the running set as we + -- read commands. The first argument is the block index and the second is the command's + -- index inside the block. For indicating which elements should be inserted/removed + -- into/from the set, create a new flow.GenKill object and then call the API functions + -- "flow.kill_value" for removal and "flow.gen_value" for insertion. The "compute_gen_kill" + -- function must return the flow.GenKill object that you created. + compute_gen_kill = compute_gen_kill, -- (block_id, cmd_id) -> flow.GenKill + + -- "init_start" is a function that will be used for initializing the "start" sets. The + -- function is called once for each basic block. It's first argument is the "start" set of + -- the block and the second is the block's index. + init_start = init_start, -- function (set, block_id) -> void + } end local function apply_gen_kill_sets(flow_state) local start = flow_state.start local finish = flow_state.finish - local gen = flow_state.gen - local kill = flow_state.kill + local gen = flow_state.gk.gen + local kill = flow_state.gk.kill local in_changed = false for v, _ in pairs(start) do @@ -173,52 +145,68 @@ local function clear_set(S) end end -local function merge_sets(blocks_states, src_indices, dest_index) - local dest = blocks_states[dest_index].start +local function merge_sets(state_list, src_indices, dest_index) + local dest = state_list[dest_index].start clear_set(dest) for _,src_i in ipairs(src_indices) do - local src = blocks_states[src_i].finish + local src = state_list[src_i].finish for v, _ in pairs(src) do dest[v] = true end end end +local function apply_cmd_gk_to_block_gk(cmd_gk, block_gk) + local cmd_gen = cmd_gk.gen + local cmd_kill = cmd_gk.kill + local block_gen = block_gk.gen + local block_kill = block_gk.kill + for v,_ in pairs(cmd_gen) do + assert(not cmd_kill[v], "cmd_gen and cmd_kill must not intersect") + block_gen[v] = true + block_kill[v] = nil + end + for v,_ in pairs(cmd_kill) do + assert(not cmd_gen[v], "cmd_gen and cmd_kill must not intersect") + block_gen[v] = nil + block_kill[v] = true + end +end + local function make_state_list(block_list, flow_info) - local blocks_states = {} + local state_list = {} local order = flow_info.order._tag for block_i, block in ipairs(block_list) do - local state = flow.FlowState.Build({},{},{},{}) - blocks_states[block_i] = state - flow_info.init_start(state.start, block_i) + local block_state = FlowState() + state_list[block_i] = block_state + flow_info.init_start(block_state.start, block_i) if order == "flow.Order.Forward" then for cmd_i = 1, #block.cmds do - flow_info.process_cmd(state, block_i, cmd_i) + local cmd_gk = flow_info.compute_gen_kill(block_i, cmd_i) + apply_cmd_gk_to_block_gk(cmd_gk, block_state.gk) end elseif order == "flow.Order.Backwards" then for cmd_i = #block.cmds, 1, -1 do - flow_info.process_cmd(state, block_i, cmd_i) + local cmd_gk = flow_info.compute_gen_kill(block_i, cmd_i) + apply_cmd_gk_to_block_gk(cmd_gk, block_state.gk) end else tagged_union.error(order) end end - return blocks_states + return state_list end function flow.flow_analysis(block_list, flow_info) - - local blocks_states = make_state_list(block_list, flow_info) + -- ({ir.BasicBlock}, flow.FlowInfo) -> { block_id -> set } + local state_list = make_state_list(block_list, flow_info) local succ_list = ir.get_successor_list(block_list) local pred_list = ir.get_predecessor_list(block_list) - local block_order -- { block_id }, order in which blocks will be traversed - local merge_src_list -- { block_id => { block_id } }, maps a block to the blocks it uses - -- for assembling it's "start" set - local dirty_propagation_list -- { block_id => { block_id } }, maps a block to the blocks it - -- should propagate the dirty flag when it's "finish" set is - -- changed + local block_order + local merge_src_list + local dirty_propagation_list local order = flow_info.order._tag if order == "flow.Order.Forward" then block_order = ir.get_successor_depth_search_topological_sort(succ_list) @@ -240,12 +228,12 @@ function flow.flow_analysis(block_list, flow_info) local first_block_i = block_order[1] local function update_block(block_i) - local state = blocks_states[block_i] + local state = state_list[block_i] -- first block's starting set is supposed to be constant if block_i ~= first_block_i then local src_indices = merge_src_list[block_i] - merge_sets(blocks_states, src_indices, block_i) + merge_sets(state_list, src_indices, block_i) end local dirty_propagation = dirty_propagation_list[block_i] @@ -271,43 +259,34 @@ function flow.flow_analysis(block_list, flow_info) end until not found_dirty_block - return blocks_states -end - -function flow.kill_value(flow_state, value) - local tag = flow_state._tag - if tag == "flow.FlowState.Build" then - flow_state.kill[value] = true - flow_state.gen[value] = nil - elseif tag == "flow.FlowState.Apply" then - flow_state.set[value] = nil - else - tagged_union.error(tag) + local block_start_list = {} + for state_i, flow_state in ipairs(state_list) do + block_start_list[state_i] = flow_state.start end + + return block_start_list end -function flow.gen_value(flow_state, value) - local tag = flow_state._tag - if tag == "flow.FlowState.Build" then - flow_state.gen[value] = true - flow_state.kill[value] = nil - elseif tag == "flow.FlowState.Apply" then - flow_state.set[value] = true - else - tagged_union.error(tag) +function flow.update_set(set, flow_info, block_i, cmd_i) -- (set, flow.FlowInfo, block_id) -> void + local gk = flow_info.compute_gen_kill(block_i, cmd_i) + for v,_ in pairs(gk.gen) do + assert(not gk.kill[v], "gen and kill must not intersect") + set[v] = true + end + for v,_ in pairs(gk.kill) do + assert(not gk.gen[v], "gen and kill must not intersect") + set[v] = nil end end -function flow.make_apply_state(flow_state) - assert(flow_state._tag == "flow.FlowState.Build") - local start = copy_set(flow_state.start) - local a_state = flow.FlowState.Apply(start) - return a_state +function flow.gen_value(gen_kill, v) -- (flow.GenKill, element) -> void + gen_kill.gen[v] = true + gen_kill.kill[v] = nil end -function flow.update_set(flow_state, flow_info, block_i, cmd_i) - assert(flow_state._tag == "flow.FlowState.Apply") - flow_info.process_cmd(flow_state, block_i, cmd_i) +function flow.kill_value(gen_kill, v) -- (flow.GenKill, element) -> void + gen_kill.gen[v] = nil + gen_kill.kill[v] = true end return flow diff --git a/src/pallene/gc.lua b/src/pallene/gc.lua index 44b3770d..ae82abe5 100644 --- a/src/pallene/gc.lua +++ b/src/pallene/gc.lua @@ -66,13 +66,14 @@ local function compute_stack_slots(func) end end - local function process_cmd(flow_state, block_i, cmd_i) + local function compute_gen_kill(block_i, cmd_i) local cmd = func.blocks[block_i].cmds[cmd_i] assert(tagged_union.typename(cmd._tag) == "ir.Cmd") + local gk = flow.GenKill() for _, dst in ipairs(ir.get_dsts(cmd)) do local typ = func.vars[dst].typ if types.is_gc(typ) then - flow.kill_value(flow_state, dst) + flow.kill_value(gk, dst) end end @@ -80,14 +81,15 @@ local function compute_stack_slots(func) if src._tag == "ir.Value.LocalVar" then local typ = func.vars[src.id].typ if types.is_gc(typ) then - flow.gen_value(flow_state, src.id) + flow.gen_value(gk, src.id) end end end + return gk end - local flow_info = flow.FlowInfo(flow.Order.Backwards, process_cmd, init_start) - local blocks_flow_states = flow.flow_analysis(func.blocks, flow_info) + local flow_info = flow.FlowInfo(flow.Order.Backwards, compute_gen_kill, init_start) + local sets_list = flow.flow_analysis(func.blocks, flow_info) -- 2) Find which GC'd variables are live at each GC spot in the program and -- which GC'd variables are live at the same time @@ -104,18 +106,18 @@ local function compute_stack_slots(func) end for block_i, block in ipairs(func.blocks) do - local lives_block = flow.make_apply_state(blocks_flow_states[block_i]) + local lives_block = sets_list[block_i] for cmd_i = #block.cmds, 1, -1 do local cmd = block.cmds[cmd_i] flow.update_set(lives_block, flow_info, block_i, cmd_i) if cmd_uses_gc(cmd) then local lives_cmd = {} - for var,_ in pairs(lives_block.set) do + for var,_ in pairs(lives_block) do table.insert(lives_cmd, var) end live_gc_vars[block_i][cmd_i] = lives_cmd - for var1,_ in pairs(lives_block.set) do - for var2,_ in pairs(lives_block.set) do + for var1,_ in pairs(lives_block) do + for var2,_ in pairs(lives_block) do if not live_at_same_time[var1] then live_at_same_time[var1] = {} end diff --git a/src/pallene/uninitialized.lua b/src/pallene/uninitialized.lua index 189be72b..1bb23e08 100644 --- a/src/pallene/uninitialized.lua +++ b/src/pallene/uninitialized.lua @@ -31,15 +31,16 @@ function uninitialized.verify_variables(module) end end - local function process_cmd(flow_state, block_i, cmd_i) + local function compute_gen_kill(block_i, cmd_i) local cmd = func.blocks[block_i].cmds[cmd_i] + local gk = flow.GenKill() for _, src in ipairs(ir.get_srcs(cmd)) do if src._tag == "ir.Value.LocalVar" then -- `SetField` instructions can count as initializers when the target is an -- upvalue box. This is because upvalue boxes are allocated, but not initialized -- upon declaration. if cmd._tag == "ir.Cmd.SetField" and cmd.rec_typ.is_upvalue_box then - flow.kill_value(flow_state, src.id) + flow.kill_value(gk, src.id) end end end @@ -47,25 +48,25 @@ function uninitialized.verify_variables(module) -- Artificial initializers introduced by the compilers do not count. if not (cmd._tag == "ir.Cmd.NewRecord" and cmd.rec_typ.is_upvalue_box) then for _, v_id in ipairs(ir.get_dsts(cmd)) do - flow.kill_value(flow_state, v_id) + flow.kill_value(gk, v_id) end end + return gk end - local flow_info = flow.FlowInfo(flow.Order.Forward, process_cmd, init_start) - local blocks_flow_states = flow.flow_analysis(func.blocks, flow_info) + local flow_info = flow.FlowInfo(flow.Order.Forward, compute_gen_kill, init_start) + local sets_list = flow.flow_analysis(func.blocks, flow_info) -- check for errors local reported_variables = {} -- (only one error message per variable) for block_i, block in ipairs(func.blocks) do - local flow_state = blocks_flow_states[block_i] - local uninit = flow.make_apply_state(flow_state) + local uninit = sets_list[block_i] for cmd_i, cmd in ipairs(block.cmds) do local loc = cmd.loc flow.update_set(uninit, flow_info, block_i, cmd_i) for _, src in ipairs(ir.get_srcs(cmd)) do local v = src.id - if src._tag == "ir.Value.LocalVar" and uninit.set[v] then + if src._tag == "ir.Value.LocalVar" and uninit[v] then if not reported_variables[v] then reported_variables[v] = true local name = assert(func.vars[v].name) @@ -77,7 +78,7 @@ function uninitialized.verify_variables(module) end end - local exit_uninit = blocks_flow_states[#func.blocks].finish + local exit_uninit = sets_list[#func.blocks] if #func.ret_vars > 0 then local ret1 = func.ret_vars[1] if exit_uninit[ret1] then