From 801b1881d377514f9712677b4982873e4dda8955 Mon Sep 17 00:00:00 2001 From: Kode-Red <130188808+Kode-Red@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:17:01 -0600 Subject: [PATCH 1/2] feat(imports): add switch statement utility Introduces lib.switch, a compact utility function that provides a structured alternative to long if/elseif chains. Features: - Supports value, multi-value list, predicate, and "default" matches. - Optional fallthrough behavior (true = JS-style, false = stop after first). - Explicit breakFn to halt fallthrough early. - Variadic arguments or table-form ({ value, ... }) supported. - Weak-key cache for compiled dispatchers to minimize overhead. This implementation preserves performance with O(1) lookups for values and lists, only falling back to predicate checks when necessary. --- imports/switch/shared.lua | 110 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 imports/switch/shared.lua diff --git a/imports/switch/shared.lua b/imports/switch/shared.lua new file mode 100644 index 000000000..3082c12be --- /dev/null +++ b/imports/switch/shared.lua @@ -0,0 +1,110 @@ +--[[ + https://github.com/communityox/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 KodeRed +]] + +--- @alias SwitchMatch any|fun(val:any):boolean|"default"|any[] +--- @alias BreakFn fun():nil +--- @class SwitchCase +--- @field match SwitchMatch +--- @field action fun(breakFn:BreakFn, ...:any):any +--- @alias SwitchCases SwitchCase[] + +local _cache = setmetatable({}, { __mode = "k" }) +local t_unpack = table.unpack + +--- compile once per cases to a fast dispatcher (kept local; not exported) +--- @param cases SwitchCases +--- @return fun(value:any, fallthrough:boolean|nil, ...:any):any +local function compile(cases) + local map, preds, defaultIndex = {}, {}, nil + local n = #cases + + for i = 1, n do + local m = cases[i].match + local mt = type(m) + if m == "default" then + defaultIndex = i + elseif mt == "function" then + preds[#preds + 1] = i + elseif mt == "table" then + for j = 1, #m do + local v = m[j] + if map[v] == nil then map[v] = i end + end + else + if map[m] == nil then map[m] = i end + end + end + + return function(value, fallthrough, ...) + local stop, res + local function breakFn() stop = true end + + local i = map[value] + if i == nil then + for k = 1, #preds do + local pi = preds[k] + if cases[pi].match(value) then + i = pi; break + end + end + if i == nil then i = defaultIndex end + end + if i == nil then return nil end + + if fallthrough then + while i <= n do + res = cases[i].action(breakFn, ...) + if stop then break end + i = i + 1 + end + return res + else + return cases[i].action(breakFn, ...) + end + end +end + +--- Switch API (value first, cases third). +--- Call as: +--- lib.switch(value, fallthrough, cases, ...) +--- lib.switch({ value, ... }, fallthrough, cases) +--- @generic R +--- @param valueOrArgs any|any[] -- single value OR { value, arg1, arg2, ... } +--- @param fallthrough boolean|nil -- true = allow fallthrough; false/nil = stop at first +--- @param cases SwitchCases +--- @param ... any +--- @return R|any +function lib.switch(valueOrArgs, fallthrough, cases, ...) + local dispatch = _cache[cases] + if not dispatch then + dispatch = compile(cases) + _cache[cases] = dispatch + end + + if type(valueOrArgs) == "table" then + local value = valueOrArgs[1] + local argc = #valueOrArgs + if argc > 1 then + local packed, p = {}, 0 + for i = 2, argc do + p = p + 1; packed[p] = valueOrArgs[i] + end + local extra = { ... } + for i = 1, #extra do + p = p + 1; packed[p] = extra[i] + end + return dispatch(value, fallthrough, t_unpack(packed)) + else + return dispatch(value, fallthrough, ...) + end + else + return dispatch(valueOrArgs, fallthrough, ...) + end +end + +return lib.switch From b26b76baf5f7b49b8576a21283c15b8bc1d16c99 Mon Sep 17 00:00:00 2001 From: Kode-Red <130188808+Kode-Red@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:52:41 -0600 Subject: [PATCH 2/2] refactor(ox_lib/switch): treat table as list of match values in switch - Changed `lib.switch` so the first argument, when a table, is interpreted strictly as a list of possible values to match against. - Removed prior behavior of mixing case value and arguments in the same table. - Arguments are now always passed starting from the 4th parameter onward. - Supports both single values and lists of values, with earliest match resolution. - Fallthrough (`true`) continues from the first matched case; without fallthrough, only the first match executes. --- imports/switch/shared.lua | 84 ++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/imports/switch/shared.lua b/imports/switch/shared.lua index 3082c12be..238d4fd46 100644 --- a/imports/switch/shared.lua +++ b/imports/switch/shared.lua @@ -16,13 +16,14 @@ local _cache = setmetatable({}, { __mode = "k" }) local t_unpack = table.unpack ---- compile once per cases to a fast dispatcher (kept local; not exported) +-- compile once per cases to a fast dispatcher (kept local; not exported) --- @param cases SwitchCases ---- @return fun(value:any, fallthrough:boolean|nil, ...:any):any +--- @return fun(value:any|any[], fallthrough:boolean|nil, ...:any):any local function compile(cases) local map, preds, defaultIndex = {}, {}, nil local n = #cases + -- Pre-index: single values + list entries -> first matching index for i = 1, n do local m = cases[i].match local mt = type(m) @@ -40,23 +41,53 @@ local function compile(cases) end end - return function(value, fallthrough, ...) - local stop, res - local function breakFn() stop = true end + -- Find earliest matching case index for a single value + local function resolveOne(v) + local i = map[v] + if i ~= nil then return i end + for k = 1, #preds do + local pi = preds[k] + if cases[pi].match(v) then return pi end + end + return nil + end - local i = map[value] - if i == nil then + -- From a list of values, pick the earliest matching index among them + local function resolveFromList(list) + local best + -- exact/list map hits (O(1) each) + for vi = 1, #list do + local idx = map[list[vi]] + if idx and (not best or idx < best) then best = idx end + end + -- predicates only if no direct hit + if not best then for k = 1, #preds do local pi = preds[k] - if cases[pi].match(value) then - i = pi; break + local pred = cases[pi].match + for vi = 1, #list do + if pred(list[vi]) then best = pi; break end end + if best then break end end - if i == nil then i = defaultIndex end end - if i == nil then return nil end + return best + end + + return function(value, fallthrough, ...) + local stop, res + local function breakFn() stop = true end + + local startIndex + if type(value) == "table" then + startIndex = resolveFromList(value) or defaultIndex + else + startIndex = resolveOne(value) or defaultIndex + end + if not startIndex then return nil end if fallthrough then + local i = startIndex while i <= n do res = cases[i].action(breakFn, ...) if stop then break end @@ -64,47 +95,28 @@ local function compile(cases) end return res else - return cases[i].action(breakFn, ...) + return cases[startIndex].action(breakFn, ...) end end end --- Switch API (value first, cases third). --- Call as: ---- lib.switch(value, fallthrough, cases, ...) ---- lib.switch({ value, ... }, fallthrough, cases) +--- lib.switch(value, fallthrough, cases, ...) -- value is a single value +--- lib.switch({v1, v2, ...}, true, cases, ...) -- value is a list; picks earliest match; with fallthrough runs forward --- @generic R ---- @param valueOrArgs any|any[] -- single value OR { value, arg1, arg2, ... } +--- @param valueOrList any|any[] -- single value OR list of values to test --- @param fallthrough boolean|nil -- true = allow fallthrough; false/nil = stop at first --- @param cases SwitchCases --- @param ... any --- @return R|any -function lib.switch(valueOrArgs, fallthrough, cases, ...) +function lib.switch(valueOrList, fallthrough, cases, ...) local dispatch = _cache[cases] if not dispatch then dispatch = compile(cases) _cache[cases] = dispatch end - - if type(valueOrArgs) == "table" then - local value = valueOrArgs[1] - local argc = #valueOrArgs - if argc > 1 then - local packed, p = {}, 0 - for i = 2, argc do - p = p + 1; packed[p] = valueOrArgs[i] - end - local extra = { ... } - for i = 1, #extra do - p = p + 1; packed[p] = extra[i] - end - return dispatch(value, fallthrough, t_unpack(packed)) - else - return dispatch(value, fallthrough, ...) - end - else - return dispatch(valueOrArgs, fallthrough, ...) - end + return dispatch(valueOrList, fallthrough, ...) end return lib.switch