blue-lang-cli 0.0.164

The blue command line: run, fmt, ast, erase, check.
use(
  "mokuroku",
  %i[
    blue_path_roots
    catalog_of
    entries_of_text
    package_text
    pkg_dir
    pkg_entries
    pkg_gloss
    pkg_name
    pkg_needs
    pkg_tests
    pkg_version
    test_blocks_of_text
  ]
)

use("retsu", %i[first flat_map last])

# blue doc: what a name is, where it lives and how it is called.
#
#   blue doc NAME [--file FILE] [--json]
#   blue doc PKG [--json]
#
# NAME is a builtin, `pkg::name`, or a bare name, which is looked up among the
# builtins and every bidama on BLUE_PATH. With --file, the file's own
# definitions are searched too, and each answer's tier is how that file
# reaches the name: `own`, `imported` (a `use` lists it), `qualified` (the
# file uses the package; write `pkg::name`), `not-imported`, or `builtin`.
# PKG, a bidama's name, lists the package.
#
# The tool is blue. It reads the bidamas as mokuroku reads them for the
# catalogue, asks the blue running it for the builtins (`blue reference`) and
# for a file's imports (`blue ast --resolved --json`), and takes a builtin's
# examples from the conformance rows compiled into that blue (`blue doc
# --rows`).

def run_blue(args)
  exec_capture(*cons(self_exe, args))
end

def captured(args)
  r = run_blue(args)
  if r[0][1] != 0
    raise(:doc, "blue #{join(args, " ")}: #{r[2][1]}")
  end
  r[1][1]
end

# The value after `flag` in the arguments, or nil.
def flag_value(args, flag)
  at = filter((i) -> args[i] == flag, range(length(args)))
  if empty?(at) or first(at) + 1 >= length(args)
    nil
  else
    args[first(at) + 1]
  end
end

# ── what is documented ─────────────────────────────────────────────────────

def builtins()
  json_get(json_parse(captured(["reference"])), :names)
end

# [id, program, expectation] for every conformance row covering the builtin.
def rows_for(name)
  filter(
    (r) -> member?("builtin:#{name}", json_get(r, :covers)),
    json_parse(captured(%w[doc --rows]))
  )
end

def builtin_entry(b, tier)
  name = json_get(b, :name)
  rows = rows_for(name)
  example = if empty?(rows)
    nil
  else
    row = first(rows)
    {
      from: "spec row #{json_get(row, "id")}",
      code: [json_get(row, :src), "=> #{json_get(row, "expect")}"]
    }
  end
  {
    name:,
    namespace: "blue",
    tier:,
    kind: json_get(b, :arbiter),
    topic: json_get(b, :topic),
    signature: json_get(b, :signature),
    doc: [json_get(b, :doc)],
    example:
  }
end

# Does `line` call `name`: `name(` not preceded by a name character?
def calls?(line, name)
  parts = split(line, "#{name}(")
  length(parts) > 1 and
    any?(
      fn(before)
        cs = chars(before)
        empty?(cs) or not name_char?(cs[length(cs) - 1])
      end,
      take(length(parts) - 1, parts)
    )
end

def name_char?(c)
  contains?(
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_?!:",
    c
  )
end

# The first test in `text` that calls `name`, as an example, or nil.
def test_example(text, name)
  blocks = filter(
    (b) -> any?((l) -> calls?(l, name), last(b)),
    test_blocks_of_text(text)
  )
  if empty?(blocks)
    nil
  else
    b = first(blocks)
    {
      from: "test \"#{first(b)}\"",
      code: map((l) -> blue::trim(l), take(8, last(b)))
    }
  end
end

# The entry for definition `e` ([name, signature, doc_lines, waivers]) of the
# package `pkg` (a mokuroku record, or nil for the file's own script).
def derived_of(pkg, name)
  r = run_blue(
    ["doc", name, "--file", "#{pkg_dir(pkg)}/#{pkg_name(pkg)}.b", "--derived"]
  )
  if r[0][1] == 0
    json_parse(r[1][1])
  end
end

def def_entry(pkg, e, text, tier)
  name = first(e)
  qualifier = if pkg.nil?
    ""
  else
    "#{pkg_name(pkg)}::"
  end
  {
    name: "#{qualifier}#{name}",
    namespace: if pkg.nil?
      nil
    else
      pkg_name(pkg)
    end,
    tier:,
    kind: "definition",
    topic: nil,
    signature: "#{qualifier}#{e[1]}",
    doc: e[2],
    example: test_example(text, name),
    derived: if pkg.nil?
      nil
    else
      derived_of(pkg, name)
    end
  }
end

# ── tiers ──────────────────────────────────────────────────────────────────

# How the file described by `ctx` reaches `name` in package `pkg`.
def tier_in(ctx, pkg, name)
  if ctx.nil?
    "qualified"
  elsif json_get(ctx, :namespace) == pkg
    "own"
  else
    uses = filter((u) -> json_get(u, :package) == pkg, json_get(ctx, :imports))
    if empty?(uses)
      "not-imported"
    elsif any?((u) -> member?(name, json_get(u, :names)), uses)
      "imported"
    else
      "qualified"
    end
  end
end

# ── lookup ─────────────────────────────────────────────────────────────────

def lookup(name, file, records)
  ctx = if file.nil?
    nil
  else
    json_parse(captured(["ast", "--resolved", "--json", file]))
  end
  parts = split(name, "::")
  qualifier = if length(parts) == 2
    first(parts)
  end
  bare = last(parts)
  own = if file.nil? or json_get(ctx, :namespace) != nil
    []
  else
    text = read_file(file)
    map(
      (e) -> def_entry(nil, e, text, "own"),
      filter((e) -> first(e) == bare, entries_of_text(text))
    )
  end
  from_bidamas = flat_map(
    filter((r) -> qualifier.nil? or qualifier == pkg_name(r), records),
    fn(r)
      text = package_text(pkg_dir(r))
      map(
        (e) -> def_entry(r, e, text, tier_in(ctx, pkg_name(r), bare)),
        filter((e) -> first(e) == bare, pkg_entries(r))
      )
    end
  )
  from_builtins = if qualifier.nil? or qualifier == "blue"
    map(
      (b) -> builtin_entry(b, "builtin"),
      filter((b) -> json_get(b, :name) == bare, builtins)
    )
  else
    []
  end
  append(own, from_bidamas, from_builtins)
end

# ── rendering ──────────────────────────────────────────────────────────────

def entry_text(e)
  where = if e[:namespace] == "blue"
    "builtin #{e[:kind]} · #{e[:topic]}"
  elsif e[:namespace].nil?
    "defined in the file"
  else
    "bidama #{e[:namespace]}"
  end
  example = e[:example]
  derived = e[:derived]
  lines = append(
    [e[:signature], "  #{where} · tier: #{e[:tier]}"],
    map((l) -> "  #{l}", e[:doc]),
    if derived.nil?
      []
    else
      map((l) -> "  · #{l}", json_get(derived, :lines))
    end,
    if example.nil?
      []
    else
      append(
        ["  example (#{example[:from]}):"],
        map((l) -> "    #{l}", example[:code])
      )
    end
  )
  join(lines, line_break)
end

def package_entry(r)
  {
    package: pkg_name(r),
    version: pkg_version(r),
    needs: pkg_needs(r),
    gloss: pkg_gloss(r),
    tests: pkg_tests(r),
    definitions: map(
      fn(e)
        {
          name: first(e),
          signature: e[1],
          doc: if empty?(e[2])
            ""
          else
            first(e[2])
          end
        }
      end,
      pkg_entries(r)
    )
  }
end

def package_text_view(p)
  needs = if empty?(p[:needs])
    "—"
  else
    join(p[:needs], ", ")
  end
  head = [
    "#{p[:package]} #{p[:version]} · needs: #{needs} · #{to_s(p[:tests])} tests",
    "  #{p[:gloss]}",
    ""
  ]
  defs = map(
    fn(d)
      signature = "  #{p[:package]}::#{d[:signature]}"
      if d[:doc] == ""
        signature
      else
        join_lines([signature, "      #{d[:doc]}"])
      end
    end,
    p[:definitions]
  )
  join(append(head, defs), line_break)
end

# What the doc says, written to stdout; true when something was found. The
# binary exits non-zero on false.
def main(args)
  if empty?(args) or starts_with?(first(args), "--")
    raise(:usage, "usage: blue doc NAME [--file FILE] [--json]")
  end
  name = first(args)
  json? = member?("--json", args)
  file = flag_value(args, "--file")
  records = catalog_of(
    blue_path_roots("#{getenv("BLUE_PATH", "")}:#{getenv("BLUE_DIST", "")}")
  )
  packages = filter((r) -> pkg_name(r) == name, records)
  if not contains?(name, "::") and not empty?(packages)
    p = package_entry(first(packages))
    if json?
      write_stdout(unlines([json_stringify(p)]))
    else
      write_stdout(unlines([package_text_view(p)]))
    end
    true
  else
    found = lookup(name, file, records)
    if empty?(found)
      where = if empty?(records)
        "the builtins (BLUE_PATH names no bidama)"
      else
        "the builtins and #{to_s(length(records))} bidamas on BLUE_PATH"
      end
      write_stderr(unlines(["blue doc: no `#{name}` in #{where}"]))
      false
    elsif json?
      map((e) -> write_stdout(unlines([json_stringify(e)])), found)
      true
    else
      write_stdout(unlines([join(map(entry_text, found), unlines(["", ""]))]))
      true
    end
  end
end

main(argv)