131 lines
6.5 KiB
Python
131 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
# Typed entry module for the Roblox build: bundle.py writes build/ImGuiTyped.luau = Luau types + `require(script.Impl)`,
|
|
# where Impl is the untouched build/ImGui.luau (the bundle itself is too large for Luau's type inference to finish,
|
|
# so the types live in this small wrapper instead). Library sources and the LÖVE build are not affected.
|
|
# Public ImGui.* signatures come from upstream imgui.h (namespace ImGui), mapped onto the port's own parameter lists.
|
|
# Anything ambiguous (overloads, char* buffers, arrays, multiple out-params, count mismatch) falls back to `any`,
|
|
# so the types never reject code that runs. Enum tables (ImGuiWindowFlags, ImGuiCol, ...) get their exact keys.
|
|
# Self-check: python3 tools/gen_types.py (prints a few signatures)
|
|
import os, re
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
NUM = re.compile(r"^(int|float|double|unsigned int|unsigned short|short|size_t|ImU32|ImS32|ImU64|ImS64|ImWchar|ImGuiID|Im\w*Flags|ImGui(Col|Cond|StyleVar|Key|MouseButton|MouseCursor|Dir|DataType|SortDirection|TableBgTarget|Axis))$")
|
|
VEC = {"ImVec2": "ImVec2", "ImVec4": "ImVec4"}
|
|
|
|
def lua_type(ctype):
|
|
t = re.sub(r"\bconst\b|&", "", ctype).strip()
|
|
t = re.sub(r"\s+", " ", t)
|
|
if t == "char*": return "string" if "const" in ctype else None
|
|
if t == "bool": return "boolean"
|
|
if NUM.match(t): return "number"
|
|
return VEC.get(t)
|
|
|
|
def split_params(s):
|
|
out, depth, cur = [], 0, ""
|
|
for ch in s:
|
|
if ch in "([": depth += 1
|
|
if ch in ")]": depth -= 1
|
|
if ch == "," and depth == 0: out.append(cur); cur = ""
|
|
else: cur += ch
|
|
if cur.strip(): out.append(cur)
|
|
return [p.strip() for p in out]
|
|
|
|
def upstream_decls():
|
|
src = open(os.path.join(ROOT, "tools", "test", "upstream", "imgui.h")).read()
|
|
body = src[src.index("namespace ImGui\n"):src.index("} // namespace ImGui")]
|
|
decls = {}
|
|
for m in re.finditer(r"IMGUI_API\s+([\w\s\*&]+?)\s*\b(\w+)\((.*?)\)\s*(?:IM_FMT\w+\(\d+\))?\s*;", body):
|
|
decls.setdefault(m.group(2), []).append((m.group(1).strip(), split_params(m.group(3)) if m.group(3).strip() not in ("", "void") else []))
|
|
return decls
|
|
|
|
LOOSE = "(...any) -> ...any"
|
|
|
|
def signature(port_params, ret, cparams):
|
|
if any(p == "..." for p in cparams):
|
|
if port_params and port_params[-1] == "...": port_params = port_params[:-1]
|
|
cparams = [p for p in cparams if p != "..."]; vararg = True
|
|
else: vararg = False
|
|
if len(port_params) != len(cparams): return LOOSE
|
|
args, outs = [], []
|
|
for name, cp in zip(port_params, cparams):
|
|
decl, _, default = cp.partition("=")
|
|
m = re.match(r"(.*?)\b(\w+)\s*(\[\w*\])?\s*$", decl.strip())
|
|
if not m: return LOOSE
|
|
ctype, array = m.group(1).strip(), m.group(3)
|
|
if "(*" in cp: return LOOSE
|
|
ptr = ctype.endswith("*") and not re.search(r"\bchar\s*\*$", ctype)
|
|
if array: t = None; outs.append("array")
|
|
elif ptr and re.search(r"\bconst\b", ctype): t = None # input array (e.g. ColorPicker4 ref_col)
|
|
elif ptr:
|
|
t = lua_type(ctype[:-1])
|
|
if t in ("boolean", "number"): outs.append(t)
|
|
else: t = None; outs.append("other")
|
|
else: t = lua_type(ctype)
|
|
if name == "...": args.append("...any"); continue
|
|
opt = "?" if (default.strip() or (ptr and t == "boolean")) else ""
|
|
args.append("%s: %s%s" % (name, t or "any", opt if t else ""))
|
|
if vararg: args.append("...any")
|
|
r = re.sub(r"\bconst\b|&|IMGUI_API", "", ret).strip()
|
|
if r == "void": rt = "...any" if outs else "()"
|
|
elif r == "bool":
|
|
scal = [o for o in outs if o != "array"]
|
|
if not scal: rt = "boolean"
|
|
elif len(scal) == 1 and scal[0] == "boolean": rt = "(boolean, boolean?)"
|
|
elif len(scal) == 1 and scal[0] == "number": rt = "(number, boolean)"
|
|
else: rt = "...any"
|
|
else:
|
|
rt = lua_type(r) or "any"
|
|
if outs: rt = "...any"
|
|
return "(%s) -> %s" % (", ".join(args), rt)
|
|
|
|
def port_functions(files):
|
|
fns = {}
|
|
for src in files.values():
|
|
for m in re.finditer(r"^function ImGui\.(\w+)\((.*?)\)", src, re.M):
|
|
fns[m.group(1)] = [p.strip() for p in m.group(2).split(",") if p.strip()]
|
|
return fns
|
|
|
|
def enums(files):
|
|
src = files.get("imgui_h.lua", "")
|
|
out = {}
|
|
for m in re.finditer(r"^(Im\w+) = \{\n(.*?)^\}", src, re.M | re.S):
|
|
keys = re.findall(r"^\s+(\w+)\s*=", m.group(2), re.M)
|
|
if keys: out[m.group(1)] = keys
|
|
for f in files.values(): # keys added later (ImGuiKey letters in loops are covered by the indexer below)
|
|
for name, key in re.findall(r"^\s*(Im\w+)\.(\w+)\s*=", f, re.M):
|
|
if name in out and key not in out[name]: out[name].append(key)
|
|
return out
|
|
|
|
def types_header(files):
|
|
decls, fns = upstream_decls(), port_functions(files)
|
|
lines = ["export type ImVec2 = { x: number, y: number, [any]: any }",
|
|
"export type ImVec4 = { x: number, y: number, z: number, w: number, [any]: any }",
|
|
"export type ImGuiAPI = {"]
|
|
for name in sorted(fns):
|
|
d = decls.get(name)
|
|
sig = signature(fns[name], *d[0]) if d and len(d) == 1 else LOOSE
|
|
lines.append(" %s: %s," % (name, sig))
|
|
lines += [" [string]: any,", "}"]
|
|
for name, keys in sorted(enums(files).items()):
|
|
lines.append("export type %s = { %s, [string]: number }" % (name, ", ".join("%s: number" % k for k in keys)))
|
|
return lines + module_type(set(enums(files)))
|
|
|
|
def wrapper(files):
|
|
return "\n".join(["--!strict", "-- GENERATED by tools/gen_types.py - do not edit. The library is the child ModuleScript 'Impl'.", ""]
|
|
+ types_header(files) + ["", "return (require(script:WaitForChild(\"Impl\")) :: any) :: ImGuiModule", ""])
|
|
|
|
def module_type(enum_names):
|
|
# the bundle is too large for Luau to infer, so its return value is cast to this explicit type
|
|
lines = ["export type ImGuiModule = {", " ImGui: ImGuiAPI,"]
|
|
lines += [" %s: (x: number?, y: number?, z: number?, w: number?) -> %s," % (n, n) for n in ("ImVec2", "ImVec4")]
|
|
lines += [" %s: %s," % (n, n) for n in sorted(enum_names)]
|
|
return lines + [" [string]: any,", "}"]
|
|
|
|
if __name__ == "__main__":
|
|
import glob
|
|
files = {os.path.basename(f): open(f).read() for f in glob.glob(os.path.join(ROOT, "*.lua"))}
|
|
lines = types_header(files)
|
|
for l in lines:
|
|
if re.match(r" (Begin|Checkbox|SliderFloat|SliderFloat3|Text|Button|Combo|GetCursorPos|InputText|DragFloatRange2):", l): print(l)
|
|
loose = sum(LOOSE in l for l in lines); print("%d functions, %d loose" % (sum(l.startswith(" ") for l in lines), loose))
|