banca / client / test / helpers / cljs-reader.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
 * A ClojureScript/EDN reader good enough to answer structural questions about
 * source text — "what is nested inside what", not "what does this mean".
 *
 * It was written inside test/source-hygiene.test.mjs for the two paren-depth
 * lints (the defect class that took chat v24 and board v12 down), and moved
 * here when a SECOND consumer appeared: helpers/facade.mjs, which freezes the
 * JS façade (`:exports`, the `unchecked-set proto` vtables, defclass arities)
 * by reading the same trees.
 *
 * Divergence note: game6 and rooms-kit carry copies of source-hygiene.test.mjs
 * with this reader still inline. The code below is byte-identical to what was
 * inline there apart from the `export` keywords — if you fix a reader bug here,
 * fix it in those copies too.
 *
 * `'` and `#` are NON-terminating macro chars — `x'` and `g#` are one symbol.
 * A reader that treats `'` as terminating mis-parses game4 and peer-kit.
 */

export const TERMINATING = '()[]{}";`~@^';
export const SPACE = " \t\n\r\f,";

/** Blank out strings and comments so only code text is scanned. */
export function strip(src) {
  let out = "";
  let inStr = false;
  for (let i = 0; i < src.length; i++) {
    const ch = src[i];
    if (inStr) {
      if (ch === "\\") { out += "  "; i++; continue; }
      if (ch === '"') inStr = false;
      out += ch === "\n" ? "\n" : " ";
      continue;
    }
    if (ch === '"') { inStr = true; out += " "; continue; }
    if (ch === ";") { while (i < src.length && src[i] !== "\n") { out += " "; i++; } out += "\n"; continue; }
    out += ch;
  }
  return out;
}

/**
 * Reads `src` into a flat list of nodes carrying {type, line, col, leading,
 * start, end, children, parent}. Strings, `;` comments, `\(` char literals and
 * `#"…"` regexes are consumed as units so their delimiters never move the depth.
 */
export function readForms(src) {
  let i = 0;
  const n = src.length;
  const lineAt = [], bolAt = [];
  { let l = 1, s = 0;
    for (let k = 0; k <= n; k++) { lineAt[k] = l; bolAt[k] = s; if (src[k] === "\n") { l++; s = k + 1; } } }

  const ws = (c) => c !== undefined && SPACE.includes(c);
  const term = (c) => c === undefined || ws(c) || TERMINATING.includes(c);
  const skip = () => {
    for (;;) {
      while (i < n && ws(src[i])) i++;
      if (src[i] === ";") { while (i < n && src[i] !== "\n") i++; continue; }
      break;
    }
  };
  const eatString = () => {
    i++;
    while (i < n) { if (src[i] === "\\") { i += 2; continue; } if (src[i] === '"') { i++; break; } i++; }
  };
  const node = (type, start, children, text) => ({
    type, start, end: i, children, text,
    line: lineAt[start], col: start - bolAt[start],
    leading: /^[\s,]*$/.test(src.slice(bolAt[start], start)),
  });

  function coll(close, type) {
    const start = i;
    i++;
    const kids = [];
    for (;;) {
      skip();
      if (i >= n) break;
      if (src[i] === close) { i++; break; }
      if (")]}".includes(src[i])) { i++; continue; }   // stray closer: step over
      const f = form();
      if (f) kids.push(f);
    }
    const nd = node(type, start, kids);
    for (const k of kids) k.parent = nd;
    return nd;
  }

  function form() {
    skip();
    if (i >= n) return null;
    const c = src[i];
    if (c === "(") return coll(")", "list");
    if (c === "[") return coll("]", "vector");
    if (c === "{") return coll("}", "map");
    if (c === '"') { const s = i; eatString(); return node("string", s, []); }
    if (c === "\\") {
      const s = i; i++;
      const named = /^(newline|space|tab|formfeed|backspace|return|u[0-9a-fA-F]{4}|o[0-7]{1,3})(?![\w-])/
        .exec(src.slice(i));
      i += named ? named[0].length : 1;
      return node("char", s, []);
    }
    if (c === "#") {
      const nx = src[i + 1];
      if (nx === "_") { i += 2; form(); return null; }                        // discard
      if (nx === '"') { const s = i; i++; eatString(); return node("regex", s, []); }
      if (nx === "{") { i++; return coll("}", "set"); }
      if (nx === "(") { const s = i; i++; const l = coll(")", "list"); return { ...l, type: "fn-literal", start: s }; }
      if (nx === "?") { i += 2; if (src[i] === "@") i++; return form(); }
      const s = i; i++;                                                       // #js, #uuid, …
      while (i < n && !term(src[i])) i++;
      const f = form();
      return f ? { ...f, start: s } : null;
    }
    if (c === "'" || c === "`" || c === "@" || c === "~") {
      const s = i;
      i += c === "~" && src[i + 1] === "@" ? 2 : 1;
      const f = form();
      return f ? { ...f, start: s, line: lineAt[s], col: s - bolAt[s],
                   leading: /^[\s,]*$/.test(src.slice(bolAt[s], s)) } : null;
    }
    if (c === "^") { i++; form(); return form(); }                            // metadata: keep the target
    if (")]}".includes(c)) { i++; return null; }
    const s = i;
    while (i < n && !term(src[i])) i++;
    if (i === s) i++;
    return node("symbol", s, [], src.slice(s, i));
  }

  const top = [];
  for (;;) { skip(); if (i >= n) break; const f = form(); if (f) top.push(f); }
  const flat = [];
  (function collect(ns) { for (const nd of ns) { flat.push(nd); if (nd.children) collect(nd.children); } })(top);
  return flat;
}

/** The head symbol of a list, or null when the head is not a plain symbol. */
export const sym = (nd) => (nd?.type === "list" && nd.children?.[0]?.type === "symbol" ? nd.children[0].text : null);

export const COLLS = new Set(["list", "vector", "map", "set", "fn-literal"]);

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/banca.git