diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aab7737 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +on: [push, pull_request] +name: CI +jobs: + build: + name: "Build on Racket '${{ matrix.racket-version }}' (${{ matrix.racket-variant }})" + runs-on: ubuntu-latest + strategy: + matrix: + racket-version: ["stable", "current"] + racket-variant: ["BC", "CS"] + steps: + - uses: actions/checkout@v2 + - uses: Bogdanp/setup-racket@v0.12 + with: + architecture: x64 + distribution: full + variant: ${{ matrix.racket-variant }} + version: ${{ matrix.racket-version }} + - name: Installing fmt and its dependencies + run: raco pkg install --no-docs --auto --name fmt + - name: Compiling fmt and building its docs + run: raco setup --check-pkg-deps --unused-pkg-deps fmt + - name: Testing fmt + run: raco test -x -p fmt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a59348 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*~ +\#* +.\#* +.DS_Store +compiled/ +/doc/ diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..cb0e623 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,13 @@ +Copyright 2021 sorawee + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..06d955f --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +fmt + +MIT License + +Copyright (c) 2021 sorawee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d7b123 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +fmt +=== + +A code formatter for Racket. + +Work in progress. + +To run, execute `racket run.rkt `. diff --git a/conventions.rkt b/conventions.rkt new file mode 100644 index 0000000..7c77c98 --- /dev/null +++ b/conventions.rkt @@ -0,0 +1,50 @@ +#lang racket/base + +(provide hook-standard + hook-define) +(require racket/match + racket/list + pprint-compact + "core.rkt") + +(define (hook-define xs pretty) + (define xs* (map pretty xs)) + (define req-last-newline? (require-newline? (last xs))) + + (apply + alt + (append + ;; fallback case + (if req-last-newline? + (list (v-append (first xs*) + (h-append space (v-concat (rest xs*))))) + (list (v-append (first xs*) + (h-append space (flush (v-concat (rest xs*))))))) + ;; fit in one line case + (match xs + [(list _ _ _) + #:when (not (ormap require-newline? xs)) + (list (flat (hs-concat xs*)))] + [_ '()]) + + ;; regular case + (match xs + [(list head _ _ _ ...) + #:when (not (require-newline? head)) + (if req-last-newline? + (list (v-append + (h-append (first xs*) + space + (second xs*)) + (h-append space (flush (v-concat (rest (rest xs*))))))) + (list (v-append + (h-append (first xs*) + space + (second xs*)) + (h-append space (v-concat (rest (rest xs*)))))))] + [_ '()])))) + +(define (hook-standard name) + (case name + [("define") hook-define] + [else #f])) diff --git a/core.rkt b/core.rkt new file mode 100644 index 0000000..b45ff72 --- /dev/null +++ b/core.rkt @@ -0,0 +1,308 @@ +#lang racket/base + +(provide program-format + (struct-out thing) + (struct-out node) + (struct-out atom) + (struct-out nl) + (struct-out line-comment) + (struct-out sexp-comment) + (struct-out bare-sexp-comment) + (struct-out toplevel) + (struct-out wrapper) + require-newline?) + +(require racket/match + racket/string + racket/list + syntax/readerr + syntax-color/module-lexer + pprint-compact + #;pprint-compact/debug + #;(only-in racket/pretty [pretty-print pp])) + +(struct token (srcloc text type) #:transparent) + +(struct thing (extra) #:transparent) +(struct node thing (opener closer content) #:transparent) +(struct atom thing (content type) #:transparent) +(struct nl thing (n) #:transparent) +(struct line-comment thing (content) #:transparent) +(struct sexp-comment thing (level content) #:transparent) +(struct bare-sexp-comment thing (tok) #:transparent) +(struct toplevel thing (content) #:transparent) +(struct wrapper thing (prefix content) #:transparent) + +;; tokenize :: string? -> (listof token?) +(define (tokenize program-source + #:max-newlines [max-newlines 2] + #:source [source #f]) + (define p (open-input-string program-source source)) + (port-count-lines! p) + (let loop ([mode #f]) + (define start-srcloc (call-with-values (λ () (port-next-location p)) list)) + (match-define-values (text type paren-type start-pos end-pos _ new-mode) + (module-lexer p 0 mode)) + (cond + [(eof-object? text) '()] + [else + (define srcloc (list (first start-srcloc) + (second start-srcloc) + (third start-srcloc) + (- end-pos start-pos))) + (define current + (cond + [(eq? type 'parenthesis) + (token srcloc text `(parenthesis ,paren-type))] + [(eq? type 'white-space) + (define num-newlines + (sub1 (length (string-split text "\n" #:trim? #f)))) + (token srcloc + "" + `(white-space ,(cond + [(> num-newlines max-newlines) max-newlines] + [else num-newlines])))] + + [(eq? type 'sexp-comment) + (define re-read + (substring program-source (sub1 start-pos) (sub1 end-pos))) + (cond + [(equal? text re-read) + (token srcloc text 'sexp-comment)] + [else (token srcloc re-read 'block-comment)])] + + ;; non-comment + [(not (eq? type 'comment)) + (token srcloc text type)] + ;; non-empty regular line comment + [(non-empty-string? text) + (token srcloc (string-append ";" text) 'line-comment)] + ;; empty regular line comment + [(= end-pos (add1 start-pos)) + (token srcloc ";" 'line-comment)] + ;; block comment + [else + (token srcloc + (substring program-source (sub1 start-pos) (sub1 end-pos)) + 'block-comment)])) + (cons current (loop new-mode))]))) + +(define (find-closer p) + (match p + ['|(| '|)|] + ['|[| '|]|] + ['|{| '|}|])) + +(define openers (list '|(| '|[| '|{|)) +(define closers (list '|)| '|]| '|}|)) + +(define (open-paren? x) + (memq x openers)) + +(define (close-paren? x) + (memq x closers)) + +(define (process-tail obj xs) + (define (do-it comment xs) + (values + (cond + [(node? obj) + (struct-copy node obj + [extra #:parent thing comment])] + [(atom? obj) + (struct-copy atom obj + [extra #:parent thing comment])]) + xs)) + (match xs + [(list (token _ _ `(white-space 0)) (token _ comment 'line-comment) xs ...) + (do-it comment xs)] + [(list (token _ comment 'line-comment) xs ...) + (do-it comment xs)] + [_ (values obj xs)])) + +(define (read-one xs #:source [source #f] #:while-reading [while-reading #f]) + (match xs + ['() (raise-read-eof-error "unexpected eof" source #f #f #f #f)] + + [(cons (token close-srcloc _ `(parenthesis ,(? close-paren? p))) _) + (cond + [while-reading + (apply raise-read-error + (format + "expected `~a` to close preceding `~a`, found instead `~a`" + (find-closer while-reading) + while-reading + p) + source + close-srcloc)] + [else + (apply raise-read-error + (format "unexpected `~a`" p) + source + close-srcloc)])] + + [(cons (token open-srcloc open-paren `(parenthesis ,(? open-paren? p))) xs) + (define closer (find-closer p)) + (let loop ([xs xs] [acc '()]) + (match xs + ['() (apply raise-read-eof-error + (format "expected a `~a` to close `~a`" closer p) + source + open-srcloc)] + [(cons (token _ close-paren `(parenthesis ,(== closer))) xs) + (process-tail (node #f + open-paren + close-paren + (dropf (reverse (dropf acc nl?)) nl?)) + xs)] + + [(cons (token _ _ `(white-space 0)) xs) + (loop xs acc)] + + [(cons (token _ _ `(white-space 1)) xs) + (loop xs acc)] + + [_ + (define-values (this xs*) + (read-one xs #:source source #:while-reading p)) + (loop xs* (cons this acc))]))] + + [(cons (token _ _ `(white-space 0)) xs) + (read-one xs #:source source #:while-reading while-reading)] + + [(cons (token _ _ `(white-space 1)) xs) + (read-one xs #:source source #:while-reading while-reading)] + + [(cons (token _ _ `(white-space ,n)) xs) + (values (nl #f (sub1 n)) xs)] + + [(cons (token _ (and c (or "'" "`" "#'" "#`")) 'constant) xs) + (define-values (this xs*) + (read-one xs #:source source #:while-reading #f)) + (values (wrapper #f c this) xs*)] + + [(cons (token _ (and c (or "," ",@" "#," "#,@")) 'other) xs) + (define-values (this xs*) + (read-one xs #:source source #:while-reading #f)) + (values (wrapper #f c this) xs*)] + + [(cons (token _ comment 'line-comment) xs) + (values (line-comment #f comment) xs)] + + [(cons (token _ tk 'sexp-comment) xs) + (match xs + [(cons (token _ _ `(white-space ,n)) xs) + #:when (positive? n) + (values (bare-sexp-comment #f tk) xs)] + [_ + (define-values (this xs*) + (read-one xs #:source source #:while-reading #f)) + (values (match this + [(bare-sexp-comment _ tk*) + (bare-sexp-comment #f (string-append tk tk*))] + [(sexp-comment _ level content) + (sexp-comment #f (add1 level) content)] + [_ (sexp-comment #f 1 this)]) + xs*)])] + + [(cons (token _ content kind) xs) + (process-tail (atom #f content kind) xs)])) + +(define (read-top xs #:source [source #f]) + (let loop ([xs xs] [acc '()]) + (match xs + ['() + (toplevel #f (reverse (dropf acc nl?)))] + + [(cons (token _ _ `(white-space 0)) xs) + (loop xs acc)] + + [(cons (token _ _ `(white-space 1)) xs) + (loop xs acc)] + + [_ + (define-values (this xs*) (read-one xs #:source source)) + (loop xs* (cons this acc))]))) + +(define sexp-comment-tok (text "#;")) + +(define (require-newline? d) + (or (thing-extra d) + (line-comment? d) + (bare-sexp-comment? d) + (nl? d))) + +(define ((pretty hook) d) + (let loop ([d d]) + (match d + [(toplevel _ xs) (v-concat (map loop xs))] + [(nl _ n) (v-concat (make-list n empty-doc))] + [(atom comment content _) + (if comment + (text (string-append content " " comment)) + (text content))] + [(line-comment _ comment) (text comment)] + [(sexp-comment _ 1 (line-comment _ comment)) + (text (string-append "#; " comment))] + [(sexp-comment _ 1 content) + (define content* (loop content)) + (alt (h-append sexp-comment-tok content*) + (v-append sexp-comment-tok content*))] + [(sexp-comment _ n content) + (define content* (loop content)) + (v-append (h-concat (make-list n sexp-comment-tok)) + content*)] + [(bare-sexp-comment _ tok) (text tok)] + [(wrapper _ pre (line-comment _ comment)) + (text (string-append pre " " comment))] + [(wrapper _ pre content) (h-append (text pre) (loop content))] + [(node comment opener closer xs) + (define (finalize d) + (define main-doc (h-append (text opener) d (text closer))) + (if comment + (h-append main-doc space (text comment)) + main-doc)) + (define (default) + (define xs* (map loop xs)) + (define req-last-newline? (require-newline? (last xs))) + (apply + alt + (append + (if req-last-newline? + (list (flush (v-concat xs*))) + (list (v-concat xs*))) + (if (ormap require-newline? xs) + '() + (list (flat (hs-concat xs*)))) + (if (require-newline? (first xs)) + '() + (if req-last-newline? + (list (h-append (flat (first xs*)) + space + (flush (v-concat (rest xs*))))) + (list (h-append (flat (first xs*)) + space + (v-concat (rest xs*))))))))) + (match xs + ['() + (if comment + (text (string-append opener closer " " comment)) + (text (string-append opener closer)))] + ;; TODO: checking the first token is not ideal, but will do it for now + [(cons (atom _ content 'symbol) _) + (define proc (hook content)) + (cond + [proc (finalize (proc xs loop))] + [else (finalize (default))])] + [_ (finalize (default))])]))) + +;; program-format :: string? -> string? +(define (program-format program-source + #:source [source #f] + #:width [width 80] + #:hook [hook (λ (name) #f)]) + (define doc + ((pretty hook) + (read-top (tokenize program-source #:source source) + #:source source))) + (pretty-format doc #:width width)) diff --git a/experiments/myfirstapp/CHANGELOG.md b/experiments/myfirstapp/CHANGELOG.md new file mode 100644 index 0000000..1fc247a --- /dev/null +++ b/experiments/myfirstapp/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for myfirstapp + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/experiments/myfirstapp/app/Main.hs b/experiments/myfirstapp/app/Main.hs new file mode 100644 index 0000000..67af540 --- /dev/null +++ b/experiments/myfirstapp/app/Main.hs @@ -0,0 +1,11 @@ +module Main where + +ones = 1:ones +xs = take 10000 ones + +f :: Int -> [Int] +f 0 = xs +f n = filter (\x -> True) (f (n - 1)) + +main :: IO () +main = putStrLn $ show $ length $ f 100000 diff --git a/experiments/myfirstapp/myfirstapp.cabal b/experiments/myfirstapp/myfirstapp.cabal new file mode 100644 index 0000000..4aeb4ce --- /dev/null +++ b/experiments/myfirstapp/myfirstapp.cabal @@ -0,0 +1,36 @@ +cabal-version: 2.4 +name: myfirstapp +version: 0.1.0.0 + +-- A short (one-line) description of the package. +-- synopsis: + +-- A longer description of the package. +-- description: + +-- A URL where users can report bugs. +-- bug-reports: + +-- The license under which the package is released. +-- license: +author: Sorawee Porncharoenwase +maintainer: sorawee.pwase@gmail.com + +-- A copyright notice. +-- copyright: +-- category: +extra-source-files: CHANGELOG.md + +executable myfirstapp + main-is: Main.hs + + -- Modules included in this executable, other than Main. + -- other-modules: + + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + build-depends: + base ^>=4.14.3.0, + pretty-compact ^>=3.0 + hs-source-dirs: app + default-language: Haskell2010 diff --git a/experiments/test.clj b/experiments/test.clj new file mode 100644 index 0000000..b870570 --- /dev/null +++ b/experiments/test.clj @@ -0,0 +1,5 @@ +(let [x 3 + y 4] + (+ (* x x) + (* y y) + (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y) (* y y))) \ No newline at end of file diff --git a/experiments/test.go b/experiments/test.go new file mode 100644 index 0000000..631e383 --- /dev/null +++ b/experiments/test.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func plus(a int, b int) int { + return a + b +} + +func main() { + fmt.Printf("%v", plus(1111111111111111111, plus(2222222222222222222, plus(2222222222222222222, plus(2222222222222222222, 2222222222222222222))))) + fmt.Println("hello world") +} diff --git a/experiments/test.hs b/experiments/test.hs new file mode 100644 index 0000000..fbdf071 --- /dev/null +++ b/experiments/test.hs @@ -0,0 +1,5 @@ +main = do + let var1 = 2 + let var2 = 3 + putStrLn "The two numbers add up to:" + print (var1 + var2) \ No newline at end of file diff --git a/experiments/test.py b/experiments/test.py new file mode 100644 index 0000000..939ea6f --- /dev/null +++ b/experiments/test.py @@ -0,0 +1,19 @@ +def test(x, y): + x + y + + +test( + 11111111, + test(11111111, 1111111), + test(11111111, 1111111), + 222, + 222, + test(11111111, 1111111), + test(11111111, 1111111), + 222, + test(11111111, 1111111), + test(11111111, 1111111), + test(11111111, 1111111), + test(11111111, 1111111), + test(11111111, 1111111), +) diff --git a/experiments/test.re b/experiments/test.re new file mode 100644 index 0000000..2a113ee --- /dev/null +++ b/experiments/test.re @@ -0,0 +1,18 @@ +type tree = + | Leaf + | Node(int, tree, tree); + +let f = (x, y) => x + y; + +let rec sum = item => + switch (item) { + | Leaf => 0 + | Node(value, left, right) => value + sum(left) + sum(right) + }; + +print_int( + f( + 1111111111111111111111111111111111, + 22222212222222222222222222222222222222, + ), +); \ No newline at end of file diff --git a/info.rkt b/info.rkt new file mode 100644 index 0000000..9e81e50 --- /dev/null +++ b/info.rkt @@ -0,0 +1,9 @@ +#lang info +(define collection "fmt") +(define deps '("base")) +(define build-deps '("scribble-lib" "racket-doc" "rackunit-lib")) +(define scribblings '(("scribblings/fmt.scrbl" ()))) +(define pkg-desc "Description Here") +(define version "0.0") +(define pkg-authors '(sorawee)) +(define license '(Apache-2.0 OR MIT)) diff --git a/run.rkt b/run.rkt new file mode 100644 index 0000000..c8bacd1 --- /dev/null +++ b/run.rkt @@ -0,0 +1,13 @@ +#lang racket + +(require racket/cmdline + "core.rkt" + "conventions.rkt") + +(define filename + (command-line + #:args (filename) + filename)) + +(display (program-format (file->string filename) + #:hook hook-standard)) diff --git a/scribblings/fmt.scrbl b/scribblings/fmt.scrbl new file mode 100644 index 0000000..7c688ce --- /dev/null +++ b/scribblings/fmt.scrbl @@ -0,0 +1,10 @@ +#lang scribble/manual +@require[@for-label[fmt + racket/base]] + +@title{fmt} +@author{sorawee} + +@defmodule[fmt] + +Package Description Here