refactor(api)!: declare callbacks as classes an app composes
The runtime has always called fields on the table main.lua returns, but @lua-global declared them as loose functions, so the stubs type-checked something that does not exist and read as "define a global". Callbacks are now @lua-app blocks that generate a class: App for the core contract, TouchHandlers and ButtonHandlers beside the namespaces they belong to. An app composes what it implements: ---@class PaintApp : App, TouchHandlers Names follow the rest of the surface: onTouchDown rather than on_touch_down, with the field names the runtime looks up renamed to match. @lua-field carries the plain fields (home, data) that were prose in a preamble before.
This commit is contained in:
+56
-15
@@ -7,13 +7,16 @@ table that registers them:
|
||||
// @lua-preamble ---@alias Feature "touch"|"lcd"
|
||||
// @lua-module sys SysLib creates the global
|
||||
// @lua-augment gui GuiLib extends a global another file created
|
||||
// @lua-global core/runtime app callbacks, written to an explicit path
|
||||
// @lua-app App a class of fields on the table an app returns
|
||||
// @lua-const MAX_READ_BYTES integer 65536 Largest portable read.
|
||||
// @lua-field home? string a plain field of a @lua-app class
|
||||
// @lua-postamble -- notes rendered after the module
|
||||
|
||||
Per function, `// --- text`, `// @param name type desc`, and `// @return type desc`. Functions come
|
||||
from the luaL_Reg entries below the directive, or from `// @lua-fn name` for the app callbacks the
|
||||
runtime calls rather than registers.
|
||||
from the luaL_Reg entries below the directive, or from `// @lua-fn name` for the callbacks the
|
||||
runtime calls on an app's table rather than registers. A `@lua-fn` name ending in `?` is one the
|
||||
app may leave out; the class it lands in is what an app composes, so a device without the feature
|
||||
contributes no fields rather than the runtime hiding some.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -25,9 +28,10 @@ SOURCES = [ROOT / "native/src/bindings", ROOT / "native/src/runtime"]
|
||||
OUTPUT = ROOT / "lua/api"
|
||||
|
||||
MODULE = re.compile(r"^// @lua-(?P<kind>module|augment) (?P<name>\w+) (?P<class_>\w+)$")
|
||||
GLOBAL = re.compile(r"^// @lua-global(?: (?P<path>\S+))?$")
|
||||
FUNCTION = re.compile(r"^// @lua-fn (?P<name>\w+)$")
|
||||
APP = re.compile(r"^// @lua-app (?P<class_>\w+)(?: (?P<path>\S+))?$")
|
||||
FUNCTION = re.compile(r"^// @lua-fn (?P<name>\w+\??)$")
|
||||
CONST = re.compile(r"^// @lua-const (?P<name>\w+) (?P<type>\S+) (?P<value>\S+)(?: (?P<desc>.*))?$")
|
||||
FIELD = re.compile(r"^// @lua-field (?P<name>\w+\??) (?P<type>\S+)(?: (?P<desc>.*))?$")
|
||||
FIX = re.compile(r"^// @lua-(?P<where>preamble|postamble) ?(?P<text>.*)$")
|
||||
TABLE = re.compile(r"^\s*const luaL_Reg (?P<var>\w+)\[\] = \{$")
|
||||
ENTRY = re.compile(r'^\s*\{"(?P<name>\w+)",\s*\w+\},?$')
|
||||
@@ -53,6 +57,7 @@ def new_module(kind, name, class_name, path=None):
|
||||
"class": class_name,
|
||||
"path": path,
|
||||
"consts": [],
|
||||
"fields": [],
|
||||
"preamble": [],
|
||||
"postamble": [],
|
||||
"functions": [],
|
||||
@@ -92,18 +97,19 @@ def parse(path):
|
||||
pending_module = new_module(module.group("kind"), module.group("name"), module.group("class_"))
|
||||
modules.append(pending_module)
|
||||
continue
|
||||
glob = GLOBAL.match(stripped)
|
||||
if glob:
|
||||
# Callbacks are globals an app defines, so they need no table and no class.
|
||||
pending_module = new_module("global", None, None, glob.group("path"))
|
||||
app = APP.match(stripped)
|
||||
if app:
|
||||
# Callbacks are fields of the table an app returns, so the block is a class
|
||||
# with no table of its own to register.
|
||||
pending_module = new_module("app", None, app.group("class_"), app.group("path"))
|
||||
modules.append(pending_module)
|
||||
current, in_table = pending_module, False
|
||||
doc = reset()
|
||||
continue
|
||||
function = FUNCTION.match(stripped)
|
||||
if function:
|
||||
if not current or current["kind"] != "global":
|
||||
raise SystemExit(f"{where}: @lua-fn outside a @lua-global block")
|
||||
if not current or current["kind"] != "app":
|
||||
raise SystemExit(f"{where}: @lua-fn outside a @lua-app block")
|
||||
if not doc["doc"]:
|
||||
raise SystemExit(f"{where}: {function.group('name')} has no description")
|
||||
current["functions"].append((function.group("name"), doc))
|
||||
@@ -120,6 +126,15 @@ def parse(path):
|
||||
)
|
||||
continuation = (target["consts"], len(target["consts"]) - 1, 3)
|
||||
continue
|
||||
field = FIELD.match(stripped)
|
||||
if field:
|
||||
if not target or target["kind"] != "app":
|
||||
raise SystemExit(f"{where}: @lua-field outside a @lua-app block")
|
||||
target["fields"].append(
|
||||
(field.group("name"), lua_type(field.group("type")), field.group("desc") or "")
|
||||
)
|
||||
continuation = (target["fields"], len(target["fields"]) - 1, 2)
|
||||
continue
|
||||
fix = FIX.match(stripped)
|
||||
if fix:
|
||||
if not target:
|
||||
@@ -135,8 +150,8 @@ def parse(path):
|
||||
raise SystemExit(f"{where}: {table.group('var')} has no @lua-module or @lua-augment")
|
||||
current, pending_module, in_table, doc = pending_module, None, True, reset()
|
||||
continue
|
||||
# A @lua-global block documents callbacks the runtime calls, so it has no table to sit in.
|
||||
if not in_table and not (current and current["kind"] == "global"):
|
||||
# A @lua-app block documents callbacks the runtime calls, so it has no table to sit in.
|
||||
if not in_table and not (current and current["kind"] == "app"):
|
||||
continue
|
||||
if stripped == "};":
|
||||
in_table = False
|
||||
@@ -169,13 +184,28 @@ def parse(path):
|
||||
doc = reset()
|
||||
|
||||
for module in modules:
|
||||
if module["kind"] == "global":
|
||||
if module["kind"] == "app":
|
||||
continue
|
||||
if not module["functions"]:
|
||||
raise SystemExit(f"{path.relative_to(ROOT)}: {module['name']} registers no annotated functions")
|
||||
return modules
|
||||
|
||||
|
||||
def signature(doc):
|
||||
"""The `fun(...)` type of one callback, for the class an app composes."""
|
||||
params = ", ".join(
|
||||
f"{param}{'?' if type_.endswith('?') else ''}: {type_.rstrip('?')}" for param, type_, _ in doc["params"]
|
||||
)
|
||||
returns = ", ".join(type_ for type_, _ in doc["returns"])
|
||||
return f"fun({params})" + (f": {returns}" if returns else "")
|
||||
|
||||
|
||||
def describe(doc):
|
||||
"""A field carries one line, so a wrapped description joins back into a sentence."""
|
||||
text = " ".join(doc["doc"])
|
||||
return f" {text}" if text else ""
|
||||
|
||||
|
||||
def render(source, modules):
|
||||
lines = ["---@meta", "", f"-- Generated from {source.relative_to(ROOT)}. Do not edit.", ""]
|
||||
for module in modules:
|
||||
@@ -184,7 +214,18 @@ def render(source, modules):
|
||||
lines.extend(module["preamble"])
|
||||
if module["preamble"]:
|
||||
lines.append("")
|
||||
if module["kind"] != "global":
|
||||
if module["kind"] == "app":
|
||||
lines.append(f"---@class {module['class']}")
|
||||
for field, type_, desc in module["fields"]:
|
||||
lines.append(f"---@field {field} {type_}{(' ' + desc) if desc else ''}")
|
||||
for function, doc in module["functions"]:
|
||||
lines.append(f"---@field {function} {signature(doc)}{describe(doc)}")
|
||||
lines.append("")
|
||||
if module["postamble"]:
|
||||
lines.extend(module["postamble"] + [""])
|
||||
continue
|
||||
|
||||
if module["kind"] != "app":
|
||||
lines.append(f"---@class {module['class']}")
|
||||
for const, type_, _, desc in module["consts"]:
|
||||
lines.append(f"---@field {const} {type_}{(' ' + desc) if desc else ''}")
|
||||
|
||||
+25
-1
@@ -3,7 +3,7 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from gen_api import ROOT, parse
|
||||
from gen_api import ROOT, parse, render
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=ROOT) as directory:
|
||||
@@ -30,4 +30,28 @@ doc = module["functions"][0][1]
|
||||
assert doc["doc"] == ["Reads one complete file."]
|
||||
assert doc["params"][0][2] == "Absolute file path."
|
||||
assert doc["returns"][0][1] == "File contents."
|
||||
|
||||
# Callbacks render as fields of a class an app composes, never as global functions.
|
||||
with tempfile.TemporaryDirectory(dir=ROOT) as directory:
|
||||
source = Path(directory) / "handlers.cpp"
|
||||
source.write_text(
|
||||
"""// @lua-app TouchHandlers
|
||||
// @lua-field home? string The route back lands on.
|
||||
// ---Fired when the finger lands.
|
||||
// @param x integer
|
||||
// @param y integer
|
||||
// @lua-fn onTouchDown?
|
||||
// ---Mounts the route.
|
||||
// @param route string
|
||||
// @param arg string|nil
|
||||
// @lua-fn start
|
||||
"""
|
||||
)
|
||||
output = render(source, parse(source))
|
||||
|
||||
assert "---@class TouchHandlers" in output
|
||||
assert "---@field home? string The route back lands on." in output
|
||||
assert "---@field onTouchDown? fun(x: integer, y: integer) Fired when the finger lands." in output
|
||||
assert "---@field start fun(route: string, arg?: string) Mounts the route." in output
|
||||
assert "function " not in output
|
||||
print("ok")
|
||||
|
||||
Reference in New Issue
Block a user