Skip to content

Commit

Permalink
feat: Add a fmt sub-command to the gluon executable
Browse files Browse the repository at this point in the history
`gluon fmt FILE1 FILE2 ...` can be used to format gluon source code. Formatting still has some rough edges so it always saves a backup file (*.bk) with the source as it was before formatting
  • Loading branch information
Marwes committed Jun 30, 2017
1 parent 3769d0c commit b9c6ea6
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 106 deletions.
29 changes: 21 additions & 8 deletions base/src/pretty_print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ pub fn ident<'b>(arena: &'b Arena<'b>, name: &'b str) -> DocBuilder<'b, Arena<'b

fn forced_new_line<Id>(expr: &SpannedExpr<Id>) -> bool {
match expr.value {
// len() == 1 is parentheses currently
Expr::Block(ref exprs) if exprs.len() > 1 => true,
Expr::LetBindings(..) |
Expr::Match(..) |
Expr::TypeBindings(..) => true,
Expr::Lambda(ref lambda) => forced_new_line(&lambda.body),
Expr::Tuple { ref elems, .. } => elems.iter().any(forced_new_line),
Expr::Record { ref exprs, .. } => {
exprs.iter().any(|field| {
field
Expand Down Expand Up @@ -182,6 +181,19 @@ impl<'a> ExprPrinter<'a> {
}
}

pub fn format<Id>(&'a self, width: usize, expr: &'a SpannedExpr<Id>) -> String
where
Id: AsRef<str>,
{
self.pretty_expr(expr)
.1
.pretty(width)
.to_string()
.lines()
.map(|s| format!("{}\n", s.trim_right()))
.collect()
}


fn newlines(&'a self, prev: BytePos, next: BytePos) -> DocBuilder<'a, Arena<'a>> {
let arena = &self.arena;
Expand All @@ -193,7 +205,6 @@ impl<'a> ExprPrinter<'a> {
self.source.line(Line::from(l)).unwrap().1.trim().is_empty()
})
.count();
println!("{} {} {}", prev, next, empty_lines);
arena.concat(repeat(arena.newline()).take(empty_lines))
}

Expand All @@ -217,11 +228,13 @@ impl<'a> ExprPrinter<'a> {

let doc = match expr.value {
Expr::App(ref func, ref args) => {
pretty(func).append(
arena
.concat(args.iter().map(|arg| arena.space().append(pretty(arg))))
.nest(INDENT),
).group()
pretty(func)
.append(
arena
.concat(args.iter().map(|arg| arena.space().append(pretty(arg))))
.nest(INDENT),
)
.group()
}
Expr::Array(ref array) => {
arena
Expand Down
13 changes: 6 additions & 7 deletions base/tests/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,8 @@ fn break_record() {
let data = |s, a| ArcType::from(type_con(s, a));

let test = data("Test", vec![data("a", vec![])]);
let typ: ArcType<&str> =

Type::record(
vec![
],
let typ: ArcType<&str> = Type::record(
vec![],
vec![
Field::new("x", Type::int()),
Field::new("test", test.clone()),
Expand All @@ -233,7 +230,8 @@ fn break_record() {
],
);
let arena = Arena::new();
let typ = arena.text("aaaaaaaaabbbbbbbbbbcccccccccc ")
let typ = arena
.text("aaaaaaaaabbbbbbbbbbcccccccccc ")
.append(pretty_print(&arena, &typ))
.append(arena.newline());
assert_eq_display!(
Expand All @@ -243,5 +241,6 @@ fn break_record() {
test : Test a,
(+) : Int -> Int -> Int
}
"#);
"#
);
}
32 changes: 17 additions & 15 deletions check/src/typecheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,21 +659,23 @@ impl<'a> Typecheck<'a> {
ref mut typ,
elems: ref mut exprs,
} => {
*typ = if exprs.is_empty() {
Type::unit()
} else {
let fields = exprs
.iter_mut()
.enumerate()
.map(|(i, expr)| {
let typ = self.typecheck(expr);
Field {
name: self.symbols.symbol(format!("_{}", i)),
typ: typ,
}
})
.collect();
Type::record(vec![], fields)
*typ = match exprs.len() {
0 => Type::unit(),
1 => self.typecheck(&mut exprs[0]),
_ => {
let fields = exprs
.iter_mut()
.enumerate()
.map(|(i, expr)| {
let typ = self.typecheck(expr);
Field {
name: self.symbols.symbol(format!("_{}", i)),
typ: typ,
}
})
.collect();
Type::record(vec![], fields)
}
};
Ok(TailCall::Type(typ.clone()))
}
Expand Down
7 changes: 1 addition & 6 deletions parser/src/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,7 @@ AtomicExpr: Expr<Id> = {
},

"(" <elems: Comma<SpExpr>> ")" =>
match elems.len() {
// Parenthesized expression - wrap in a block to ensure
// that it survives operator reparsing
1 => Expr::Block(elems),
_ => Expr::Tuple { typ: type_cache.hole(), elems: elems },
},
Expr::Tuple { typ: type_cache.hole(), elems: elems },

"[" <elems: Comma<SpExpr>> "]" => Expr::Array(Array {
typ: type_cache.hole(),
Expand Down
16 changes: 1 addition & 15 deletions parser/tests/pretty_print.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
extern crate env_logger;
extern crate pretty;
extern crate difference;
extern crate itertools;

extern crate gluon_parser as parser;
extern crate gluon_base as base;

use std::fs::File;
use std::io::{Read, Write};
use std::iter::repeat;
use std::path::Path;

use difference::assert_diff;

use itertools::Itertools;

use base::source::Source;
use base::pretty_print::ExprPrinter;

Expand All @@ -40,17 +36,7 @@ fn test_format(name: &str) {

let source = Source::new(&contents);
let printer = ExprPrinter::new(&source);
let doc = printer.pretty_expr(&expr);
let mut out = Vec::new();
doc.1.render(100, &mut out).unwrap();
out.push(b'\n');
let out_str = ::std::str::from_utf8(&out).unwrap();
// Remove any trailing whitespace that pretty has emitted (on lines that only contains whitespace)
let out_str = out_str
.lines()
.map(|line| line.trim_right())
.interleave_shortest(repeat("\n"))
.collect::<String>();
let out_str = printer.format(100, &expr);
if contents != out_str {
let out_path = Path::new(env!("OUT_DIR")).join(name.file_name().unwrap());
File::create(out_path)
Expand Down
3 changes: 3 additions & 0 deletions repl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ env_logger = { version = "0.3.4", optional = true }
lazy_static = "0.2.0"
rustyline = "1.0.0"

[dev-dependencies]
pretty_assertions = "0.2.0"

[features]
default = ["env_logger"]
47 changes: 46 additions & 1 deletion repl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,35 @@ fn init_env_logger() {
#[cfg(not(feature = "env_logger"))]
fn init_env_logger() {}

fn fmt_file(name: &str) -> Result<()> {
use std::io::{Read, Seek, SeekFrom, Write};
use std::fs::{File, OpenOptions};

use gluon::base::pretty_print::ExprPrinter;
use gluon::base::source::Source;

let mut input_file = OpenOptions::new()
.read(true)
.write(true)
.open(name)?;

let mut buffer = String::new();
input_file.read_to_string(&mut buffer)?;

let mut backup = File::create(&format!("{}.bk", name))?;
backup.write_all(buffer.as_bytes())?;

let expr = Compiler::new().parse_expr("", &buffer)?;

let source = Source::new(&buffer);
let printer = ExprPrinter::new(&source);

let output = printer.format(100, &expr);
input_file.seek(SeekFrom::Start(0))?;
input_file.write_all(output.as_bytes())?;
Ok(())
}

fn main() {
const GLUON_VERSION: &'static str = env!("CARGO_PKG_VERSION");

Expand All @@ -56,9 +85,25 @@ fn main() {
(version: GLUON_VERSION)
(about: "executes gluon programs")
(@arg REPL: -i --interactive "Starts the repl")
(@subcommand fmt =>
(about: "Formats gluon source code")
(@arg INPUT: ... "Formats each file")
)
(@arg INPUT: ... "Executes each file as a gluon program")
).get_matches();
if matches.is_present("REPL") {
if let Some(fmt_matches) = matches.subcommand_matches("fmt") {
if let Some(args) = fmt_matches.values_of("INPUT") {
for arg in args {
if let Err(err) = fmt_file(arg) {
println!("{}", err);
std::process::exit(1);
}
}
} else {
println!("Expected input arguments to `fmt`");
std::process::exit(1);
}
} else if matches.is_present("REPL") {
if let Err(err) = repl::run() {
println!("{}", err);
}
Expand Down
84 changes: 41 additions & 43 deletions repl/src/repl.glu
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ let load_file filename : String -> IO String =
| None -> 0
| Some i -> i + 1
let modulename = string.slice filename last_slash (string.length filename - 3)
let read_result = io.catch (io.read_file_to_string filename >>= \x -> pure (Ok x)) (\err -> pure (Err err))
read_result >>= \result ->
let read_result =
io.catch (io.read_file_to_string filename >>= \x -> pure (Ok x)) (\err -> pure (Err err))
read_result
>>= \result ->
match result with
| Ok expr -> io.load_script modulename expr
| Err msg -> pure msg

type Cmd = {
info : String,
action : String -> IO Bool
}
type Cmd = { info : String, action : String -> IO Bool }

let commands : Map String Cmd =
let print_result result =
Expand All @@ -35,46 +34,47 @@ let commands : Map String Cmd =

let commands = ref empty
let cmds =
singleton "q" { info = "Quit the REPL", action = \_ -> pure False }
<> singleton "t" {
info = "Prints the type with an expression",
action = \arg -> repl_prim.type_of_expr arg >>= print_result *> pure True
}
singleton "q" { info = "Quit the REPL", action = \_ -> pure False } <> singleton "t" {
info = "Prints the type with an expression",
action = \arg -> repl_prim.type_of_expr arg >>= print_result *> pure True
}
<> singleton "i" {
info = "Prints information about the given name",
action = \arg -> repl_prim.find_info arg >>= print_result *> pure True
}
info = "Prints information about the given name",
action = \arg -> repl_prim.find_info arg >>= print_result *> pure True
}
<> singleton "k" {
info = "Prints the kind with the given type",
action = \arg -> repl_prim.find_kind arg >>= print_result *> pure True
}
info = "Prints the kind with the given type",
action = \arg -> repl_prim.find_kind arg >>= print_result *> pure True
}
<> singleton "l" {
info = "Loads the file at 'folder/module.ext' and stores it at 'module'",
action = \arg -> load_file arg >>= io.println *> pure True
}
info = "Loads the file at \'folder/module.ext\' and stores it at \'module\'",
action = \arg -> load_file arg >>= io.println *> pure True
}
<> singleton "h" {
info = "Print this help",
action = \_ ->
io.println "Available commands\n" *>
forM_ (to_list (load commands)) (\cmd ->
//FIXME This type declaration should not be needed
let cmd : { key : String, value : Cmd } = cmd
io.println (" :" ++ cmd.key ++ " " ++ cmd.value.info)
) *>
pure True
}
info = "Print this help",
action = \_ ->
io.println "Available commands\n"
*> forM_ (to_list (load commands)) (\cmd ->
let cmd : { key : String, value : Cmd } = cmd
io.println (" :" ++ cmd.key ++ " " ++ cmd.value.info))
*> pure True
}
commands <- cmds
load commands

let do_command line : String -> IO Bool =
if string.length line >= 2 then
let cmd = string.slice line 1 2
let arg = if string.length line >= 3 then string.trim (string.slice line 3 (string.length line)) else ""
let arg =
if string.length line >= 3
then string.trim (string.slice line 3 (string.length line))
else ""
match find cmd commands with
| Some command -> command.action arg
| None -> io.println ("Unknown command '" ++ cmd ++ "'") *> pure True
| None -> io.println ("Unknown command \'" ++ cmd ++ "\'") *> pure True
else
io.println "Expected a command such as `:h`" *> pure True
io.println "Expected a command such as `:h`"
*> pure True

let store line : String -> IO Bool =
let line = string.trim line
Expand All @@ -87,20 +87,18 @@ let store line : String -> IO Bool =

let loop editor : Editor -> IO () =
let run_line line =
if string.is_empty (string.trim line) then
pure True
else if string.starts_with line ":" then
do_command line
if string.is_empty (string.trim line)
then pure True
else
io.catch (repl_prim.eval_line line) pure
>>= io.println
*> pure True
if string.starts_with line ":"
then do_command line
else io.catch (repl_prim.eval_line line) pure >>= io.println *> pure True

rustyline.readline editor "> " >>= \line_opt ->
rustyline.readline editor "> "
>>= \line_opt ->
match line_opt with
| None -> pure ()
| Some line -> run_line line >>= \continue ->
if continue then loop editor else pure ()
| Some line -> run_line line >>= \continue -> if continue then loop editor else pure ()

let run x : () -> IO () =
io.println "gluon (:h for help, :q to quit)"
Expand Down
Loading

0 comments on commit b9c6ea6

Please sign in to comment.