~technomancy/fennel

8cf5bb8eab82d80ffc2c135d32135d07f06ef61e — Andrey Orst 4 months ago 3dbee7d
fennelview rewrite

This patch completely re-implements fennelview.fnl module, in order to
produce more concise indentation of printed data structure
representations, also changing how __fennelview metamethod works, to
make it possible to implement indentation semantics for custom data
structures.

This is a pretty big change, so I've tried to write as many new tests
as I could, and all existing tests remain working without
modifications (except for test-macrodebug which used its own line
concatenation method).

From now on fennelview will pretty-print tables in a more Lisp-y way,
putting opening and closing delimiters on the same line as the first
and last items in the table.  This required major changes in
indentation calculation algorithms, and modification of __fennelview
metamethod.

In this implementation __fennelview metamethod accepts 4 arguments.
First argument is a table being serialized, second argument is a
function, that holds fennelview function, third argument is the same
set of options as fennelview takes, and fourth argument is current
amount of indentation.  Nested data structures can receive meaningful
indentation amount from __fennelview and be printed correctly.

This requires __fennelview to return a table of strings, each
representing individual line, indented as if the data structure were
the only one printed.  Alternatively __fennelview can return a string,
which can be useful if you know if your data structure is never
multi-line, or you need to implement custom algorithm that transforms
data structure to single line more meaningfully (for example, if you
want key and value pairs to be separated with double spaces).

Inside __fennelview metamethod, the Options table contains same
options that were passed to fennelview, and also a new visible-cycle?
function - an fennelview API function to detect, and save information
about cycles in tables.  Should be used in combination with
options.seen table when __fennelview implements cycle
detection. fennelview function also accepts additional boolean
argument, which controls if strings should be printed as a
colon-strings when possible.
7 files changed, 678 insertions(+), 366 deletions(-)

M changelog.md
M fennelview.fnl
M fennelview.lua
M test/core.fnl
M test/fennelview.fnl
M test/macro.fnl
M test/quoting.fnl
M changelog.md => changelog.md +3 -0
@@ 2,6 2,9 @@

## 0.8.0 / ???

* fennelview: full rewrite for better indentatiom handling. See fennelview
  docstring for detailed description of API changes
* Improve printing of destructuring in function arg-list in documentation
* Allow plugins to provide repl commands
* Fix a bug where decimal numbers would be emitted with commas in some locales
* Lua output labels autogenerated locals for improved readability

M fennelview.fnl => fennelview.fnl +283 -169
@@ 1,65 1,42 @@
;; A pretty-printer that outputs tables in Fennel syntax.
;; Loosely based on inspect.lua: http://github.com/kikito/inspect.lua

(fn view-quote [str] (.. "\"" (: str :gsub "\"" "\\\"") "\""))

(local short-control-char-escapes
       {"\a" "\\a" "\b" "\\b" "\f" "\\f" "\n" "\\n"
        "\r" "\\r" "\t" "\\t" "\v" "\\v"})

(local long-control-char-escapes
       (let [long {}]
         (for [i 0 31]
           (let [ch (string.char i)]
             (when (not (. short-control-char-escapes ch))
               (tset short-control-char-escapes ch (.. "\\" i))
               (tset long ch (: "\\%03d" :format i)))))
         long))

(fn escape [str]
  (-> str
      (: :gsub "\\" "\\\\")
      (: :gsub "(%c)%f[0-9]" long-control-char-escapes)
      (: :gsub "%c" short-control-char-escapes)))

(fn sequence-key? [k len]
  (and (= (type k) "number")
       (<= 1 k)
       (<= k len)
       (= (math.floor k) k)))

(local type-order {:number 1 :boolean 2 :string 3 :table 4
                   :function 5 :userdata 6 :thread 7})

(fn sort-keys [[a] [b]]
  ;; Sort keys depending on the `type-order`.
  (let [ta (type a) tb (type b)]
    (if (and (= ta tb)
             (or (= ta "string") (= ta "number")))
        (< a b)
        (let [dta (. type-order ta)
              dtb (. type-order tb)]
          (if (and dta dtb)
              (< dta dtb)
          (if (and dta dtb) (< dta dtb)
              dta true
              dtb false
              :else (< ta tb))))))
              false)))))

(fn get-sequence-length [t]
  (var len 0)
  (each [i (ipairs t)] (set len i))
  len)
(fn table-kv-pairs [t]
  ;; Return table of tables with first element representing key and second
  ;; element representing value.  Second value indicates table type, which is
  ;; either sequential or associative.

(fn get-nonsequential-keys [t]
  (let [keys []
        sequence-length (get-sequence-length t)]
  ;; [:a :b :c] => [[1 :a] [2 :b] [3 :c]]
  ;; {:a 1 :b 2} => [[:a 1] [:b 2]]
  (var assoc? false)
  (let [kv []
        insert table.insert]
    (each [k v (pairs t)]
      (when (not (sequence-key? k sequence-length))
        (table.insert keys [k v])))
    (table.sort keys sort-keys)
    (values keys sequence-length)))
      (when (not= (type k) :number)
        (set assoc? true))
      (insert kv [k v]))
    (table.sort kv sort-keys)
    (if (= (length kv) 0)
        (values kv :empty)
        (values kv (if assoc? :table :seq)))))

(fn count-table-appearances [t appearances]
  (when (= (type t) "table")
  (when (= (type t) :table)
    (if (not (. appearances t))
        (do (tset appearances t 1)
            (each [k v (pairs t)]


@@ 68,145 45,282 @@
        (tset appearances t (+ (or (. appearances t) 0) 1))))
  appearances)

(fn save-table [t seen]
  ;; Save table `t` in `seen` storing `t` as key, and its index as an id.
  (let [seen (or seen {:len 0})
        id (+ seen.len 1)]
    (when (not (. seen t))
      (tset seen t id)
      (set seen.len id))
    seen))

(fn detect-cycle [t seen]
  ;; Return `true` if table `t` appears in itself.
  (let [seen (or seen {})]
    (tset seen t true)
    (each [k v (pairs t)]
      (when (and (= (type k) :table)
                 (or (. seen k) (detect-cycle k seen)))
        (lua "return true"))
      (when (and (= (type v) :table)
                 (or (. seen v) (detect-cycle v seen)))
        (lua "return true")))))

(fn visible-cycle? [t options]
  ;; Detect cycle, save table's ID in seen tables, and determine if
  ;; cycle is visible.  Exposed via options table to use in
  ;; __fennelview metamethod implementations
  (and options.detect-cycles?
       (detect-cycle t)
       (save-table t options.seen)
       (< 1 (or (. options.appearances t) 0))))

(fn table-indent [t indent id]
  ;; When table contains cycles, it is printed with a prefix before opening
  ;; delimiter.  Prefix has a variable length, as it contains `id` of the table
  ;; and fixed part of `2` characters `@` and either `[` or `{` depending on
  ;; `t`type.  If `t` has visible cycles, we need to increase indent by the size
  ;; of the prefix.
  (let [opener-length (if id
                          (+ (length (tostring id)) 2)
                          1)]
    (+ indent opener-length)))

(local pp {})

(fn concat-table-lines
  [elements options multiline? indent table-type prefix]
  (.. (or prefix "")
      (if (= :seq table-type) "[" "{")
      (table.concat
       elements
       (if (and (not options.one-line?)
                (or multiline?
                    (> (length elements) (if (= table-type :seq)
                                             options.sequential-length
                                             options.associative-length))
                    (> indent 40)))
           (.. "\n" (string.rep " " indent))
           " "))
      (if (= :seq table-type) "]" "}")))

(fn pp-associative [t kv options indent key?]
  (var multiline? false)
  (let [elements []
        id (. options.seen t)]
    (if (>= options.level options.depth) "{...}"
        (and id options.detect-cycles?) (.. "@" id "{...}")
        (let [visible-cycle? (visible-cycle? t options)
              id (and visible-cycle? (. options.seen t))
              indent (table-indent t indent id)
              slength (or (and options.utf8? (-?> (rawget _G :utf8) (. :len)))
                          #(length $))
              prefix (if visible-cycle? (.. "@" id) "")]
          (each [i [k v] (pairs kv)]
            (when (or (= (type k) :table) (= (type v) :table))
              (set multiline? true))
            (let [k (pp.pp k options (+ indent 1) true)
                  v (pp.pp v options (+ indent (slength k) 1))]
              (table.insert elements (.. k " " v))))
          (concat-table-lines
           elements options multiline? indent :table prefix)))))

(fn pp-sequence [t kv options indent]
  (var multiline? false)
  (let [elements []
        id (. options.seen t)]
    (if (>= options.level options.depth) "[...]"
        (and id options.detect-cycles?) (.. "@" id "[...]")
        (let [visible-cycle? (visible-cycle? t options)
              id (and visible-cycle? (. options.seen t))
              indent (table-indent t indent id)
              prefix (if visible-cycle? (.. "@" id) "")]
          (each [_ [_ v] (pairs kv)]
            (when (= (type v) :table)
              (set multiline? true))
            (table.insert elements (pp.pp v options indent)))
          (concat-table-lines
           elements options multiline? indent :seq prefix)))))

(fn concat-lines [lines options indent one-line?]
  (if (= (length lines) 0)
      (if options.empty-as-sequence? "[]" "{}")
      (if (and (not options.one-line?)
               (not one-line?))
          (table.concat lines (.. "\n" (string.rep " " indent)))
          (-> (icollect [_ line (ipairs lines)]
                (line:gsub "^%s+" " "))
              table.concat))))

(fn pp-metamethod [t metamethod options indent]
  (if (>= options.level options.depth)
      (if options.empty-as-sequence? "[...]" "{...}")
      (let [_ (set options.visible-cycle? #(visible-cycle? $ options))
            (lines force-one-line?) (metamethod t pp.pp options indent)]
        (set options.visible-cycle? nil)
        (match (type lines)
          :string lines ;; assuming that it is already single line
          :table  (concat-lines lines options indent force-one-line?)
          _ (error "Error: __fennelview metamethod must return a table of lines")))))

(fn pp-table [x options indent]
  ;; Generic table pretty-printer.  Supports associative and
  ;; sequential tables, as well as tables, that contain __fennelview
  ;; metamethod.
  (set options.level (+ options.level 1))
  (let [x (match (if options.metamethod? (-?> x getmetatable (. :__fennelview)))
            metamethod (pp-metamethod x metamethod options indent)
            _ (match (table-kv-pairs x)
                (_ :empty) (if options.empty-as-sequence? "[]" "{}")
                (kv :table) (pp-associative x kv options indent)
                (kv :seq) (pp-sequence x kv options indent)))]
    (set options.level (- options.level 1))
    x))



(var put-value nil) ; mutual recursion going on; defined below

(fn puts [self ...]
  (each [_ v (ipairs [...])]
    (table.insert self.buffer v)))

(fn tabify [self] (puts self "\n" (: self.indent :rep self.level)))

(fn already-visited? [self v] (not= (. self.ids v) nil))

(fn get-id [self v]
  (var id (. self.ids v))
  (when (not id)
    (let [tv (type v)]
      (set id (+ (or (. self.max-ids tv) 0) 1))
      (tset self.max-ids tv id)
      (tset self.ids v id)))
  (tostring id))

(fn put-sequential-table [self t len]
  (puts self "[")
  (set self.level (+ self.level 1))
  (each [k v (ipairs t)]
    (when (< 1 k (+ 1 len))
      (puts self " "))
    (put-value self v))
  (set self.level (- self.level 1))
  (puts self "]"))

(fn put-key [self k]
  (if (and (= (type k) "string")
           (: k :find "^[-%w?\\^_!$%&*+./@:|<=>]+$"))
      (puts self ":" k)
      (put-value self k)))

(fn put-kv-table [self t ordered-keys]
  (puts self "{")
  (set self.level (+ self.level 1))
  ;; first, output sorted nonsequential keys
  (each [i [k v] (ipairs ordered-keys)]
    (when (or self.table-edges (not= i 1))
      (tabify self))
    (put-key self k)
    (puts self " ")
    (put-value self v))
  ;; next, output any sequential keys
  (each [i v (ipairs t)]
    (tabify self)
    (put-key self i)
    (puts self " ")
    (put-value self v))
  (set self.level (- self.level 1))
  (when self.table-edges
    (tabify self))
  (puts self "}"))

(fn put-table [self t]
  (let [metamethod (and self.metamethod? (-?> t getmetatable (. :__fennelview)))]
    (if (and (already-visited? self t) self.detect-cycles?)
        (puts self "#<table @" (get-id self t) ">")
        (>= self.level self.depth)
        (puts self "{...}")
        metamethod
        (puts self (metamethod t self.fennelview))
        :else
        (let [(non-seq-keys len) (get-nonsequential-keys t)
              id (get-id self t)]
          ;; fancy metatable stuff can result in self.appearances not including
          ;; a table, so if it's not found, assume we haven't seen it; we can't
          ;; do cycle detection in that case.
          (when (and (< 1 (or (. self.appearances t) 0)) self.detect-cycles?)
            (puts self "@" id))
          (if (and (= (length non-seq-keys) 0) (= (length t) 0))
              (puts self (if self.empty-as-square "[]" "{}"))
              (= (length non-seq-keys) 0)
              (put-sequential-table self t len)
              :else
              (put-kv-table self t non-seq-keys))))))

(fn put-number [self n]
  (puts self (match (math.modf n)
               (int 0) (tostring int)
               ((0 frac) ? (< frac 0)) (.. "-0." (: (tostring frac) :gsub "^-?0." ""))
               (int frac) (.. int "." (: (tostring frac) :gsub "^-?0." "")))))

(set put-value
     (fn [self v]
       (let [tv (type v)]
         (if (= tv :string)
             (puts self (view-quote (escape v)))
             (= tv :number)
             (put-number self v)
             (or  (= tv :boolean) (= tv :nil))
             (puts self (tostring v))
             (or (= tv :table)
                 (and (= tv :userdata)
                      (not= nil (-?> (getmetatable v) (. :__fennelview)))))
             (put-table self v)
             (puts self "#<" (tostring v) ">")))))
(fn number->string [n]
  ;; Transform number to a string without depending on correct `os.locale`
  (match (math.modf n)
    (int 0) (tostring int)
    ((0 frac) ? (< frac 0)) (.. "-0." (: (tostring frac) :gsub "^-?0." ""))
    (int frac) (.. int "." (: (tostring frac) :gsub "^-?0." ""))))

(fn colon-string? [s]
  ;; Test if given string is valid colon string.
  (s:find "^[-%w?\\^_!$%&*+./@:|<=>]+$"))



(fn one-line [str]
  ;; save return value as local to ignore gsub's extra return value
  (let [ret (-> str
                (: :gsub "\n" " ")
                (: :gsub "%[ " "[") (: :gsub " %]" "]")
                (: :gsub "%{ " "{") (: :gsub " %}" "}")
                (: :gsub "%( " "(") (: :gsub " %)" ")"))]
    ret))
(fn make-options [t options]
  (let [;; defaults are used when options are not provided
        defaults {:sequential-length 10
                  :associative-length 4
                  :one-line? false
                  :depth 128
                  :detect-cycles? true
                  :empty-as-sequence? false
                  :metamethod? true
                  :utf8? true}
        ;; overrides can't be accessed via options
        overrides {:level 0
                   :appearances (count-table-appearances t {})
                   :seen {:len 0}}]
    (each [k v (pairs (or options {}))]
      (tset defaults k v))
    (each [k v (pairs overrides)]
      (tset defaults k v))
    defaults))

(fn pp.pp [x options indent key?]
  ;; main serialization loop, entry point is defined below
  (let [indent (or indent 0)
        options (or options (make-options x))
        tv (type x)]
    (if (or (= tv :table)
            (and (= tv :userdata)
                 (-?> (getmetatable x) (. :__fennelview))))
        (pp-table x options indent)
        (= tv :number)
        (number->string x)
        (and (= tv :string) key? (colon-string? x))
        (.. ":" x)
        (= tv :string)
        (string.format "%q" x)
        (or (= tv :boolean) (= tv :nil))
        (tostring x)
        (.. "#<" (tostring x) ">"))))

(fn fennelview [x options]
  "Return a string representation of x.

Can take an options table with these keys:
* :one-line (boolean: default: false) keep the output string as a one-liner
* :one-line? (boolean: default: false) keep the output string as a one-liner
* :depth (number, default: 128) limit how many levels to go (default: 128)
* :indent (string, default: \"  \") use this string to indent each level
* :detect-cycles? (boolean, default: true) don't try to traverse a looping table
* :metamethod? (boolean: default: true) use the __fennelview metamethod if found
* :table-edges (boolean: default: true) put {} table brackets on their own line
* :empty-as-square (boolean: default: false) render empty tables as [], not {}
* :empty-as-sequence? (boolean, default: false) render empty tables as []
* :sequential-length (number, default: 10) amount of elements at which
  multi-line sequence ouptut is produced.
* :associative-length (number, default: 4) amount of elements at which
  multi-line table ouptut is produced.
* :utf8? (boolean, default true) whether to use utf8 module to compute string
  lengths

The __fennelview metamethod should take the table being serialized as its first
argument and a function as its second arg which can be used on table elements to
continue the fennelview process on them.
argument, a function as its second argument, options table as third argument,
and current amount of indentation as its last argument:

(fn [t view inspector indent] ...)

`view` function contains pretty printer, that can be used to serialize elements
stored within the table being serialized.  If your metamethod produces indented
representation, you should pass `indent` parameter to `view` increased by the
amount of addition indentation you've introduced.

`inspector` table contains options described above, and also `visible-cycle?`
function, that takes a table being serialized, detects and saves information
about possible reachable cycle.  Should be used in __fennelview to implement
cycle detection.

`__fennelview` metamethod should always return a table of correctly indented
lines when producing multi-line output, or a string when returning single-line
item. If single-line representation is needed in some cases, there's no need to
concatenate table manually, instead `__fennelview` should return two values - a
table of lines, and a boolean indicating if one-line representation should be
forced.

There's no need to incorporate indentation beyond needed to correctly align
elements within the printed representation of your data structure.  For example,
if you want to print a multi-line table, like this:

@my-table[1
          2
          3]

__fennelview should return a sequence of lines:

[\"@my-table[1\"
 \"          2\"
 \"          3]\"]

Note, since we've introduced inner indent string of length 10, when calling
`view` function from within __fennelview metamethod, in order to keep inner
tables indented correctly, `indent` must be increased by this amount of extra
indentation.

`view` function also accepts additional boolean argument, which controls if
strings should be printed as a colon-strings when possible. Set it to `true`
when `view` is being called on the key of a table.

Here's an implementation of such pretty-printer for an arbitrary sequential
table:

(fn pp-doc-example [t view inspector indent]
  (let [lines (icollect [i v (ipairs t)]
                (let [v (view v inspector (+ 10 indent))]
                  (if (= i 1) v
                      (.. \"          \" v))))]
    (doto lines
      (tset 1 (.. \"@my-table[\" (or (. lines 1) \"\")))
      (tset (length lines) (.. (. lines (length lines)) \"]\")))))

Setting table's __fennelview metamethod to this function will provide correct
results regardless of nesting:

>> {:my-table (setmetatable [{:a {} :b [[1] [2]]} 3]
                            {:__fennelview pp-doc-example})
    :normal-table [{:c [1 2 3] :d :some-data} 4]}
{:my-table @my-table[{:a {}
                      :b [[1]
                          [2]]}
                     3]
 :normal-table [{:c [1 2 3]
                 :d \"some-data\"}
                4]}

Note that even though we've only indented inner elements of our table with 10
spaces, the result is correctly indented in terms of outer table, and inner
tables also remain indented correctly.
"
  (let [options (or options {})
        inspector {:appearances (count-table-appearances x {})
                   :depth (or options.depth 128)
                   :level 0 :buffer {} :ids {} :max-ids {}
                   :indent (or options.indent (if options.one-line "" "  "))
                   :detect-cycles? (not (= false options.detect-cycles?))
                   :metamethod? (not (= false options.metamethod?))
                   :fennelview #(fennelview $1 options)
                   :table-edges (not= options.table-edges false)
                   :empty-as-square options.empty-as-square}]
    (put-value inspector x)
    (let [str (table.concat inspector.buffer)]
      (if options.one-line (one-line str) str))))
  (pp.pp x (make-options x options) 0))

M fennelview.lua => fennelview.lua +277 -182
@@ 1,25 1,3 @@
local function view_quote(str)
  return ("\"" .. str:gsub("\"", "\\\"") .. "\"")
end
local short_control_char_escapes = {["\11"] = "\\v", ["\12"] = "\\f", ["\13"] = "\\r", ["\7"] = "\\a", ["\8"] = "\\b", ["\9"] = "\\t", ["\n"] = "\\n"}
local long_control_char_escapes = nil
do
  local long = {}
  for i = 0, 31 do
    local ch = string.char(i)
    if not short_control_char_escapes[ch] then
      short_control_char_escapes[ch] = ("\\" .. i)
      long[ch] = ("\\%03d"):format(i)
    end
  end
  long_control_char_escapes = long
end
local function escape(str)
  return str:gsub("\\", "\\\\"):gsub("(%c)%f[0-9]", long_control_char_escapes):gsub("%c", short_control_char_escapes)
end
local function sequence_key_3f(k, len)
  return ((type(k) == "number") and (1 <= k) and (k <= len) and (math.floor(k) == k))
end
local type_order = {["function"] = 5, boolean = 2, number = 1, string = 3, table = 4, thread = 7, userdata = 6}
local function sort_keys(_0_0, _1_0)
  local _1_ = _0_0


@@ 39,28 17,34 @@ local function sort_keys(_0_0, _1_0)
      return true
    elseif dtb then
      return false
    elseif "else" then
      return (ta < tb)
    else
      return false
    end
  end
end
local function get_sequence_length(t)
  local len = 0
  for i in ipairs(t) do
    len = i
  end
  return len
end
local function get_nonsequential_keys(t)
  local keys = {}
  local sequence_length = get_sequence_length(t)
local function table_kv_pairs(t)
  local assoc_3f = false
  local kv = {}
  local insert = table.insert
  for k, v in pairs(t) do
    if not sequence_key_3f(k, sequence_length) then
      table.insert(keys, {k, v})
    if (type(k) ~= "number") then
      assoc_3f = true
    end
    insert(kv, {k, v})
  end
  table.sort(kv, sort_keys)
  if (#kv == 0) then
    return kv, "empty"
  else
    local function _2_()
      if assoc_3f then
        return "table"
      else
        return "seq"
      end
    end
    return kv, _2_()
  end
  table.sort(keys, sort_keys)
  return keys, sequence_length
end
local function count_table_appearances(t, appearances)
  if (type(t) == "table") then


@@ 76,193 60,304 @@ local function count_table_appearances(t, appearances)
  end
  return appearances
end
local put_value = nil
local function puts(self, ...)
  for _, v in ipairs({...}) do
    table.insert(self.buffer, v)
local function save_table(t, seen)
  local seen0 = (seen or {len = 0})
  local id = (seen0.len + 1)
  if not seen0[t] then
    seen0[t] = id
    seen0.len = id
  end
  return nil
end
local function tabify(self)
  return puts(self, "\n", (self.indent):rep(self.level))
end
local function already_visited_3f(self, v)
  return (self.ids[v] ~= nil)
  return seen0
end
local function get_id(self, v)
  local id = self.ids[v]
  if not id then
    local tv = type(v)
    id = ((self["max-ids"][tv] or 0) + 1)
    self["max-ids"][tv] = id
    self.ids[v] = id
  end
  return tostring(id)
end
local function put_sequential_table(self, t, len)
  puts(self, "[")
  self.level = (self.level + 1)
  for k, v in ipairs(t) do
    local _2_ = (1 + len)
    if ((1 < k) and (k < _2_)) then
      puts(self, " ")
local function detect_cycle(t, seen)
  local seen0 = (seen or {})
  seen0[t] = true
  for k, v in pairs(t) do
    if ((type(k) == "table") and (seen0[k] or detect_cycle(k, seen0))) then
      return true
    end
    if ((type(v) == "table") and (seen0[v] or detect_cycle(v, seen0))) then
      return true
    end
    put_value(self, v)
  end
  self.level = (self.level - 1)
  return puts(self, "]")
  return nil
end
local function visible_cycle_3f(t, options)
  return (options["detect-cycles?"] and detect_cycle(t) and save_table(t, options.seen) and (1 < (options.appearances[t] or 0)))
end
local function put_key(self, k)
  if ((type(k) == "string") and k:find("^[-%w?\\^_!$%&*+./@:|<=>]+$")) then
    return puts(self, ":", k)
local function table_indent(t, indent, id)
  local opener_length = nil
  if id then
    opener_length = (#tostring(id) + 2)
  else
    return put_value(self, k)
    opener_length = 1
  end
  return (indent + opener_length)
end
local function put_kv_table(self, t, ordered_keys)
  puts(self, "{")
  self.level = (self.level + 1)
  for i, _2_0 in ipairs(ordered_keys) do
    local _3_ = _2_0
    local k = _3_[1]
    local v = _3_[2]
    if (self["table-edges"] or (i ~= 1)) then
      tabify(self)
local pp = {}
local function concat_table_lines(elements, options, multiline_3f, indent, table_type, prefix)
  local function _2_()
    if ("seq" == table_type) then
      return "["
    else
      return "{"
    end
    put_key(self, k)
    puts(self, " ")
    put_value(self, v)
  end
  for i, v in ipairs(t) do
    tabify(self)
    put_key(self, i)
    puts(self, " ")
    put_value(self, v)
  local function _3_()
    local _3_
    if (table_type == "seq") then
      _3_ = options["sequential-length"]
    else
      _3_ = options["associative-length"]
    end
    if (not options["one-line?"] and (multiline_3f or (#elements > _3_) or (indent > 40))) then
      return ("\n" .. string.rep(" ", indent))
    else
      return " "
    end
  end
  self.level = (self.level - 1)
  if self["table-edges"] then
    tabify(self)
  local function _4_()
    if ("seq" == table_type) then
      return "]"
    else
      return "}"
    end
  end
  return puts(self, "}")
  return ((prefix or "") .. _2_() .. table.concat(elements, _3_()) .. _4_())
end
local function put_table(self, t)
  local metamethod = nil
  local function _3_()
    local _2_0 = t
    if _2_0 then
      local _4_0 = getmetatable(_2_0)
      if _4_0 then
        return _4_0.__fennelview
local function pp_associative(t, kv, options, indent, key_3f)
  local multiline_3f = false
  local elements = {}
  local id = options.seen[t]
  if (options.level >= options.depth) then
    return "{...}"
  elseif (id and options["detect-cycles?"]) then
    return ("@" .. id .. "{...}")
  else
    local visible_cycle_3f0 = visible_cycle_3f(t, options)
    local id0 = (visible_cycle_3f0 and options.seen[t])
    local indent0 = table_indent(t, indent, id0)
    local slength = nil
    local function _3_()
      local _2_0 = rawget(_G, "utf8")
      if _2_0 then
        return _2_0.len
      else
        return _4_0
        return _2_0
      end
    end
    local function _4_(_241)
      return #_241
    end
    slength = ((options["utf8?"] and _3_()) or _4_)
    local prefix = nil
    if visible_cycle_3f0 then
      prefix = ("@" .. id0)
    else
      return _2_0
      prefix = ""
    end
    for i, _6_0 in pairs(kv) do
      local _7_ = _6_0
      local k = _7_[1]
      local v = _7_[2]
      if ((type(k) == "table") or (type(v) == "table")) then
        multiline_3f = true
      end
      local k0 = pp.pp(k, options, (indent0 + 1), true)
      local v0 = pp.pp(v, options, (indent0 + slength(k0) + 1))
      table.insert(elements, (k0 .. " " .. v0))
    end
    return concat_table_lines(elements, options, multiline_3f, indent0, "table", prefix)
  end
  metamethod = (self["metamethod?"] and _3_())
  if (already_visited_3f(self, t) and self["detect-cycles?"]) then
    return puts(self, "#<table @", get_id(self, t), ">")
  elseif (self.level >= self.depth) then
    return puts(self, "{...}")
  elseif metamethod then
    return puts(self, metamethod(t, self.fennelview))
  elseif "else" then
    local non_seq_keys, len = get_nonsequential_keys(t)
    local id = get_id(self, t)
    if ((1 < (self.appearances[t] or 0)) and self["detect-cycles?"]) then
      puts(self, "@", id)
end
local function pp_sequence(t, kv, options, indent)
  local multiline_3f = false
  local elements = {}
  local id = options.seen[t]
  if (options.level >= options.depth) then
    return "[...]"
  elseif (id and options["detect-cycles?"]) then
    return ("@" .. id .. "[...]")
  else
    local visible_cycle_3f0 = visible_cycle_3f(t, options)
    local id0 = (visible_cycle_3f0 and options.seen[t])
    local indent0 = table_indent(t, indent, id0)
    local prefix = nil
    if visible_cycle_3f0 then
      prefix = ("@" .. id0)
    else
      prefix = ""
    end
    if ((#non_seq_keys == 0) and (#t == 0)) then
      local function _5_()
        if self["empty-as-square"] then
          return "[]"
        else
          return "{}"
        end
    for _, _3_0 in pairs(kv) do
      local _4_ = _3_0
      local _0 = _4_[1]
      local v = _4_[2]
      if (type(v) == "table") then
        multiline_3f = true
      end
      return puts(self, _5_())
    elseif (#non_seq_keys == 0) then
      return put_sequential_table(self, t, len)
    elseif "else" then
      return put_kv_table(self, t, non_seq_keys)
      table.insert(elements, pp.pp(v, options, indent0))
    end
    return concat_table_lines(elements, options, multiline_3f, indent0, "seq", prefix)
  end
end
local function put_number(self, n)
  local function _5_()
    local _2_0, _3_0, _4_0 = math.modf(n)
    if ((nil ~= _2_0) and (_3_0 == 0)) then
      local int = _2_0
      return tostring(int)
local function concat_lines(lines, options, indent, one_line_3f)
  if (#lines == 0) then
    if options["empty-as-sequence?"] then
      return "[]"
    else
      local _6_
      do
        local frac = _3_0
        _6_ = (((_2_0 == 0) and (nil ~= _3_0)) and (frac < 0))
      end
      if _6_ then
        local frac = _3_0
        return ("-0." .. tostring(frac):gsub("^-?0.", ""))
      elseif ((nil ~= _2_0) and (nil ~= _3_0)) then
        local int = _2_0
        local frac = _3_0
        return (int .. "." .. tostring(frac):gsub("^-?0.", ""))
      return "{}"
    end
  else
    if (not options["one-line?"] and not one_line_3f) then
      return table.concat(lines, ("\n" .. string.rep(" ", indent)))
    else
      local function _2_()
        local tbl_0_ = {}
        for _, line in ipairs(lines) do
          tbl_0_[(#tbl_0_ + 1)] = line:gsub("^%s+", " ")
        end
        return tbl_0_
      end
      return table.concat(_2_())
    end
  end
  return puts(self, _5_())
end
local function _2_(self, v)
  local tv = type(v)
  if (tv == "string") then
    return puts(self, view_quote(escape(v)))
  elseif (tv == "number") then
    return put_number(self, v)
  elseif ((tv == "boolean") or (tv == "nil")) then
    return puts(self, tostring(v))
local function pp_metamethod(t, metamethod, options, indent)
  if (options.level >= options.depth) then
    if options["empty-as-sequence?"] then
      return "[...]"
    else
      return "{...}"
    end
  else
    local _4_
    do
      local _3_0 = getmetatable(v)
    local _ = nil
    local function _2_(_241)
      return visible_cycle_3f(_241, options)
    end
    options["visible-cycle?"] = _2_
    _ = nil
    local lines, force_one_line_3f = metamethod(t, pp.pp, options, indent)
    options["visible-cycle?"] = nil
    local _3_0 = type(lines)
    if (_3_0 == "string") then
      return lines
    elseif (_3_0 == "table") then
      return concat_lines(lines, options, indent, force_one_line_3f)
    else
      local _0 = _3_0
      return error("Error: __fennelview metamethod must return a table of lines")
    end
  end
end
local function pp_table(x, options, indent)
  options.level = (options.level + 1)
  local x0 = nil
  do
    local _2_0 = nil
    if options["metamethod?"] then
      local _3_0 = x
      if _3_0 then
        _4_ = _3_0.__fennelview
        local _4_0 = getmetatable(_3_0)
        if _4_0 then
          _2_0 = _4_0.__fennelview
        else
          _2_0 = _4_0
        end
      else
        _4_ = _3_0
        _2_0 = _3_0
      end
    else
    _2_0 = nil
    end
    if ((tv == "table") or ((tv == "userdata") and (nil ~= _4_))) then
      return put_table(self, v)
    if (nil ~= _2_0) then
      local metamethod = _2_0
      x0 = pp_metamethod(x, metamethod, options, indent)
    else
      return puts(self, "#<", tostring(v), ">")
      local _ = _2_0
      local _4_0, _5_0 = table_kv_pairs(x)
      if (true and (_5_0 == "empty")) then
        local _0 = _4_0
        if options["empty-as-sequence?"] then
          x0 = "[]"
        else
          x0 = "{}"
        end
      elseif ((nil ~= _4_0) and (_5_0 == "table")) then
        local kv = _4_0
        x0 = pp_associative(x, kv, options, indent)
      elseif ((nil ~= _4_0) and (_5_0 == "seq")) then
        local kv = _4_0
        x0 = pp_sequence(x, kv, options, indent)
      else
      x0 = nil
      end
    end
  end
  options.level = (options.level - 1)
  return x0
end
put_value = _2_
local function one_line(str)
  local ret = str:gsub("\n", " "):gsub("%[ ", "["):gsub(" %]", "]"):gsub("%{ ", "{"):gsub(" %}", "}"):gsub("%( ", "("):gsub(" %)", ")")
  return ret
local function number__3estring(n)
  local _2_0, _3_0, _4_0 = math.modf(n)
  if ((nil ~= _2_0) and (_3_0 == 0)) then
    local int = _2_0
    return tostring(int)
  else
    local _5_
    do
      local frac = _3_0
      _5_ = (((_2_0 == 0) and (nil ~= _3_0)) and (frac < 0))
    end
    if _5_ then
      local frac = _3_0
      return ("-0." .. tostring(frac):gsub("^-?0.", ""))
    elseif ((nil ~= _2_0) and (nil ~= _3_0)) then
      local int = _2_0
      local frac = _3_0
      return (int .. "." .. tostring(frac):gsub("^-?0.", ""))
    end
  end
end
local function fennelview(x, options)
  local options0 = (options or {})
  local inspector = nil
  local function _3_(_241)
    return fennelview(_241, options0)
local function colon_string_3f(s)
  return s:find("^[-%w?\\^_!$%&*+./@:|<=>]+$")
end
local function make_options(t, options)
  local defaults = {["associative-length"] = 4, ["detect-cycles?"] = true, ["empty-as-sequence?"] = false, ["metamethod?"] = true, ["one-line?"] = false, ["sequential-length"] = 10, ["utf8?"] = true, depth = 128}
  local overrides = {appearances = count_table_appearances(t, {}), level = 0, seen = {len = 0}}
  for k, v in pairs((options or {})) do
    defaults[k] = v
  end
  local function _4_()
    if options0["one-line"] then
      return ""
  for k, v in pairs(overrides) do
    defaults[k] = v
  end
  return defaults
end
pp.pp = function(x, options, indent, key_3f)
  local indent0 = (indent or 0)
  local options0 = (options or make_options(x))
  local tv = type(x)
  local function _3_()
    local _2_0 = getmetatable(x)
    if _2_0 then
      return _2_0.__fennelview
    else
      return "  "
      return _2_0
    end
  end
  inspector = {["detect-cycles?"] = not (false == options0["detect-cycles?"]), ["empty-as-square"] = options0["empty-as-square"], ["max-ids"] = {}, ["metamethod?"] = not (false == options0["metamethod?"]), ["table-edges"] = (options0["table-edges"] ~= false), appearances = count_table_appearances(x, {}), buffer = {}, depth = (options0.depth or 128), fennelview = _3_, ids = {}, indent = (options0.indent or _4_()), level = 0}
  put_value(inspector, x)
  local str = table.concat(inspector.buffer)
  if options0["one-line"] then
    return one_line(str)
  if ((tv == "table") or ((tv == "userdata") and _3_())) then
    return pp_table(x, options0, indent0)
  elseif (tv == "number") then
    return number__3estring(x)
  elseif ((tv == "string") and key_3f and colon_string_3f(x)) then
    return (":" .. x)
  elseif (tv == "string") then
    return string.format("%q", x)
  elseif ((tv == "boolean") or (tv == "nil")) then
    return tostring(x)
  else
    return str
    return ("#<" .. tostring(x) .. ">")
  end
end
local function fennelview(x, options)
  return pp.pp(x, make_options(x, options), 0)
end
return fennelview

M test/core.fnl => test/core.fnl +111 -10
@@ 315,17 315,118 @@
      (l.assertEquals (fennel.eval code {:correlate true}) expected code))))

(fn test-fennelview []
  (let [cases {"((require :fennelview) (let [t {}] [t t]) {:detect-cycles? false})"
               "[{} {}]"
               "((require :fennelview) (let [t {}] [t t]))"
               "[@2{} #<table @2>]"
               "((require :fennelview) {:a 1 :b 52})"
               "{
  :a 1
  :b 52
}"
               "((require :fennelview) {:a 1 :b 5} {:one-line true})"
  (let [cases {"((require :fennelview) \"123\")"
               "\"123\""
               "((require :fennelview) \"123 \\\"456\\\" 789\")"
               "\"123 \\\"456\\\" 789\""
               "((require :fennelview) 123)"
               "123"
               "((require :fennelview) 2.4)"
               "2.4"
               "((require :fennelview) [] {:empty-as-sequence? true})"
               "[]"
               "((require :fennelview) [])"
               "{}"
               "((require :fennelview) [1 2 3])"
               "[1 2 3]"
               "((require :fennelview) [0 1 2 3 4 5 6 7 8 9 10])"
               "[0\n 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9\n 10]"
               "((require :fennelview) {:a 1 \"a b\" 2})"
               "{:a 1 \"a b\" 2}"
               "((require :fennelview) [])"
               "{}"
               "((require :fennelview) [] {:empty-as-sequence? true})"
               "[]"
               "((require :fennelview) [1 2 3])"
               "[1 2 3]"
               "((require :fennelview) [0 1 2 3 4 5 6 7 8 9 10])"
               "[0\n 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9\n 10]"
               "((require :fennelview) {:a 1 \"a b\" 2})"
               "{:a 1 \"a b\" 2}"
               "((require :fennelview) {:a 1 :b 52} {:associative-length 1})"
               "{:a 1\n :b 52}"
               "((require :fennelview) {:a 1 :b 5} {:one-line? true :associative-length 1})"
               "{:a 1 :b 5}"
               ;; nesting
               "((require :fennelview) (let [t {}] [t t]) {:detect-cycles? false})"
               "[{}\n {}]"
               "((require :fennelview) (let [t {}] [t t]))"
               "[{}\n {}]"
               "((require :fennelview) [{}])"
               "[{}]"
               "((require :fennelview) {[{}] []})"
               "{[{}] {}}"
               "((require :fennelview) {[[]] {[[]] [[[]]]}} {:empty-as-sequence? true})"
               "{[[]] {[[]] [[[]]]}}"
               "((require :fennelview) [1 2 [3 4]])"
               "[1\n 2\n [3 4]]"
               "((require :fennelview) {[1] [2 [3]] :data {4 {:data 5} 6 [0 1 2 3]}} {:sequential-length 3})"
               "{:data [{:data 5}\n        [0\n         1\n         2\n         3]]\n [1] [2\n      [3]]}"
               "((require :fennelview) {{:b 2} {:c 3 :d 4} {:a 1} {:b 2 :c 3}})"
               "{{:a 1} {:b 2 :c 3}\n {:b 2} {:c 3 :d 4}}"
               "((require :fennelview) [{:aaa [1 2 3]}] {:sequential-length 2})"
               "[{:aaa [1\n        2\n        3]}]"
               ;; Unicode
               "((require :fennelview) \"ваыв\")"
               "\"ваыв\""
               "((require :fennelview) {[1] [2 [3]] :ваыв {4 {:ваыв 5} 6 [0 1 2 3]}} {:sequential-length 3})"
               "{\"ваыв\" [{\"ваыв\" 5}\n         [0\n          1\n          2\n          3]]\n [1] [2\n      [3]]}"
               ;; the next one may look incorrect in some editors, but is actually correct
               "((require :fennelview) {:ǍǍǍ {} :ƁƁƁ {:ǍǍǍ {} :ƁƁƁ {}}})"
               "{\"ƁƁƁ\" {\"ƁƁƁ\" {}\n        \"ǍǍǍ\" {}}\n \"ǍǍǍ\" {}}"
               ;; cycles
               "(local t1 {}) (tset t1 :t1 t1) ((require :fennelview) t1)"
               "@1{:t1 @1{...}}"
               "(local t1 {}) (tset t1 t1 t1) ((require :fennelview) t1)"
               "@1{@1{...} @1{...}}"
               "(local v1 []) (table.insert v1 v1) ((require :fennelview) v1)"
               "@1[@1[...]]"
               "(local t1 {}) (local t2 {:t1 t1}) (tset t1 :t2 t2) ((require :fennelview) t1)"
               "@1{:t2 {:t1 @1{...}}}"
               "(local t1 {:a 1 :c 2}) (local v1 [1 2 3]) (tset t1 :b v1) (table.insert v1 2 t1) ((require :fennelview) t1)"
               "@1{:a 1\n   :b [1\n       @1{...}\n       2\n       3]\n   :c 2}"
               "(local v1 [1 2 3]) (local v2 [1 2 v1]) (local v3 [1 2 v2]) (table.insert v1 v2) (table.insert v1 v3) ((require :fennelview) v1)"
               "@1[1\n   2\n   3\n   @2[1\n      2\n      @1[...]]\n   [1\n    2\n    @2[...]]]"
               "(local v1 []) (table.insert v1 v1) ((require :fennelview) v1 {:detect-cycles? false :one-line? true :depth 10})"
               "[[[[[[[[[[...]]]]]]]]]]"
               "(local t1 []) (tset t1 t1 t1) ((require :fennelview) t1 {:detect-cycles? false :one-line? true :depth 4})"
               "{{{{...} {...}} {{...} {...}}} {{{...} {...}} {{...} {...}}}}"
               ;; sorry :)
               "(local v1 []) (local v2 [v1]) (local v3 [v1 v2]) (local v4 [v2 v3]) (local v5 [v3 v4]) (local v6 [v4 v5]) (local v7 [v5 v6]) (local v8 [v6 v7]) (local v9 [v7 v8]) (local v10 [v8 v9]) (local v11 [v9 v10]) (table.insert v1 v2) (table.insert v1 v3) (table.insert v1 v4) (table.insert v1 v5) (table.insert v1 v6) (table.insert v1 v7) (table.insert v1 v8) (table.insert v1 v9) (table.insert v1 v10) (table.insert v1 v11) ((require :fennelview) v1)"
               "@1[@2[@1[...]]\n   @3[@1[...]\n      @2[...]]\n   @4[@2[...]\n      @3[...]]\n   @5[@3[...]\n      @4[...]]\n   @6[@4[...]\n      @5[...]]\n   @7[@5[...]\n      @6[...]]\n   @8[@6[...]\n      @7[...]]\n   @9[@7[...]\n      @8[...]]\n   @10[@8[...]\n       @9[...]]\n   [@9[...]\n    @10[...]]]"
               "(local v1 []) (local v2 [v1]) (local v3 [v1 v2]) (local v4 [v2 v3]) (local v5 [v3 v4]) (local v6 [v4 v5]) (local v7 [v5 v6]) (local v8 [v6 v7]) (local v9 [v7 v8]) (local v10 [v8 v9]) (local v11 [v9 v10]) (table.insert v1 v2) (table.insert v1 v3) (table.insert v1 v4) (table.insert v1 v5) (table.insert v1 v6) (table.insert v1 v7) (table.insert v1 v8) (table.insert v1 v9) (table.insert v1 v10) (table.insert v1 v11) (table.insert v2 v11) ((require :fennelview) v1)"
               "@1[@2[@1[...]\n      @3[@4[@5[@6[@7[@1[...]\n                     @2[...]]\n                  @8[@2[...]\n                     @7[...]]]\n               @9[@8[...]\n                  @6[...]]]\n            @10[@9[...]\n                @5[...]]]\n         @11[@10[...]\n             @4[...]]]]\n   @7[...]\n   @8[...]\n   @6[...]\n   @9[...]\n   @5[...]\n   @10[...]\n   @4[...]\n   @11[...]\n   @3[...]]"
               ;; __fennelview metamethod test
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) l1)"
               "(1\n 2\n 3)"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) [l1])"
               "[(1\n  2\n  3)]"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) [1 l1 2])"
               "[1\n (1\n  2\n  3)\n 2]"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) [[1 l1 2]])"
               "[[1\n  (1\n   2\n   3)\n  2]]"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) {:abc [l1]})"
               "{:abc [(1\n        2\n        3)]}"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) l1 {:one-line? true})"
               "(1 2 3)"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) [l1] {:one-line? true})"
               "[(1 2 3)]"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) ((require :fennelview) {:abc [l1]} {:one-line? true})"
               "{:abc [(1 2 3)]}"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) l2)"
               "(:a\n \"a b\"\n [1 2 3]\n {:a (1\n      2\n      3)\n  :b {}})"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) {:list l2})"
               "{:list (:a\n        \"a b\"\n        [1 2 3]\n        {:a (1\n             2\n             3)\n         :b {}})}"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) [l2])"
               "[(:a\n  \"a b\"\n  [1 2 3]\n  {:a (1\n       2\n       3)\n   :b {}})]"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) {:abc [l1]})"
               "{:abc [(1\n        2\n        3)]}"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) l1 {:one-line? true})"
               "(1 2 3)"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) [l1] {:one-line? true})"
               "[(1 2 3)]"
               "(fn pp-list [x pp opts indent] (icollect [i v (ipairs x)] (let [v (pp v opts (+ 1 indent) true)] (if (= i 1) (.. \"(\" v) (= i (length x)) (.. \" \" v \")\") (.. \" \" v))))) (local l1 (setmetatable [1 2 3] {:__fennelview pp-list})) (local l2 (setmetatable [\"a\" \"a b\" [1 2 3] {:a l1 :b []}] {:__fennelview pp-list})) ((require :fennelview) {:abc [l1]} {:one-line? true})"
               "{:abc [(1 2 3)]}"
               ;; ensure it works on lists/syms inside compiler
               "(eval-compiler
                  (set _G.out ((require :fennelview) '(a {} [1 2]))))

M test/fennelview.fnl => test/fennelview.fnl +2 -2
@@ 42,11 42,11 @@
  (let [view-target {:my-userdata io.stdout}
        expected-with-mt "{:my-userdata \"HI, I AM USERDATA\"}"
        expected-without-mt "^%{%:my%-userdata %#%<file %([x0-9a-f]+%)%>%}$"]
    (l.assertStrContains (view view-target {:one-line true})
    (l.assertStrContains (view view-target {:one-line? true})
                         expected-without-mt
                         true)
    (tset (getmetatable io.stdout) :__fennelview #"\"HI, I AM USERDATA\"")
    (l.assertEquals (view view-target {:one-line true})
    (l.assertEquals (view view-target {:one-line? true})
                    expected-with-mt)
    (tset (getmetatable io.stdout) :__fennelview nil)))


M test/macro.fnl => test/macro.fnl +1 -1
@@ 96,7 96,7 @@
(fn test-macrodebug []
  (let [eval-normalize #(-> (pick-values 1 (fennel.eval $1 $2))
                            (: :gsub "table: 0x[0-9a-f]+" "#<TABLE>")
                            (: :gsub "\n%s*" ""))
                            (: :gsub "\n%s*" " "))
        code "(macrodebug (when (= 1 1) (let [x :X] {: x})) true)"
        expected-fennelview "(if (= 1 1) (do (let [x \"X\"] {:x x})))"
        expected-no-fennelview "(if (= 1 1) (do (let #<TABLE> #<TABLE>)))"]

M test/quoting.fnl => test/quoting.fnl +1 -2
@@ 6,7 6,7 @@
  (fennel.compileString code {:allowedGlobals false :compiler-env _G}))

(fn v [code]
  (fennelview ((fennel.loadCode (c code) _G)) {:one-line true}))
  (fennelview ((fennel.loadCode (c code) _G)) {:one-line? true}))

(fn test-quote []
  (l.assertEquals (c "`:abcde") "return \"abcde\"" "simple string quoting")


@@ 33,4 33,3 @@
  (l.assertStrContains msg "unknown:5" "quoted tables have source data"))

{: test-quote : test-quoted-source}