luau_introspect Module
Parses Luau/asset source TEXT into structured declarations. Pure
string/pattern parsing (string.find / string.match / string.gmatch)
over the literal source — no compiling, no VM spawn, no runtime
state, no engine FFI. Given source text, it extracts what is written
in the literals. This is the shared parser every per-assetType
`M.ref.inspect` hook uses to turn a component/module/tool's own
source into the structured record `asset.inspect` returns.
Structural scanning runs over a COMMENT- and STRING-aware "mask" of the
source: `maskNonCode` produces a same-length copy where comment bodies
and string-literal contents are blanked to spaces (delimiters and
newlines preserved). Because the mask is index-aligned with the source,
pattern matches and brace/paren balancing run on the mask (so a brace,
`Field.`, or `Event(` inside a comment or string never registers), while
captured values are sliced from the ORIGINAL source at the same indices.
Consumers:
local Introspect = require("modules.luau_introspect")
local docs = Introspect.docstrings(src)
local fields = Introspect.publicFields(src)
trim(s: string) → string
Internal: trim leading/trailing whitespace.
| arg | type | description |
|---|
| s | string | |
stripCR(line: string) → string
Internal: strip a trailing carriage return so \r\n source still lines up.
| arg | type | description |
|---|
| line | string | |
longBracketOpen(s: string, i: number) → void
Internal: if a long-bracket opener (`[`, then zero+ `=`, then `[`) starts
at `i`, return (level, contentStart) — the `=` count and the index just
past the opener. Otherwise (nil, nil).
| arg | type | description |
|---|
| s | string | |
| i | number | |
longBracketClose(s: string, contentStart: number, level: number) → number
Internal: index just past the close of a long bracket at `level` whose
content begins at `contentStart`. Returns #s + 1 when unterminated.
| arg | type | description |
|---|
| s | string | |
| contentStart | number | |
| level | number | |
maskNonCode(s: string) → string
Internal: a same-length copy of `s` where comment bodies and
string-literal contents are replaced by spaces. Newlines and string
delimiters are preserved so line structure and "this is a string" cues
survive; every structural character in real CODE stays intact. All entry
detection and brace/paren balancing scans this — captured values are
sliced from the original source at the same (aligned) indices.
| arg | type | description |
|---|
| s | string | |
stripComments(s: string) → string
Internal: `s` with comment bodies removed but string literals kept
verbatim. Used to clean a captured value slice (which may carry a
trailing comment) without destroying a real string default.
| arg | type | description |
|---|
| s | string | |
cleanArg(s: string) → string
Internal: clean a captured value slice — drop any comment, trim edges.
| arg | type | description |
|---|
| s | string | |
balancedParenEnd(mask: string, openIdx: number) → number
Internal: index of the ")" that balances the "(" at `openIdx` in the
mask (nil if unbalanced). The mask has no string/comment content, so a
plain paren-depth count suffices.
| arg | type | description |
|---|
| mask | string | |
| openIdx | number | |
balancedBraceEnd(mask: string, openIdx: number) → number
Internal: index of the "}" that balances the "{" at `openIdx` in the mask.
| arg | type | description |
|---|
| mask | string | |
| openIdx | number | |
argRanges(mask: string, from: number, to: number) → void
Internal: split masked text [from..to] on top-level commas; return the
inclusive [a,b] ranges (full-string coordinates) of each argument.
Nesting inside (), {}, [] is respected. An all-whitespace span yields an
empty list (so "no arguments" reads as zero args), but a trailing comma
still produces its (possibly empty) final range so a caller scanning
from the end can skip it.
| arg | type | description |
|---|
| mask | string | |
| from | number | |
| to | number | |
splitTopLevel(mask: string) → void
Internal: split masked text into top-level comma-separated substrings.
| arg | type | description |
|---|
| mask | string | |
callArgsAt(mask: string, src: string, openIdx: number) → void
Internal: given the "(" index in `mask`/`src` (which are aligned), return
the argument list and the matching ")" index (nil if unbalanced).
| arg | type | description |
|---|
| mask | string | |
| src | string | |
| openIdx | number | |
extractSync(args: { Arg }) → string
Internal: the trailing `Sync` / `NoSync` identifier among the args, or nil.
Scans from the end, skipping empty (trailing-comma) args.
| arg | type | description |
|---|
| args | { Arg } | |
argSrc(args: { Arg }, i: number) → string
Internal: the source text of argument `i`, or nil when it is absent or empty.
| arg | type | description |
|---|
| args | { Arg } | |
| i | number | |
fieldFromCall(name: string, kind: string, args: { Arg }) → FieldEntry
Internal: build a FieldEntry from a parsed `Field.<kind>(...)` call.
| arg | type | description |
|---|
| name | string | |
| kind | string | |
| args | { Arg } | |
parseFieldEntries(bodyMask: string, bodySrc: string) → void
Internal: scan a masked body (with its aligned source) for
`name = Field.<kind>(<args>)` entries. Only literal identifier keys match
— a computed `[expr] = Field...` key is never captured.
| arg | type | description |
|---|
| bodyMask | string | |
| bodySrc | string | |
parseEventEntries(bodyMask: string, bodySrc: string, docs: DocMap) → void
Internal: scan a masked events-block body for `name = Event(schema?,
syncMode?)` entries. `docs` (from `docstrings(src)`) supplies each event's
`desc` when a `--!desc` run sits above its declaration.
| arg | type | description |
|---|
| bodyMask | string | |
| bodySrc | string | |
| docs | DocMap | |
extractAnchor(line: string) → string
Internal: the identifier a doc-comment run binds to, read from the next
code line — the name after `function` / `public:` / `local function` /
`M.` / `public.<name>`, or the key of a `<name> = Field.<...>` /
`<name> = Event(...)` field/event declaration.
| arg | type | description |
|---|
| line | string | |
docstrings(src: string) → DocMap
Scans `src` for `--!desc` / `--!arg` / `--!return` / `--!example`
/ `--!deprecated` / `--!hidden` doc-comment runs and binds each run to
the name of the declaration on the next code line (the identifier after
`function`, `public:`, `local function`, `M.`, `public.<name>`, or a
`<name> = Field.<...>` /
`<name> = Event(...)` field/event declaration). A continuation line
("--! " with no tag word) extends whichever field the run's last tag
opened. Blank and plain `--` comment lines between the run and the
declaration are skipped without terminating the run.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local d = I.docstrings(src); print(d.takeDamage.desc)
appendDesc(text: string) → void
| arg | type | description |
|---|
| text | string | |
lastTarget(t: string) → void
| arg | type | description |
|---|
| t | string | |
lastTarget(t: string) → void
| arg | type | description |
|---|
| t | string | |
lastTarget(t: string) → void
| arg | type | description |
|---|
| t | string | |
lastTarget(t: string) → void
| arg | type | description |
|---|
| t | string | |
literalTableFields(mask: string, src: string, docs: DocMap, tableName: string, results: { FieldEntry }) →
Parses `public = { name = Field.<kind>(...), ... }` table-literal
entries and module-scope `public.<name> = Field.<kind>(...)`
assignments. For a ref constructor (`assetRef` / `dataRef` /
`resource` / `componentRef` / `taggedRef`) the first positional argument
is captured as `category` and the second as `default`. An `enum`
reports its option list as `values`, a `range` its `min` and `max`, a
`bitmask` its width as `bits` — each ahead of its `default` — and an
`alias` the fields it stands for as `targets`, with no default. For
every other kind the first argument is the `default`. All are raw
trimmed source-text
slices, comments stripped. A trailing `Sync`/`NoSync` identifier
becomes `sync`. Entries whose key is a computed/indirect expression
(`[expr] = Field...`) are omitted. Comments and string literals never
register as entries. Folds in `desc` from `docstrings(src)`.
| arg | type | description |
|---|
| mask | string | |
| src | string | Luau/asset source text. |
| docs | DocMap | |
| tableName | string | |
| results | { FieldEntry } | |
examples
local f = I.publicFields(src); print(f[1].name, f[1].category)
publicFields(src: string) → void
| arg | type | description |
|---|
| src | string | |
privateFields(src: string) →
Parses the `private = { name = Field.<kind>(...), ... }` table
literal the way `publicFields` parses `public`: each entry's kind, its
default and the arguments ahead of it, and its `sync`. A private field
a component marks `Serialized` is saved with the scene like a public
one, so a reader comparing saved state against declared defaults needs
both.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local f = I.privateFields(src); print(f[1].name, f[1].default)
events(src: string) →
Parses `events = { name = Event(payloadSchema?, syncMode?), ... }`
table-literal entries. The payload schema — a `{ k = Field.<kind>(...) }`
table — is parsed like `publicFields` and reduced to `{name, type}`
pairs; a payloadless `Event()` yields an empty payload. `sync` is true
only when the SECOND positional `Event` argument is the literal
identifier `Sync`. Comments and string literals never register. Folds in
`desc` from `docstrings(src)`.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local e = I.events(src); print(e[1].name, e[1].sync)
signatureFrom(mask: string, openIdx: number) → void
Internal: parse the parameter list + return annotation of a function
whose `(` is at `openIdx` in the mask. Returns the param entries, the
return-type string (nil when unannotated), and the index just past the
close (or `openIdx + 1` when the parens are unbalanced). A `...` variadic
is captured with `name = "..."`.
| arg | type | description |
|---|
| mask | string | |
| openIdx | number | |
methods(src: string) →
Parses `function public:<name>(<params>)` and `typed function
public:<name>(<params>)` declarations — with an optional `: <ret>`
return-type annotation immediately after the closing paren — into
`{name, params, returns}`. Each param is split on top-level commas
into `{name, type?}`; a `...` variadic is captured with `name = "..."`.
Folds in `desc` from `docstrings(src)`.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local m = I.methods(src); print(m[1].name, m[1].returns)
refMethods(src: string) →
Parses the methods a `behavior.luau`-style module exposes on a
`M.ref = { name = fn, ... }` table — the assetType's per-asset behavior
surface (`ref:method(...)`). Each entry's key maps to its backing
`local function <fn>(self, <params>): <ret>` (or `function <fn>(...)`)
declaration: the leading `self` parameter is dropped (it is the ref the
method is called on), the remaining params + return annotation + the
declaration's `--!desc` are captured, with `hidden` and `deprecated`
from its `--!hidden` / `--!deprecated` lines, so a listing can leave a
hidden method out while a type shape still carries it. A key whose
value is a table literal or an expression other than a bare function
name is skipped.
Runs over the comment/string-aware mask, so a commented `M.ref` never
registers.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local api = I.refMethods(behaviorSrc); print(api[1].name, api[1].returns)
lifecycleHooks(src: string, catalog: { string }) →
Finds top-level `function <name>(` declarations whose name is in
the caller-supplied `catalog`. Excludes `local function`, `public:`
methods, and `M.` exports — only a bare top-level `function NAME(`
declaration matches. Never hardcodes the lifecycle-callback catalog;
the caller passes the list of names to match against. Commented-out
declarations never match.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
| catalog | { string } | Array of lifecycle-callback names to match against. |
examples
local hooks = I.lifecycleHooks(src, { "awake", "update" })moduleExports(src: string) →
Parses `M.<name> = function(...)` assignments, `function
M.<name>(...)` declarations, and a trailing `return { foo = foo, ... }`
export table into `{name, kind, signature?, desc?}`. `kind` is
`"function"` for a function assignment/declaration, `"value"` for any
other `M.<name> = <expr>` assignment (a `==` comparison is not an
assignment). A name that appears only in the `return { ... }` table
infers its kind from whether a same-named `function <name>(` /
`local function <name>(` declaration exists. Folds in `desc` from
`docstrings(src)`.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local exports = I.moduleExports(src); print(exports[1].name)
ensure(name: string) → ExportEntry
| arg | type | description |
|---|
| name | string | |
signatureAt(openIdx: number) → string
| arg | type | description |
|---|
| openIdx | number | |
maskNonCode(src: string) → string
A same-length copy of `src` where comment bodies and string-literal
contents are replaced by spaces (delimiters and newlines preserved),
so structural pattern matching over the result never registers a
comment or string body. Index-aligned with `src` — a match position in
the mask is the same position in `src`.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
examples
local mask = I.maskNonCode(src); string.find(mask, "operations%s*=%s*{")tableLiteralKeys(src: string, assignmentName: string) →
Finds `<assignmentName> = { ... }` in `src` and returns the top-level
literal-identifier keys of that table (e.g. `tableLiteralKeys(src,
"operations")` on `operations = { generate = {...}, from_image =
{...} }` returns `{"generate","from_image"}`). Runs over the
comment/string-aware mask, so a `--` comment or string literal
mentioning the assignment name never registers. Comment/string/nesting
inside a value never breaks the scan (balanced brace matching, same as
`publicFields`/`events`). A computed `["key"]` entry is omitted — only
bare identifier keys are recognised. Returns `{}` when the assignment
is absent.
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
| assignmentName | string | The table's assignment name (e.g. "operations"). |
examples
local ops = I.tableLiteralKeys(src, "operations")
parseTableBody(mask: string, src: string, from: number, to: number) → TableNode
Internal: parse the top-level `key = value` entries of a table body whose
masked/source slices span the full-coordinate range [from, to]. A value
that starts with `{` recurses into a nested `TableNode`; any other value is
captured as its trimmed source-text `scalar` (quotes and all). Computed
(`[expr] =`) and positional entries are skipped — only bare identifier keys.
| arg | type | description |
|---|
| mask | string | |
| src | string | |
| from | number | |
| to | number | |
tableLiteral(src: string, assignmentName: string) → TableNode
Parses a named table literal `assignmentName = { ... }` into a nested
`TableNode` — each top-level `key = value` becomes an entry whose value is
either a `scalar` (the trimmed source text of a non-table value, quotes
included) or a nested `table` (`TableNode`) when the value is itself a
`{ ... }`. Runs over the comment/string-aware mask, so a `key`, `{`, or
`}` inside a comment or string never registers. Computed (`[expr] =`) and
positional entries are skipped. Returns nil when the assignment is absent.
"operations" or "M%.tokens").
| arg | type | description |
|---|
| src | string | Luau/asset source text. |
| assignmentName | string | The table's assignment name (a Lua pattern, e.g. |
examples
local ops = I.tableLiteral(src, "operations")
forSource(src: string, computeFn: (string) →
Memoises `computeFn(src)` keyed on `src` itself in a bounded
module-local table. The same source text returns the cached result
without re-invoking `computeFn`; different source text recomputes. The
cache is capped (oldest key evicted first), so an evicted key
recomputes on its next request.
`computeFn`.
| arg | type | description |
|---|
| src | string | Source text — both the memo key and the argument passed to |
| computeFn | (string | Called as `computeFn(src)` on a cache miss. |
examples
local detail = I.forSource(src, buildDetail)