mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-21 05:06:29 +00:00
347 lines
8.8 KiB
Rust
347 lines
8.8 KiB
Rust
//! Defines module for defining a small Lua AST for simple codegen. Rojo uses
|
|
//! this module to convert JSON into generated Lua code.
|
|
|
|
use std::{
|
|
fmt::{self, Write},
|
|
num::FpCategory,
|
|
};
|
|
|
|
/// Trait that helps turn a type into an equivalent Lua snippet.
|
|
///
|
|
/// Designed to be similar to the `Display` trait from Rust's std.
|
|
trait FmtLua {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result;
|
|
|
|
/// Used to override how this type will appear when used as a table key.
|
|
/// Some types, like strings, can have a shorter representation as a table
|
|
/// key than the default, safe approach.
|
|
fn fmt_table_key(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
write!(output, "[")?;
|
|
self.fmt_lua(output)?;
|
|
write!(output, "]")
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
struct DisplayLua<T>(T);
|
|
|
|
impl<T> fmt::Display for DisplayLua<T>
|
|
where
|
|
T: FmtLua,
|
|
{
|
|
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let mut stream = LuaStream::new(output);
|
|
self.0.fmt_lua(&mut stream)
|
|
}
|
|
}
|
|
|
|
pub(crate) enum Statement {
|
|
Return(Expression),
|
|
}
|
|
|
|
impl FmtLua for Statement {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Return(literal) => {
|
|
write!(output, "return ")?;
|
|
literal.fmt_lua(output)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Statement {
|
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut stream = LuaStream::new(output);
|
|
FmtLua::fmt_lua(self, &mut stream)
|
|
}
|
|
}
|
|
|
|
pub(crate) enum Expression {
|
|
Nil,
|
|
Bool(bool),
|
|
Number(f64),
|
|
String(String),
|
|
Table(Table),
|
|
|
|
/// Arrays are not technically distinct from other tables in Lua, but this
|
|
/// representation is more convenient.
|
|
Array(Vec<Expression>),
|
|
}
|
|
|
|
impl Expression {
|
|
pub fn table(entries: Vec<(Expression, Expression)>) -> Self {
|
|
Self::Table(Table { entries })
|
|
}
|
|
}
|
|
|
|
impl FmtLua for Expression {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Nil => write!(output, "nil"),
|
|
Self::Bool(inner) => inner.fmt_lua(output),
|
|
Self::Number(inner) => inner.fmt_lua(output),
|
|
Self::String(inner) => inner.fmt_lua(output),
|
|
Self::Table(inner) => inner.fmt_lua(output),
|
|
Self::Array(inner) => inner.fmt_lua(output),
|
|
}
|
|
}
|
|
|
|
fn fmt_table_key(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Nil => panic!("nil cannot be a table key"),
|
|
Self::Bool(inner) => inner.fmt_table_key(output),
|
|
Self::Number(inner) => inner.fmt_table_key(output),
|
|
Self::String(inner) => inner.fmt_table_key(output),
|
|
Self::Table(inner) => inner.fmt_table_key(output),
|
|
Self::Array(inner) => inner.fmt_table_key(output),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<String> for Expression {
|
|
fn from(value: String) -> Self {
|
|
Self::String(value)
|
|
}
|
|
}
|
|
|
|
impl From<&'_ str> for Expression {
|
|
fn from(value: &str) -> Self {
|
|
Self::String(value.to_owned())
|
|
}
|
|
}
|
|
|
|
impl From<Table> for Expression {
|
|
fn from(value: Table) -> Self {
|
|
Self::Table(value)
|
|
}
|
|
}
|
|
|
|
impl FmtLua for bool {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
write!(output, "{}", self)
|
|
}
|
|
}
|
|
|
|
impl FmtLua for f64 {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
match self.classify() {
|
|
FpCategory::Nan => write!(output, "0/0"),
|
|
FpCategory::Infinite => {
|
|
if self.is_sign_positive() {
|
|
write!(output, "math.huge")
|
|
} else {
|
|
write!(output, "-math.huge")
|
|
}
|
|
}
|
|
_ => write!(output, "{}", self),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wrapper struct to display the wrapped string using Lua's string escaping
|
|
/// rules.
|
|
struct LuaEscape<'a>(&'a str);
|
|
|
|
impl fmt::Display for LuaEscape<'_> {
|
|
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
for c in self.0.chars() {
|
|
match c {
|
|
'"' => output.write_str("\\\"")?,
|
|
'\r' => output.write_str("\\r")?,
|
|
'\n' => output.write_str("\\n")?,
|
|
'\t' => output.write_str("\\t")?,
|
|
'\\' => output.write_str("\\\\")?,
|
|
_ => output.write_char(c)?,
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl FmtLua for String {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
write!(output, "\"{}\"", LuaEscape(self))
|
|
}
|
|
|
|
fn fmt_table_key(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
if is_valid_ident(self) {
|
|
write!(output, "{}", self)
|
|
} else {
|
|
write!(output, "[\"{}\"]", LuaEscape(self))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FmtLua for Vec<Expression> {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
write!(output, "{{")?;
|
|
|
|
for (index, value) in self.iter().enumerate() {
|
|
value.fmt_lua(output)?;
|
|
|
|
if index < self.len() - 1 {
|
|
write!(output, ", ")?;
|
|
}
|
|
}
|
|
|
|
write!(output, "}}")
|
|
}
|
|
}
|
|
|
|
pub(crate) struct Table {
|
|
pub entries: Vec<(Expression, Expression)>,
|
|
}
|
|
|
|
impl FmtLua for Table {
|
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
|
writeln!(output, "{{")?;
|
|
output.indent();
|
|
|
|
for (key, value) in &self.entries {
|
|
key.fmt_table_key(output)?;
|
|
write!(output, " = ")?;
|
|
value.fmt_lua(output)?;
|
|
writeln!(output, ",")?;
|
|
}
|
|
|
|
output.unindent();
|
|
write!(output, "}}")
|
|
}
|
|
}
|
|
|
|
fn is_valid_ident_char_start(value: char) -> bool {
|
|
value.is_ascii_alphabetic() || value == '_'
|
|
}
|
|
|
|
fn is_valid_ident_char(value: char) -> bool {
|
|
value.is_ascii_alphanumeric() || value == '_'
|
|
}
|
|
|
|
fn is_keyword(value: &str) -> bool {
|
|
matches!(
|
|
value,
|
|
"and"
|
|
| "break"
|
|
| "do"
|
|
| "else"
|
|
| "elseif"
|
|
| "end"
|
|
| "false"
|
|
| "for"
|
|
| "function"
|
|
| "if"
|
|
| "in"
|
|
| "local"
|
|
| "nil"
|
|
| "not"
|
|
| "or"
|
|
| "repeat"
|
|
| "return"
|
|
| "then"
|
|
| "true"
|
|
| "until"
|
|
| "while"
|
|
)
|
|
}
|
|
|
|
/// Tells whether the given string is a valid Lua identifier.
|
|
fn is_valid_ident(value: &str) -> bool {
|
|
if is_keyword(value) {
|
|
return false;
|
|
}
|
|
|
|
let mut chars = value.chars();
|
|
|
|
match chars.next() {
|
|
Some(first) => {
|
|
if !is_valid_ident_char_start(first) {
|
|
return false;
|
|
}
|
|
}
|
|
None => return false,
|
|
}
|
|
|
|
chars.all(is_valid_ident_char)
|
|
}
|
|
|
|
/// Wraps a `fmt::Write` with additional tracking to do pretty-printing of Lua.
|
|
///
|
|
/// Behaves similarly to `fmt::Formatter`. This trait's relationship to `LuaFmt`
|
|
/// is very similar to `Formatter`'s relationship to `Display`.
|
|
struct LuaStream<'a> {
|
|
indent_level: usize,
|
|
is_start_of_line: bool,
|
|
inner: &'a mut (dyn fmt::Write + 'a),
|
|
}
|
|
|
|
impl fmt::Write for LuaStream<'_> {
|
|
/// Method to support the `write!` and `writeln!` macros. Instead of using a
|
|
/// trait directly, these macros just call `write_str` on their first
|
|
/// argument.
|
|
///
|
|
/// This method is also available on `io::Write` and `fmt::Write`.
|
|
fn write_str(&mut self, value: &str) -> fmt::Result {
|
|
let mut is_first_line = true;
|
|
|
|
for line in value.split('\n') {
|
|
if is_first_line {
|
|
is_first_line = false;
|
|
} else {
|
|
self.line()?;
|
|
}
|
|
|
|
if !line.is_empty() {
|
|
if self.is_start_of_line {
|
|
self.is_start_of_line = false;
|
|
let indentation = "\t".repeat(self.indent_level);
|
|
self.inner.write_str(&indentation)?;
|
|
}
|
|
|
|
self.inner.write_str(line)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'a> LuaStream<'a> {
|
|
fn new(inner: &'a mut (dyn fmt::Write + 'a)) -> Self {
|
|
LuaStream {
|
|
indent_level: 0,
|
|
is_start_of_line: true,
|
|
inner,
|
|
}
|
|
}
|
|
|
|
fn indent(&mut self) {
|
|
self.indent_level += 1;
|
|
}
|
|
|
|
fn unindent(&mut self) {
|
|
assert!(self.indent_level > 0);
|
|
self.indent_level -= 1;
|
|
}
|
|
|
|
fn line(&mut self) -> fmt::Result {
|
|
self.is_start_of_line = true;
|
|
self.inner.write_str("\n")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
/// Regression test for https://github.com/rojo-rbx/rojo/issues/314
|
|
#[test]
|
|
fn bug_314() {
|
|
let my_value = "\"\r\n\t\\".to_owned();
|
|
let displayed = format!("{}", DisplayLua(my_value));
|
|
|
|
assert_eq!(displayed, "\"\\\"\\r\\n\\t\\\\\"");
|
|
}
|
|
}
|