Repository: gluon-lang/gluon
Pull request #312 (2017-07-08 19:37:33)

[WIP] feat(parser): Let gluon files specify shebang
Not sure if this was the proper direction to go but I figured it was better to do it within the parser rather than before the parsing happened to help maintain proper line numbers for any tooling.

If this direction is OK I will probably tackle the pretty printer next.


Patch:
diff --git a/base/src/pretty_print.rs b/base/src/pretty_print.rs
index df8edbb6..9341d276 100644
--- a/base/src/pretty_print.rs
+++ b/base/src/pretty_print.rs
@@ -264,7 +264,7 @@ impl<'a> ExprPrinter<'a> {
     where
         Id: AsRef<str>,
     {
-        self.pretty_expr_(BytePos::from(0), expr).append(
+        self.pretty_expr_with_shebang_line(expr).append(
             self.comments(Span::new(
                 expr.span.end,
                 BytePos::from(self.source.src().len()),
@@ -272,6 +272,36 @@ impl<'a> ExprPrinter<'a> {
         )
     }
 
+    fn find_shebang_line(&'a self) -> Option<&str> {
+        let src = self.source.src();
+        if src.starts_with("#!") {
+            src.lines().next()
+        }
+        else {
+            None
+        }
+    }
+
+    pub fn pretty_expr_with_shebang_line<Id>(
+        &'a self,
+        expr: &'a SpannedExpr<Id>,
+    ) -> DocBuilder<'a, Arena<'a>>
+    where
+        Id: AsRef<str>,
+    {
+        let arena = &self.arena;
+        match self.find_shebang_line() {
+            Some(shebang_line) => { 
+                arena
+                    .text(shebang_line)
+                    .append(
+                        self.pretty_expr_(BytePos::from(shebang_line.len()), expr)
+                    )
+            },
+            None => self.pretty_expr_(BytePos::from(0), expr)
+        }
+    }
+
     pub fn pretty_expr_<Id>(
         &'a self,
         previous_end: BytePos,
diff --git a/check/src/completion.rs b/check/src/completion.rs
index c30299a8..82ac74ff 100644
--- a/check/src/completion.rs
+++ b/check/src/completion.rs
@@ -375,20 +375,14 @@ where
                 self.0.visit_expr(e);
             } else {
                 match e.value {
-                    Expr::TypeBindings(ref type_bindings, ref e) => {
+                    Expr::TypeBindings(ref type_bindings, _) => {
                         for type_binding in type_bindings {
                             self.0.on_found.on_alias(&type_binding.alias.value);
                         }
-                        self.0.visit_expr(e);
                     }
-                    Expr::LetBindings(ref bindings, ref e) => {
-                        for binding in bindings {
-                            self.0.on_found.on_pattern(&binding.name);
-                        }
-                        self.0.visit_expr(e);
-                    }
-                    _ => walk_expr(self, e),
+                    _ => ()
                 }
+                walk_expr(self, e);
             }
         }
     }
diff --git a/parser/src/grammar.lalrpop b/parser/src/grammar.lalrpop
index 283fb997..083398fb 100644
--- a/parser/src/grammar.lalrpop
+++ b/parser/src/grammar.lalrpop
@@ -21,6 +21,7 @@ extern {
     type Error = Spanned<Error, BytePos>;
 
     enum Token<'input> {
+        "shebang line" => Token::ShebangLine(<&'input str>),
         "identifier" => Token::Identifier(<&'input str>),
         "operator" => Token::Operator(<&'input str>),
         "string literal" => Token::StringLiteral(<String>),
@@ -504,7 +505,7 @@ SpExpr: SpannedExpr<Id> = {
 };
 
 pub TopExpr: SpannedExpr<Id> = {
-    <expr: SpExpr> => expr,
+    "shebang line"? <expr: SpExpr> => expr,
 };
 
 pub LetOrExpr: Result<SpannedExpr<Id>, ValueBinding<Id>> = {
diff --git a/parser/src/layout.rs b/parser/src/layout.rs
index b8b4cb08..0e16108f 100644
--- a/parser/src/layout.rs
+++ b/parser/src/layout.rs
@@ -173,6 +173,7 @@ where
         loop {
             // Retrieve the current indentation level if one exists
             let offside = match (&token.value, self.indent_levels.last().cloned()) {
+                (&Token::ShebangLine(_), _) => return Ok(token),
                 (_, Some(offside)) => offside,
                 (&Token::EOF, None) => return Ok(token),
                 (_, None) => {
diff --git a/parser/src/token.rs b/parser/src/token.rs
index b3f9c00c..43d5698b 100644
--- a/parser/src/token.rs
+++ b/parser/src/token.rs
@@ -7,6 +7,7 @@ use self::Error::*;
 
 #[derive(Clone, PartialEq, Debug)]
 pub enum Token<'input> {
+    ShebangLine(&'input str),
     Identifier(&'input str),
     Operator(&'input str),
 
@@ -54,6 +55,7 @@ impl<'input> fmt::Display for Token<'input> {
         use self::Token::*;
 
         let s = match *self {
+            ShebangLine(_) => "ShebangLine",
             Identifier(_) => "Identifier",
             Operator(_) => "Operator",
             StringLiteral(_) => "StringLiteral",
@@ -364,6 +366,20 @@ impl<'input> Tokenizer<'input> {
         self.error(start, UnterminatedStringLiteral)
     }
 
+    fn shebang_line(&mut self, start: Location) -> Option<SpannedToken<'input>> {
+
+        let (end, line) = self.take_until(start, |ch| ch == '\n');
+        
+        if line.starts_with("#!") {
+            let skip = 2;
+            let result = line[skip..].trim_right();
+            let tok = Token::ShebangLine(result);
+            Some(pos::spanned2(start, end, tok))
+        } else {
+            None
+        }
+    }
+
     fn char_literal(&mut self, start: Location) -> Result<SpannedToken<'input>, SpError> {
         let ch = match self.bump() {
             Some((_, '\\')) => self.escape_code()?,
@@ -473,6 +489,12 @@ impl<'input> Iterator for Tokenizer<'input> {
                         Err(err) => Some(Err(err)),
                     }
                 }
+                '#' if start.absolute == BytePos::from(0) && self.test_lookahead(|ch| ch == '!') => {
+                    match self.shebang_line(start) {
+                        Some(token) => Some(Ok(token)),
+                        None => continue,
+                    }
+                }
 
                 ch if is_ident_start(ch) => Some(Ok(self.identifier(start))),
                 ch if is_digit(ch) || (ch == '-' && self.test_lookahead(is_digit)) => {
@@ -523,14 +545,19 @@ mod test {
 
     fn test(input: &str, expected: Vec<(&str, Token)>) {
         let mut tokenizer = tokenizer(input);
-
+        let mut count = 0;
+        let length = expected.len();
         for (token, (expected_span, expected_tok)) in tokenizer.by_ref().zip(expected.into_iter()) {
+            count += 1;
             println!("{:?}", token);
             let start = loc(expected_span.find("~").unwrap());
             let end = loc(expected_span.rfind("~").unwrap() + 1);
             assert_eq!(Ok(pos::spanned2(start, end, expected_tok)), token);
         }
 
+        assert_eq!(count, length);
+        assert_eq!(true, count > 0);
+
         // Make sure that there is nothing else to consume
         assert_eq!(None, tokenizer.next());
     }
@@ -795,4 +822,21 @@ mod test {
             ],
         );
     }
+
+    #[test]
+    fn shebang_line_token_test() {
+        test(
+            "#!/bin/gluon\nhi /// hellooo/// hi",
+            vec![
+                (r#"~~~~~~~~~~~~                   "#, ShebangLine("/bin/gluon")),
+                (r#"            ~~                 "#, Identifier("hi")),
+                (r#"              ~~~~~~~~~~~~~~~~~"#,
+                    DocComment(Comment {
+                        typ: CommentType::Line,
+                        content: "hellooo/// hi".to_string(),
+                    })
+                ),
+            ],
+        );
+    }
 }


Test patch:
diff --git a/check/tests/completion.rs b/check/tests/completion.rs
index 5d5d1359..504035cb 100644
--- a/check/tests/completion.rs
+++ b/check/tests/completion.rs
@@ -458,30 +458,6 @@ match Abc 1 with
     assert_eq!(result, expected);
 }
 
-#[test]
-fn suggest_implicit_import_from_pattern() {
-    let _ = env_logger::init();
-
-    let text = r#"
-let { Test } =
-    type Test = | Abc Int
-    { Test }
-match Abc 1 with
-| 
-"#;
-    let env = MockEnv::new();
-
-    let (mut expr, _result) = support::typecheck_partial_expr(text);
-    expr.span.expansion_id = pos::UNKNOWN_EXPANSION;
-    let result: Vec<_> = completion::suggest(&env, &mut expr, 74.into())
-        .into_iter()
-        .map(|s| s.name)
-        .collect();
-
-    let expected = ["Abc".to_string()];
-    assert_eq!(result, expected);
-}
-
 #[test]
 fn metadata_at_variable() {
     let _ = env_logger::init();
diff --git a/parser/tests/basic.rs b/parser/tests/basic.rs
index 8e6dc479..23a6cc7d 100644
--- a/parser/tests/basic.rs
+++ b/parser/tests/basic.rs
@@ -726,3 +726,15 @@ fn doc_comment_on_record_field() {
         })
     )
 }
+
+#[test]
+fn shebang_at_top_is_ignored() {
+    let _ = ::env_logger::init();
+    let text = r"#!/bin/gluon
+{ Test, x }";
+    let e = parse_new!(text);
+    assert_eq!(
+        e,
+        record_a(vec![("Test".into(), None)], vec![("x".into(), None)])
+    )
+}
diff --git a/parser/tests/pretty_print.rs b/parser/tests/pretty_print.rs
index b6f7860b..b4e1c85c 100644
--- a/parser/tests/pretty_print.rs
+++ b/parser/tests/pretty_print.rs
@@ -146,3 +146,14 @@ x
 "#;
     assert_eq!(&format_expr(expr).unwrap(), expr);
 }
+
+#[test]
+fn preserve_shebang_line() {
+    let expr = r#"#!/bin/gluon
+/* test */
+let x = { field = f /* test */ 123 /* doc */ }
+/* test */
+x
+"#;
+    assert_eq!(&format_expr(expr).unwrap(), expr);
+}