RESW

$web-only$ While ress and ressa consume text and generate data structures, resw is going to consume data structures and write out text. This means it can do the heavy lifting when solving the problem our debug logging project left us with. However instead of just sweeping that under the rug, we are going to go over how resw works. Because the nature of JavaScript, resw makes some style decisions that might not work for everyone, by going over the project in detail the hope is that other's will feel enabled to either contribute a configuration option into resw or even implement their own project that consumes ressa's AST and generates text.

If you are just interested in seeing how we are going to finish the project from the last chapter, feel free to move ahead.

Similar to the structure of ressa, resw exposes a struct that will keep track of the context for us called Writer. There are 2 methods for constructing a Writer, the first is the ::new method the second is the ::builder method that utilizes the builder pattern to customize some options. Those options include

  • New line character (default \n)
  • Quote (default to use origin quotation mark)
    • Setting this to any value will force all of the string literals in the provided JavaScript to be re-written with the provided quotes
  • Indent (default 4 spaces)

Either method you are going to need to provide the destination, this can be anything that implements the std::io::Write trait. For testing purposes the crate provides an implementor of Write in WriteString, we are not going to cover that here but a more detailed explanation can be found in the appendix.

Once a Writer is constructed, it provides an API surface that should cover most of the ressa AST. The primary entry-point for is going to be either write_program or write_part. For the most part, the primary role of the writer is going to be incrementally move down the AST until we find something that we are confident in exactly what to write. Let's take the following js as an example.

function Thing(stuff) {
    this.stuff = stuff;
}
let thing = new Thing('argument');

If we run that that through the ressa::Parser, we would see the following AST.

Decl(
    Function(
        Function {
            id: Some(
                "Thing"
            ),
            params: [
                Pat(
                    Identifier(
                        "stuff"
                    )
                )
            ],
            body: [
                Stmt(
                    Expr(
                        Assignment(
                            AssignmentExpr {
                                operator: Equal,
                                left: Expr(
                                    Member(
                                        MemberExpr {
                                            object: ThisExpr,
                                            property: Ident(
                                                "stuff"
                                            ),
                                            computed: false
                                        }
                                    )
                                ),
                                right: Ident(
                                    "stuff"
                                )
                            }
                        )
                    )
                )
            ],
            generator: false,
            is_async: false
        }
    )
)
Decl(
    Variable(
        Let,
        [
            VariableDecl {
                id: Identifier(
                    "thing"
                ),
                init: Some(
                    New(
                        NewExpr {
                            callee: Ident(
                                "Thing"
                            ),
                            arguments: [
                                Literal(
                                    String(
                                        "\'argument\'"
                                    )
                                )
                            ]
                        }
                    )
                )
            }
        ]
    )
)

Using that, let's take a look at how resw would generate the text to represent our AST. First we would enter at write_part with the first ProgramPart.


#![allow(unused)]
fn main() {
pub fn write_part(&mut self, part: &ProgramPart) -> Res {
    self.at_top_level = true;
    self._write_part(part)?;
    self.write_new_line()?;
    Ok(())
}
}

Interestingly enough, write_part is really more concerned with maintaining a context flag for if we are at the top level or not, this becomes important when trying to determine if any expression needs to be wrapped in parentheses. Almost all of the work is going to be passed off to an internal private function _write_part.


#![allow(unused)]
fn main() {
fn _write_part(&mut self, part: &ProgramPart) -> Res {
    self.write_leading_whitespace()?;
    match part {
        ProgramPart::Decl(decl) => self.write_decl(decl)?,
        ProgramPart::Dir(dir) => self.write_directive(dir)?,
        ProgramPart::Stmt(stmt) => self.write_stmt(stmt)?,
    }
    Ok(())
}
}

The first thing we want to do is make sure that any leading whitespace is included with write_leading_whitespace.


#![allow(unused)]
fn main() {
pub fn write_leading_whitespace(&mut self) -> Res {
    self.write(&self.indent.repeat(self.current_indent))?;
    Ok(())
}
}

This is achieved by looking at the current_indent and writing the configurable property indent to the destination repeated the for our current indent level, so if our indent was \t and we were at level 2 it would write "\t\t". Internally the write method just writes a single &str to the destination. After we write our leading whitespace, we can start to descend the AST, we do that by matching on the part. You can see that there is a branch for each of the possible enum variants, looking back at the example, we know the next step would be to head to write_decl.


#![allow(unused)]
fn main() {
pub fn write_decl(&mut self, decl: &Decl) -> Res {
    match decl {
        Decl::Variable(ref kind, ref decls) => self.write_variable_decls(kind, decls)?,
        Decl::Class(ref class) => {
            self.at_top_level = false;
            self.write_class(class)?;
            self.write_new_line()?;
        },
        Decl::Function(ref func) => {
            self.at_top_level = false;
            self.write_function(func)?;
            self.write_new_line()?;
        },
        Decl::Export(ref exp) => self.write_export_decl(exp)?,
        Decl::Import(ref imp) => self.write_import_decl(imp)?,
    };
    Ok(())
}
}

Moving further down we simply match on the the declaration handling each variant as needed. For our example we would move into the Decl::Function branch. The first step in that branch is to set the context flag at_top_level to false and then move into the write_function method.


#![allow(unused)]
fn main() {
pub fn write_function(&mut self, func: &Function) -> Res {
    if func.is_async {
        self.write("async ")?;
    }
    self.write("function")?;
    if let Some(ref id) = func.id {
        self.write(" ")?;
        if func.generator {
            self.write("*")?;
        }
        self.write(id)?;
    } else if func.generator {
        self.write("*")?;
    }
    self.write_function_args(&func.params)?;
    self.write(" ")?;
    self.write_function_body(&func.body)
}
}

Here we are going to actually start writing some information out to our destination. First is we check the flag on Function to see if we need to write the async keyword, next we write the keyword function followed by a check to see if the id is Some. If so we need to check the flag on Function to see if that function is a generator, if it is we need to add a * before the id, and Lastly we write the id

Now that we have gotten though that we can start to look at the parameters and body. First we are going to pass off the parameters to write_function_args.


#![allow(unused)]
fn main() {
/// Write the arguments of a function or method definition
/// ```js
/// function(arg1, arg2) {
/// }
/// ```
pub fn write_function_args(&mut self, args: &[FunctionArg]) -> Res {
    self.write("(")?;
    let mut after_first = false;
    for ref arg in args {
        if after_first {
            self.write(", ")?;
        } else {
            after_first = true;
        }
        self.write_function_arg(arg)?;
    }
    self.write(")")?;
    Ok(())
}
}

The first step here is to write the open parenthesis, next we are going to use a flag after_first to help with handing if a comma should be written before the argument. This is the first place that we have seen where resw is making a style choice, all function parameters will not include a trailing comma. Ideally style choices will be configurable in the future but currently this one is not. Now that we have handled the comma situation we can pass the argument off to write_function_arg.


#![allow(unused)]
fn main() {
pub fn write_function_arg(&mut self, arg: &FunctionArg) -> Res {
    match arg {
        FunctionArg::Expr(ref ex) => self.write_expr(ex)?,
        FunctionArg::Pat(ref pa) => self.write_pattern(pa)?,
    }
    Ok(())
}
}

Here we see another function that simply move us further down the AST. Function arguments can be either expressions or patterns so we need to handle both. For our example we are going to head down the Pat branch with write_pattern.


#![allow(unused)]
fn main() {
pub fn write_pattern(&mut self, pattern: &Pat) -> Res {
    match pattern {
        Pat::Identifier(ref i) => self.write(i),
        Pat::Object(ref o) => self.write_object_pattern(o),
        Pat::Array(ref a) => self.write_array_pattern(a.as_slice()),
        Pat::RestElement(ref r) => self.write_rest_element(r),
        Pat::Assignment(ref a) => self.write_assignment_pattern(a),
    }
}
}

Most of the options here are simply going to continue branching down our AST, however for our example we are going to head down the first match arm with Pat::Identifer and just write that string out to our destination.

Moving back up we only had one parameter for our function signature so we finish out write_function_args with a closing parenthesis. That then leads us to write_function_body.


#![allow(unused)]
fn main() {
pub fn write_function_body(&mut self, body: &FunctionBody) -> Res {
    if body.len() == 0 {
        self.write("{ ")?;
    } else {
        self.write_open_brace()?;
        self.write_new_line()?;
    }
    for ref part in body {
        self._write_part(part)?;
    }
    if body.len() == 0 {
        self.write("}")?;
    } else {
        self.write_close_brace()?;
    }
    Ok(())
}
}

The first thing we need to do is take a look at the &FunctionBody which is a type alias for Vec<ProgramPart>. We check to see if this function has any body, if not we just write a single open curly brace, if it does we want to write the curly brace using write_open_brace, this is a convenience method for writing the character and also incrementing the current_indent, lastly we write a new line. Now we loop over each of the ProgramParts in body and pass that off to _write_body. For our example there is only going to be one part. This part is a ProgramPart::Stmt which would be handled by write_stmt.


#![allow(unused)]
fn main() {
pub fn write_stmt(&mut self, stmt: &Stmt) -> Res {
    let mut semi = true;
    let mut new_line = true;
    let cached_state = self.at_top_level;
    match stmt {
        Stmt::Empty => {
            new_line = false;
        },
        Stmt::Debugger => self.write_debugger_stmt()?,
        Stmt::Expr(ref stmt) => {
            let wrap = match stmt {
                Expr::Literal(_)
                | Expr::Object(_)
                | Expr::Function(_) 
                | Expr::Binary(_) => true,
                _ => false,
            };
            if wrap {
                self.write_wrapped_expr(stmt)?
            } else {
                self.write_expr(stmt)?
            }
        },
        Stmt::Block(ref stmt) => {
            self.at_top_level = false;
            self.write_block_stmt(stmt)?;
            semi = false;
            new_line = false;
            self.at_top_level = cached_state;
        }
        Stmt::With(ref stmt) => {
            self.write_with_stmt(stmt)?;
            semi = false;
        }
        Stmt::Return(ref stmt) => self.write_return_stmt(stmt)?,
        Stmt::Labeled(ref stmt) => {
            self.write_labeled_stmt(stmt)?;
            semi = false;
        }
        Stmt::Break(ref stmt) => self.write_break_stmt(stmt)?,
        Stmt::Continue(ref stmt) => self.write_continue_stmt(stmt)?,
        Stmt::If(ref stmt) => {
            self.write_if_stmt(stmt)?;
            semi = false;
        }
        Stmt::Switch(ref stmt) => {
            self.at_top_level = false;
            self.write_switch_stmt(stmt)?;
            semi = false;
        }
        Stmt::Throw(ref stmt) => self.write_throw_stmt(stmt)?,
        Stmt::Try(ref stmt) => {
            self.write_try_stmt(stmt)?;
            semi = false;
        }
        Stmt::While(ref stmt) => {
            new_line = self.write_while_stmt(stmt)?;
            semi = false;
        }
        Stmt::DoWhile(ref stmt) => self.write_do_while_stmt(stmt)?,
        Stmt::For(ref stmt) => {
            self.at_top_level = false;
            new_line = self.write_for_stmt(stmt)?;
            semi = false;
        }
        Stmt::ForIn(ref stmt) => {
            self.at_top_level = false;
            new_line = self.write_for_in_stmt(stmt)?;
            semi = false;
        }
        Stmt::ForOf(ref stmt) => {
            self.at_top_level = false;
            new_line = self.write_for_of_stmt(stmt)?;
            semi = false;
        }
        Stmt::Var(ref stmt) => self.write_var_stmt(stmt)?,
    };
    if semi {
        self.write_empty_stmt()?;
    }
    if new_line {
        self.write_new_line()?;
    }
    self.at_top_level = cached_state;
    Ok(())
}
}

That is a pretty big match statement! Before we enter that we have a couple of context flags to help us with formatting write_semi and new_line, both with a default value of true. Looking at our example, we would enter the Stmt::Expr arm of the match which handles handles the possible requirement that this statement be wrapped in parentheses. Primitive literals, object literals, functions, and binary operations would require parentheses when not part of a larger statement. There is a convenience method called write_wrapped_expr that just writes parentheses around a call to write_expr.


#![allow(unused)]
fn main() {
pub fn write_expr(&mut self, expr: &Expr) -> Res {
    let cached_state = self.at_top_level;
    match expr {
        Expr::Literal(ref expr) => self.write_literal(expr)?,
        Expr::This => self.write_this_expr()?,
        Expr::Super => self.write_super_expr()?,
        Expr::Array(ref expr) => self.write_array_expr(expr)?,
        Expr::Object(ref expr) => self.write_object_expr(expr)?,
        Expr::Function(ref expr) => {
            self.at_top_level = false;
            self.write_function(expr)?;
            self.at_top_level = cached_state;
        }
        Expr::Unary(ref expr) => self.write_unary_expr(expr)?,
        Expr::Update(ref expr) => self.write_update_expr(expr)?,
        Expr::Binary(ref expr) => self.write_binary_expr(expr)?,
        Expr::Assignment(ref expr) => {
            self.at_top_level = false;
            self.write_assignment_expr(expr)?
        },
        Expr::Logical(ref expr) => self.write_logical_expr(expr)?,
        Expr::Member(ref expr) => self.write_member_expr(expr)?,
        Expr::Conditional(ref expr) => self.write_conditional_expr(expr)?,
        Expr::Call(ref expr) => self.write_call_expr(expr)?,
        Expr::New(ref expr) => self.write_new_expr(expr)?,
        Expr::Sequence(ref expr) => self.write_sequence_expr(expr)?,
        Expr::Spread(ref expr) => self.write_spread_expr(expr)?,
        Expr::ArrowFunction(ref expr) => {
            self.at_top_level = false;
            self.write_arrow_function_expr(expr)?;
            self.at_top_level = cached_state;
        }
        Expr::Yield(ref expr) => self.write_yield_expr(expr)?,
        Expr::Class(ref expr) => {
            self.at_top_level = false;
            self.write_class(expr)?;
            self.at_top_level = cached_state;
        }
        Expr::MetaProperty(ref expr) => self.write_meta_property(expr)?,
        Expr::Await(ref expr) => self.write_await_expr(expr)?,
        Expr::Ident(ref expr) => self.write_ident(expr)?,
        Expr::TaggedTemplate(ref expr) => self.write_tagged_template(expr)?,
        _ => unreachable!(),
    }
    Ok(())
}
}

The first step here is to keep a copy of the previous at_top_level flag so that we can revert back to it after writing, some of the arms are going to change it. Next we enter another very large match statement. Our example would take the Expr::Assignment arm, passing further work off to write_assignment_expr.


#![allow(unused)]
fn main() {
pub fn write_assignment_expr(&mut self, assignment: &AssignmentExpr) -> Res {
    let wrap_self = match &assignment.left {
        AssignmentLeft::Expr(ref e) => match &**e {
            Expr::Object(_) 
            | Expr::Array(_) => true,
            _ => false,
        }, 
        AssignmentLeft::Pat(ref p) => match p {
            Pat::Array(_) => true,
            Pat::Object(_) => true,
            _ => false,
        }
    };
    if wrap_self {
        self.write("(")?;
    }
    match &assignment.left {
        AssignmentLeft::Expr(ref e) => self.write_expr(e)?,
        AssignmentLeft::Pat(ref p) => self.write_pattern(p)?,
    }
    self.write(" ")?;
    self.write_assignment_operator(&assignment.operator)?;
    self.write(" ")?;
    self.write_expr(&assignment.right)?;
    if wrap_self {
        self.write(")")?;
    }
    Ok(())
}
}

Here we are first we need to determine if the whole assignment expression needs to be wrapped in parentheses which would only be true if the left hand side was an object or array literal. Next we test the assignment.left property since it can be either an Expr or a Pat, our example would take us back to the write_expr method. This would take us back up through write_expr but this time we would pass into the Expr::Member arm which passes its work off to write_member_expr.


#![allow(unused)]
fn main() {
pub fn write_member_expr(&mut self, member: &MemberExpr) -> Res {
    match &*member.object {
        Expr::Assignment(_) 
        | Expr::Literal(Literal::Number(_))
        | Expr::Conditional(_)
        | Expr::Logical(_) 
        | Expr::Function(_)
        | Expr::ArrowFunction(_)
        | Expr::Object(_)
        | Expr::Binary(_) 
        | Expr::Unary(_)
        | Expr::Update(_) => self.write_wrapped_expr(&member.object)?,
        _ => self.write_expr(&member.object)?,
    }
    if member.computed {
        self.write("[")?;
    } else {
        self.write(".")?;
    }
    self.write_expr(&member.property)?;
    if member.computed {
        self.write("]")?;
    }
    Ok(())
}
}

Here we first check to see if the object property is required to be wrapped in parentheses for us though we just want to pass that along to write_expr. This time though there we are going to end up at Expr::ThisExpr which just writes out the literal word this. Next we are going to look at the flag on MemberExpr "computed" to see if this was written originally with the bracket notation (this['stuff']) or the dot notation (this.stuff), writing the appropriate character. Now we are again going to pass some work back to write_expr, this time with the property property. This would end on the branch for Expr::Ident which just writes that value to the destination. If the member expression was computed we would need to write the ] but for our example it is not.

At this point we are back up at write_assignment_expr where we are going to write a single space and then pass the assignment.operator off to write_assignment_operator.


#![allow(unused)]
fn main() {
pub fn write_assignment_operator(&mut self, op: &AssignmentOperator) -> Res {
    let s = match op {
        AssignmentOperator::AndEqual => "&=",
        AssignmentOperator::DivEqual => "/=",
        AssignmentOperator::Equal => "=",
        AssignmentOperator::LeftShiftEqual => "<<=",
        AssignmentOperator::MinusEqual => "-=",
        AssignmentOperator::ModEqual => "%=",
        AssignmentOperator::OrEqual => "|=",
        AssignmentOperator::PlusEqual => "+=",
        AssignmentOperator::PowerOfEqual => "**=",
        AssignmentOperator::RightShiftEqual => ">>=",
        AssignmentOperator::TimesEqual => "*=",
        AssignmentOperator::UnsignedRightShiftEqual => ">>>=",
        AssignmentOperator::XOrEqual => "^=",
    };
    self.write(s)?;
    Ok(())
}
}

This is a relatively straight forward process of looking at which operator was provided and then writing out the text that represents that operator. For our example it would be =, we then need to write a single space. The last step in write_assignment_expr is to handle the assignment.right which is also an Expr so we pass that off to write_expr. Our example will head to the Expr::Ident match arm and then just write to the destination. With that we have now reached the last step in write_function_body which is to write_close_brace similar to write_open_brace here we are decrementing the current_indent context property. That also brings us to the end of write_function, write_decl, and _write_part. The last thing we do in write_part is to add a trailing new line, another style choice.

As our example continues we would then start again at write_part with the next part. This is going to move though _write_part the same as before, however when we get to write_decl we have a new branch to head down. This is the Decl::Variable arm which passes its work off to write_variable_decls.


#![allow(unused)]
fn main() {
pub fn write_variable_decls(&mut self, kind: &VariableKind, decls: &[VariableDecl]) -> Res {
    self.write_variable_kind(kind)?;
    let mut after_first = false;
    for decl in decls {
        if after_first {
            self.write(", ")?;
        } else {
            after_first = true;
        }
        self.write_variable_decl(decl)?;
    }
    self.write_empty_stmt()?;
    self.write_new_line()
}
}

As you might expect the first thing we want to do is to write the variable kind. We pass off the kind variable to write_variable_kind.


#![allow(unused)]
fn main() {
pub fn write_variable_kind(&mut self, kind: &VariableKind) -> Res {
    let s = match kind {
        VariableKind::Const => "const ",
        VariableKind::Let => "let ",
        VariableKind::Var => "var ",
    };
    self.write(s)
}
}

Similar to our examination of write_assignment_operator we are going to simply look at which keyword was used and then write that out, with a trailing space.

Next we need to keep track of two flags after_first which should be familiar from write_function_args. In our loop, we pass of each of the declarations to write_variable_decl.


#![allow(unused)]
fn main() {
pub fn write_variable_decl(&mut self, decl: &VariableDecl) -> Res {
    self.write_pattern(&decl.id)?;
    if let Some(ref init) = decl.init {
        self.write(" = ")?;
        self.write_expr(init)?;
    }
    Ok(())
}
}

Here we first write out the id of this variable by passing it off to write_pattern. Thankfully our example is pretty simple so we are again going to take that first branch for Pat::Ident and write the identifer to our destination. After that we want to check if this variable is initialized, ours is, and if so we would write the " = " and then write the expression by passing that off to write_expr. For this pass through write_expr we are going to travel down the Expr::New arm which passes its work off to write_new_expr.


#![allow(unused)]
fn main() {
pub fn write_new_expr(&mut self, new: &NewExpr) -> Res {
    self.write("new ")?;
    match &*new.callee {
        Expr::Assignment(_) 
        | Expr::Call(_) => self.write_wrapped_expr(&new.callee)?,
        _ => self.write_expr(&new.callee)?,
    }
    self.write_sequence_expr(&new.arguments)?;
    Ok(())
}
}

At this point we want to first write the new keyword followed by a space. Next we want to write out what the new.callee is which would again bring us to write_expr. Our example would travel to the Expr::Ident arm which just writes that out. Next we need to write an open parenthesis followed by the provided arguments. This time we are going to use the write_sequence_expr method to do that.


#![allow(unused)]
fn main() {
pub fn write_sequence_expr(&mut self, sequence: &[Expr]) -> Res {
    let mut after_first = false;
    self.write("(")?;
    for ref e in sequence {
        if after_first {
            self.write(", ")?;
        }
        self.write_expr(e)?;
        after_first = true;
    }
    self.write(")")?;
    Ok(())
}
}

At this point the structure of this function's body should look familiar, we are going to loop over the provide expressions and write them out with a comma and space before all but the first one. For our example we are going only hit this once so no comma, then we are going to pass that off to write_expr. This time as we pass through the match in write_expr we are going to hit the Expr::Literal arm which passes its work off to write_literal.


#![allow(unused)]
fn main() {
pub fn write_literal(&mut self, lit: &Literal) -> Res {
    match lit {
        Literal::Boolean(b) => self.write_bool(*b),
        Literal::Null => self.write("null"),
        Literal::Number(n) => self.write(&n),
        Literal::String(s) => self.write_string(s),
        Literal::RegEx(r) => self.write_regex(r),
        Literal::Template(t) => self.write_template(t),
    }
}
}

Here we see another match statement, our example will take us down the Literal::String arm which passes off work to write_string. You may be wondering why that is, since writing strings is all we have really been doing. The answer is that this is one of the few style preferences that is currently configurable as you'll see.


#![allow(unused)]
fn main() {
pub fn write_string(&mut self, s: &str) -> Res {
    if let Some(c) = self.quote {
        self.re_write_string(s, c)?;
    } else {
        self.write(s)?;
    }
    Ok(())
}
}

We first check to see if the self.quote property has been set, this would indicate that the user has a quote preference. If it is set then we want to re-write the string to use this quote, this involves re-writing any internal escaped quotes for the old quote and escaping the new quote that might appear in the contents. If that property is None then we would just write it out normally as the ressa::node::Literal::String preserves the original quotation mark.

After that we are again back at write_new_expr where the last thing to do is write the closing parenthesis, after which we are at the bottom of write_variable_decl. When we move up again to the write_variable_decls we would write a semi-colon and new line to close that out. This brings us to the bottom of write_decl, _write_part, and write_part, it also brings us to the end of our example JavaScript. While we didn't touch every part of how resw works, there is a lot of surface area to cover, hopefully it has provided enough information for you feel confident in how it works. For more information you can check out the ressa docs and the resw docs.

Up next we are going to see how you would use resw to complete our debug log helper.

$web-only-end$

$slides-only$

  • Writer takes ProgramParts
  • Somewhat Configurable
  • Writes to impl Write $slides-only$