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..414437c9 --- /dev/null +++ b/src/pallene/flow.lua @@ -0,0 +1,299 @@ +-- 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. +-- +-- 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. +-- +-- 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 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 inputs are "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 FlowInfo object and the functions it receives as arguments (see the flow.FlowInfo +-- constructor to learn what those functions are supposed to do). +-- +-- +-- 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. +-- +-- +-- 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). +-- +-- 3.1) Inside the loop that iterates over a block's commands, call "flow.update_set" to update +-- the set. + +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 = {}, +}) + +function flow.GenKill() + return { + kill = {}, -- set of values + gen = {}, -- set of values + } +end + + +local function FlowState() + return { + start = {}, -- set of values when we start analysing the block + finish = {}, -- set of values when we finish analysing the block, + gk = flow.GenKill(), + } +end + +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.gk.gen + local kill = flow_state.gk.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(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 = 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 state_list = {} + local order = flow_info.order._tag + for block_i, block in ipairs(block_list) do + 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 + 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 + 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 state_list +end + +-- Does flow analysis on a list of ir.BasicBlock objects. "block_list" is the list of blocks and +-- "flow_info" is an object of type flow.FlowInfo which contains information about how the flow +-- analysis will be done. The function returns the list of starting sets, each set in the list +-- corresponds to the basic block indexed by the same value. +function flow.flow_analysis(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 + 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) + 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 = 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(state_list, 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 + + 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 + +-- Updates a set "set" according to the "compute_gen_kill" function inside a ir.FlowInfo object +-- "flow_info". "block_i" and "cmd_i" are the coordinates of the ir.Cmd object that will be used to +-- update the set, "block_i" is the index of the block and "cmd_i" is the index of the command +-- inside the block's list of commands. +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.gen_value(gen_kill, v) -- (flow.GenKill, element) -> void + gen_kill.gen[v] = true + gen_kill.kill[v] = nil +end + +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 35b6a60d..ae82abe5 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,42 @@ 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 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(gk, 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(gk, src.id) + end + end + end + return gk 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, 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 @@ -222,31 +106,11 @@ 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 = sets_list[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 table.insert(lives_cmd, var) @@ -293,6 +157,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 +169,4 @@ function gc.compute_stack_slots(func) } end - return gc diff --git a/src/pallene/uninitialized.lua b/src/pallene/uninitialized.lua index 7ede0165..1bb23e08 100644 --- a/src/pallene/uninitialized.lua +++ b/src/pallene/uninitialized.lua @@ -9,167 +9,61 @@ -- 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 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(gk, 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(gk, v_id) + end end + return gk 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, 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 uninit = copy_set(state_list[block_i].input) - for _, cmd in ipairs(block.cmds) do + local uninit = sets_list[block_i] + 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 @@ -184,7 +78,7 @@ function uninitialized.verify_variables(module) end end - local exit_uninit = state_list[#func.blocks].output + local exit_uninit = sets_list[#func.blocks] if #func.ret_vars > 0 then local ret1 = func.ret_vars[1] if exit_uninit[ret1] then