Skip to content

feat(ladfile_builder): First look into a native Luau (.d.luau) LAD backend#546

Open
jonasdedden wants to merge 1 commit into
makspll:mainfrom
jonasdedden:feat/luau-lad-backend
Open

feat(ladfile_builder): First look into a native Luau (.d.luau) LAD backend#546
jonasdedden wants to merge 1 commit into
makspll:mainfrom
jonasdedden:feat/luau-lad-backend

Conversation

@jonasdedden

Copy link
Copy Markdown

This is a first proposal for generating Luau type definition files (corresponding issue: #545). Please let me know whether you like the general approach or not.

Tests

src/lib.rs carries module tests that parse a real registry dump (src/test_assets/bindings.lad.json) and assert both modes:

  • converts_general_surface — classes/fields/world/host globals present, and no Reg<T> leakage in the default output.
  • converts_branded_surfaceReg<T>, the generic get_component, and the …Type handle branding all present when opted in.
  • sanitizes_and_escapes — reserved-word handling.

Plus a well-formedness sweep asserting no Luau keyword is emitted as a bare field/method/param name. cargo test in this folder runs them against ladfile.

Luau grammar edges handled

  • reserved words can't be bare identifiers — quoted as ["end"] for fields, suffixed (end_) for methods/params/type names;
  • the unit type is () only in a return arrow — rendered as nil elsewhere;
  • BMS exposes every reflected type as a static accessor global — is_static separates those from real instance handles like world;
  • unknown/foreign types resolve to any (Luau treats it permissively), kept focused via focus_crates.

bevy_mod_scripting can dump its reflection registry to a LAD file and ships
post-processors for the Lua Language Server (`--- @class` .lua) and mdbook, but
no native Luau one — and luau-lsp cannot consume the LuaLS dialect. Luau users
(the `luau` runtime feature already exists) therefore have no way to type-check
their scripts from the registry.

Add that backend as a feature-gated module in ladfile_builder:

- `luau::lad_to_luau(&LadFile, &LuauBackendConfig) -> String`: the whole backend
  as a pure conversion, emitting `declare class … end` / `declare name: T`.
- `luau::LuauLadPlugin`: a `LadFilePlugin` processor, wired into
  `default_processors()` behind the new `luau_files` feature.

It needs no new dependencies (pure string generation over the `ladfile` types),
which is why it's a module here rather than a separate backend crate like the
LuaLS one. Handles Luau grammar edges: reserved words can't be bare identifiers
(quoted `["end"]` for fields, suffixed elsewhere), the unit type is `()` only in
a return arrow (`nil` otherwise), static accessor globals are excluded from real
instance handles, and unknown types resolve to `any`. `focus_crates` scopes which
crates' types get full classes so the output stays readable.

An opt-in `HandleBranding` config emits a `Reg<T>` phantom brand: it rewrites the
component getter to `get_component: <T>(…, reg: Reg<T>) -> T?` and brands each
host-registered `<Component><suffix>` registration global as `Reg<Component>`,
giving cast-free, statically-typed component access. Off by default since it
assumes a host naming convention.

Tested against `ladfile::EXAMPLE_LADFILE` (general surface, brand gating) and a
small crafted fixture (full brand path); generated output verified to load and
type-check under luau-lsp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@semanticdiff-com

semanticdiff-com Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  crates/ladfile_builder/CHANGELOG.md Unsupported file format
  crates/ladfile_builder/Cargo.toml Unsupported file format
  crates/ladfile_builder/src/lib.rs  0% smaller
  crates/ladfile_builder/src/luau.rs  0% smaller
  crates/ladfile_builder/src/plugin.rs  0% smaller
  crates/ladfile_builder/src/test_assets/branded.lad.json  0% smaller

@makspll makspll left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I had a look and I can see at least a couple problems:

  • the whole implementation lives in ladfile_builder, that crate serves to purely create ladfiles not provide language specific implementations, the whole implementation should live in its own crate
  • Some conventions here are somewhat opinionated and require non-default bindings (branding for example), generally in LAD backends this is not the case and type code only describes what already exists in BMS
  • Escaped identifiers (with underscore suffix) don't have support in the LUA backend in BMS, so even though you type them this way they won't have anything backing them.
  • Hardcoding types like World and Entity, these do also live in ladfiles so backends should be able to deal with them fully dynamically with some minor exceptions (for example ladfiles store 'is_core' information which is also user customizable, which drives type priority in generated documentation for example)

Overall especially since I am not familiar with luau too much, I'd suggest the following:

  • Since new LAD backends don't require to live in the BMS crate, this can exist in its own separate crate fully
  • Once it has gained traction and many BMS consumers are using it, I would consider then merging the crate into the BMS project after all the kinks are worked out

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Binding Differences Detected

The following changes were detected in generated bindings:

b/crates/bindings/bevy_asset_bms_bindings/src/lib.rs
index 776c17c..312c549 100644
--- a/crates/bindings/bevy_asset_bms_bindings/src/lib.rs
+++ b/crates/bindings/bevy_asset_bms_bindings/src/lib.rs
@@ -334,7 +334,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The bitwise negation (`!`) of the bits in a flags value, truncating the result.",
+            " The bitwise negation (`!`) of the bits in `self`, truncating the result.",
             &["_self"],
         )
         .register_documented(
@@ -355,7 +355,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " Whether all set bits in a source flags value are also set in a target flags value.",
+            " Whether all set bits in `other` are also set in `self`.",
             &["_self", "other"],
         )
         .register_documented(
@@ -376,7 +376,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The intersection of a source flags value with the complement of a target flags\n value (`&!`).\n This method is not equivalent to `self & !other` when `other` has unknown bits set.\n `difference` won't truncate `other`, but the `!` operator will.",
+            " The intersection of `self` with the complement of `other` (`&!`).\n This method is not equivalent to `self & !other` when `other` has unknown bits set.\n `difference` won't truncate `other`, but the `!` operator will.",
             &["_self", "other"],
         )
         .register_documented(
@@ -466,7 +466,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The bitwise or (`|`) of the bits in two flags values.",
+            " The bitwise or (`|`) of the bits in `self` and `other`.",
             &["_self", "other"],
         )
         .register_documented(
@@ -487,7 +487,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The bitwise and (`&`) of the bits in two flags values.",
+            " The bitwise and (`&`) of the bits in `self` and `other`.",
             &["_self", "other"],
         )
         .register_documented(
@@ -508,7 +508,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " Whether any set bits in a source flags value are also set in a target flags value.",
+            " Whether any set bits in `other` are also set in `self`.",
             &["_self", "other"],
         )
         .register_documented(
@@ -542,7 +542,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " Whether all bits in this flags value are unset.",
+            " Whether all bits in `self` are unset.",
             &["_self"],
         )
         .register_documented(
@@ -563,7 +563,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The intersection of a source flags value with the complement of a target flags\n value (`&!`).\n This method is not equivalent to `self & !other` when `other` has unknown bits set.\n `remove` won't truncate `other`, but the `!` operator will.",
+            " The intersection of `self` with the complement of `other` (`&!`).\n This method is not equivalent to `self & !other` when `other` has unknown bits set.\n `remove` won't truncate `other`, but the `!` operator will.",
             &["_self", "other"],
         )
         .register_documented(
@@ -606,7 +606,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The intersection of a source flags value with the complement of a target flags value (`&!`).\n This method is not equivalent to `self & !other` when `other` has unknown bits set.\n `difference` won't truncate `other`, but the `!` operator will.",
+            " The intersection of `self` with the complement of `other` (`&!`).\n This method is not equivalent to `self & !other` when `other` has unknown bits set.\n `difference` won't truncate `other`, but the `!` operator will.",
             &["_self", "other"],
         )
         .register_documented(
@@ -627,7 +627,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The bitwise exclusive-or (`^`) of the bits in two flags values.",
+            " The bitwise exclusive-or (`^`) of the bits in `self` and `other`.",
             &["_self", "other"],
         )
         .register_documented(
@@ -648,7 +648,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The bitwise exclusive-or (`^`) of the bits in two flags values.",
+            " The bitwise exclusive-or (`^`) of the bits in `self` and `other`.",
             &["_self", "other"],
         )
         .register_documented(
@@ -669,7 +669,7 @@ pub(crate) fn register_render_asset_usages_functions(world: &mut World) {
                 };
                 output
             },
-            " The bitwise or (`|`) of the bits in two flags values.",
+            " The bitwise or (`|`) of the bits in `self` and `other`.",
             &["_self", "other"],
         );
     let registry = world.get_resource_or_init::();

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchfeat/luau-lad-backend
Testbedlinux-gha

⚠️ WARNING: Truncated view!

The full continuous benchmarking report exceeds the maximum length allowed on this platform.

🐰 View full continuous benchmarking report in Bencher

@Ownezx

Ownezx commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

I was following this to see where it would go. I've been using Luau in my project and I am interested in creating LAD files for it.

If you do create a new crate with the LAD backend please let me know, I'd be happy to give it a go and give feedback. If I have the energy I might also contribute.

@jonasdedden

Copy link
Copy Markdown
Author

Hey @makspll and @Ownezx,

Thanks for your feedback! I'm happy to extract this into a separate crate that I can bring upstream on my own for now. :) The current status is just a reflection of the bare minimum vibecoded status that I'm using right now locally to enable it in general, happy to clean it up based on the remarks you gave.

I really want to prevent name collisions and unwarranted connections to your project: would you be fine with the name bevy_mod_scripting_luau? If you would be happy with a different crate name, please let me know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants