Skip to content

Commit

Permalink
Generate JavaScript for match expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
Jordan Mackie committed Apr 19, 2022
1 parent 8847623 commit 35ba923
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Test exports (..);

type Maybe(a) = Just(a) | Nothing;


with_default = (maybe: Maybe(a), default: a): a ->
match maybe with
| Just(a) -> a
| Nothing -> default;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function Just($0) {
return ["Just", $0];
}
const Nothing = ["Nothing"];
function withDefault(maybe, $default) {
return maybe[0] === "Nothing"
? $default
: maybe[0] === "Just"
? (() => {
const a = maybe[1];
return a;
})()
: (() => {
throw new Error("Pattern match error");
})();
}
export { Just, Nothing, withDefault };
54 changes: 46 additions & 8 deletions crates/ditto-codegen-js/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,34 @@ pub enum ModuleStatement {
}

/// A bunch of statements surrounded by braces.
#[derive(Clone)]
pub struct Block(pub Vec<BlockStatement>);

/// A single JavaScript statement.
///
/// These end with a semicolon.
#[derive(Clone)]
pub enum BlockStatement {
/// ```javascript
/// const ident = expression;
/// ```
_ConstAssignment { ident: Ident, value: Expression },
ConstAssignment { ident: Ident, value: Expression },
/// ```javascript
/// console.log("hi");
/// ```
_Expression(Expression),
/// ```javascript
/// throw new Error("message")
/// ```
Throw(String),
/// ```javascript
/// return bar;
/// return;
/// ```
Return(Option<Expression>),
}

#[derive(Clone)]
pub enum Expression {
/// `true`
True,
Expand All @@ -86,22 +97,22 @@ pub enum Expression {
/// function(argument, argument, argument)
/// ```
Call {
function: Box<Expression>,
arguments: Vec<Expression>,
function: Box<Self>,
arguments: Vec<Self>,
},
/// ```javascript
/// condition ? true_clause : false_clause
/// ```
Conditional {
condition: Box<Expression>,
true_clause: Box<Expression>,
false_clause: Box<Expression>,
condition: Box<Self>,
true_clause: Box<Self>,
false_clause: Box<Self>,
},
/// ```javascript
/// []
/// [5, 5, 5]
/// ```
Array(Vec<Expression>),
Array(Vec<Self>),
/// ```javascript
/// 5
/// 5.0
Expand All @@ -115,9 +126,36 @@ pub enum Expression {
/// undefined
/// ```
Undefined,
/// IIFE
///
/// ```javascript
/// (() => { block })()
/// ```
Block(Block),
/// ```javascript
/// 1 + 2
/// x && y
/// ```
Operator {
op: Operator,
lhs: Box<Self>,
rhs: Box<Self>,
},
IndexAccess {
target: Box<Self>,
index: Box<Self>,
},
}

/// A binary operator.
#[derive(Clone)]
pub enum Operator {
And,
Equals,
}

/// The _body_ of an arrow function.
#[derive(Clone)]
pub enum ArrowFunctionBody {
/// ```javascript
/// () => expression;
Expand All @@ -126,5 +164,5 @@ pub enum ArrowFunctionBody {
/// ```javascript
/// () => { block }
/// ```
_Block(Block),
Block(Block),
}
108 changes: 107 additions & 1 deletion crates/ditto-codegen-js/src/convert.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::ast::{
ArrowFunctionBody, Block, BlockStatement, Expression, Ident, ImportStatement, Module,
ModuleStatement,
ModuleStatement, Operator,
};
use convert_case::{Case, Casing};
use ditto_ast::graph::Scc;
Expand Down Expand Up @@ -300,6 +300,112 @@ fn convert_expression(
ditto_ast::Expression::True { .. } => Expression::True,
ditto_ast::Expression::False { .. } => Expression::False,
ditto_ast::Expression::Unit { .. } => Expression::Undefined, // REVIEW could use `null` or `null` here?
ditto_ast::Expression::Match {
span: _,
box expression,
arms,
..
} => {
let expression = convert_expression(imported_idents, expression);
let err = Expression::Block(Block(vec![BlockStatement::Throw(String::from(
// TODO: mention the file location here?
"Pattern match error",
))]));
arms.into_iter()
.fold(err, |false_clause, (pattern, arm_expression)| {
let (condition, assignments) = convert_pattern(expression.clone(), pattern);

let expression = if assignments.is_empty() {
convert_expression(imported_idents, arm_expression)
} else {
let mut block_statements = assignments
.into_iter()
.map(|(ident, value)| BlockStatement::ConstAssignment { ident, value })
.collect::<Vec<_>>();
let arm_expression = convert_expression(imported_idents, arm_expression);
block_statements.push(BlockStatement::Return(Some(arm_expression)));
Expression::Block(Block(block_statements))
};

if let Some(condition) = condition {
Expression::Conditional {
condition: Box::new(condition),
true_clause: Box::new(expression),
false_clause: Box::new(false_clause),
}
} else {
expression
}
})
}
}
}

type Assignment = (Ident, Expression);
type Assignments = Vec<Assignment>;

fn convert_pattern(
expression: Expression,
pattern: ditto_ast::Pattern,
) -> (Option<Expression>, Assignments) {
let mut conditions = Vec::new();
let mut assignments = Vec::new();
convert_pattern_rec(expression, pattern, &mut conditions, &mut assignments);
if let Some((condition, conditions)) = conditions.split_first() {
let condition =
conditions
.iter()
.fold(condition.clone(), |rhs, lhs| Expression::Operator {
op: Operator::And,
lhs: Box::new(lhs.clone()),
rhs: Box::new(rhs),
});
(Some(condition), assignments)
} else {
(None, assignments)
}
}

fn convert_pattern_rec(
expression: Expression,
pattern: ditto_ast::Pattern,
conditions: &mut Vec<Expression>,
assignments: &mut Vec<Assignment>,
) {
match pattern {
ditto_ast::Pattern::Variable { name, .. } => {
let assignment = (name.into(), expression);
assignments.push(assignment)
}
ditto_ast::Pattern::LocalConstructor {
constructor,
arguments,
..
} => {
let condition = Expression::Operator {
op: Operator::Equals,
lhs: Box::new(Expression::IndexAccess {
target: Box::new(expression.clone()),
index: Box::new(Expression::Number(String::from("0"))),
}),
rhs: Box::new(Expression::String(constructor.0)),
};
conditions.push(condition);
for (i, pattern) in arguments.into_iter().enumerate() {
let expression = Expression::IndexAccess {
target: Box::new(expression.clone()),
index: Box::new(Expression::Number((i + 1).to_string())),
};
convert_pattern_rec(expression, pattern, conditions, assignments);
}
}
ditto_ast::Pattern::ImportedConstructor {
constructor: _,
arguments: _,
..
} => {
todo!();
}
}
}

Expand Down
71 changes: 67 additions & 4 deletions crates/ditto-codegen-js/src/render.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::ast::{
ArrowFunctionBody, Block, BlockStatement, Expression, Ident, ImportStatement, Module,
ModuleStatement,
ModuleStatement, Operator,
};

pub fn render_module(module: Module) -> String {
Expand Down Expand Up @@ -113,7 +113,16 @@ impl Render for BlockStatement {
expression.render(accum);
accum.push(';');
}
Self::_ConstAssignment { ident, value } => {
Self::_Expression(expression) => {
expression.render(accum);
accum.push(';');
}
Self::Throw(message) => {
accum.push_str("throw new Error(\"");
accum.push_str(message);
accum.push_str("\");");
}
Self::ConstAssignment { ident, value } => {
accum.push_str(&format!("const {ident} = ", ident = ident.0));
value.render(accum);
accum.push(';');
Expand Down Expand Up @@ -204,14 +213,41 @@ impl Render for Expression {
Self::Undefined => {
accum.push_str("undefined");
}
Self::Block(block) => {
// IIFE
accum.push('(');
let arrow_function = Self::ArrowFunction {
parameters: Vec::new(),
body: Box::new(ArrowFunctionBody::Block(block.clone())),
};
arrow_function.render(accum);
accum.push_str(")()");
}
Self::Operator { op, lhs, rhs } => {
// Always use parens rather than worry about precedence
accum.push('(');
lhs.render(accum);
accum.push_str(match op {
Operator::And => " && ",
Operator::Equals => " === ",
});
rhs.render(accum);
accum.push(')');
}
Self::IndexAccess { target, index } => {
target.render(accum);
accum.push('[');
index.render(accum);
accum.push(']');
}
}
}
}

impl Render for ArrowFunctionBody {
fn render(&self, accum: &mut String) {
match self {
Self::_Block(block) => block.render(accum),
Self::Block(block) => block.render(accum),
Self::Expression(expression) => expression.render(accum),
}
}
Expand Down Expand Up @@ -257,7 +293,7 @@ mod tests {
assert_render!(
Expression::ArrowFunction {
parameters: vec![ident!("a")],
body: Box::new(ArrowFunctionBody::_Block(Block(vec![
body: Box::new(ArrowFunctionBody::Block(Block(vec![
BlockStatement::Return(Some(Expression::String("hello".to_string())))
]))),
},
Expand Down Expand Up @@ -334,6 +370,29 @@ mod tests {
},
"(true?true:false)?false?0:1:false?2:3"
);
assert_render!(
Expression::Operator {
op: Operator::Equals,
lhs: Box::new(Expression::Operator {
op: Operator::And,
lhs: Box::new(Expression::False),
rhs: Box::new(Expression::True),
}),
rhs: Box::new(Expression::True),
},
"((false && true) === true)"
);

assert_render!(
Expression::IndexAccess {
target: Box::new(Expression::IndexAccess {
target: Box::new(Expression::Variable(ident!("foo"))),
index: Box::new(Expression::String(String::from("bar")))
}),
index: Box::new(Expression::String(String::from("baz")))
},
r#"foo["bar"]["baz"]"#
);
}

#[test]
Expand All @@ -343,6 +402,10 @@ mod tests {
"return true;"
);
assert_render!(BlockStatement::Return(None), "return;");
assert_render!(
BlockStatement::Throw(String::from("aaaahhh")),
"throw new Error(\"aaaahhh\");"
)
}

#[test]
Expand Down

0 comments on commit 35ba923

Please sign in to comment.