-
Notifications
You must be signed in to change notification settings - Fork 73
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?
Conversation
|
|
||
| ## 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). |
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.
.luaux might be a more appropriate extension for luau
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.
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 .luau and that is what is used everywhere, .lua is supported for backwards compatibility. That Roblox's internal tooling is too broken to support .luau consistently 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.
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
| 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. | ||
| 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 comment
The 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
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.
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.
|
It feels very weird that this proposal describes the addition as JSX. There's no JavaScript here (which is good, we would of course never accept embedding JavaScript in the middle of our language). At minimum, you should probably reframe this as adding XML syntax to Luau since that's what you're actually proposing here. |
|
|
||
| ## 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. |
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.
createElement is more like a flaw in the API design of React than that the language itself cannot express both properties and children. You can very clearly construct equivalents to XML structures in Luau with tables directly and no function calls at all:
<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.
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.
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.
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.
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.
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.
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. |
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.
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.
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.
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
|
|
||
| ### 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. |
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.
runtime meaning is entirely controlled by the
jsxfactory function chosen by the host/project
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 jsx is especially horrible since there is no JavaScript here in any way at all) that you just happen to ambiently assign to before using the sugar. Syntactic sugar in Luau must expand to something that is actually a "complete" piece of a program in Luau. You could propose, for instance, that the XML syntax expands to a mixed table, the actual data that exists in Luau, and then make your library process that correctly, but as-is this is a hard dealbreaker IMO.
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.
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.
|
|
||
| ### Goals | ||
|
|
||
| - **No transpilation step**: JSX is parsed directly by Luau’s parser when enabled. |
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.
You don't need to say this. Anything that is a language feature is parsed by the language.
| - **No transpilation step**: JSX is parsed directly by Luau’s parser when enabled. | ||
| - **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. |
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.
This is also a requirement for all syntax proposals too, it doesn't need to be said to be a goal.
|
|
||
| ### Performance considerations | ||
|
|
||
| - Parsing adds new branches and lookahead, but is expected to be negligible compared to overall parsing cost. |
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.
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.
| ### Performance considerations | ||
|
|
||
| - Parsing adds new branches and lookahead, but is expected to be negligible compared to overall parsing cost. | ||
| - Desugaring into existing AST nodes avoids adding new runtime node kinds and keeps later stages (typechecking, codegen) unchanged. |
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 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.
|
|
||
| 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. |
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.
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.
| 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. | ||
| 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. | ||
| 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. |
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.
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.
| ## 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. |
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.
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.
fair enough. I used the framing of JSX just because it's well understood what that is, but you're right the "JS" in "JSX" means absolutely nothing here. I'll rephrase |
|
FWIW--most of the Roblox community has standardized on |
Problem
React is commonly used with many Luau projects including Roblox's entire client frontend. Traditionally React development is done in conjunction with JSX, an extension of javascript that allows you to write component trees in JS with HTML-like syntax. Luau does not support this which makes developing with React verbose and hard to read.
Solution
This PR implements a first pass at JSX parsing in Luau natively by de-sugaring it into a simple function call
jsx(tag, props, ...children)- leaving it up to the user to implement. This provides a declarative alternative to calling nested function trees that's not dependent on any specific UI framework and provides general language utility beyond React or even UI development.See https://github.com/DanFessler/luau-jsx/tree/jsx-syntax-rfc for first pass implementation
Read full RFC for details.