-
Notifications
You must be signed in to change notification settings - Fork 81
Add native JSX syntax support #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,338 @@ | ||
| # JSX syntax for Luau via `.luax` (experimental) | ||
|
|
||
| ## Summary | ||
|
|
||
| This RFC proposes adding an **optional JSX-like syntax** to Luau, enabled only for files with a `.luax` extension and gated behind a feature flag, that desugars directly into existing Luau AST constructs (no separate transpile step). | ||
|
|
||
| ## Motivation | ||
|
|
||
| JSX is a concise, widely understood syntax for describing UI trees. In Roblox’s ecosystem, UI is commonly built using React-like libraries (e.g. Roact/React), but Luau today requires verbose `createElement(...)` calls or helper builders. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
<div class="foo">
<child />
<child data="other" />
</div>{ tag = "div", class = "foo",
{ tag = "child" },
{ tag = "child", data = "other" },
}If you're willing to write a small bit of helper functions, you can even have: div { class = "foo",
child {},
child { data = "other" }
}You can try to make an argument that you nevertheless think XML syntax is so compelling as to be worth building into the language, but the motivation of "a library I use has an unpleasant API" is not really a problem that the language should be solving.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we updated the React API in this way, a whole bunch of problems line up neatly: Things like syntax highlighters and autocomplete will work perfectly with no code changes required on their part.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The argument indeed boils down to compelling syntax for declarative tree structures, but the framing of "library I use has an unpleasant API" is a bit of a straw man (or at least I could have done better to justify it) as the RFC describes why this isn't just a React-motivated value-add - harnessing it with other UI frameworks, declarative scene compositions, behavior trees, state machines, etc - none of which require React to find justify utility. Your proposed syntax using pure tables has less legibility than XML does as everything has the same "weight" or treatment for parts that mean very different things. your third proposal is a bit better (though I still would argue not as good as XML for reason above, but now you have to require every intrinsic element before use which is overkill.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've long said createElement is an API so bad that to make it usable javascript had to create new syntax. We don't need to make the same mistake of creating new syntax to make a bad API usable. Just make a better API. aatxe's third example is a much better API, and is (almost) the API that the popular UI library Vide uses, and people using Vide don't think the language needs XML syntax. tldr: the language should not change for React, React should change for the language |
||
|
|
||
| This proposal aims to: | ||
|
|
||
| - Make UI code **more readable** and **more ergonomic**. | ||
| - Preserve Luau’s “no mandatory build step” workflow by implementing parsing in the language front-end. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This point is very unconvincing because it only makes sense if you already believe that XML must be part of the syntax. If you don't, then making an optional tool that compiles your XML syntax to Luau (which is how this works in the actual web ecosystem you're cribbing it from) seems like the obvious solution.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the primary justification for "no build" is that Luau has already made a similar decision in regards to types which also could have been stripped via transpilation similar to TS->JS. Both approaches have their benefits, but one major win for going native is the amount of machinery/pipelines that become involved with build steps which is a common pain-point in the web ecosystem |
||
| - Avoid hard-coding any specific UI library into the language by desugaring to a **user-provided factory function**. | ||
|
|
||
| ### Why JSX belongs at the language level (and is not “React syntax”) | ||
|
|
||
| A common concern is that JSX is “too specific” to React or UI frameworks to warrant native syntax. This RFC argues the opposite: **JSX is a general-purpose notation for declarative, structured function calls**, and the runtime meaning is entirely controlled by the `jsx` factory function chosen by the host/project. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is extremely jarring, and an immediate hard no for this feature. We will not add syntactic sugar that expands to a magically named function (calling is
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can understand this perspective. Shipping a language feature that is by default broken does seem like a hard pill to swallow. Your proposal of defaulting to a data table is an elegant solution. Will revise. |
||
|
|
||
| In other words, JSX is not “React in the language”; it is: | ||
|
|
||
| - A compact syntax for constructing nested call trees (`jsx(tag, props, ...children)`). | ||
| - A convenient way to bind named parameters via record-like props (`{ key = value }`). | ||
| - A structured form that is easier for humans and tools to read than deeply nested function calls. | ||
|
|
||
| This makes JSX useful beyond UI, including data-driven scene graphs, entity graphs, or declarative configuration trees. | ||
|
|
||
| ## Design | ||
|
|
||
| ### Goals | ||
|
|
||
| - **No transpilation step**: JSX is parsed directly by Luau’s parser when enabled. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to say this. Anything that is a language feature is parsed by the language. |
||
| - **Opt-in**: no behavior change for `.lua` / `.luau` unless explicitly enabled. | ||
| - **Library-agnostic**: JSX lowers to an ordinary Luau call (`jsx(...)`) so projects can bind it to Roact/React/etc. | ||
| - **Minimize grammar ambiguity** with existing and future Luau syntax. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also a requirement for all syntax proposals too, it doesn't need to be said to be a goal. |
||
|
|
||
| ### Non-goals (initially) | ||
|
|
||
| These are intentionally excluded from the initial proposal to reduce ambiguity and implementation risk: | ||
|
|
||
| - Raw text children (e.g. `<Foo>hello</Foo>`) | ||
| - Fragments (e.g. `<>...</>`) | ||
| - Spread attributes (e.g. `<Foo {...props} />`) | ||
| - Namespaced attributes (e.g. `on:click=...`) | ||
| - JSX comments syntax | ||
|
|
||
| These can be added via follow-on RFCs once the core parsing approach proves robust. | ||
|
|
||
| ### Opt-in surface area | ||
|
|
||
| **File extension**: `.luax` indicates JSX may appear in the file. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file extension should clearly be
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can revise. Extension is just for cli use. My implementation also accepts a parseOptions.allowJsx for embedded contexts |
||
|
|
||
| **Feature flag**: The syntax must be gated by a flag (e.g. `FFlag::LuauJsx`) so it can ship experimentally and be rolled out carefully. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All changes to the language must be flagged. This doesn't need to be in the RFC at all. |
||
|
|
||
| ### Syntax | ||
|
|
||
| When enabled, an additional `simpleexp` form is introduced: | ||
|
|
||
| - `jsxElement ::= '<' jsxTagName jsxAttributes ( '/>' | '>' jsxChildren '</' jsxTagName '>' )` | ||
| - `jsxTagName ::= Name { '.' Name }` | ||
| - `jsxAttributes ::= { jsxAttribute }` | ||
| - `jsxAttribute ::= Name [ '=' ( String | '{' expr '}' ) ]` | ||
| - `jsxChildren ::= { jsxChild }` | ||
| - `jsxChild ::= jsxElement | '{' expr '}'` | ||
|
|
||
| Notes: | ||
|
|
||
| - `{ expr }` is used for embedding arbitrary Luau expressions in children and attribute values. | ||
| - Attribute shorthand (`<Foo disabled />`) is allowed and is equivalent to `disabled={true}`. | ||
|
|
||
| ### Desugaring | ||
|
|
||
| JSX constructs are desugared in the parser into an ordinary Luau function call: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is missing an example, it seems. |
||
|
|
||
| #### Tag lowering | ||
|
|
||
| - If the tag begins with a lowercase identifier and has no dots (e.g. `<frame />`), it lowers to a string tag: `"frame"`. | ||
| - Otherwise it lowers to an identifier/dotted expression (e.g. `<Foo.Bar />` lowers to `Foo.Bar`). | ||
|
Comment on lines
+78
to
+79
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels very non-obvious, and like it would be a constant footgun for using this feature. Why is this the semantics you want to propose? Why are we not proposing a single, consistent semantics for tags?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the convention of JSX, which alone is not justification for how we should do it, but does give the benefit of skill transferability for developers already familiar with it. One counter argument against the convention is it makes mapping strings to intrinsic elements harder to manage as theres no guarantee your instances can match the casing (like Roblox's instances which typically use pascal case) which will usually require an implementer to provide a mapping from strings to intrinsic element names. Again the largest argument against this is to leverage familiar widely adopted conventions.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative approach is to wrap intrinsics in quotes and anything else is a function |
||
|
|
||
| #### Props lowering | ||
|
|
||
| Attributes lower into a table literal (record fields). If there are no attributes, props lower to `nil`. | ||
|
|
||
| #### Children lowering | ||
|
|
||
| Children lower into additional positional arguments after `(tag, props)`. | ||
|
|
||
| #### Examples | ||
|
|
||
| Self-closing: | ||
|
|
||
| ```lua | ||
| return <Foo bar={x} /> | ||
| ``` | ||
|
|
||
| Desugars to (conceptually): | ||
|
|
||
| ```lua | ||
| return jsx(Foo, { bar = x }) | ||
| ``` | ||
|
|
||
| Lowercase tag: | ||
|
|
||
| ```lua | ||
| return <frame /> | ||
| ``` | ||
|
|
||
| Desugars to: | ||
|
|
||
| ```lua | ||
| return jsx("frame", nil) | ||
| ``` | ||
|
|
||
| Nested child: | ||
|
|
||
| ```lua | ||
| return <Foo><Bar /></Foo> | ||
| ``` | ||
|
|
||
| Desugars to: | ||
|
|
||
| ```lua | ||
| return jsx(Foo, nil, jsx(Bar, nil)) | ||
| ``` | ||
|
|
||
| Boolean attribute shorthand: | ||
|
|
||
| ```lua | ||
| return <Foo disabled /> | ||
| ``` | ||
|
|
||
| Desugars to: | ||
|
|
||
| ```lua | ||
| return jsx(Foo, { disabled = true }) | ||
| ``` | ||
|
|
||
| ### Runtime / library integration | ||
|
|
||
| Luau does **not** include React/Roact. JSX desugars to a call to a function named `jsx`. Projects can provide this in whichever way fits their runtime: | ||
|
|
||
| - `jsx = React.createElement` | ||
| - `jsx = Roact.createElement` | ||
| - `jsx = MyCustomElementFactory` | ||
|
Comment on lines
+143
to
+145
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be a strange oddity in the language. There is no feature in Luau whose behavior is defined by you assigning to a magic name. It's inconsistent, and the RFC says nothing of what you intend to happen when the user, inevitably, does not author one of these assignments to give it semantics. The default outcome would be "'nil' cannot be called" which is entirely divorced from what the user wrote, and would require them to understand exactly how this extension was implemented to know what is going on.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intended result would indeed be "attempt to call nil" but as discussed above I agree this isn't ideal and should instead provide a functional default behavior of building a data table |
||
|
|
||
| This keeps the language neutral and allows different runtimes to adopt the syntax without coupling Luau to any one library. | ||
|
|
||
| #### Intrinsics mapping for lowercase tags | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole section feels like a direct argument for why the split behavior in your "tag lowering" section is a mistake. One piece of the design is creating a new problem that the other part solves. We should simply not create the problem.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Responded in another comment thread with justifications and alternative approach |
||
|
|
||
| Because lowercase tags lower to string values (e.g. `<frame />` → `jsx("frame", ...)`), most runtimes will want a small **lookup table** that maps these “intrinsic” names to the runtime’s native element types (for Roblox, typically Instance class names such as `"Frame"`). | ||
|
|
||
| For example: | ||
|
|
||
| ```lua | ||
| local Intrinsics = { | ||
| frame = "Frame", | ||
| textlabel = "TextLabel", | ||
| uilistlayout = "UIListLayout", | ||
| -- ... (many more) | ||
| } | ||
|
|
||
| function jsx(tag, props, ...) | ||
| if type(tag) == "string" then | ||
| local className = Intrinsics[tag] | ||
| assert(className, ("Unknown intrinsic tag %q"):format(tag)) | ||
| -- dispatch to your runtime using className | ||
| else | ||
| -- tag is a component (function/table/etc.) -> dispatch accordingly | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| This approach is explicit and predictable, avoids ambiguous casing rules, and allows aliases (`div = "Frame"`) if desired. | ||
|
|
||
| ### Example: binding JSX to Fusion (community framework) | ||
|
|
||
| Fusion is a popular community framework for Roblox that uses a declarative style for Instances and reactive state. Without changing the JSX proposal, a project can adopt JSX by binding `jsx` to a Fusion element factory. | ||
|
|
||
| In practice, Fusion code commonly looks like `Fusion.New("Frame"){ ... }` and uses special keys like `Fusion.Children` for child lists. The exact adapter shape may vary by project, but a representative sketch is: | ||
|
|
||
| ```lua | ||
| local Fusion = require(Packages.Fusion) | ||
| local New = Fusion.New | ||
| local Children = Fusion.Children | ||
|
|
||
| local Intrinsics = { | ||
| textlabel = "TextLabel", | ||
| frame = "Frame", | ||
| -- ... | ||
| } | ||
|
|
||
| -- One possible adapter shape: | ||
| -- jsx("frame", props, ...children) -> Fusion.New("Frame")(props) | ||
| -- jsx(FooComponent, props, ...children) -> FooComponent(props, ...children) (project-defined) | ||
| function jsx(tag, props, ...) | ||
| props = props or {} | ||
| props[Children] = { ... } | ||
|
|
||
| if type(tag) == "string" then | ||
| return New(Intrinsics[tag])(props) | ||
| else | ||
| -- component case is project-defined; this is just a sketch | ||
| return tag(props) | ||
| end | ||
| end | ||
|
|
||
| local State = Fusion.Value("Hello") | ||
|
|
||
| local ui = | ||
| <textlabel | ||
| Text={State} | ||
| Size={UDim2.fromScale(1, 1)} | ||
| /> | ||
| ``` | ||
|
|
||
| This example illustrates the key point: **JSX is a “call tree” surface syntax**, and Fusion (not Luau) defines the semantics. | ||
|
|
||
| ### Example: non-UI usage (scene graph / “fiber-like” tree) | ||
|
|
||
| JSX can also be used for non-UI declarative graphs, e.g. building a 3D scene tree out of Roblox Instances (a “fiber-like” idea: a declarative tree that produces runtime objects). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really think we need to have an argument about the idea that XML syntax, whose entire raison d'etre is "authoring trees," can represent trees. Nobody is disputing that, the points of contention, as you can hopefully tell from the rest of the comments here, would be things like "what does embedded XML syntax in Luau mean?" and "does it even make sense for the language to concern itself with hybridizing itself with another language?" Those are the questions that this RFC needs to answer, and it does the former (though with details I think are insufficient to make the cut for the language), and seems to make no argument for the latter.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair. Will make a revision with clearer arguments |
||
|
|
||
| ```lua | ||
| local Intrinsics = { | ||
| model = "Model", | ||
| part = "Part", | ||
| pointlight = "PointLight", | ||
| attachment = "Attachment", | ||
| -- ... | ||
| } | ||
|
|
||
| -- A minimal Instance-producing factory: | ||
| function jsx(tag, props, ...) | ||
| assert(type(tag) == "string", "This example expects intrinsic (lowercase) tags") | ||
| local className = assert(Intrinsics[tag], ("Unknown intrinsic tag %q"):format(tag)) | ||
|
|
||
| local inst = Instance.new(className) | ||
| props = props or {} | ||
|
|
||
| -- Apply properties | ||
| for k, v in pairs(props) do | ||
| inst[k] = v | ||
| end | ||
|
|
||
| -- Parent children to this instance | ||
| for _, child in ipairs({ ... }) do | ||
| if typeof(child) == "Instance" then | ||
| child.Parent = inst | ||
| end | ||
| end | ||
|
|
||
| return inst | ||
| end | ||
|
|
||
| local scene = | ||
| <model Name="Root"> | ||
| <part Name="Ground" Anchored={true} Size={Vector3.new(50, 1, 50)} /> | ||
| <part Name="Lamp" Position={Vector3.new(0, 5, 0)}> | ||
| <pointlight Brightness={3.5} Range={24} /> | ||
| </part> | ||
| </model> | ||
| ``` | ||
|
|
||
| This keeps the same core idea: the tag is a string, props are a table, and children are a list. The meaning is defined by the chosen runtime factory (`jsx`), and JSX is simply a concise way to express the tree. | ||
|
|
||
| ### Compatibility & ambiguity analysis | ||
|
|
||
| #### Backwards compatibility | ||
|
|
||
| With `.luax` + flag gating, existing `.lua`/`.luau` source is unaffected. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Flags are flipped on and that leads to the feature being enabled, so flags are not an argument for existing programs being unaffected. Indeed, most users of Luau are running it in environments where every flag that starts with
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add details about parseOptions as mentioned above |
||
|
|
||
| Within `.luax`, code that uses `<` for comparisons (e.g. `a < b`) remains valid; JSX is only recognized when the parser expects a `simpleexp` and sees `<` as the next token in that position. | ||
|
|
||
| #### Grammar ambiguity concerns | ||
|
|
||
| Key ambiguity sources: | ||
|
|
||
| - `<` as binary operator vs JSX element start | ||
| - `</` sequences inside other contexts | ||
| - Interaction with existing type syntax, including explicit type instantiation (`<<T>>()`) | ||
|
|
||
| Mitigations: | ||
|
|
||
| - Restrict JSX recognition to positions where a `simpleexp` is parsed. | ||
| - Keep JSX syntax small initially (no raw text children, fragments, or spreads). | ||
| - Continue gating behind a flag to allow collecting real-world compatibility data. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Flagging is not a mitigation against syntax ambiguity. We are not going to add ambiguous syntax to Luau intentionally. So, either the syntax is unambiguous and can be considered for addition to the language, or it is ambiguous and needs to be redesigned with that consideration in mind. |
||
|
|
||
| ### Editor integration & tooling considerations | ||
|
|
||
| Adding JSX affects: | ||
|
|
||
| - Tokenization/highlighting for `.luax` | ||
| - Parsing for autocomplete, formatter/pretty-printer, and incremental parse | ||
| - Error recovery expectations (JSX introduces new “paired delimiter” structures) | ||
|
|
||
| Mitigations: | ||
|
|
||
| - Keep JSX AST lowering to existing nodes so downstream tools operate on familiar AST shapes. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You earlier argued that this would not use a compilation step, and then this sentence is literally describing a compilation step. This clouds my understanding of what you intend a lot here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The point being made here is that there are no new AST nodes to deal with here either internally by the interpreter or externally if there are other tools that do depend on luau's AST. I'm not arguing for a build step as part of this proposal. That said, as mentioned in another comment, it may be desirable to introduce new AST nodes for exactly this reason - so external consumers can leverage them for things like syntax highlighting, formatting, etc. this should be revised to state there should revised to say no new CST nodes, but would likely introduce new AST ones (I'm making some assumptions about the internal workings of luau I need to double check) |
||
| - Ensure error recovery always makes progress to avoid hangs in partial code states. | ||
| - Treat `.luax` as a distinct language mode/extension for editors. | ||
|
|
||
| ### Security / sandboxing considerations | ||
|
|
||
| The syntax itself is not inherently unsafe, but it makes calling a factory function extremely easy. Sandboxing concerns are the same as for ordinary function calls: | ||
|
|
||
| - The host controls which globals exist (including `jsx`). | ||
| - Environments can restrict or replace `jsx` to safe implementations. | ||
|
Comment on lines
+302
to
+307
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, this section gets at something that was not obvious at all earlier. You intend on the function being a built-in global provided by the embedder. That's certainly more reasonable than "the user will author an assignment statement somewhere," but the RFC needs to both be much, much clearer that that is the intention, and also needs to actually describe what the semantics of the builtin function is for standalone Luau. It's nevertheless still a design I find uncomfortable though because it feels like it immediately leads to questions for every embedder of "what framework do I privilege by making it the default here?" which is not something we want to require for users of the language.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes sorry I'll make this point much clearer. I think the language default of outputting a structured table is a reasonable one that doesn't bless any particular framework. In certain contexts, blessing is exactly what you want. My ideal for Roblox for example is it defaults to instantiating native instances (non-reactive). I have a demo of this behavior that feels great for Roblox |
||
|
|
||
| ### Performance considerations | ||
|
|
||
| - Parsing adds new branches and lookahead, but is expected to be negligible compared to overall parsing cost. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a non-statement about the performance considerations. Earlier, you were saying that the syntax might be ambiguous (or rather, you were ambiguous about whether or not the syntax is unambiguously parseable). If it isn't, then there's considerable performance considerations that you're glossing over entirely here by just saying "it adds some new branches and lookahead." Adding more lookahead to the parser can entirely change the overall complexity class of it! That is not a trivial addition, and it is not something we can gloss over when designing new syntax. |
||
| - Desugaring into existing AST nodes avoids adding new runtime node kinds and keeps later stages (typechecking, codegen) unchanged. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Luau's code base works directly on a single AST today. You're proposing adding a multi-stage compiler effectively, which while reasonable is, again, a much more significant architectural change to the product than how you're trying to describe it here. |
||
|
|
||
| ### Status | ||
|
|
||
| **Implemented (flagged / experimental)** in this repository as a spike: | ||
|
|
||
| - `FFlag::LuauJsx` gating | ||
| - `.luax` extension used to enable `ParseOptions::allowJsx` | ||
| - Parser desugars JSX to `jsx(tag, props, ...children)` | ||
| - Parser tests cover collision edge cases and error recovery | ||
|
|
||
| This RFC is still required before any user-facing rollout. | ||
|
|
||
| ## Drawbacks | ||
|
|
||
| - Adds a significant new surface area to the language syntax, which increases learning and maintenance cost. | ||
| - JSX has a history of subtle grammar ambiguities; even with careful gating, future syntax additions may conflict. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a huge drawback that runs dramatically against the interests of the language. You'll need to make a very, very strong case that embedding XML into Luau would be so useful as to be worth a miserable experience trying to make any other change to the syntax in the future. |
||
| - Editor/tooling support must be updated (highlighting, formatting, autocomplete), and mismatches can degrade UX. | ||
| - The “factory function” convention (`jsx`) is another community contract to standardize (or allow configuring). | ||
|
|
||
| ## Alternatives | ||
|
|
||
| 1. **Do nothing**: keep using `createElement(...)` calls and helper builders. | ||
| 2. **Library-level DSL**: implement an embedded DSL using Lua syntax only (functions/tables/metatables). This avoids parser changes but is typically more verbose and harder to read. | ||
| 3. **Transpile step**: introduce a `.luax -> .luau` transformer. This conflicts with the “interpreted/no build step” constraint and complicates tooling. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You invented the constraint that you cannot use a compiler, so if you want to argue that this option is a bad alternative, you need to do a lot more than write a single sentence saying it violates the constraint that you set up, but did not justify, earlier in the document. You should be answering why that constraint is in place, and justifying in detail why you believe having a separate tool compile your specialized syntactic sugar would be a problem. |
||
| 4. **New AST node kinds**: keep JSX nodes in the AST and handle them in later passes. This can improve tooling fidelity but increases implementation complexity across compiler/typechecker/codegen and makes sandboxing harder to reason about. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The luau parser supports parsing not just the AST but also the CST. Not having dedicated AST nodes for JSX would likely be problematic as we would lose necessary syntactic information required to for a clean round-trip
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's a good point. might be good to keep it in the AST, but still ultimately boils down to a function call in the CST. one goal is I didn't want to introduce new fundamental language features with this. |
||
| 5. **Configurable factory** (future): allow `--!jsxFactory=...` or similar to avoid requiring `jsx` to be global; still requires a syntax proposal, and introduces additional complexity in binding resolution. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You cannot really propose this as a future change because the world would be different if we did your current proposal and then this vs. if we just did this. If we just did this, there'd never exist the global, whereas if we do your current proposal, the global will exist forever and the only way to do this script directive would essentially be expanding it into assignment to that global. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.luauxmight be a more appropriate extension for luauThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the CLI currently uses .lua for luau files, so I followed that convention
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Luau's official syntax extension is
.luauand that is what is used everywhere,.luais supported for backwards compatibility. That Roblox's internal tooling is too broken to support.luauconsistently is a problem we aim to rectify, not something we would commit to making worse. Luau is not Lua, even if Lua 5.1 code can mostly run unchanged in Luau.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense I'm all for switching it