# full-moon: a lossless Lua parser written in Rust

> full-moon parses Lua 5.1 through 5.4, plus Luau and CfxLua, and keeps everything: the comments, the whitespace, the choices of style. That means you can turn source into a syntax tree, change the tree, and print it back out as the same code you started with, which is what makes it usable for formatting, refactoring and language tooling rather than only for analysis.

**Kampfkarren/full-moon** — A lossless Lua 5.1 parser

- Repository: https://github.com/Kampfkarren/full-moon
- Stars: 545 · Forks: 71
- Language: Rust
- License: MPL-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/kampfkarren-full-moon

## What lossless actually means here

full-moon is a parser for Lua written in Rust, and the word in the repository description that matters is lossless.

The README puts it plainly: full-moon preserves comments, whitespace, style choices and so on. With full-moon, you are able to convert your Lua code into an AST and a syntax tree and convert it back to the original code exactly.

That is a stronger claim than the usual parser promise. Most parsers throw away what they consider noise: the comments, the line breaks, the indentation, the spacing around an operator. What you get back is a tree of meaningful values, and printing it produces code that is equivalent but not identical. For a linter that is perfect, since nobody reads the output. For a formatter, a refactoring tool or an editor, it is not, because every untouched line of the file would show up as changed.

The second half of the promise follows from the first. Using full-moon, you are able to modify the AST directly and re-export it back to Lua, all while preserving the style in which you write.

So the library's contract is a round trip. Parse a file, change one node, print the file, and the only difference between the input and the output is that one change. Everything else, including the comment you wrote in 2019 and the trailing whitespace somebody's editor added, comes back unchanged.

That contract is the whole design. Everything else in the project follows from it.

## The dialect coverage is wider than the name suggests

The README's one-line description of the library lists the languages it handles: Lua 5.1, 5.2, 5.3 and 5.4, then Luau and then CfxLua, with links to the Luau site and to the FiveM scripting documentation.

The extra two are worth pausing on, because they are the reason a Lua parser in Rust is interesting in 2026.

Luau is the dialect that Roblox built for its platform, a Lua derivative with its own type system, its own syntax extensions and its own performance work. CfxLua is the Lua runtime that FiveM and the other CitizenFX platforms use for server-side scripting. Both are Lua in the sense that the code looks familiar and the semantics are close, but both have extended the language in ways that a parser pinned to the reference implementation would reject.

Covering them means a tool built on full-moon can handle Roblox and FiveM codebases, which is where a large share of the Lua in the wild actually lives. A linter that rejects the game's own scripts is not much use.

Covering all four reference versions matters for a different reason. A tool that has to work across projects, some on 5.1 and some on 5.4, cannot be pinned to one grammar, and the differences between the versions are exactly the kind of thing that makes a parser's grammar documentation worth reading.

That is what the GRAMMAR.md file at the repository root is for, and it is a good sign. A parser whose grammar is written down in the repository is a parser whose behaviour you can check without reading the source.

## Five tools that need the original code back

The Why section of the README lists the kinds of project the library is usable in, and the list is really a specification of what lossless parsing buys.

Static analysis, with Luacheck and rust-clippy given as the examples. Static typing, with TypeScript given as the example of what a type checker for a dynamic language looks like. Automatic mass code refactoring, with jscodeshift as the example. Automatic code formatting, with rustfmt as the example. And a language server for Lua, following the Language Server Protocol.

These are five different tools with one shared requirement. Each of them takes code, changes some of it, and gives the changed code back. None of them wants to reformat the whole file while it is at it, and none of them wants to drop the comments it did not understand.

The formatting case is the clearest. A formatter that parses into a tree without trivia cannot put the comments back where they were, because it no longer knows where they were. It can preserve them by counting lines, which is a heuristic, and a formatter built on a lossless tree simply has them attached to the nodes they follow.

The refactoring case is the second clearest. A mass rewrite that renames one function should produce a diff with one line changed. If the tool reprints the file, the diff contains every line the printer formatted differently from the original style, which is a diff nobody can review and a merge conflict waiting to happen.

The linter and the language server cases are the ones where losslessness matters least, since neither gives the code back. They are listed because the same tree serves them, and a tool that wants both analysis and rewriting only needs one parser.

## Two projects that the design comes from

The Inspiration section names two projects, and they are the closest relatives of what full-moon does.

The first is LPGhatguy's mab, a Lua parser, and full-moon is described as heavily inspired by it. The second is benjamn's recast, the JavaScript tool that parses code into an AST and prints it back preserving the original formatting.

Recast is the more instructive of the two, because it is the tool that made lossless parsing a known idea outside the Ruby and JavaScript worlds. Its trick is to keep the original source text and to have the printer work from the original text wherever the tree has not been modified, falling back to a code generator only for the parts that changed. A file that came in formatted in a particular style comes out in that style, and only the edited region looks different.

That is a different implementation strategy from the one full-moon describes, where the tree itself carries the comments, the whitespace and the style choices, and the printer emits them back. The trade is between memory and fidelity: attaching trivia to the tree makes every node bigger and the API has to expose them, but it makes the printing straightforward and the round trip exact.

For a Lua parser, the second approach has an appeal that the JavaScript tool did not have to think about. Lua source is small, the dialect is small, and the grammar is documented, so a full tree with trivia is affordable. And the Lua community has a culture of heavily commented code, which makes dropping comments unacceptable for any tool that gives code back.

The naming follows the same idea. A full moon is the whole of it, and the point is that nothing is lost.

## The workspace, the grammar file and the benchmarks

The repository is a Cargo workspace with two members and the second edition resolver:

```toml
[workspace]

members = [
	"full-moon",
	"full-moon-derive",
]

resolver = "2"
```

The split into a library and a derive crate is the standard Rust arrangement for a project with a procedural macro, and a derive macro is what a lossless parser for a language with flexible syntax tends to need: the node definitions themselves can be generated, which keeps the several hundred node types from being written by hand.

The rest of the tree is small and tells you what the project cares about. There is GRAMMAR.md at the root, which is the written grammar. There is CHANGELOG.md. There is a Rustfmt.toml, so the source formatting is pinned, and a .editorconfig and a .vscode/ directory for the editors. And there is benches/, a directory of benchmarks, which for a parser is the responsible thing to have: parsing speed is the characteristic performance of this kind of tool, and the trivia handling in particular is the part that could be slow.

The library is published to crates.io as full-moon, which is where the version badge in the README points, and there are no GitHub releases. The licence is MPL-2.0, the file for it being LICENSE.md, and the repository topic is hacktoberfest, which is a leftover from a contributor drive rather than a description of the library.

The primary language is Rust, and the last push to the repository was on 2026-10-01.

## Where full-moon is the wrong tool

There are cases where a lossless parser is the wrong choice, and they come from the same property that makes it valuable.

If you only need to read values out of Lua at runtime, losslessness is pure cost. A tree that carries every comment and every space is larger, and slower to build, and more awkward to work with than a tree of values. A runtime hook or a simple inspection tool would be better served by a normal parser that discards what it does not need.

The same applies to a linter that never gives the code back. Luacheck, the linter named in the README's own list, does not rewrite files, so the trivia it would carry is work for nothing. Lossless parsing pays off when the output matters, and that includes the case where a linter has an autofix mode and rewrites the file.

The second limit is the dialect list. A parser covering Lua 5.1 to 5.4 with Luau and CfxLua handles most of the Lua in the world, but a language with different syntax, such as a domain specific language that embeds Lua, or a dialect nobody has declared, is out of scope, and the grammar file will tell you exactly where the edges are.

The third is the cost of the tree in memory for large files, and the benchmarks directory exists precisely because that is a question worth measuring rather than assuming. A generated file of fifty thousand lines is not a realistic target for a lossless tree in a language server, and the honest answer for that case is that the tool is for source files people edit.

The alternative approach, then, is the ordinary parser plus a formatter pass, where the parser discards trivia and a separate tool decides the layout. The difference in approach is that the two step version can reformat a file that was not consistently formatted, which is a feature for some projects and a violation of the review diff for others, and the lossless approach guarantees that the parts of the file it did not touch come back byte for byte.

## Conclusion

full-moon suits a Rust developer building a Lua formatter, a linter, a type checker or a language server, or anyone running a bulk rewrite over a Lua codebase where the diff has to stay small. It does not suit a project that only needs to read Lua values at runtime, where the extra weight of preserving every comment and space is pure cost, and it does not suit a user of another Lua dialect that is not on the list. Check three things before you adopt it: which dialect your code is written in, since the parser covers Lua 5.1 to 5.4 with Luau and CfxLua, whether your tooling needs the trivia to survive the round trip, which is the whole point of the library, and whether the tree size is acceptable for the files you will parse, since a lossless tree is larger than a lossy one by design. The last push to the repository was on 2026-10-01 and the library is published to crates.io rather than through GitHub releases.

## FAQ

### What is full-moon?

It is a lossless parser for Lua written in Rust, covering Lua 5.1, 5.2, 5.3 and 5.4, as well as Luau and CfxLua. It is published on crates.io as full-moon and released under the MPL-2.0 licence.

### What does lossless mean in full-moon?

The parser preserves comments, whitespace and style choices, so you can convert Lua source into an AST and syntax tree and convert it back to the original code exactly. You can then modify the AST directly and re-export it back to Lua while preserving the style in which you wrote.

### What is full-moon used for?

The README lists static analysis such as Luacheck and clippy, static typing as in TypeScript, automatic mass code refactoring as in jscodeshift, automatic code formatting as in rustfmt, and a language server for Lua. Each of those needs the original code back with the untouched parts unchanged.

### Which projects inspired full-moon?

The README says full-moon is heavily inspired by LPGhatguy's mab, a Lua parser, and by the possibilities brought on by benjamn's recast, the JavaScript tool that prints an AST back with the original formatting preserved.

### How is the full-moon repository structured?

It is a Cargo workspace with two members, full-moon and full-moon-derive, using resolver 2. The root also holds GRAMMAR.md, CHANGELOG.md, Rustfmt.toml and a benches/ directory, and the library is published to crates.io rather than through GitHub releases.

## Sources

- [Issues](https://github.com/Kampfkarren/full-moon/issues)
- [Kampfkarren/full-moon on GitHub](https://github.com/Kampfkarren/full-moon)
- [License: MPL-2.0](https://github.com/Kampfkarren/full-moon/blob/main/LICENSE)
- [README](https://github.com/Kampfkarren/full-moon/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kampfkarren-full-moon
