├── .gitignore ├── LICENSE ├── README.md ├── doc └── intro.md ├── project.clj ├── resources ├── example.clj ├── example.pr └── grammar.bnf ├── src ├── leiningen │ └── prose.clj └── prose │ └── core.clj └── test └── prose └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aaron Lebo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prose 2 | 3 | *No subject is terrible if the story is true, if the __prose__ is clean and honest, and if it affirms courage and grace under pressure.* - Ernest Hemingway as played by Corey Stoll in *Midnight in Paris* 4 | 5 | ## tldr 6 | 7 | ``` 8 | buffalo = defn(end, 9 | let(strings: 1 * 8 repeat("buffalo"), 10 | idxs: [0, 2, 6], 11 | f: fn(idx, itm, 12 | if(idx2 fn(idx2 == idx) some(idxs), 13 | itm string/capitalize, 14 | itm 15 | ) 16 | ), 17 | res: f map-indexed(strings), 18 | " " string/join(res) str(end case( 19 | period: ".", 20 | qmark: "?", 21 | "!" 22 | )) 23 | ) 24 | ) 25 | 26 | :period buffalo 27 | buffalo(:qmark) 28 | 29 | excited-buffalo = buffalo partial(:exmark) 30 | excited-buffalo() 31 | ``` 32 | 33 | [Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo](http://en.wikipedia.org/wiki/Buffalo_buffalo_Buffalo_buffalo_buffalo_buffalo_Buffalo_buffalo). English yall. 34 | 35 | prose's syntax is based on [Io](http://iolanguage.org/) and [Ioke](https://ioke.org/). Unlike Io and Ioke, it is not particularly object-oriented. Instead it is based on and compiles to readable [Clojure](http://clojure.org/) and [ClojureScript](https://github.com/clojure/clojurescript). This allows us to hook into the JVM and anything ClojureScript runs on (which is anywhere JavaScript runs), and gives us very powerful features like multimethods, protocols, and the async library. Oh, and we also get macros, without the traditional lisp syntax. 36 | 37 | The core idea behind prose is that in Clojure or any other functional language, function application is the most important aspect of the language; it should also be the most important part of the syntax. As such, whitespace denotes function application (multiple expressions are denoted by newlines). 38 | 39 | ``` 40 | sum = reduce partial(+) 41 | [1, 2, 3] sum + 1 println 42 | ``` 43 | 44 | This is equivalent to the following Clojure code. 45 | 46 | ``` 47 | (def sum (partial reduce +)) 48 | (println (+ 1 (sum [1 2 3]))) 49 | ``` 50 | 51 | Notice how the prose example allows the use of operators and also reads more like natural language. 52 | 53 | If you want to know more about the language, [resources/test.pr](https://github.com/aaron-lebo/prose/blob/master/resources/example.pr) has numerous examples and is currently considered the canonical test of compiler. The results of compiling that file are found in [resources/example.clj](https://github.com/aaron-lebo/prose/blob/master/resources/example.clj). The parser is written using the incredible [Instaparse](https://github.com/Engelberg/instaparse) library. I cannot overstate how cool it is. It takes [this grammar definition](https://github.com/aaron-lebo/prose/blob/master/resources/grammar.bnf) and generates an AST. It is easily the best lexing/parsing tool I've ever used. The compiler attempts to keep expressions on the same line when going from input to output file in order to have more understandable error messages. It isn't perfect, but Clojure has terrible error messages anyway, so it can't hurt too much. I'm kidding? 54 | 55 | NOTE: There are subtle bugs with comments; for now I'm leaving them instead of ignoring comments entirely. 56 | 57 | ## installation 58 | 59 | 1. [Install Leiningen](https://github.com/technomancy/leiningen#installation). 60 | 2. ```git clone https://github.com/aaron-lebo/prose.git``` 61 | 3. ```cd prose``` 62 | 4. ```lein deps``` 63 | 5. Inside the project directory you can now use ```lein prose``` (once I properly register it as a lein plugin it will be usable anywhere). 64 | 65 | ## usage 66 | 67 | Calling the command with a single argument will output the Instaparse AST (the AST currently keeps extraeneous whitespace information that is only necessary for the compiler). 68 | 69 | ```lein prose resources/example.pr``` 70 | 71 | Calling the command with a second argument will generate a Clojure file at that location. 72 | 73 | ```lein prose resources/example.pr resources/example.clj``` 74 | 75 | ## why 76 | 77 | Lisp dialects are often talked about as though they were religious experiences that everyone should experience at one point in their lifetime. You can even find John McCarthy's creation of the original LISP in 1956 spoken of in terms of discovery, as though lisp was an immutable law or truth like gravity, just waiting for human beings to become enlightened enough to realize it was there. 78 | 79 | Perhaps this is for good reason. There are many features in the original that we are only just taking for granted in modern languages today. Then you've got Scheme and its incredibly clean, small, yet powerful core, and Common Lisp which has features that a lot of modern dynamic languages are still missing: speed, true compilation, the ability to jack into and modify a running program. The lisp family of languages is fascinating and any student of programming is doing a disservice to themselves if they haven't gone back and tried to understand what it all means. 80 | 81 | However, lisp's greatest advantage, most distinguishing feature, as well as greatest disadvantage as far as adoption goes is syntax. Syntax is a superficial thing. Spend enough time working in a language and you can get used to anything. The quote by Matt Damon's character in *The Departed* comes to mind: *I'm fucking Irish, I'll deal with something being wrong for the rest of my life*. Lisp syntax isn't even particularly bad. People mock it because of all the parentheses, but the fact of the matter is, it has no more parentheses than a lot of other mainstream languages and its lack of visual variety allows for one very powerful thing: macros. 82 | 83 | Despite this, there is no doubt that lisp syntax can be jarring, especially in contrast to just about every other language. Complaints about syntax may be superficial, and they may be things that you get over after continued use, but many people never reach this point because they see the syntax and never give it a second chance. You can complain all day about how this is intellectually lazy, but its just how human beings work. People have been struggling for years to figure out how to get lisp to be adopted in the mainstream. It really is pretty simple: change human nature and the masses will start using lisp. 84 | 85 | Yes, that was a joke. The thing is, despite my own continued use of lisp, there is one aspect of the syntax that I do believe is detrimental to readability. This is nesting. Let's take a closer look. Say you want to take an array of numbers, sum them up, add one to that total, and then print the result. In many mainstream languages, the code reads very similarly to that actual description. With the correct methods defined, the following code works in Python, Ruby, JavaScript, and numerous other languages. 86 | 87 | ``` 88 | ([1, 2, 3].sum() + 1).print() 89 | ``` 90 | 91 | Aside from the fact that lisps do not have true operators, in lisps this reads inside-out. 92 | 93 | ``` 94 | (print (+ 1 (sum [1 2 3]))) 95 | ``` 96 | 97 | The former example reads much more like UNIX piping: one value gets passed to the next. I personally believe that this is intuitively easier for most people to follow, at least if you read a language that reads from left to right, and perhaps even if you do not. Outside of just that, you constantly hear people complain about lisp having poor syntax for math; we are trained from youth to use operators. These are hard habits to break. 98 | 99 | Enough bashing on lisp. The question is can we can take three great advantages of lisp syntax: uniformity, homoiconicity (for easy macros), and simplicity, and incorporate true operators while eliminating too much nesting? We can, and prose is an attempt at this. 100 | 101 | ## what 102 | 103 | prose is heavily inspired by Io and Ioke. Io is Steve Dekorte's minimalist and radical experiment in language design and Ioke is Ola Bini's later work, inspired by Io, which runs on the JVM. Both of these languages are incredibly elegant; you might call them art house programming languages. They are very aesthetically pleasing, and that in and of itself is a noble goal. David Heinemeier Hanson, the creator of Rails, used to make a big deal about "beautiful code". This might seem silly and even pretentious, but I firmly believe that coding is both a creative and artistic endeavor. In an industry that is so often thought of in such mechanical and cold terms, sometimes we forget about the artistry and magic of putting together the right algorithm and then writing that code in an elegant manner. You can do this in any language, but some languages emphasize it more than others. 104 | 105 | But, hey, the underlying mechanics of it all do matter. Languages are more than syntax. Io and Ioke are incredibly, perhaps overly dynamic languages. They are both difficult to efficiently compile. Being radical, they also have some features which deviate from more mainstream languages. This makes adoption difficult. 106 | 107 | What we want is a language which captures the essence of their syntax while still aligning with more traditional semantics. At the heart of the Io family of languages is the use of whitespace. Some of you will freak out about this, but it is not whitespace the way Python uses it. In Io, methods are invoked via whitespace. Our earlier Python/Ruby/JS/etc example might look something more like the following. 108 | 109 | ``` 110 | list(1, 2, 3) sum + 1 println 111 | ``` 112 | 113 | This eliminates a lot of noise and makes the language read more like natural language than many others. Multiple expressions are denoted by newlines. 114 | 115 | ``` 116 | arr := list(1, 2, 3) 117 | res := arr sum + 1 118 | res println 119 | ``` 120 | 121 | Now, Io and Ioke are both prototype-based languages. Within the context of mainstream lisps this doesn't make a lot of sense. Some lisps, like Common Lisp even are object-oriented or make object orientation a core aspect of the language. Fundamentally, though, they are based around functions. Functions can also be expressed in a piping manner. Clojure even has a macro which does this, the threading operator. 122 | 123 | ``` 124 | (-> [1, 2, 3] (reduce +) inc println) 125 | ``` 126 | 127 | This works great, but why should it be an additional operator that you have to pull out from time to time? Function application is the most important aspect of a lisp, why should it not be the most favored syntax? Let's take Io's use of whitespace as method invocation and use it for function application. 128 | 129 | ``` 130 | [1, 2, 3] sum + 1 println 131 | ``` 132 | 133 | If you are having trouble following, the vector of numbers gets passed as the first argument to sum which is then called, the result of that is passed in as the first argument to + which is called, and the final result get passed to println which is then called. 134 | 135 | This is prose, and it naturally compiles down to Clojure and ClojureScript. Why Clojure and ClojureScript? The Clojure family of languages are modern lisps which can harness the power and ecosystems of Java and JavaScript respectively. They are based around immutability and functional programming. They also have a lot of really interesting features like multimethods, protocols, and the async library. I'm of the personal opinion that feature for feature they are the most well-designed dynamic languages today. They also make a really great compilation target because the syntax is so simple and everything is an expression. 136 | 137 | To sum it all up: prose is a simple language inspired by Io and Ioke that compiles to Clojure and ClojureScript. 138 | 139 | ## the future 140 | 141 | This is partially a pet project, partially an experiment in syntax, and partially a test to gauge whether other people are interested in working with me on making this truly usable. I have no formal training in compilers and as such prose is the result of years of fumbling around. The grammar definition and compiler could probably be much cleaner and simpler. If there was enough interest, it might make more sense to stop using Clojure and ClojureScript as compilation targets and instead to hook into the same code generation tools they use (perhaps compiling to Clojure and ClojureScript ASTs). They are seen as compilation targets instead of hard standards first and foremost, but interoperability is very important. Their communities are too smart, it does not make sense to diverge. Additionally, the future seems to be with gradual typing. It would be interesting to make a project like [Typed Clojure](http://typedclojure.org/) a core part of the syntax. 142 | 143 | I want make this more than just an experiment. New Github issues and pull requests are very appreciated. 144 | 145 | ## license 146 | 147 | MIT 148 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to esoteric 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject prose "0.1.0-SNAPSHOT" 2 | :description "a simple language, inspired by Io and Ioke, that compiles to Clojure and ClojureScript" 3 | :url "http://github.com/aaron-lebo/prose" 4 | :license {:name "The MIT License (MIT)" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.6.0"] 7 | [instaparse "1.3.5"]] 8 | :plugins [[lein-cljfmt "0.1.10"]] 9 | :eval-in-leiningen true) 10 | -------------------------------------------------------------------------------- /resources/example.clj: -------------------------------------------------------------------------------- 1 | ;; a comment (note: two semicolons and not one unlike Clojure) 2 | 3 | ;; literals (integer, float, string, regex, keyword, vector, set, list, boolean) 4 | 5 | 1 6 | 1.0 7 | "string" 8 | #"rege" 9 | :keyword 10 | [1 2 3] 11 | #{:a :b :c} 12 | (1 2 3) 13 | ();; empty list 14 | (1);; list with one item 15 | 1;; the expression 1 16 | true 17 | false 18 | nil 19 | 20 | ;; hash-maps and pairs 21 | 22 | {:a 1 :b 2 :c 3};; hash-map 23 | :a 1 24 | 0 1 ;; becomes 0 1;; becomes 0 1 25 | "hello" "world" ;; becomes "hello" "world";; becomes "hello" "world" 26 | 27 | ;; this behavior is mostly to allow for nice-looking hash maps, default args, etc 28 | 29 | ;; symbols - like Clojure,a wide variety of identifiers not available in other languages 30 | 31 | awesome? 32 | awesome! 33 | so-good 34 | 35 | 36 | ;; calls 37 | 38 | (println 1) 39 | (println 1 2 3) 40 | (if true :yes :no) 41 | (my-function "test" :test "this");; keyword args, when applicable 42 | 43 | ;; application - functions can be applied in a way that looks like method invocation or UNIX pipes 44 | 45 | (println 1) 46 | (println 1 2 3) 47 | (if true :yes :no) 48 | (my-function "test" :test "this") 49 | (println (dec (inc 1)));; can also be chained 50 | 51 | ;; newlines - denote multiple expressions, much like Ruby (you can also use semicolons) 52 | ;; multiple expressions get wrapped in a Clojure do form except where that is uncessary (let, defn, etc bodies). 53 | 54 | (if true :yes 55 | (do (println :no) 56 | :no)) 57 | 58 | (if true :yes (do (println :no) :no)) 59 | 60 | ;; operators - symbols which contain only non-alphanumeric characters (currently, any combination of .!$%&*_+=|<>?-) 61 | 62 | + 63 | - 64 | * 65 | / 66 | ? 67 | !!! 68 | <|> 69 | 70 | ;; operations - space is imporant 71 | 72 | ;; 1+1 is a syntax error, not an operation 73 | (+ 1 1);; this is an operation 74 | 75 | ;; space between args and operators allows operators to act like symbols in many contexts 76 | 77 | (def sum (partial reduce +)) 78 | (+ 1 2) 79 | 80 | ;; with a single exception (as we'll see), there's no precedence 81 | 82 | (* (+ 1 1) 2);; = 4 83 | 84 | ;; lack of precedence keeps both the implementation and usage simple 85 | ;; use parens to dictate flow 86 | 87 | (+ 1 (* 1 2));; = 3 88 | 89 | ;; assignment - lower precedence than all other operations 90 | ;; translates to (def left right) for all right-hand expressions except for calls 91 | 92 | (def a 1) 93 | (def b [1 2 3]) 94 | (def c (partial reduce +)) 95 | 96 | ;; = with function/macro/special form calls are essentially syntax sugar, all of thse are the same 97 | 98 | (defn fun [x] (+ x x)) 99 | (defn fun [x] (+ x x)) 100 | (defn fun [x] (+ x x)) 101 | 102 | ;; if you want to test equality, use == 103 | 104 | (= 0 0);; becomes (= 0 0) 105 | 106 | ;; now we know enough to write more complete programs 107 | 108 | ;; single arg 109 | (defn hello [name] 110 | (str "Hello, " name)) 111 | 112 | 113 | ;; no args 114 | (defn hello-hello 115 | [] (hello "Hello")) 116 | 117 | 118 | ;; default/keyword arg 119 | (defn hello-dallas [& :keys [name] :or {name "Dallas"}] 120 | (hello name)) 121 | 122 | 123 | ;; multiple args and multiple expressions 124 | (def add x y 125 | (let [total (+ x y)] 126 | (println total) 127 | total)) 128 | 129 | 130 | 131 | (add 1 2) 132 | (add 1 2) 133 | 134 | ;; just to show we can 135 | (def ? or) 136 | (? (? (second [1]) nil) 1) 137 | 138 | ;; let's test a macro 139 | ;; https://clojuredocs.org/clojure.core/defmacro#example-542692d2c026201cdc326f7a 140 | 141 | (defmacro unless [pred a b] 142 | `(if (not ~[:symbol "pred"]) ~[:symbol "a"] ~[:symbol "b"])) 143 | 144 | 145 | ;; java and JS interop look normal 146 | 147 | (.get System/getProperties "os.name") 148 | (.put (.put (new java.util.HashMap) "a" 1) "b" 2) 149 | 150 | ;; buffalo example from README 151 | 152 | (defn buffalo [end] 153 | (let [strings (repeat (* 1 8) "buffalo") 154 | idxs [0 2 6] 155 | f (fn [idx itm] 156 | (if (some (fn [idx2] (= idx2 idx)) idxs) 157 | (string/capitalize itm) 158 | itm)) 159 | 160 | 161 | res (map-indexed f strings)] 162 | (str (string/join " " res) (case end 163 | :period "." 164 | :qmark "?" 165 | "!")))) 166 | 167 | 168 | 169 | 170 | (buffalo :period) 171 | (buffalo :qmark) 172 | 173 | (def excited-buffalo (partial buffalo :exmark)) 174 | (excited-buffalo) -------------------------------------------------------------------------------- /resources/example.pr: -------------------------------------------------------------------------------- 1 | ;; a comment (note: two semicolons and not one unlike Clojure) 2 | 3 | ;; literals (integer, float, string, regex, keyword, vector, set, list, boolean) 4 | 5 | 1 6 | 1.0 7 | "string" 8 | #"rege" 9 | :keyword 10 | [1, 2, 3] 11 | #{:a, :b, :c} 12 | (1, 2, 3) 13 | (,) ;; empty list 14 | (1,) ;; list with one item 15 | (1) ;; the expression 1 16 | true 17 | false 18 | nil 19 | 20 | ;; hash-maps and pairs 21 | 22 | {a: 1, b: 2, c: 3} ;; hash-map 23 | a: 1 ;; a pair, becomes :a 1, symbols on the left side get turned into keywords by default 24 | 0: 1 ;; becomes 0 1 25 | "hello": "world" ;; becomes "hello" "world" 26 | 27 | ;; this behavior is mostly to allow for nice-looking hash maps, default args, etc 28 | 29 | ;; symbols - like Clojure,a wide variety of identifiers not available in other languages 30 | 31 | awesome? 32 | awesome! 33 | so-good 34 | 35 | 36 | ;; calls 37 | 38 | println(1) 39 | println(1, 2, 3) 40 | if(true, :yes, :no) 41 | my-function("test", test: "this") ;; keyword args, when applicable 42 | 43 | ;; application - functions can be applied in a way that looks like method invocation or UNIX pipes 44 | 45 | 1 println 46 | 1 println(2, 3) 47 | true if(:yes, :no) 48 | "test" my-function(test: "this") 49 | 1 inc dec println ;; can also be chained 50 | 51 | ;; newlines - denote multiple expressions, much like Ruby (you can also use semicolons) 52 | ;; multiple expressions get wrapped in a Clojure do form except where that is uncessary (let, defn, etc bodies). 53 | 54 | true if(:yes, 55 | :no println 56 | :no 57 | ) 58 | true if(:yes, :no println; :no) 59 | 60 | ;; operators - symbols which contain only non-alphanumeric characters (currently, any combination of .!$%&*_+=|<>?-) 61 | 62 | + 63 | - 64 | * 65 | / 66 | ? 67 | !!! 68 | <|> 69 | 70 | ;; operations - space is imporant 71 | 72 | ;; 1+1 is a syntax error, not an operation 73 | 1 + 1 ;; this is an operation 74 | 75 | ;; space between args and operators allows operators to act like symbols in many contexts 76 | 77 | sum = reduce partial(+) 78 | +(1, 2) 79 | 80 | ;; with a single exception (as we'll see), there's no precedence 81 | 82 | 1 + 1 * 2 ;; = 4 83 | 84 | ;; lack of precedence keeps both the implementation and usage simple 85 | ;; use parens to dictate flow 86 | 87 | 1 + (1 * 2) ;; = 3 88 | 89 | ;; assignment - lower precedence than all other operations 90 | ;; translates to (def left right) for all right-hand expressions except for calls 91 | 92 | a = 1 ;; (def a 1) 93 | b = [1, 2, 3] ;; (def b [1 2 3]) 94 | c = reduce partial(+) ;; (def c (partial reduce +) 95 | 96 | ;; = with function/macro/special form calls are essentially syntax sugar, all of thse are the same 97 | 98 | fun = defn(x, x + x) 99 | fun defn(x, x + x) 100 | defn(fun, x, x + x) 101 | 102 | ;; if you want to test equality, use == 103 | 104 | 0 == 0 ;; becomes (= 0 0) 105 | 106 | ;; now we know enough to write more complete programs 107 | 108 | ;; single arg 109 | hello = defn(name, 110 | "Hello, " str(name) 111 | ) 112 | 113 | ;; no args 114 | hello-hello = defn( 115 | "Hello" hello 116 | ) 117 | 118 | ;; default/keyword arg 119 | hello-dallas = defn(name: "Dallas", 120 | name hello 121 | ) 122 | 123 | ;; multiple args and multiple expressions 124 | add = def(x, y, 125 | let(total: x + y, 126 | total println 127 | total 128 | ) 129 | ) ;; end def 130 | 131 | 1 add(2) 132 | add(1, 2) 133 | 134 | ;; just to show we can 135 | ? = or 136 | [1] second ? nil ? 1 137 | 138 | ;; let's test a macro 139 | ;; https://clojuredocs.org/clojure.core/defmacro#example-542692d2c026201cdc326f7a 140 | 141 | unless = defmacro(pred, a, b, 142 | `if(~pred not, ~a, ~b) 143 | ) 144 | 145 | ;; java and JS interop look normal 146 | 147 | System/getProperties .get("os.name") 148 | java.util.HashMap new .put("a", 1) .put("b", 2) 149 | 150 | ;; buffalo example from README 151 | 152 | buffalo = defn(end, 153 | let(strings: 1 * 8 repeat("buffalo"), 154 | idxs: [0, 2, 6], 155 | f: fn(idx, itm, 156 | if(idx2 fn(idx2 == idx) some(idxs), 157 | itm string/capitalize, 158 | itm 159 | ) 160 | ), 161 | res: f map-indexed(strings), 162 | " " string/join(res) str(end case( 163 | period: ".", 164 | qmark: "?", 165 | "!" 166 | )) 167 | ) 168 | ) 169 | 170 | :period buffalo 171 | buffalo(:qmark) 172 | 173 | excited-buffalo = buffalo partial(:exmark) 174 | excited-buffalo() 175 | -------------------------------------------------------------------------------- /resources/grammar.bnf: -------------------------------------------------------------------------------- 1 | program = s? exps s? 2 | = do | exp 3 | do = exp ( (n+ | <';'>) exp)+ 4 | = atom | assignment | operation | comment | !pair exp p? comment 5 | = symbol | boolean | pair | keyword | number | call | list | group | map | vector | set | regex | string | syntax-quote | unquote 6 | comment = ';;' #'.*' 7 | 8 | syntax-quote = '`' atom 9 | unquote = '~' atom 10 | 11 | boolean = 'true' | 'false' | 'nil' 12 | number = digits ('.' digits)? 13 | symbol = '::'? !boolean id 14 | keyword = ':' id 15 | string = quoted 16 | regex = '#' quoted 17 | = '"' #'[^\\"]|\\.'* '"' 18 | 19 | assignment = exp exp 20 | equals =

'='

21 | operation = (operation | !pair atom) !(comment | equals | '.') (operator | space) !pair atom 22 | operator =

special

23 | space =

24 | 25 | = '/' | qualified ('/' qualified)? 26 | = non-numeric (':'? (non-numeric | digits))* 27 | = #'[0-9]+' 28 | = #'[a-zA-Z]+' | special 29 | = ('.' | #'[!$%&*_+=|<>?-]')+ 30 | 31 | group = lp s? exp? s? rp 32 | list = lp (comma | s? (exps comma | exps (comma exps)+) s?) rp 33 | vector = <'['> args <']'> 34 | map = <'{'> args <'}'> 35 | pair = exp <':'> exp !(p? comment) 36 | set = <'#{'> args <'}'> 37 | call = !pair atom lp args rp 38 | = s? | s? exps (comma exps)* s? 39 | = s? <','> s? 40 | = <'('> 41 | = <')'> 42 | 43 | = ( n |

)+ 44 |

= #'\p{Blank}+' 45 | n = <'\n'|'\r\n'> 46 | 47 | -------------------------------------------------------------------------------- /src/leiningen/prose.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.prose 2 | (:require [prose.core :as pr])) 3 | 4 | (defn prose [project path & [out-path]] 5 | (if out-path 6 | (pr/compile-file path out-path) 7 | (pr/pretty path))) 8 | -------------------------------------------------------------------------------- /src/prose/core.clj: -------------------------------------------------------------------------------- 1 | (ns prose.core 2 | (:require [clojure.string :as string] 3 | [clojure.pprint :as pprint] 4 | [instaparse.core :as insta])) 5 | 6 | (declare gen) 7 | (def indent (atom 0)) 8 | (def newlines (atom [])) 9 | 10 | (defn str+ [out f sequence] 11 | (str out (string/join (map f sequence)))) 12 | 13 | (defn node? [head node] 14 | (= head (first node))) 15 | 16 | (defn ->call [sym & args] 17 | (concat [:call [:symbol sym]] (apply concat args))) 18 | 19 | (defn join [out args & [left right]] 20 | (let [res (string/join " " (map (partial gen "") args)) 21 | regex #"\s+(;;.*)?$" 22 | match (re-find regex res)] 23 | (str out left (string/replace res regex "") right (apply str match)))) 24 | 25 | (defn keep-indexed* [tail ? key] 26 | (keep-indexed (fn [idx [f-itm & _]] (if (? key f-itm) idx)) tail)) 27 | 28 | (defn program [out [_ & [[ff-tail & rf-tail] & _ :as tail]]] 29 | (str+ out (partial gen "") (if (= :do ff-tail) rf-tail tail))) 30 | 31 | (defn do-node [out [_ & [f-tail & r-tail :as tail]]] 32 | (gen out (->call "do" (if (node? :n f-tail) r-tail tail)))) 33 | 34 | (defn n [out _] 35 | (swap! newlines #(conj % "\n")) 36 | out) 37 | 38 | (defn fn-node [start out tail] 39 | (let [tail (vec tail) 40 | keep-indexed* (partial keep-indexed* tail) 41 | body-idx (last (keep-indexed* not= :n)) 42 | arg-idxs (drop start (keep-indexed* = :symbol)) 43 | arg-idxs (if (= body-idx (last arg-idxs)) (butlast arg-idxs) arg-idxs) 44 | subvec* (partial subvec tail) 45 | args (if (seq arg-idxs) (subvec* (first arg-idxs) (inc (last arg-idxs))) []) 46 | karg-idxs (keep-indexed* = :pair) 47 | karg-idxs (if (= body-idx (last karg-idxs)) (butlast karg-idxs) karg-idxs) 48 | kargs (if (seq karg-idxs) (subvec* (first karg-idxs) (inc (last karg-idxs))) []) 49 | kargs (map #(if (node? :pair %) (assoc-in % [0] :symbol-pair) %) kargs) 50 | keys (if (seq kargs) 51 | (concat 52 | [[:symbol "&"] [:keyword ":keys"] 53 | (cons :vector (keep #(if (node? :symbol-pair %) (second %)) kargs)) 54 | [:keyword ":or"] (cons :map kargs)]) 55 | kargs) 56 | params (cons :vector (concat args keys)) 57 | [_ & r-body :as body] (nth tail body-idx) 58 | body (if (node? :do body) r-body [body])] 59 | (join out 60 | (concat (subvec* 0 (or (first arg-idxs) (first karg-idxs) body-idx)) 61 | [params] 62 | (subvec* (inc (or (last karg-idxs) (last arg-idxs) (dec body-idx))) body-idx) 63 | body 64 | (subvec tail (inc body-idx))) 65 | "(" ")"))) 66 | 67 | (defn let-node [out [head & _ :as tail]] 68 | (let [tail (vec tail) 69 | idx (last (keep-indexed* tail not= :n)) 70 | kargs (cons :vector 71 | (map #(if (node? :pair %) (assoc-in % [0] :symbol-pair) %) 72 | (subvec tail 1 idx))) 73 | [_ & r-body :as body] (nth tail idx) 74 | body (if (node? :do body) r-body [body])] 75 | (join out (concat [head kargs] body (subvec tail (inc idx))) "(" ")"))) 76 | 77 | (defn call [out [_ & [head _ :as tail]]] 78 | (case head 79 | [:symbol "fn"] (fn-node 1 out tail) 80 | [:symbol "defn"] (fn-node 2 out tail) 81 | [:symbol "defmacro"] (fn-node 2 out tail) 82 | [:symbol "let"] (let-node out tail) 83 | (join out tail "(" ")"))) 84 | 85 | (defn operation [out [_ left op right]] 86 | (gen out 87 | (case op 88 | [:space] (if (node? :call right) 89 | (concat (subvec right 0 2) [left] (subvec right 2)) 90 | [:call right left]) 91 | [:operator "=" "="] (->call "=" [left right]) 92 | [:call op left right]))) 93 | 94 | (defn assignment [out [_ & [left right]]] 95 | (gen out 96 | (if (node? :call right) 97 | (concat [:call (second right) left] (subvec right 2)) 98 | (->call "def" [left right])))) 99 | 100 | (defn list-node [out [_ & tail]] 101 | (join out tail "(" ")")) 102 | 103 | (defn map-node [out [_ & tail]] 104 | (join out tail "{" "}")) 105 | 106 | (defn pair [out [_ & tail]] 107 | (join out tail)) 108 | 109 | (defn pair! [out [_ & [[_ s-left :as left] right :as tail]]] 110 | (join out 111 | (if (node? :symbol left) 112 | [[:keyword ":" s-left] right] 113 | tail))) 114 | 115 | (defn set-node [out [_ & tail]] 116 | (join out tail "#{" "}")) 117 | 118 | (defn vector-node [out [_ & tail]] 119 | (join out tail "[" "]")) 120 | 121 | (defn group [out [_ exp]] 122 | (gen out exp)) 123 | 124 | (defn syntax-quote [out [_ & [f-tail & r-tail :as tail]]] 125 | (join out r-tail f-tail)) 126 | 127 | (defn default [out [_ & tail]] 128 | (str out (string/join tail))) 129 | 130 | (defn shift [node-fn] 131 | (fn [& args] 132 | (swap! indent inc) 133 | (let [res (apply node-fn args)] 134 | (swap! indent dec) 135 | res))) 136 | 137 | (defn gen [out [head & tail :as node]] 138 | (let [node-fn (case head 139 | :program program 140 | :do do-node 141 | :n n 142 | :call (shift call) 143 | :operation operation 144 | :assignment assignment 145 | :list (shift list-node) 146 | :map (shift map-node) 147 | :pair pair! 148 | :symbol-pair pair 149 | :set (shift set-node) 150 | :vector (shift vector-node) 151 | :group group 152 | :syntax-quote syntax-quote 153 | default) 154 | res (str+ out #(str % (string/join (repeat (* @indent 2) \space))) @newlines)] 155 | (reset! newlines []) 156 | (node-fn res node))) 157 | 158 | (def parser (insta/parser (clojure.java.io/resource "grammar.bnf"))) 159 | 160 | (defn parse [path] 161 | (insta/parse parser (slurp (clojure.java.io/file path)))) 162 | 163 | (defn pretty [path] 164 | (pprint/pprint (parse path))) 165 | 166 | (defn compile-file [path path-out] 167 | (let [res (parse path)] 168 | (if (vector? res) 169 | (spit (clojure.java.io/file path-out) (gen "" res)) 170 | (println res)))) 171 | -------------------------------------------------------------------------------- /test/prose/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns prose.core-test 2 | (:require [clojure.test :refer :all] 3 | [prose.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | --------------------------------------------------------------------------------