UNPKG

squint-cljs

Version:

<img src="./logo/logo.svg" width="100%">

4,723 lines 127 kB
/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_"}]*/

// __toFn is not public API - the leading underscores mark it as an
// implementation helper shared with other squint runtime modules
// (e.g. multi.js). Signature and semantics may change without notice.
export function __toFn(x) {
  if (x == null || typeof x === 'function') return x;
  if (typeof x === 'string') return (coll, d) => get(coll, x, d);
  // a value is callable as a lookup only if it is a collection or a custom
  // type implementing ILookup; a seq, list or opaque object throws when
  // called, like a non-IFn in CLJS
  switch (typeConst(x)) {
    case MAP_TYPE:
    case ARRAY_TYPE:
    case OBJECT_TYPE:
    case SET_TYPE:
      return (k, d) => get(x, k, d);
    case INSTANCE_TYPE:
      if (x[ILookup__lookup] !== undefined) return (k, d) => get(x, k, d);
  }
  return x;
}

// inlined and modified version of https://github.com/lukeed/dequal
var has = Object.prototype.hasOwnProperty;

function findKey(iter, tar, key) {
  for (key of iter.keys()) {
    if (dequal(key, tar)) return key;
  }
}

function isSortedMap(m) {
  return m != null && m[SORTED_TAG] === true && m[TYPE_TAG] === MAP_TYPE;
}
function isSetLike(s) {
  return s != null && (s instanceof Set || s[TYPE_TAG] === SET_TYPE);
}
function isMapLike(m) {
  return (
    m != null &&
    typeof m === 'object' &&
    (m.constructor === Object || m instanceof Map || m[TYPE_TAG] === MAP_TYPE)
  );
}
function mapHas(m, k) {
  return m instanceof Map || m[TYPE_TAG] === MAP_TYPE ? m.has(k) : has.call(m, k);
}
function mapGet(m, k) {
  return m instanceof Map || m[TYPE_TAG] === MAP_TYPE ? m.get(k) : m[k];
}
function mapCount(m) {
  return m instanceof Map || m[TYPE_TAG] === MAP_TYPE
    ? m.size
    : Object.keys(m).length;
}

// shared by dequal's fast path and fall-through. element-wise for arrays, own
// enumerable keys otherwise; callers guarantee a shared ctor, no brand, no -equiv
function dequalSameCtor(foo, bar, ctor) {
  var len;
  if (ctor === Array) {
    if ((len = foo.length) === bar.length) {
      while (len-- && dequal(foo[len], bar[len]));
    }
    return len === -1;
  }
  len = 0;
  for (const k in foo) {
    if (has.call(foo, k) && ++len && !has.call(bar, k)) return false;
    if (!(k in bar) || !dequal(foo[k], bar[k])) return false;
  }
  return Object.keys(bar).length === len;
}

function dequal(foo, bar) {
  // supports primitives, Array, Set, Map and plain objects
  // like CLJS: does not support NaN
  if (foo === bar) return true;
  // null and undefined are both nil in CLJS, so they compare equal
  if (foo == null) return bar == null;
  if (bar == null) return false;
  // a primitive is only equal by identity, checked above; bail before the
  // property checks below box it
  if (typeof foo !== 'object' || typeof bar !== 'object') return false;
  var ctor = foo.constructor, tmp;

  // same-constructor plain objects and arrays skip the protocol, sorted and
  // cross-type checks below; neither type can carry -equiv or a brand
  if (ctor === bar.constructor && (ctor === Object || ctor === Array)) {
    return dequalSameCtor(foo, bar, ctor);
  }

  // -equiv dispatches on the left argument, like CLJS =
  if (typeof foo === 'object' && foo[IEquiv__equiv] !== undefined) return !!foo[IEquiv__equiv](foo, bar);
  // when only the right side has -equiv (the left is e.g. a plain object),
  // dispatch on it so = stays symmetric
  if (typeof bar === 'object' && bar[IEquiv__equiv] !== undefined) return !!bar[IEquiv__equiv](bar, foo);

  // A sorted map compares by entries against any map type (object, Map, sorted).
  const fooSorted = isSortedMap(foo);
  if (fooSorted || isSortedMap(bar)) {
    const sm = fooSorted ? foo : bar;
    const other = sm === foo ? bar : foo;
    if (!isMapLike(other) || mapCount(sm) !== mapCount(other)) return false;
    for (const k of sm.keys()) {
      if (!mapHas(other, k) || !dequal(sm.get(k), mapGet(other, k))) return false;
    }
    return true;
  }

  // A plain object and a js/Map are both map reps; compare by entries.
  // Same-type pairs skip this and keep their fast paths below.
  if (isMapLike(foo) && isMapLike(bar) && foo.constructor !== bar.constructor) {
    if (mapCount(foo) !== mapCount(bar)) return false;
    for (const k of foo instanceof Map ? foo.keys() : Object.keys(foo)) {
      if (!mapHas(bar, k) || !dequal(mapGet(foo, k), mapGet(bar, k))) return false;
    }
    return true;
  }

  // Sets (hash or sorted) compare by elements, across concrete types.
  if (isSetLike(foo) || isSetLike(bar)) {
    if (!isSetLike(foo) || !isSetLike(bar) || foo.size !== bar.size) return false;
    for (let e of foo) {
      if (e && typeof e === 'object') {
        e = findKey(bar, e);
        if (!e) return false;
      }
      if (!bar.has(e)) return false;
    }
    return true;
  }

  if (foo && bar && ctor === bar.constructor) {
    if (ctor === Date) return foo.getTime() === bar.getTime();
    // regexes only compare by identity, like CLJS
    if (ctor === RegExp) return false;
    // no Array branch: same-ctor arrays already returned via the fast path

    if (ctor === Map) {
      if (foo.size !== bar.size) {
        return false;
      }
      for (const kv of foo) {
        tmp = kv[0];
        if (tmp && typeof tmp === 'object') {
          tmp = findKey(bar, tmp);
          if (!tmp) return false;
        }
        if (!dequal(kv[1], bar.get(tmp))) {
          return false;
        }
      }
      return true;
    }

    // LazyIterable falls through to the sequential-equality path below; it is an
    // object but must compare element-wise, not by enumerable properties
    if ((!ctor || typeof foo === 'object') && foo[TYPE_TAG] !== LAZY_ITERABLE_TYPE) {
      return dequalSameCtor(foo, bar, Object);
    }
  }

  // Cross-type sequential equality, like CLJS `(= '(1 2) [1 2])`: vectors,
  // lists and lazy seqs compare element-wise regardless of concrete type. Only
  // reached when the same-constructor paths above did not apply, so equal-typed
  // collections keep their fast paths.
  if (
    foo && bar &&
    (Array.isArray(foo) || foo[TYPE_TAG] === LAZY_ITERABLE_TYPE) &&
    (Array.isArray(bar) || bar[TYPE_TAG] === LAZY_ITERABLE_TYPE)
  ) {
    const fi = foo[Symbol.iterator]();
    const bi = bar[Symbol.iterator]();
    for (;;) {
      const a = fi.next();
      const b = bi.next();
      if (a.done || b.done) return !!(a.done && b.done);
      if (!dequal(a.value, b.value)) return false;
    }
  }

  return false;
}
// end inlined version of dequals

function walkArray(arr, comp) {
  return arr.every(function (x, i) {
    return i === 0 || comp(arr[i - 1], x);
  });
}

export function _EQ_(...xs) {
  return walkArray(xs, (x, y) => dequal(x, y));
}

export function _GT_(...xs) {
  return walkArray(xs, (x, y) => x > y);
}

export function _GT__EQ_(...xs) {
  return walkArray(xs, (x, y) => x >= y);
}

export function _LT_(...xs) {
  return walkArray(xs, (x, y) => x < y);
}

export function _LT__EQ_(...xs) {
  return walkArray(xs, (x, y) => x <= y);
}

export function _PLUS_(...xs) {
  return xs.reduce((x, y) => x + y, 0);
}

export function _STAR_(...xs) {
  return xs.reduce((x, y) => x * y, 1);
}

export function _(...xs) {
  if (xs.length == 1) {
    return 0 - xs[0];
  }
  return xs.reduce((x, y) => x - y);
}

export function _SLASH_(...xs) {
  if (xs.length === 1) {
    return 1 / xs[0];
  }
  return xs.reduce((x, y) => x / y);
}

export const __protocol_satisfies = {};

export function satisfies_QMARK_(protocol, x) {
  if (x == null) {
    return protocol[null];
  }
  if (typeof protocol == 'symbol') return x[protocol];
  return x[protocol.__sym];
}

function mapAssocMut(m, k, v) {
  m.set(k, v);
  return m;
}

function objAssocMut(m, k, v) {
  m[k] = v;
  return m;
}

function getAssocMut(m) {
  switch (typeConst(m)) {
    case MAP_TYPE:
      return mapAssocMut;
    case ARRAY_TYPE:
    case OBJECT_TYPE:
    case INSTANCE_TYPE:
      return objAssocMut;
  }
}

function validateArrayKeys(o, k, kvs) {
  // like CLJS: a vector key is an index in [0, count], count appends
  let len = o.length;
  for (let i = 0; i < kvs.length + 2; i += 2) {
    const key = i === 0 ? k : kvs[i - 2];
    if (!Number.isInteger(key)) {
      throw new Error("Vector's key for assoc must be a number.");
    }
    if (key < 0 || key > len) {
      throw new Error(`Index ${key} out of bounds [0,${len}]`);
    }
    if (key === len) len++;
  }
}

export function assoc_BANG_(m, k, v, ...kvs) {
  if (arguments.length < 3 || kvs.length % 2 !== 0) {
    throw new Error('Illegal argument: assoc expects an odd number of arguments.');
  }
  switch (typeConst(m)) {
    case MAP_TYPE:
      m.set(k, v);

      for (let i = 0; i < kvs.length; i += 2) {
        m.set(kvs[i], kvs[i + 1]);
      }
      break;
    case ARRAY_TYPE:
      validateArrayKeys(m, k, kvs);
      m[k] = v;

      for (let i = 0; i < kvs.length; i += 2) {
        m[kvs[i]] = kvs[i + 1];
      }
      break;
    case INSTANCE_TYPE:
      if (m[ITransientAssociative__assoc_BANG_] !== undefined) {
        // re-read the slot off the current value: an -assoc! impl may return a
        // different handle
        let ret = m[ITransientAssociative__assoc_BANG_](m, k, v);
        for (let i = 0; i < kvs.length; i += 2) {
          ret = ret[ITransientAssociative__assoc_BANG_](ret, kvs[i], kvs[i + 1]);
        }
        return ret;
      }
    // fall through: an instance without -assoc! keeps the object behavior
    case OBJECT_TYPE:
      m[k] = v;

      for (let i = 0; i < kvs.length; i += 2) {
        m[kvs[i]] = kvs[i + 1];
      }
      break;
    default:
      throw new Error(
        `Illegal argument: assoc! expects a Map, Array, or Object as the first argument, but got ${typeof m}.`
      );
  }

  return m;
}

// value-producing ops (copy, empty, conj, into) carry metadata by
// forwarding the instance-level meta slots onto the fresh structure
function copyMeta(from, to) {
  const f = from?.[IMeta__meta];
  if (f !== undefined) {
    to[IMeta__meta] = f;
    to[IWithMeta__with_meta] = from[IWithMeta__with_meta];
  }
  return to;
}

function copy(o) {
  switch (typeConst(o)) {
    case MAP_TYPE:
      // new o.constructor(o) preserves a SortedMap; for a plain Map it is new Map(o)
      return copyMeta(o, new o.constructor(o));
    case SET_TYPE:
      return copyMeta(o, new o.constructor(o));
    case ARRAY_TYPE:
      return copyMeta(o, [...o]);
    case INSTANCE_TYPE:
    case OBJECT_TYPE:
      return copyMeta(o, { ...o });
    case LIST_TYPE:
      return copyMeta(o, new List(...o));
    default:
      throw new Error(`Don't know how to copy object of type ${typeof o}.`);
  }
}

export function assoc(o, k, v, ...kvs) {
  if (arguments.length < 3 || kvs.length % 2 !== 0) {
    throw new Error('Illegal argument: assoc expects an odd number of arguments.');
  }
  // only nil puns to an empty map; assoc on false throws, like CLJS
  if (o == null) {
    o = {};
  }
  // plain objects and arrays never carry the slot: skip the lookup, like get
  if (!isObj(o) && !Array.isArray(o) && o[IAssociative__assoc] !== undefined) {
    let ret = o[IAssociative__assoc](o, k, v);
    for (let i = 0; i < kvs.length; i += 2) {
      ret = ret[IAssociative__assoc](ret, kvs[i], kvs[i + 1]);
    }
    return ret;
  }
  const ret = copy(o);
  assoc_BANG_(ret, k, v, ...kvs);
  return ret;
}

// squint has no distinct hash-map or array-map type; both build a plain object.
export function hash_map(...kvs) {
  if (kvs.length === 0) return {};
  if (kvs.length % 2 !== 0) {
    throw new Error('No value supplied for key: ' + kvs[kvs.length - 1]);
  }
  return assoc({}, ...kvs);
}

export const array_map = hash_map;

// req! and some-vals are clojure.core fns added for 1.13 destructuring.
// Ported from Clojure (clojure/core.clj), Copyright (c) Rich Hickey and
// contributors, Eclipse Public License 1.0.
const REQ_NOT_FOUND = {};

// Like arity-2 get, but throws if key not present.
export function req_BANG_(m, k) {
  const v = get(m, k, REQ_NOT_FOUND);
  if (v === REQ_NOT_FOUND) throw new Error('Missing required key: ' + k);
  return v;
}

// Returns a map with only the non-nil values of m, or nil if there are none.
export function some_vals(m) {
  if (m == null) return null;
  let ret = null;
  for (const [k, v] of iterable(m)) {
    if (v !== null && v !== undefined) {
      if (ret === null) ret = {};
      ret[k] = v;
    }
  }
  return ret;
}

// https://clojure.org/reference/special_forms#keyword-arguments
export function seq_to_map_for_destructuring(s) {
  const arr = Array.isArray(s) ? s : [...iterable(s)];
  const n = arr.length;
  if (n < 2) return n ? arr[0] : {};
  const m = {};
  for (let i = 0; i < n; i += 2) {
    // an odd count leaves a trailing map
    if (i === n - 1) for (const [k, v] of iterable(arr[i])) m[k] = v;
    else m[arr[i]] = arr[i + 1];
  }
  return m;
}

const MAP_TYPE = 1;
const ARRAY_TYPE = 2;
const OBJECT_TYPE = 3;
const LIST_TYPE = 4;
const SET_TYPE = 5;
const LAZY_ITERABLE_TYPE = 6;
// a class instance or null-prototype object: the extension point for the
// map-facing protocols. Plain objects keep the OBJECT_TYPE fast path.
const INSTANCE_TYPE = 7;

// type tag set in each collection ctor, read by typeConst (DCE: no instanceof).
// the registry keys are permanent and deliberately not the const names: change a
// key and two evaluated copies of core stop recognizing each other's collections.
const TYPE_TAG = /* @__PURE__ */ Symbol.for('squint.core/type');
const SORTED_TAG = /* @__PURE__ */ Symbol.for('squint.core/sorted');

// @__NO_SIDE_EFFECTS__ lets a bundler drop unused defclass/withApply calls; see doc/dev/dce.md
// @__NO_SIDE_EFFECTS__
function defclass(c) {
  return c;
}
// @__NO_SIDE_EFFECTS__
function withApply(f, applyFn) {
  f.squint$lang$variadic = applyFn;
  return f;
}

function emptyOfType(type) {
  switch (type) {
    case MAP_TYPE:
      return new Map();
    case ARRAY_TYPE:
      return [];
    case OBJECT_TYPE:
      return {}; // Object.create?
    case LIST_TYPE:
      return new List();
    case SET_TYPE:
      return new Set();
    case LAZY_ITERABLE_TYPE:
      return lazy(function* () {
        return;
      });
  }
  return undefined;
}

function isObj(coll) {
  return coll.constructor === Object;
}

function isVectorArray(x) {
  return Array.isArray(x) && x[TYPE_TAG] !== LIST_TYPE;
}

export function object_QMARK_(coll) {
  return coll != null && isObj(coll);
}

function typeConst(obj) {
  if (obj == null) {
    return undefined;
  }
  // optimize for object
  if (isObj(obj)) {
    return OBJECT_TYPE;
  }
  if (obj instanceof Map) return MAP_TYPE;
  if (obj instanceof Set) return SET_TYPE;
  // brand, not instanceof, so dispatch does not reference the classes
  const tag = obj[TYPE_TAG];
  if (tag !== undefined) return tag;
  if (isVectorArray(obj)) return ARRAY_TYPE;
  // any remaining object (class instance, null-proto) is associative
  if (typeof obj === 'object') return INSTANCE_TYPE;

  return undefined;
}

function assoc_in_with(f, fname, o, keys, value) {
  keys = vec(keys);
  o = o || {}; // default nil behavior is JS object
  const baseType = typeConst(o);
  if (baseType !== MAP_TYPE && baseType !== ARRAY_TYPE && baseType !== OBJECT_TYPE && baseType !== INSTANCE_TYPE)
    throw new Error(
      `Illegal argument: ${fname} expects the first argument to be a Map, Array, or Object.`
    );

  const chain = [o];
  let lastInChain = o;

  for (let i = 0; i < keys.length - 1; i += 1) {
    const k = keys[i];
    let chainValue;
    if (lastInChain instanceof Map) chainValue = lastInChain.get(k);
    else if (lastInChain != null && lastInChain[ILookup__lookup] !== undefined) {
      chainValue = lastInChain[ILookup__lookup](lastInChain, k, undefined);
    } else chainValue = lastInChain[k];
    if (!chainValue) {
      // an instance root has no empty-of-type: missing levels become plain maps
      chainValue = emptyOfType(baseType) ?? {};
    }
    chain.push(chainValue);
    lastInChain = chainValue;
  }

  chain.push(value);

  for (let i = chain.length - 2; i >= 0; i -= 1) {
    chain[i] = f(chain[i], keys[i], chain[i + 1]);
  }

  return chain[0];
}

export function assoc_in(o, keys, value) {
  return assoc_in_with(assoc, 'assoc-in', o, keys, value);
}

export function assoc_in_BANG_(o, keys, value) {
  keys = vec(keys);
  var currObj = o;
  const baseType = typeConst(o);
  for (const k of keys.splice(0, keys.length - 1)) {
    let v = get(currObj, k);
    if (v === undefined) {
      v = emptyOfType(baseType);
      assoc_BANG_(currObj, k, v);
    }
    currObj = v;
  }
  assoc_BANG_(currObj, keys[keys.length - 1], value);
  return o;
}

export function comp(...fs) {
  fs = fs.map(__toFn);
  if (fs.length === 0) {
    return identity;
  } else if (fs.length === 1) {
    return fs[0];
  }
  const [f, ...more] = fs.slice().reverse();
  return function (...args) {
    let x = f(...args);
    for (const g of more) {
      x = g(x);
    }
    return x;
  };
}

function conj_BANG_set(o, rest) {
  for (const x of rest) {
    o.add(x);
  }
  return o;
}

export function conj_BANG_(...xs) {
  const n = xs.length;
  if (n === 0) {
    return vector();
  }
  // single arg: return the coll unchanged, including nil, like CLJS
  if (n === 1) {
    return xs[0];
  }

  let o = xs[0];
  if (o === null || o === undefined) {
    o = [];
  }

  // Fast path for the common single-element conj! onto an array or set,
  // avoiding the rest-array allocation and spread.
  if (n === 2) {
    switch (typeConst(o)) {
      case ARRAY_TYPE:
        o.push(xs[1]);
        return o;
      case SET_TYPE:
        o.add(xs[1]);
        return o;
    }
  }

  const rest = xs.slice(1);

  switch (typeConst(o)) {
    case SET_TYPE:
      conj_BANG_set(o, rest);
      break;
    case LIST_TYPE:
      o.unshift(...rest.reverse());
      break;
    case ARRAY_TYPE:
      o.push(...rest);
      break;
    case MAP_TYPE:
      for (const x of rest) {
        if (isVectorArray(x)) { asMapEntry(x); o.set(x[0], x[1]); }
        else for (const kv of mapEntriesOf(x)) o.set(kv[0], kv[1]);
      }
      break;
    case INSTANCE_TYPE:
      if (o[ITransientCollection__conj_BANG_] !== undefined) {
        // re-dispatch per element: a -conj! impl may return a different handle
        let acc = o[ITransientCollection__conj_BANG_](o, rest[0]);
        for (let i = 1; i < rest.length; i++) acc = conj_BANG_(acc, rest[i]);
        return acc;
      }
    // fall through: an instance without -conj! keeps the object behavior
    case OBJECT_TYPE:
      for (const x of rest) {
        if (isVectorArray(x)) { asMapEntry(x); o[x[0]] = x[1]; }
        else for (const kv of mapEntriesOf(x)) o[kv[0]] = kv[1];
      }
      break;
    default:
      throw new Error(
        'Illegal argument: conj! expects a Set, Array, List, Map, or Object as the first argument.'
      );
  }

  return o;
}

// entries carried by a non-entry conj arg onto a map: a map merges, a
// seqable must contain entry vectors, like CLJS
function* mapEntriesOf(x) {
  if (isMapLike(x)) {
    yield* iterable(x);
    return;
  }
  for (const kv of iterable(x)) {
    if (!isVectorArray(kv)) {
      throw new Error('conj on a map takes map entries or seqables of map entries');
    }
    yield kv;
  }
}

function asMapEntry(x) {
  if (x.length < 2) {
    throw new Error('Vector arg to map conj must be a pair');
  }
  return x;
}

export function conj(...xs) {
  if (xs.length === 0) {
    return vector();
  }

  const [_o, ...rest] = xs;
  // (conj coll) with nothing to add returns coll unchanged, including nil.
  if (rest.length === 0) return _o;

  let o = _o;
  if (o === null || o === undefined) {
    o = list();
  }
  let m, o2;

  switch (typeConst(o)) {
    case SET_TYPE:
      // brand, not instanceof, so conj does not pin SortedSet
      if (o[SORTED_TAG]) {
        // prevent re-sorting of collection
        return copyMeta(o, conj_BANG_set(new o.constructor(o), rest));
      } else {
        return copyMeta(o, new o.constructor([...o, ...rest]));
      }
    case LIST_TYPE:
      return copyMeta(o, new List(...rest.reverse(), ...o));
    case ARRAY_TYPE:
      return copyMeta(o, [...o, ...rest]);
    case MAP_TYPE:
      m = new Map(o);
      for (const x of rest) {
        if (isVectorArray(x)) { asMapEntry(x); m.set(x[0], x[1]); }
        else for (const kv of mapEntriesOf(x)) m.set(kv[0], kv[1]);
      }

      return copyMeta(o, m);
    case LAZY_ITERABLE_TYPE:
      return lazy(function* () {
        yield* rest;
        yield* o;
      });
    case INSTANCE_TYPE:
      if (o[ICollection__conj] !== undefined) {
        // re-dispatch per element: a -conj impl may return a different type
        o2 = o[ICollection__conj](o, rest[0]);
        for (let i = 1; i < rest.length; i++) o2 = conj(o2, rest[i]);
        return o2;
      }
    // fall through: an instance without -conj keeps the object behavior
    case OBJECT_TYPE:
      o2 = { ...o };

      for (const x of rest) {
        if (isVectorArray(x)) { asMapEntry(x); o2[x[0]] = x[1]; }
        else for (const kv of mapEntriesOf(x)) o2[kv[0]] = kv[1];
      }

      return copyMeta(o, o2);
    default:
      throw new Error(
        'Illegal argument: conj expects a Set, Array, List, Map, or Object as the first argument.'
      );
  }
}

export function disj_BANG_(s, ...xs) {
  if (s != null && s[ITransientSet__disjoin_BANG_] !== undefined) {
    let ret = s;
    for (const x of xs) {
      ret = ret != null && ret[ITransientSet__disjoin_BANG_] !== undefined ? ret[ITransientSet__disjoin_BANG_](ret, x) : disj_BANG_(ret, x);
    }
    return ret;
  }
  for (const x of xs) {
    s.delete(x);
  }
  return s;
}

export function disj(s, ...xs) {
  if (s == null) return s;
  if (xs.length === 0) return s;
  if (s[ISet__disjoin] !== undefined) {
    let ret = s[ISet__disjoin](s, xs[0]);
    for (let i = 1; i < xs.length; i++) ret = disj(ret, xs[i]);
    return ret;
  }
  // pass s itself (not a spread) so a SortedSet keeps its comparator
  const s1 = new s.constructor(s);
  return copyMeta(s, disj_BANG_(s1, ...xs));
}

export function contains_QMARK_(coll, v) {
  if (typeof coll === 'string') {
    return int_QMARK_(v) && v >= 0 && v < coll.length;
  }
  switch (typeConst(coll)) {
    case SET_TYPE:
    case MAP_TYPE:
      return coll.has(v);
    case undefined:
      return false;
    case INSTANCE_TYPE:
      if (coll[IAssociative__contains_key_QMARK_] !== undefined) {
        return coll[IAssociative__contains_key_QMARK_](coll, v);
      }
    // fall through
    default:
      return v in coll;
  }
}

export function dissoc_BANG_(m, ...ks) {
  if (m != null && m[ITransientMap__dissoc_BANG_] !== undefined) {
    let ret = m;
    for (const k of ks) ret = ret != null && ret[ITransientMap__dissoc_BANG_] !== undefined ? ret[ITransientMap__dissoc_BANG_](ret, k) : dissoc_BANG_(ret, k);
    return ret;
  }
  for (const k of ks) {
    delete m[k];
  }

  return m;
}

export function dissoc(m, ...ks) {
  if (!m) return;
  if (ks.length === 0) return m;
  const tc = typeConst(m);
  if (tc !== MAP_TYPE && tc !== OBJECT_TYPE && tc !== INSTANCE_TYPE) {
    throw new Error('dissoc expects a map, got: ' + typeof m);
  }
  if (tc === INSTANCE_TYPE && m[IMap__dissoc] !== undefined) {
    let ret = m;
    // re-dispatch per key: a -dissoc impl may return a different type
    for (const k of ks) {
      if (ret == null) return ret;
      ret = ret[IMap__dissoc] !== undefined ? ret[IMap__dissoc](ret, k) : dissoc(ret, k);
    }
    return ret;
  }
  if (tc === MAP_TYPE) {
    let present = false;
    for (const k of ks) if (m.has(k)) { present = true; break; }
    if (!present) return m;
    const m2 = copy(m);
    for (const k of ks) m2.delete(k);
    return m2;
  }
  let present = false;
  for (const k of ks) if (k in m) { present = true; break; }
  if (!present) return m;
  const m2 = copy(m);
  for (const k of ks) delete m2[k];
  return m2;
}

export function inc(n) {
  return n + 1;
}

export function dec(n) {
  return n - 1;
}

export const _STAR_print_newline_STAR_ = { val: false };
export const _STAR_print_fn_STAR_ = { val: (s) => console.log(s) };
export const _STAR_print_err_fn_STAR_ = { val: (s) => console.error(s) };

export function print(...args) {
  _STAR_print_fn_STAR_.val(args.map((v) => toEDN(v, undefined, false)).join(' '));
}

export function println(...args) {
  print(...args);
  if (_STAR_print_newline_STAR_.val) _STAR_print_fn_STAR_.val('\n');
}

export function print_str(...args) {
  return args.map((v) => toEDN(v, undefined, false)).join(' ');
}

export function println_str(...args) {
  return print_str(...args) + '\n';
}

export function pr(...xs) {
  _STAR_print_fn_STAR_.val(pr_str(...xs));
}

export function nth(coll, idx, orElse) {
  if (typeof idx !== 'number') {
    throw new Error('Index argument to nth must be a number');
  }
  const hasDefault = arguments.length > 2;
  // nil coll puns to nil, like Clojure
  if (coll == null) return hasDefault ? orElse : null;
  // "found" is decided by the index bound, not the value. An in-bounds element
  // that happens to be undefined is still found.
  if (Array.isArray(coll)) {
    if (idx >= 0 && idx < coll.length) {
      return coll[idx];
    }
  } else if (coll[IIndexed__nth] !== undefined) {
    return hasDefault ? coll[IIndexed__nth](coll, idx, orElse) : coll[IIndexed__nth](coll, idx);
  } else if (idx >= 0) {
    // non-array: skip whole chunks instead of counting elements (handles
    // infinite seqs since it stops once idx is reached)
    const next = chunkCursor(coll);
    let base = 0;
    let ch;
    while ((ch = next()) !== null) {
      if (idx < base + ch.length) return ch[idx - base];
      base += ch.length;
    }
  }
  // out of bounds. With a default return it, otherwise throw like Clojure
  if (hasDefault) return orElse;
  throw new Error('Index out of bounds: ' + idx);
}

export function get(coll, key, otherwise = undefined) {
  if (coll == null) {
    return otherwise;
  }
  let v;
  // optimize for getting values out of objects
  if (isObj(coll)) {
    v = coll[key];
    if (v === undefined) {
      return otherwise;
    } else {
      return v;
    }
  }
  let g;
  switch (typeConst(coll)) {
    case SET_TYPE:
      if (coll.has(key)) v = key;
      break;
    case MAP_TYPE:
      v = coll.get(key);
      break;
    case ARRAY_TYPE:
      v = coll[key];
      break;
    default:
      if (coll[ILookup__lookup] !== undefined) {
        v = coll[ILookup__lookup](coll, key, otherwise);
        return v === undefined ? otherwise : v;
      }
      // we choose .get as the default implementation, e.g. fetch Headers are not Maps, but do implement a .get method
      g = coll['get'];
      if (typeof g === 'function') {
        try {
          v = coll.get(key);
          break;
        } catch (e) {
          // ignore error
        }
      }
      v = coll[key];
      break;
  }
  return v !== undefined ? v : otherwise;
}

export function seq_QMARK_(x) {
  return x != null && !!x[Symbol.iterator];
}

export function sequential_QMARK_(x) {
  // vectors and lists are arrays; lazy seqs and cons carry the lazy brand.
  // Sets, maps and strings are iterable but not sequential.
  return Array.isArray(x) || x?.[TYPE_TAG] === LAZY_ITERABLE_TYPE || (x != null && x[IVector.__sym] !== undefined);
}

export function seqable_QMARK_(x) {
  return (
    x === null ||
    x === undefined ||
    // plain objects (squint maps) are seqable via Object.entries in `iterable`,
    // even though they lack Symbol.iterator.
    object_QMARK_(x) ||
    // we used to check instanceof Object but this returns false for TC39 Records
    // also we used to write `Symbol.iterator in` but this does not work for strings and some other types
    !!x[Symbol.iterator] ||
    !!x[ISEQABLE_SYM]
  );
}

// squint has no distinct MapEntry type (map entries are plain 2-element
// arrays). We tag entries produced from a map with this marker symbol so
// map-entry? can tell them apart from ordinary vectors. Symbol-keyed props are
// invisible to =, into, iteration and JSON, so the effect is contained.
const MAP_ENTRY = /* @__PURE__ */ Symbol.for('squint.core/map-entry');

function tagMapEntry(e) {
  e[MAP_ENTRY] = true;
  return e;
}

export function map_entry_QMARK_(x) {
  return Array.isArray(x) && x[MAP_ENTRY] === true;
}

export function iterable(x) {
  // nil puns to empty iterable, support passing nil to first/rest/reduce, etc.
  if (x === null || x === undefined) {
    return [];
  }
  // fast path: anything with Symbol.iterator (arrays, strings, sets, maps,
  // lazy seqs). Inlined rather than calling seqable?, which also reports plain
  // objects as seqable; those are handled by the Object.entries branch below.
  if (x[Symbol.iterator]) {
    return x;
  }
  // a type extended to ISeqable seqs through its -seq method
  if (x[ISeqable__seq] !== undefined) return iterable(x[ISeqable__seq](x));
  // only a plain object is a squint map; a class instance without a native
  // iterator or ISeqable is not iterable, matching seqable? and CLJS, and
  // never leaks its internal fields
  if (isObj(x)) return Object.entries(x).map(tagMapEntry);
  throw new TypeError(`${x} is not iterable`);
}

export const IIterable = /* @__PURE__ */ Symbol.for('squint.core/IIterable');

export const IIterable__iterator = Symbol.iterator;

export function _iterator(coll) {
  return coll[Symbol.iterator]();
}

export const es6_iterator = _iterator;

export function seq(x) {
  if (x == null) return x;
  if (!seqable_QMARK_(x)) throw new TypeError(x + ' is not ISeqable');
  // a string seqs into its characters, like CLJS.
  if (typeof x === 'string') return x.length ? [...x] : null;
  const iter = iterable(x);
  // return nil for terminal checking
  if (iter.length === 0 || iter.size === 0) {
    return null;
  }
  // a set or map is iterable but not a sequence; materialize its entries
  // into a distinct seq so the result is not itself a set or map, like CLJS
  if (iter instanceof Set || iter[TYPE_TAG] === SET_TYPE) {
    return [...iter];
  }
  if (iter instanceof Map || iter[TYPE_TAG] === MAP_TYPE) {
    return [...iter].map(tagMapEntry);
  }
  // an instance with -dissoc is a map rep: same distinct entry seq
  if (iter[IMap__dissoc] !== undefined) {
    const entries = [...iter].map(tagMapEntry);
    return entries.length === 0 ? null : entries;
  }
  const _i = iter[Symbol.iterator]();
  if (_i.next().done) return null;
  return iter;
}

export function first(coll) {
  if (coll == null) return undefined;
  if (Array.isArray(coll)) return coll[0];
  if (coll instanceof LazyIterable) {
    coll.force();
    return coll.chunk === null ? undefined : coll.chunk[0];
  }
  // destructuring uses iterable protocol
  const [first] = iterable(coll);
  return first;
}

export function second(coll) {
  if (coll instanceof LazyIterable) {
    coll.force();
    const ch = coll.chunk;
    if (ch === null) return undefined;
    return ch.length > 1 ? ch[1] : first(coll._rest);
  }
  const [_, v] = iterable(coll);
  return v;
}

export function ffirst(coll) {
  return first(first(coll));
}

export function fnext(coll) {
  return first(next(coll));
}

export function nfirst(coll) {
  return next(first(coll));
}

export function rest(coll) {
  // chunk-aware: drop the first element of the first chunk, keep the rest of
  // the chain chunked (preserves chunkedness, unlike re-iterating element-wise)
  const cell = chunkCells(coll);
  cell.force();
  const ch = cell.chunk;
  if (ch === null) return cell; // (rest ()) is ()
  if (ch.length > 1) {
    const c = new LazyIterable(null);
    c.realized = true;
    c.chunk = ch.slice(1);
    c._rest = cell._rest;
    return c;
  }
  return cell._rest; // first chunk had one element; the next cell is the rest
}

const REDUCED_DEREF = (self) => self.value;

class Reduced {
  value;
  constructor(x) {
    this.value = x;
    this[IDeref__deref] = REDUCED_DEREF;
  }
}

export function last(coll) {
  coll = iterable(coll);
  if (Array.isArray(coll)) {
    return coll[coll.length - 1];
  }
  // non-array: walk chunks, keep the last chunk's last element
  const next = chunkCursor(coll);
  let lastEl;
  let ch;
  while ((ch = next()) !== null) lastEl = ch[ch.length - 1];
  return lastEl;
}

export function reduced(x) {
  return new Reduced(x);
}

export function reduced_QMARK_(x) {
  return x instanceof Reduced;
}

export function reduce(f, arg1, arg2) {
  f = __toFn(f);
  const hasInit = arguments.length !== 2;
  const coll = hasInit ? arg2 : arg1;
  let val = hasInit ? arg1 : undefined;

  // fast path: index loop over an array
  if (Array.isArray(coll)) {
    let i = 0;
    if (!hasInit) {
      if (coll.length === 0) return f();
      val = coll[0];
      i = 1;
    }
    if (val instanceof Reduced) return val.value;
    for (; i < coll.length; i++) {
      val = f(val, coll[i]);
      if (val instanceof Reduced) return val.value;
    }
    return val;
  }

  // non-array: walk chunks (chunked cell or any other seqable)
  const next = chunkCursor(coll);
  let ch = next();
  let i = 0;
  if (!hasInit) {
    if (ch === null) return f();
    val = ch[0];
    i = 1;
  }
  if (val instanceof Reduced) return val.value;
  while (ch !== null) {
    for (; i < ch.length; i++) {
      val = f(val, ch[i]);
      if (val instanceof Reduced) return val.value;
    }
    ch = next();
    i = 0;
  }
  return val;
}

function* _reductions2(f, s) {
  const vd = s.next();
  if (vd.done) {
    yield f();
  } else {
    yield* _reductions3(f, vd.value, s);
  }
}

function* _reductions3(f, init, coll) {
  let i = init;
  const rst = coll;
  while (true) {
    if (reduced_QMARK_(i)) {
      yield i.value;
      return;
    } else yield i;
    const vd = rst.next();
    if (vd.done) {
      break;
    }
    i = f(i, vd.value);
  }
}

export function reductions(f, arg1, arg2) {
  f = __toFn(f);
  if (arguments.length === 2) {
    const it = es6_iterator(iterable(arg1));
    return lazy(function* () {
      yield* _reductions2(f, it);
    });
  }
  const it = es6_iterator(iterable(arg2));
  return lazy(function* () {
    yield* _reductions3(f, arg1, it);
  });
}

// Deprecated: lazy values are now cached, so reuse no longer recomputes. Kept
// for API compatibility; remove in a future release.
export function warn_on_lazy_reusage_BANG_() {
  console.warn(
    'warn-on-lazy-reusage! is deprecated and does nothing: lazy values are now cached.',
  );
}

const CHUNK_SIZE = 32;

// One cell of a self-caching chunked seq. `step` is a thunk returning
// [nonEmptyChunkArray, nextStep] or null at the end. See doc/dev/lazy-seqs.md.
const LazyIterable = defclass(
class LazyIterable {
  constructor(step) {
    this[TYPE_TAG] = LAZY_ITERABLE_TYPE;
    this[IIterable] = true; // Closure compatibility
    this.step = step;
    this.realized = false;
    this.chunk = null; // array, or null when this is the terminal (empty) cell
    this._rest = null;
  }
  force() {
    if (!this.realized) {
      this.realized = true;
      const r = this.step();
      this.step = null;
      if (r !== null && r !== undefined) {
        this.chunk = r[0];
        this._rest = new LazyIterable(r[1]);
      }
    }
    return this;
  }
  [Symbol.iterator]() {
    let cell = this;
    let i = 0;
    return {
      next() {
        for (;;) {
          cell.force();
          const ch = cell.chunk;
          if (ch === null) return { value: undefined, done: true };
          if (i < ch.length) return { value: ch[i++], done: false };
          cell = cell._rest;
          i = 0;
        }
      },
      [Symbol.iterator]() {
        return this;
      },
    };
  }
  // Mirrors Array.prototype.indexOf so lazy seqs support (.indexOf coll x):
  // reference equality, returns -1 when absent. Unlike cljs.core, not by value.
  indexOf(x, fromIndex = 0) {
    let i = 0;
    for (const v of this) {
      if (i >= fromIndex && v === x) return i;
      i++;
    }
    return -1;
  }
}
);

// One-element chunks: an unchunked seq, realized one element at a time.
function unchunkedSteps(iter) {
  const step = () => {
    const r = iter.next();
    return r.done ? null : [[r.value], step];
  };
  return step;
}

// f is a zero-arg generator function; its result is an unchunked lazy seq.
export function lazy(f) {
  return new LazyIterable(unchunkedSteps(f()));
}

// gen(it) over a hoisted iterator: keeps the input head unpinned (streaming)
function lazyIter(coll, gen) {
  const it = es6_iterator(iterable(coll));
  return lazy(() => gen(it));
}

// A chunked view of any seqable for chunk-aware ops, preserving chunkedness:
// cells pass through, arrays slice into CHUNK_SIZE batches, others stay unchunked.
function chunkCells(coll) {
  if (coll instanceof LazyIterable) return coll;
  if (Array.isArray(coll)) {
    const step = (pos) => () => {
      if (pos >= coll.length) return null;
      const end = Math.min(pos + CHUNK_SIZE, coll.length);
      return [coll.slice(pos, end), step(end)];
    };
    return new LazyIterable(step(0));
  }
  return new LazyIterable(unchunkedSteps(es6_iterator(iterable(coll))));
}

// A cursor over a seq's chunks for realizers: returns a function yielding the
// next chunk array or null. Drive it with an inline loop (keeps an accumulator a
// plain local, unlike a callback). Callers keep their own array shortcut.
function chunkCursor(coll) {
  if (coll instanceof LazyIterable) {
    let cell = coll;
    return () => {
      if (cell === null) return null;
      cell.force();
      const ch = cell.chunk;
      cell = ch === null ? null : cell._rest;
      return ch;
    };
  }
  const it = es6_iterator(iterable(coll));
  return () => {
    const b = [];
    for (let i = 0; i < CHUNK_SIZE; i++) {
      const r = it.next();
      if (r.done) break;
      b.push(r.value);
    }
    return b.length === 0 ? null : b;
  };
}

// Build a lazy seq transforming each input chunk via xf(chunk, baseIndex) -> new
// chunk array, preserving chunkedness. Empty results are skipped (e.g. filter).
// baseIndex is the input element count before the chunk, for indexed ops.
function mapChunks(coll, xf) {
  const src = chunkCells(coll);
  const step = (cell, base) => () => {
    let c = cell;
    let b = base;
    for (;;) {
      c.force();
      const ch = c.chunk;
      if (ch === null) return null;
      const out = xf(ch, b);
      const rest = c._rest;
      b += ch.length;
      if (out.length !== 0) return [out, step(rest, b)];
      c = rest;
    }
  };
  return new LazyIterable(step(src, 0));
}

export const Cons = defclass(
class Cons {
  constructor(x, coll) {
    this[TYPE_TAG] = LAZY_ITERABLE_TYPE;
    this.x = x;
    this.coll = coll;
  }
  [Symbol.iterator]() {
    const x = this.x;
    let coll = this.coll;
    let started = false;
    let it = null;
    return {
      next() {
        if (!started) {
          started = true;
          return { value: x, done: false };
        }
        if (!it) {
          it = es6_iterator(iterable(coll));
          coll = null; // release the tail head so a single pass streams
        }
        return it.next();
      },
      [Symbol.iterator]() {
        return this;
      },
    };
  }
}
);

export function cons(x, coll) {
  // like CLJS cons, which seqs a non-ISeq tail
  if (!seqable_QMARK_(coll)) throw new TypeError(coll + ' is not ISeqable');
  return new Cons(x, coll);
}

export function map(f, ...colls) {
  f = __toFn(f);
  switch (colls.length) {
    case 0:
      return (rf) => {
        return (...args) => {
          switch (args.length) {
            case 0: {
              return rf();
            }
            case 1: {
              return rf(args[0]);
            }
            case 2: {
              return rf(args[0], f(args[1]));
            }
            default: {
              return rf(args[0], f(...args.slice(1)));
            }
          }
        };
      };
    case 1:
      return mapChunks(colls[0], (ch) => {
        const out = new Array(ch.length);
        for (let i = 0; i < ch.length; i++) out[i] = f(ch[i]);
        return out;
      });
    default: {
      const iters = colls.map((coll) => es6_iterator(iterable(coll)));
      return lazy(function* () {
        while (true) {
          const args = [];
          for (const i of iters) {
            const nextVal = i.next();
            if (nextVal.done) {
              return;
            }
            args.push(nextVal.value);
          }
          yield f(...args);
        }
      });
    }
  }
}

// 0/1 arities pass through to rf; step(rf) is the 2-arity reducer
function transducer(step) {
  return (rf) => {
    const s = step(rf);
    return (...args) =>
      args.length === 0 ? rf() : args.length === 1 ? rf(args[0]) : s(args[0], args[1]);
  };
}

function filter1(pred) {
  return transducer((rf) => (r, x) => (truth_(pred(x)) ? rf(r, x) : r));
}

export function filter(pred, coll) {
  if (arguments.length === 1) {
    return filter1(pred);
  }
  pred = __toFn(pred);
  return mapChunks(coll, (ch) => {
    const out = [];
    for (let i = 0; i < ch.length; i++) {
      const x = ch[i];
      if (truth_(pred(x))) out.push(x);
    }
    return out;
  });
}

export function filterv(pred, coll) {
  // filter is chunked; vec bulk-appends its chunks
  return pushAll([], filter(pred, coll));
}

export function random_sample(prob, coll) {
  if (arguments.length === 1) {
    return filter((_) => rand() < prob);
  }
  return filter((_) => rand() < prob, coll);
}

export function remove(pred, coll) {
  if (arguments.length === 1) {
    return filter1(complement(pred));
  }
  return filter(complement(pred), coll);
}

function map_indexed1(f) {
  return transducer((rf) => {
    let i = -1;
    return (r, x) => rf(r, f(++i, x));
  });
}

export function map_indexed(f, coll) {
  f = __toFn(f);
  if (arguments.length === 1) {
    return map_indexed1(f);
  }
  return mapChunks(coll, (ch, base) => {
    const out = new Array(ch.length);
    for (let i = 0; i < ch.length; i++) out[i] = f(base + i, ch[i]);
    return out;
  });
}

function keep_indexed2(f, coll) {
  f = __toFn(f);
  return mapChunks(coll, (ch, base) => {
    const out = [];
    for (let i = 0; i < ch.length; i++) {
      const v = f(base + i, ch[i]);
      if (truth_(v)) out.push(v);
    }
    return out;
  });
}

function keep_indexed1(f) {
  return transducer((rf) => {
    let ia = -1;
    return (r, x) => {
      const v = f(++ia, x);
      return v == null ? r : rf(r, v);
    };
  });
}

export function keep_indexed(f, coll) {
  if (arguments.length === 1) {
    return keep_indexed1(f);
  } else {
    return keep_indexed2(f, coll);
  }
}

export function str(...xs) {
  return xs.join('');
}

export function name(x) {
  if (typeof x === 'string') {
    // keywords/symbols are strings in squint; name is the part after the "/"
    // ns separator (consistent with `namespace`, which returns the part before)
    const i = x.indexOf('/');
    return i >= 1 ? x.slice(i + 1) : x;
  }
  throw new Error("Doesn't support name: " + typeof x);
}

export function not(expr) {
  return !truth_(expr);
}

export function nil_QMARK_(v) {
  return v == null;
}

export const PROTOCOL_SENTINEL = {};

// marker protocols so (satisfies? IAtom x) works, like CLJS. Marked in the
// constructor, not on the prototype, so no top-level mutation pins Atom
// into bundles that do not use it.
const IATOM_SYM = /* @__PURE__ */ Symbol.for('squint.core/IAtom');
const IDEREF_SYM = /* @__PURE__ */ Symbol.for('squint.core/IDeref');
const ISEQABLE_SYM = /* @__PURE__ */ Symbol.for('squint.core/ISeqable');
export const IAtom = { __sym: IATOM_SYM };
export const IDeref = { __sym: IDEREF_SYM };
// method slot for (-deref x), named like the defprotocol emission so
// (extend-type T IDeref (-deref [x] ...)) fills it
export const IDeref__deref = /* @__PURE__ */ Symbol.for('squint.core/-deref');
export function _deref(o) {
  if (o != null && o[IDeref__deref] !== undefined) return o[IDeref__deref](o);
  return nilImpl(_deref, 'IDeref.-deref', o)(o);
}
export const ISeqable = { __sym: ISEQABLE_SYM };
export const ISeqable__seq = /* @__PURE__ */ Symbol.for('squint.core/-seq');

// map-facing protocols. Each dispatches through its slot in the extension
// path (INSTANCE_TYPE) of the corresponding core fn, so plain objects and
// arrays never pay for them. Slot symbols are separate consts so a bundle
// using only e.g. conj pulls one symbol, not the whole protocol set.
export const ILookup = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ILookup') };
export const ILookup__lookup = /* @__PURE__ */ Symbol.for('squint.core/-lookup');
export const IAssociative = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IAssociative') };
export const IAssociative__assoc = /* @__PURE__ */ Symbol.for('squint.core/-assoc');
export const IAssociative__contains_key_QMARK_ = /* @__PURE__ */ Symbol.for('squint.core/-contains-key?');
export const IMap = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IMap') };
export const IMap__dissoc = /* @__PURE__ */ Symbol.for('squint.core/-dissoc');
export const ICounted = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ICounted') };
export const ICounted__count = /* @__PURE__ */ Symbol.for('squint.core/-count');
export const IKVReduce = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IKVReduce') };
export const IKVReduce__kv_reduce = /* @__PURE__ */ Symbol.for('squint.core/-kv-reduce');
export const ICollection = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ICollection') };
export const ICollection__conj = /* @__PURE__ */ Symbol.for('squint.core/-conj');
export const IEmptyableCollection = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IEmptyableCollection') };
export const IEmptyableCollection__empty = /* @__PURE__ */ Symbol.for('squint.core/-empty');
export const IEquiv = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IEquiv') };
export const IEquiv__equiv = /* @__PURE__ */ Symbol.for('squint.core/-equiv');
// set and transient protocols, same extension-path dispatch as the map-facing
// protocols above
export const ISet = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ISet') };
export const ISet__disjoin = /* @__PURE__ */ Symbol.for('squint.core/-disjoin');
export const IEditableCollection = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IEditableCollection') };
export const IEditableCollection__as_transient = /* @__PURE__ */ Symbol.for('squint.core/-as-transient');
export const ITransientCollection = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ITransientCollection') };
export const ITransientCollection__conj_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-conj!');
export const ITransientCollection__persistent_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-persistent!');
export const ITransientAssociative = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ITransientAssociative') };
export const ITransientAssociative__assoc_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-assoc!');
export const ITransientMap = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ITransientMap') };
export const ITransientMap__dissoc_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-dissoc!');
export const ITransientSet = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ITransientSet') };
export const ITransientSet__disjoin_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-disjoin!');
// metadata protocols, like CLJS: types implement the slots, plain values
// get instance-level impls installed by with-meta
export const IMeta = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IMeta') };
export const IMeta__meta = /* @__PURE__ */ Symbol.for('squint.core/-meta');
export const IWithMeta = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IWithMeta') };
export const IWithMeta__with_meta = /* @__PURE__ */ Symbol.for('squint.core/-with-meta');
// hashing (Murmur3, like CLJS). The contract: (= a b) implies (hash a) ===
// (hash b), where = is dequal. None of this is referenced by =, so bundles
// that only compare pay nothing.
// Ported from ClojureScript (cljs/core.cljs), Copyright (c) Rich Hickey and
// contributors, Eclipse Public License 1.0. MurmurHash3 by Austin Appleby
// (public domain).

// IHash: a custom type opts into value hashing
export const IHash = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IHash') };
export const IHash__hash = /* @__PURE__ */ Symbol.for('squint.core/-hash');

// the equality hashed collections key by: identical or -equiv, never a
// deep compare like =
export function equiv(x, y) {
  if (x === y) return true;
  if (x == null) return y == null;
  if (y == null) return false;
  // a primitive is only equal by identity, checked above; bail before the
  // slot checks below box it
  if (typeof x !== 'object' || typeof y !== 'object') return false;
  if (x[IEquiv__equiv] !== undefined) return !!x[IEquiv__equiv](x, y);
  if (y[IEquiv__equiv] !== undefined) return !!y[IEquiv__equiv](y, x);
  if (x instanceof Date && y instanceof Date) return x.getTime() === y.getTime();
  return false;
}

// identity uid for reference-keyed values, like CLJS goog/getUid
const UIDS = /* @__PURE__ */ new WeakMap();
let uidCounter = 0;

function uid(o) {
  let id = UIDS.get(o);
  if (id === undefined) {
    id = ++uidCounter;
    UIDS.set(o, id);
  }
  return id;
}

const imul = Math.imul;
const M3_C1 = 0xcc9e2d51 | 0;
const M3_C2 = 0x1b873593 | 0;

function rotl(x, n) {
  return (x << n) | (x >>> (32 - n));
}

function m3MixK1(k1) {
  return imul(rotl(imul(k1 | 0, M3_C1), 15), M3_C2);
}

function m3MixH1(h1, k1) {
  return (imul(rotl((h1 | 0) ^ (k1 | 0), 13), 5) + (0xe6546b64 | 0)) | 0;
}

function m3Fmix(h1, len) {
  h1 = (h1 ^ len) | 0;
  h1 = (h1 ^ (h1 >>> 16)) | 0;
  h1 = imul(h1, 0x85ebca6b | 0);
  h1 = (h1 ^ (h1 >>> 13)) | 0;
  h1 = imul(h1, 0xc2b2ae35 | 0);
  return (h1 ^ (h1 >>> 16)) | 0;
}

function m3HashInt(x) {
  return x === 0 ? 0 : m3Fmix(m3MixH1(0, m3MixK1(x)), 4);
}

let stringHashCache = /* @__PURE__ */ Object.create(null);
let stringHashCacheCount = 0;

function hashStringRaw(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (imul(31, h) + s.charCodeAt(i)) | 0;
  return h;
}

function hashString(s) {
  if (stringHashCacheCount > 8192) {
    stringHashCache = Object.create(null);
    stringHashCacheCount = 0;
  }
  let h = stringHashCache[s];
  if (typeof h !== 'number') {
    h = hashStringRaw(s);
    stringHashCache[s] = h;
    stringHashCacheCount++;
  }
  return h;
}

// one IIFE, not two consts: `new Int32Array(F64.buffer)` reads a property,
// which pins F64 into bundles even under a pure annotation
const HASH_BUF = /* @__PURE__ */ (() => {
  const f64 = new Float64Array(1);
  return { f64, i32: new Int32Array(f64.buffer) };
})();

function hashDouble(n) {
  HASH_BUF.f64[0] = n;
  return (HASH_BUF.i32[0] ^ HASH_BUF.i32[1]) | 0;
}

function mixCollectionHash(hashBasis, count) {
  return m3Fmix(m3MixH1(0, m3MixK1(hashBasis)), count);
}

export function hash_ordered_coll(coll) {
  let n = 0;
  let h = 1;
  for (const x of coll) {
    h = (imul(31, h) + hash(x)) | 0;
    n++;
  }
  return mixCollectionHash(h, n);
}

export function hash_unordered_coll(coll) {
  let n = 0;
  let h = 0;
  for (const x of coll) {
    h = (h + hash(x)) | 0;
    n++;
  }
  return mixCollectionHash(h, n);
}

// a map entry hashes as (hash-ordered-coll [k v])
function hashEntry(k, v) {
  return mixCollectionHash((imul(31, (imul(31, 1) + hash(k)) | 0) + hash(v)) | 0, 2);
}

function hashMapEntries(entries) {
  let n = 0;
  let h = 0;
  for (const [k, v] of entries) {
    h = (h + hashEntry(k, v)) | 0;
    n++;
  }
  return mixCollectionHash(h, n);
}

// (equiv a b) implies (hash a) === (hash b): plain mutable data hashes by
// uid, a type with -equiv but no -hash gets a structural entry hash
export function hash(o) {
  if (o == null) return 0;
  switch (typeof o) {
    case 'number':
      if (Number.isFinite(o)) {
        return Number.isSafeInteger(o) ? o % 2147483647 | 0 : hashDouble(o);
      }
      if (o === Infinity) return 2146435072;
      if (o === -Infinity) return -1048576;
      return 2146959360; // NaN
    case 'boolean':
      return o ? 1231 : 1237;
    case 'string':
      return m3HashInt(hashString(o));
    case 'bigint':
      return Number(o % 2147483647n) | 0;
    case 'function':
      return uid(o) | 0;
    case 'object':
      break;
    default:
      // JS symbols compare by identity; a constant hash is consistent
      return 0;
  }
  if (o[IHash__hash] !== undefined) return o[IHash__hash](o) | 0;
  if (o instanceof Date) return o.valueOf() | 0;
  if (o[IEquiv__equiv] !== undefined) return hashMapEntries(Object.entries(o));
  return uid(o) | 0;
}

// printing protocols, like CLJS: a type prints itself through
// (-pr-writer [obj writer opts]), writing strings via (-write writer s)
export const IWriter = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IWriter') };
export const IWriter__write = /* @__PURE__ */ Symbol.for('squint.core/-write');
export const IPrintWithWriter = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IPrintWithWriter') };
export const IPrintWithWriter__pr_writer = /* @__PURE__ */ Symbol.for('squint.core/-pr-writer');
export function _write(writer, s) {
  if (writer != null && writer[IWriter__write] !== undefined) return writer[IWriter__write](writer, s);
  return nilImpl(_write, 'IWriter.-write', writer)(writer, s);
}
export function write_all(writer, ...ss) {
  for (const s of ss) _write(writer, s);
}
// vector-facing protocols, dispatched like the map-facing set above
export const IStack = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IStack') };
export const IStack__peek = /* @__PURE__ */ Symbol.for('squint.core/-peek');
export const IStack__pop = /* @__PURE__ */ Symbol.for('squint.core/-pop');
export const IIndexed = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IIndexed') };
export const IIndexed__nth = /* @__PURE__ */ Symbol.for('squint.core/-nth');
export const ITransientVector = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ITransientVector') };
export const ITransientVector__pop_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-pop!');
// marker protocol: a non-array type that counts as a vector (vector?,
// sequential?, vec, subvec route through it)
export const IVector = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IVector') };
// a type converts itself in clj->js through this slot, like CLJS IEncodeJS;
// the impl receives (x, recur) with recur a cycle-safe clj->js
export const IEncodeJS = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IEncodeJS') };
export const IEncodeJS__clj__GT_js = /* @__PURE__ */ Symbol.for('squint.core/-clj->js');
// marker protocol set by defrecord
export const IRecord = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IRecord') };

export function record_QMARK_(x) {
  return x != null && x[IRecord.__sym] !== undefined;
}

// The protocol-method fns below share one shape but stay hand-written on
// purpose: a shared factory makes V8 share type feedback across all of them,
// turning every slot access megamorphic (measured 1.8 -> 11 ns per call).
export function _lookup(o, k, nf) {
  if (o != null && o[ILookup__lookup] !== undefined) return o[ILookup__lookup](o, k, nf);
  return nilImpl(_lookup, 'ILookup.-lookup', o)(o, k, nf);
}
export function _assoc(o, k, v) {
  if (o != null && o[IAssociative__assoc] !== undefined) return o[IAssociative__assoc](o, k, v);
  return nilImpl(_assoc, 'IAssociative.-assoc', o)(o, k, v);
}
export function _contains_key_QMARK_(o, k) {
  if (o != null && o[IAssociative__contains_key_QMARK_] !== undefined) return o[IAssociative__contains_key_QMARK_](o, k);
  return nilImpl(_contains_key_QMARK_, 'IAssociative.-contains-key?', o)(o, k);
}
export function _dissoc(o, k) {
  if (o != null && o[IMap__dissoc] !== undefined) return o[IMap__dissoc](o, k);
  return nilImpl(_dissoc, 'IMap.-dissoc', o)(o, k);
}
export function _count(o) {
  if (o != null && o[ICounted__count] !== undefined) return o[ICounted__count](o);
  return nilImpl(_count, 'ICounted.-count', o)(o);
}
export function _kv_reduce(o, f, init) {
  if (o != null && o[IKVReduce__kv_reduce] !== undefined) return o[IKVReduce__kv_reduce](o, f, init);
  return nilImpl(_kv_reduce, 'IKVReduce.-kv-reduce', o)(o, f, init);
}
export function _conj(o, x) {
  if (o != null && o[ICollection__conj] !== undefined) return o[ICollection__conj](o, x);
  return nilImpl(_conj, 'ICollection.-conj', o)(o, x);
}
export function _empty(o) {
  if (o != null && o[IEmptyableCollection__empty] !== undefined) return o[IEmptyableCollection__empty](o);
  return nilImpl(_empty, 'IEmptyableCollection.-empty', o)(o);
}
export function _equiv(o, other) {
  if (o != null && o[IEquiv__equiv] !== undefined) return o[IEquiv__equiv](o, other);
  return nilImpl(_equiv, 'IEquiv.-equiv', o)(o, other);
}
export function _seq(o) {
  if (o != null && o[ISeqable__seq] !== undefined) return o[ISeqable__seq](o);
  return nilImpl(_seq, 'ISeqable.-seq', o)(o);
}
export function _disjoin(o, x) {
  if (o != null && o[ISet__disjoin] !== undefined) return o[ISet__disjoin](o, x);
  return nilImpl(_disjoin, 'ISet.-disjoin', o)(o, x);
}
export function _as_transient(o) {
  if (o != null && o[IEditableCollection__as_transient] !== undefined) return o[IEditableCollection__as_transient](o);
  return nilImpl(_as_transient, 'IEditableCollection.-as-transient', o)(o);
}
export function _conj_BANG_(o, x) {
  if (o != null && o[ITransientCollection__conj_BANG_] !== undefined) return o[ITransientCollection__conj_BANG_](o, x);
  return nilImpl(_conj_BANG_, 'ITransientCollection.-conj!', o)(o, x);
}
export function _persistent_BANG_(o) {
  if (o != null && o[ITransientCollection__persistent_BANG_] !== undefined) return o[ITransientCollection__persistent_BANG_](o);
  return nilImpl(_persistent_BANG_, 'ITransientCollection.-persistent!', o)(o);
}
export function _assoc_BANG_(o, k, v) {
  if (o != null && o[ITransientAssociative__assoc_BANG_] !== undefined) return o[ITransientAssociative__assoc_BANG_](o, k, v);
  return nilImpl(_assoc_BANG_, 'ITransientAssociative.-assoc!', o)(o, k, v);
}
export function _dissoc_BANG_(o, k) {
  if (o != null && o[ITransientMap__dissoc_BANG_] !== undefined) return o[ITransientMap__dissoc_BANG_](o, k);
  return nilImpl(_dissoc_BANG_, 'ITransientMap.-dissoc!', o)(o, k);
}
export function _disjoin_BANG_(o, x) {
  if (o != null && o[ITransientSet__disjoin_BANG_] !== undefined) return o[ITransientSet__disjoin_BANG_](o, x);
  return nilImpl(_disjoin_BANG_, 'ITransientSet.-disjoin!', o)(o, x);
}

// an extend-type on nil stores the impl on the dispatch fn under null,
// the same convention the generated protocol dispatch fns use
function nilImpl(dispatchFn, protoMethod, o) {
  const f = dispatchFn[null];
  if (f === undefined) throw missing_protocol(protoMethod, o);
  return f;
}
export const IReset = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IReset') };
export const IReset__reset_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-reset!');
export function _reset_BANG_(o, v) {
  if (o != null && o[IReset__reset_BANG_] !== undefined) return o[IReset__reset_BANG_](o, v);
  return nilImpl(_reset_BANG_, 'IReset.-reset!', o)(o, v);
}
export const ISwap = { __sym: /* @__PURE__ */ Symbol.for('squint.core/ISwap') };
export const ISwap__swap_BANG_ = /* @__PURE__ */ Symbol.for('squint.core/-swap!');
export function _swap_BANG_(o, f, ...args) {
  if (o != null && o[ISwap__swap_BANG_] !== undefined) return o[ISwap__swap_BANG_](o, f, ...args);
  return nilImpl(_swap_BANG_, 'ISwap.-swap!', o)(o, f, ...args);
}
export const IWatchable = { __sym: /* @__PURE__ */ Symbol.for('squint.core/IWatchable') };
export const IWatchable__add_watch = /* @__PURE__ */ Symbol.for('squint.core/-add-watch');
export const IWatchable__remove_watch = /* @__PURE__ */ Symbol.for('squint.core/-remove-watch');
export const IWatchable__notify_watches = /* @__PURE__ */ Symbol.for('squint.core/-notify-watches');
export function _add_watch(o, k, f) {
  if (o != null && o[IWatchable__add_watch] !== undefined) return o[IWatchable__add_watch](o, k, f);
  return nilImpl(_add_watch, 'IWatchable.-add-watch', o)(o, k, f);
}
export function _remove_watch(o, k) {
  if (o != null && o[IWatchable__remove_watch] !== undefined) return o[IWatchable__remove_watch](o, k);
  return nilImpl(_remove_watch, 'IWatchable.-remove-watch', o)(o, k);
}
export function _notify_watches(o, oldv, newv) {
  if (o != null && o[IWatchable__notify_watches] !== undefined) return o[IWatchable__notify_watches](o, oldv, newv);
  return nilImpl(_notify_watches, 'IWatchable.-notify-watches', o)(o, oldv, newv);
}

// shared protocol impls: one fn per operation, a pointer per instance
const ATOM_DEREF = (self) => self.val;
const ATOM_RESET = (self, x) => {
  if (self._validator && !truth_(self._validator(x))) {
    throw new Error('Validator rejected reference state');
  }
  const old_val = self.val;
  self.val = x;
  if (self._hasWatches) {
    for (const [k, f] of Object.entries(self._watches)) f(k, self, old_val, x);
  }
  return x;
};
const ATOM_SWAP = function (self, f, a, b, xs) {
  switch (arguments.length) {
    case 2:
      return ATOM_RESET(self, f(self.val));
    case 3:
      return ATOM_RESET(self, f(self.val, a));
    case 4:
      return ATOM_RESET(self, f(self.val, a, b));
    default:
      return ATOM_RESET(self, f(self.val, a, b, ...xs));
  }
};
const ATOM_ADD_WATCH = (self, k, f) => {
  self._watches[k] = f;
  self._hasWatches = true;
};
const ATOM_REMOVE_WATCH = (self, k) => {
  delete self._watches[k];
};
const ATOM_NOTIFY = (self, oldv, newv) => {
  for (const [k, f] of Object.entries(self._watches)) f(k, self, oldv, newv);
};

export class Atom {
  constructor(init) {
    this.val = init;
    this._watches = {};
    this._hasWatches = false;
    this[IATOM_SYM] = true;
    this[IDEREF_SYM] = true;
    this[IDeref__deref] = ATOM_DEREF;
    this[IReset.__sym] = true;
    this[IReset__reset_BANG_] = ATOM_RESET;
    this[ISwap.__sym] = true;
    this[ISwap__swap_BANG_] = ATOM_SWAP;
    this[IWatchable.__sym] = true;
    this[IWatchable__add_watch] = ATOM_ADD_WATCH;
    this[IWatchable__remove_watch] = ATOM_REMOVE_WATCH;
    this[IWatchable__notify_watches] = ATOM_NOTIFY;
  }
}

export function atom(init, ...opts) {
  const a = new Atom(init);
  for (let i = 0; i < opts.length; i += 2) {
    // IMeta only, like CLJS Atom: keeps with-meta's copy machinery out of atom-only bundles
    if (opts[i] === 'meta') {
      const mv = opts[i + 1];
      a[IMeta__meta] = () => mv;
    }
    else if (opts[i] === 'validator') a._validator = opts[i + 1];
  }
  return a;
}

export function get_validator(ref) {
  return ref._validator ?? null;
}

export function set_validator_BANG_(ref, f) {
  if (f != null && !truth_(f(deref(ref)))) {
    throw new Error('Validator rejected reference state');
  }
  ref._validator = f;
  return null;
}

// the CLJS missing-protocol error, used by every protocol dispatch miss
export function missing_protocol(proto, obj) {
  let ty;
  if (obj === null) ty = 'null';
  else if (obj === undefined) ty = 'undefined';
  else if (Array.isArray(obj)) ty = 'array';
  else if (typeof obj === 'object' && obj.constructor && obj.constructor !== Object) {
    ty = obj.constructor.name;
  } else ty = typeof obj;
  return new Error(
    `No protocol method ${proto} defined for type ${ty}: ${obj ?? ''}`
  );
}

export function deref(ref) {
  if (ref?.[IDeref__deref] !== undefined) return ref[IDeref__deref](ref);
  return nilImpl(_deref, 'IDeref.-deref', ref)(ref);
}

export function reset_BANG_(atm, v) {
  if (atm?.[IReset__reset_BANG_] !== undefined) return atm[IReset__reset_BANG_](atm, v);
  return nilImpl(_reset_BANG_, 'IReset.-reset!', atm)(atm, v);
}

export function swap_BANG_(atm, f, ...args) {
  f = __toFn(f);
  if (atm?.[ISwap__swap_BANG_] !== undefined) {
    // the CLJS -swap! contract: up to two positional args, the rest packed
    switch (args.length) {
      case 0:
        return atm[ISwap__swap_BANG_](atm, f);
      case 1:
        return atm[ISwap__swap_BANG_](atm, f, args[0]);
      case 2:
        return atm[ISwap__swap_BANG_](atm, f, args[0], args[1]);
      default:
        return atm[ISwap__swap_BANG_](atm, f, args[0], args[1], args.slice(2));
    }
  }
  const v = f(deref(atm), ...args);
  reset_BANG_(atm, v);
  return v;
}

export function swap_vals_BANG_(atm, f, ...args) {
  const oldv = deref(atm);
  f = __toFn(f);
  const newv = f(oldv, ...args);
  reset_BANG_(atm, newv);
  return [oldv, newv];
}

export function reset_vals_BANG_(atm, newv) {
  const oldv = deref(atm);
  reset_BANG_(atm, newv);
  return [oldv, newv];
}

export function compare_and_set_BANG_(atm, oldv, newv) {
  if (deref(atm) === oldv) {
    reset_BANG_(atm, newv);
    return true;
  } else {
    return false;
  }
}

export function range(begin, end, step) {
  // range is a chunked source: it realizes CHUNK_SIZE values at a time
  let b = begin,
    e = end,
    s = step;
  if (end === undefined) {
    b = 0;
    e = begin;
  }
  const start = b || 0;
  s = step ?? 1;
  const ascending = s >= 0;
  const more = (i) => e === undefined || (ascending && i < e) || (!ascending && e < i);
  const mkStep = (from) => () => {
    if (!more(from)) return null;
    const out = [];
    let i = from;
    while (out.length < CHUNK_SIZE && more(i)) {
      out.push(i);
      i += s;
    }
    return [out, mkStep(i)];
  };
  return new LazyIterable(mkStep(start));
}

export function re_matches(re, s) {
  const matches = re.exec(s);
  if (matches && s === matches[0]) {
    if (matches.length === 1) {
      return matches[0];
    } else {
      return matches;
    }
  }
  return null;
}

export function re_find(re, s) {
  if (string_QMARK_(s)) {
    const matches = re.exec(s);
    if (matches != null) {
      if (matches.length === 1) return matches[0];
      else {
        return [...matches];
      }
    }
    return null;
  } else {
    throw new TypeError('re-find must match against a string.');
  }
}

export function re_pattern(s) {
  if (s instanceof RegExp) {
    return s;
  }

  // Allow all flags available in JavaScript
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#flags
  const flagMatches = s.match(/^\(\?([dgimsuvy]*)\)/);

  if (flagMatches) {
    return new RegExp(s.slice(flagMatches[0].length), flagMatches[1]);
  }

  return new RegExp(s);
}

export function subvec(arr, start, end) {
  // an IVector type slices through its own slots (bounds-checked nth + conj)
  if (arr != null && arr[IVector.__sym] !== undefined) {
    if (end === undefined) end = _count(arr);
    if (start == null || end == null) {
      throw new Error('subvec: start and end must not be nil');
    }
    start = start | 0;
    end = end | 0;
    if (start < 0 || end < start || end > _count(arr)) {
      throw new Error('subvec: index out of bounds');
    }
    let ret = arr[IEmptyableCollection__empty](arr);
    for (let i = start; i < end; i++) ret = ret[ICollection__conj](ret, arr[IIndexed__nth](arr, i));
    return ret;
  }
  if (!isVectorArray(arr)) {
    throw new Error('subvec: argument must be a vector');
  }
  if (end === undefined) end = arr.length;
  if (start == null || end == null) {
    throw new Error('subvec: start and end must not be nil');
  }
  // CLJS coerces the indices with (int x) before bounds-checking.
  start = start | 0;
  end = end | 0;
  if (start < 0 || end < start || end > arr.length) {
    throw new Error('subvec: index out of bounds');
  }
  return arr.slice(start, end);
}

export function vector(...args) {
  return args;
}

export const array = vector;

export function vector_QMARK_(x) {
  if (x == null) return false;
  return isVectorArray(x) || x[IVector.__sym] !== undefined;
}

export function mapv(...args) {
  if (args.length === 2) {
    const [_f, coll] = args;
    const f = __toFn(_f);
    const iter = iterable(coll);
    if (Array.isArray(iter)) {
      // explicit for loop was faster
      const ret = new Array(iter.length);
      for (var i = 0; i < iter.length; i++) {
        ret[i] = f(iter[i]);
      }
      return ret;
    } else {
      // non-array: map whole chunks straight into the result array
      const ret = [];
      const next = chunkCursor(iter);
      let ch;
      while ((ch = next()) !== null) {
        for (let i = 0; i < ch.length; i++) ret.push(f(ch[i]));
      }
      return ret;
    }
  }
  return [...map(...args)];
}

// Append every element of `from` to array `out`, bulk-appending whole chunks
// for a chunked seq. Returns out.
function pushAll(out, from) {
  if (from instanceof LazyIterable) {
    let cell = from;
    for (;;) {
      cell.force();
      const ch = cell.chunk;
      if (ch === null) return out;
      Array.prototype.push.apply(out, ch);
      cell = cell._rest;
    }
  }
  for (const x of iterable(from)) out.push(x);
  return out;
}

// Materialize a seq into a fresh, mutable array (bulk-copies chunked seqs).
function toArray(coll) {
  if (coll instanceof LazyIterable) return pushAll([], coll);
  return [...iterable(coll)];
}

export function vec(x) {
  if (isVectorArray(x)) return x;
  if (x != null && x[IVector.__sym] !== undefined) return x;
  return pushAll([], x);
}

export function set(coll) {
  return new Set(iterable(coll));
}

export function set_QMARK_(x) {
  if (x == null) return false;
  return typeConst(x) === SET_TYPE || x[ISet.__sym] !== undefined;
}

export function hash_set(...xs) {
  return new Set(xs);
}

export function sorted_QMARK_(x) {
  return x != null && x[SORTED_TAG] === true;
}

export function char_QMARK_(x) {
  return typeof x === 'string' && x.length === 1;
}

export function char$(x) {
  if (typeof x === 'string' && x.length === 1) return x;
  if (typeof x === 'number') return String.fromCharCode(x);
  throw new Error('Argument to char must be a character or number: ' + x);
}

export function apply(f, ...args) {
  f = __toFn(f);
  const xs = args.slice(0, args.length - 1);
  const last = args[args.length - 1];
  // variadic impl hint; see doc/ai/adr/0001
  const v = f.squint$lang$variadic;
  if (v) {
    // pull maxfa fixed args (bounded, lazy-safe); more left -> variadic, else fixed
    const maxfa = v.length - 1;
    const fixed = [];
    let i = 0, rest;
    for (; i < maxfa && i < xs.length; i++) fixed.push(xs[i]);
    if (i < maxfa) {
      let s = seq(last);
      for (; i < maxfa && s != null; i++) {
        fixed.push(first(s));
        s = next(s);
      }
      rest = s;
    } else {
      rest = i < xs.length ? concat1([xs.slice(i), last]) : last;
    }
    rest = rest == null ? null : seq(rest);
    if (rest == null) return f(...fixed);
    return v(...fixed, rest);
  }
  return f(...xs, ...iterable(last));
}

export function even_QMARK_(x) {
  if (!Number.isInteger(x)) {
    throw new Error(`Argument must be an integer: ${x}`);
  }
  return x % 2 == 0;
}

export function odd_QMARK_(x) {
  return !even_QMARK_(x);
}

export function complement(f) {
  f = __toFn(f);
  return (...args) => not(f(...args));
}

export function constantly(x) {
  return (..._) => x;
}

class List extends Array {
  constructor(...args) {
    super();
    this[TYPE_TAG] = LIST_TYPE;
    this.push(...args);
  }
}

export function list_QMARK_(x) {
  return x?.[TYPE_TAG] === LIST_TYPE;
}

export function list(...args) {
  return new List(...args);
}

export function list_STAR_(...args) {
  const last = args.pop();
  return new List(...args, ...iterable(last));
}

export function array_QMARK_(x) {
  return Array.isArray(x);
}

const CONCAT_DONE = {};

function concat1(colls) {
  // chunk-aware: pass each coll's chunks through, preserving chunkedness. Each
  // coll head is released once consumed so a single pass streams.
  const isArr = Array.isArray(colls);
  const arr = isArr ? colls.slice() : null;
  const collIter = isArr ? null : es6_iterator(iterable(colls));
  let idx = 0;
  const nextColl = () => {
    if (isArr) {
      if (idx >= arr.length) return CONCAT_DONE;
      const c = arr[idx];
      arr[idx] = null;
      idx++;
      return c;
    }
    const r = collIter.next();
    return r.done ? CONCAT_DONE : r.value;
  };
  // src is null, a chunked cell, or {a, pos} for an array (raw-array fast path:
  // no per-coll wrapper). Continuations carry immutable state, so a single pass
  // streams.
  const step = (src) => () => {
    for (;;) {
      if (src !== null) {
        if (src instanceof LazyIterable) {
          src.force();
          if (src.chunk !== null) return [src.chunk, step(src._rest)];
          src = null;
        } else {
          const a = src.a;
          const pos = src.pos;
          if (pos < a.length) {
            const end = Math.min(pos + CHUNK_SIZE, a.length);
            // slice (copy) so a cached chunk never aliases the caller's array
            return [a.slice(pos, end), step({ a, pos: end })];
          }
          src = null;
        }
      }
      const nc = nextColl();
      if (nc === CONCAT_DONE) return null;
      src =
        nc instanceof LazyIterable ? nc : Array.isArray(nc) ? { a: nc, pos: 0 } : chunkCells(nc);
    }
  };
  return new LazyIterable(step(null));
}

export const concat = withApply(
  (...colls) => concat1(colls),
  (colls) => concat1(colls), // lazy seqable argument
);

export function mapcat(f, ...colls) {
  if (colls.length === 0) {
    return comp(map(f), cat);
  }
  return concat1(map(f, ...colls));
}

export function identity(x) {
  return x;
}

export function interleave(...colls) {
  const iters = colls.map((coll) => es6_iterator(iterable(coll)));
  return lazy(function* () {
    while (true) {
      const res = [];
      for (const i of iters) {
        const nextVal = i.next();
        if (nextVal.done) {
          return;
        }
        res.push(nextVal.value);
      }
      yield* res;
    }
  });
}

function interpose1(sep) {
  return transducer((rf) => {
    let started = false;
    return (r, x) => {
      if (!started) {
        started = true;
        return rf(r, x);
      }
      const sepr = rf(r, sep);
      return reduced_QMARK_(sepr) ? sepr : rf(sepr, x);
    };
  });
}

export function interpose(sep, coll) {
  if (arguments.length === 1) return interpose1(sep);
  return drop(1, interleave(repeat(sep), coll));
}

export function select_keys(o, ks) {
  const type = typeConst(o);
  // always a map, like CLJS; a js/Map source keeps its rep
  const ret = type === MAP_TYPE ? new Map() : {};
  // iterable puns nil to no keys and a map to its entries, like CLJS seq
  for (const k of iterable(ks)) {
    const v = get(o, k);
    if (v !== undefined) {
      assoc_BANG_(ret, k, v);
    }
  }
  return ret;
}

export function unreduced(x) {
  if (reduced_QMARK_(x)) {
    return deref(x);
  } else {
    return x;
  }
}

function partition_all1(n) {
  return (rf) => {
    let a = [];
    return (...args) => {
      let result, v;
      switch (args.length) {
        case 0:
          return rf();
        case 1: {
          result = args[0];
          if (a.length !== 0) {
            v = [...a];
            a = [];
            result = unreduced(rf(result, v));
          }
          return rf(result);
        }
        case 2: {
          result = args[0];
          a.push(args[1]);
          if (n === a.length) {
            v = [...a];
            a = [];
            return rf(result, v);
          } else {
            return result;
          }
        }
      }
    };
  };
}

export function partition_all(n, ...args) {
  if (arguments.length === 1) {
    return partition_all1(n);
  }
  let step = n,
    coll = args[0];

  if (args.length === 2) {
    [step, coll] = args;
  }
  return partitionInternal(n, step, [], coll, true);
}

export function partition(n, ...args) {
  // pad stays undefined for arity 1 and 2: only arity 4 provides a pad, which
  // makes the final partial partition be emitted (padded when pad has elements).
  let step = n,
    pad,
    coll = args[0];

  if (args.length === 2) {
    [step, coll] = args;
  } else if (args.length > 2) {
    [step, pad, coll] = args;
  }
  return partitionInternal(n, step, pad, coll, false);
}

export const partitionv = partition; // partition already returns a lazy of arrays
export const partitionv_all = partition_all;

function partitionInternal(n, step, pad, coll, all) {
  return lazyIter(coll, function* (it) {
    let p = [];
    let i = 0;
    for (const x of it) {
      if (i < n) {
        p.push(x);
        if (p.length === n) {
          yield p;
          p = step < n ? p.slice(step) : [];
        }
      }
      i++;
      if (i === step) {
        i = 0;
      }
    }

    if (p.length > 0) {
      if (p.length === n || all) {
        yield p;
      } else if (pad !== undefined) {
        if (pad != null) {
          p.push(...[...iterable(pad)].slice(0, n - p.length));
        }
        yield p;
      }
    }
  });
}

function partition_by1(f) {
  return (rf) => {
    let a = [];
    const none = {};
    let pa = none;
    return (...args) => {
      const l = args.length;
      if (l === 0) {
        return rf();
      }
      if (l === 1) {
        let result = args[0];
        if (a.length !== 0) {
          const v = [...a];
          a = [];
          result = unreduced(rf(result, v));
        }
        return rf(result);
      }
      if (l === 2) {
        const result = args[0];
        const input = args[1];
        const pval = pa;
        const val = f(input);
        pa = val;
        if (pval === none || val === pval) {
          a.push(input);
          return result;
        } else {
          const v = [...a];
          a = [];
          const ret = rf(result, v);
          if (!reduced_QMARK_(ret)) {
            a.push(input);
          }
          return ret;
        }
      }
    };
  };
}

export function partition_by(f, coll) {
  f = __toFn(f);
  if (arguments.length === 1) {
    return partition_by1(f);
  }
  const iter = es6_iterator(coll);
  return lazy(function* () {
    const _fst = iter.next();
    if (_fst.done) {
      yield* null;
    }
    const fst = _fst.value;
    let fv = f(fst);
    let run = [fst];
    let rst = [];
    while (true) {
      const next = iter.next();
      if (next.done) {
        yield run;
        break;
      }
      const _v = next.value;
      const _fv = f(_v);
      if (fv == _fv) {
        run.push(_v);
      } else {
        yield run;
        rst.push(_v);
        run = rst;
        fv = _fv;
        rst = [];
      }
    }
  });
}

export function empty(coll) {
  const type = typeConst(coll);
  if (type != null) {
    if (type === INSTANCE_TYPE) {
      if (coll[IEmptyableCollection__empty] !== undefined) return coll[IEmptyableCollection__empty](coll);
      // a null-prototype object is a map rep; any other instance is not an
      // emptyable collection, like CLJS
      return coll.constructor === undefined ? copyMeta(coll, {}) : null;
    }
    return copyMeta(coll, emptyOfType(type));
  }
  // non-collections give nil, like CLJS
  return null;
}

export function merge(...args) {
  // with no truthy maps, return nil like CLJS `(when (some identity maps) ...)`.
  if (!args.some(truth_)) return null;
  // if the first arg is nil we coerce it into a map.
  const firstArg = args[0];
  let obj;
  if (firstArg === null || firstArg === undefined) {
    obj = {};
  } else if (typeConst(firstArg) === undefined) {
    // a non-collection passes through; conj! throws when maps follow, like CLJS
    obj = firstArg;
  } else if (firstArg[ICollection__conj] !== undefined) {
    // a -conj type is immutable: no defensive copy needed, and a record has
    // no empty to rebuild from
    obj = firstArg;
  } else {
    obj = into(empty(firstArg), firstArg);
  }
  // an ICollection target merges through -conj instead of mutation
  if (obj != null && obj[ICollection__conj] !== undefined) {
    for (let i = 1; i < args.length; i++) {
      obj = obj != null && obj[ICollection__conj] !== undefined
        ? obj[ICollection__conj](obj, args[i])
        : conj_BANG_(obj, args[i]);
    }
    return obj;
  }
  return conj_BANG_(obj, ...args.slice(1));
}

export function key(entry) {
  return entry[0];
}

export function val(entry) {
  return entry[1];
}

export function merge_with(f, ...maps) {
  f = __toFn(f);
  var hasMap = false;
  for (const m of maps) {
    if (m != null) {
      hasMap = true;
      break;
    }
  }
  if (hasMap) {
    const mergeEntry = (m, e) => {
      const k = key(e);
      const v = val(e);
      if (contains_QMARK_(m, k)) {
        return assoc(m, k, f(get(m, k), v));
      } else {
        return assoc(m, k, v);
      }
    };
    const merge2 = (m1, m2) => {
      return reduce(mergeEntry, m1 || {}, seq(m2));
    };
    return reduce(merge2, maps);
  } else {
    return null;
  }
}

export function system_time() {
  return performance.now();
}

export function into(...args) {
  let to, xform, from, c, rf;
  switch (args.length) {
    case 0:
      return [];
    case 1:
      return args[0];
    case 2:
      // vector target bulk-appends chunks (copy preserves metadata); lists conj
      // at the head, other targets need conj!
      to = args[0] ?? [];
      if (isVectorArray(to)) {
        return pushAll(copy(to), args[1]);
      }
      // a type with -conj is immutable: fold through the slot instead of
      // mutating a copy. Direct slot calls keep conj out of the bundle.
      if (to[ICollection__conj] !== undefined) {
        return reduce(
          (acc, x) =>
            acc != null && acc[ICollection__conj] !== undefined
              ? acc[ICollection__conj](acc, x)
              : conj_BANG_(acc, x),
          to,
          args[1]
        );
      }
      return reduce(conj_BANG_, copy(to), args[1]);
    case 3:
      to = args[0];
      xform = args[1];
      from = args[2];
      c = to != null && to[ICollection__conj] !== undefined ? to : copy(to);
      rf = (coll, v) => {
        if (v === undefined) {
          return coll;
        }
        return coll != null && coll[ICollection__conj] !== undefined
          ? coll[ICollection__conj](coll, v)
          : conj_BANG_(coll, v);
      };
      return transduce(xform, rf, c, from);
    default:
      throw TypeError(`Invalid arity call of into: ${args.length}`);
  }
}

export function identical_QMARK_(x, y) {
  return x === y;
}

export function repeat(...args) {
  if (args.length == 0 || args.length > 2) {
    throw new Error(`Invalid arity: ${args.length}`);
  }
  if (args.length == 1) {
    const x = args[0];
    return lazy(function* () {
      while (true) yield x;
    });
  }
  const [n, x] = args;
  return lazy(function* () {
    for (var i = 0; i < n; i++) yield x;
  });
}

export function ensure_reduced(x) {
  if (reduced_QMARK_(x)) {
    return x;
  } else {
    return reduced(x);
  }
}

function take1(n) {
  return transducer((rf) => {
    let na = n;
    return (r, x) => {
      const nn = --na + 1;
      if (nn > 0) r = rf(r, x);
      return nn > 1 ? r : ensure_reduced(r);
    };
  });
}

function assertNumber(n) {
  if (typeof n !== 'number') {
    throw new Error('Assert failed: (number? n)');
  }
  return n;
}

export function take(n, coll) {
  assertNumber(n);
  if (arguments.length === 1) {
    return take1(n);
  }
  return lazyIter(coll, function* (it) {
    let i = n - 1;
    for (const x of it) {
      if (i-- >= 0) {
        yield x;
      }
      if (i < 0) {
        return;
      }
    }
  });
}

export function take_last(n, coll) {
  assertNumber(n);
  if (n <= 0) {
    return null;
  }
  if (Array.isArray(coll)) {
    return seq(coll.slice(-n));
  } else {
    const lastN = new Array(n);
    let i = 0;
    for (const x of iterable(coll)) {
      lastN[i % n] = x;
      i++;
    }
    // an empty or nil coll has no last n elements; return nil like CLJS.
    if (i === 0) return null;
    if (i % n !== 0 && i >= n) {
      return lastN.slice(i % n).concat(lastN.slice(0, i % n));
    } else {
      lastN.length = Math.min(i, n);
      return lastN;
    }
  }
}

function take_while1(pred) {
  return transducer((rf) => (r, x) => (truth_(pred(x)) ? rf(r, x) : reduced(r)));
}

export function take_while(pred, coll) {
  pred = __toFn(pred);
  if (arguments.length === 1) {
    return take_while1(pred);
  }
  return lazyIter(coll, function* (it) {
    for (const o of it) {
      if (truth_(pred(o))) yield o;
      else return;
    }
  });
}

function take_nth1(n) {
  return transducer((rf) => {
    let ia = -1;
    return (r, x) => (rem(++ia, n) === 0 ? rf(r, x) : r);
  });
}

export function take_nth(n, coll) {
  assertNumber(n);
  if (arguments.length === 1) return take_nth1(n);
  if (n <= 0) {
    return repeat(first(coll));
  }

  return lazyIter(coll, function* (it) {
    let i = 0;
    for (const x of it) {
      if (i % n === 0) {
        yield x;
      }
      i++;
    }
  });
}

export function partial(f, ...xs) {
  f = __toFn(f);
  return function (...args) {
    return f(...xs, ...args);
  };
}

export function cycle(coll) {
  // seq the coll eagerly: nil or empty cycles to an empty seq and a
  // non-iterable throws, like the seq call in CLJS cycle. The first lap
  // is cached and replayed, like the retained seq in CLJS Cycle, so a
  // one-shot iterator source cycles instead of ending after one lap.
  const it = iterable(coll);
  return lazy(function* () {
    const cache = [];
    for (const x of it) {
      cache.push(x);
      yield x;
    }
    while (cache.length) yield* cache;
  });
}

function drop1(n) {
  return transducer((rf) => {
    let na = n;
    return (r, x) => (na-- > 0 ? r : rf(r, x));
  });
}

export function drop(n, xs) {
  assertNumber(n);
  if (arguments.length === 1) return drop1(n);
  return lazyIter(xs, function* (iter) {
    for (let x = 0; x < n; x++) {
      iter.next();
    }
    yield* iter;
  });
}

function drop_while1(pred) {
  return transducer((rf) => {
    let da = true;
    return (r, x) => {
      if (da && truth_(pred(x))) return r;
      da = false;
      return rf(r, x);
    };
  });
}

export function drop_while(pred, xs) {
  pred = __toFn(pred);
  if (arguments.length === 1) return drop_while1(pred);
  return lazyIter(xs, function* (iter) {
    while (true) {
      const nextItem = iter.next();
      if (nextItem.done) {
        break;
      }
      const value = nextItem.value;
      if (!truth_(pred(value))) {
        yield value;
        break;
      }
    }
    yield* iter;
  });
}

function distinct1() {
  return transducer((rf) => {
    const seen = new Set();
    return (r, x) => {
      if (seen.has(x)) return r;
      seen.add(x);
      return rf(r, x);
    };
  });
}

export function distinct(coll) {
  if (arguments.length === 0) return distinct1();
  return lazyIter(coll, function* (it) {
    const seen = new Set();
    for (const x of it) {
      if (!seen.has(x)) yield x;
      seen.add(x);
    }
    return;
  });
}

const DEDUPE_NONE = {};

function dedupe1() {
  return transducer((rf) => {
    let prev = DEDUPE_NONE;
    return (r, x) => {
      const skip = prev !== DEDUPE_NONE && truth_(_EQ_(prev, x));
      prev = x;
      return skip ? r : rf(r, x);
    };
  });
}

export function dedupe(coll) {
  if (arguments.length === 0) return dedupe1();
  return lazyIter(coll, function* (it) {
    let prev = DEDUPE_NONE;
    for (const x of it) {
      if (prev === DEDUPE_NONE || !truth_(_EQ_(prev, x))) yield x;
      prev = x;
    }
    return;
  });
}

export function update(coll, k, f, ...args) {
  f = __toFn(f);
  return assoc(coll, k, f(get(coll, k), ...args));
}

export function get_in(coll, path, orElse) {
  let entry = coll;
  // iterable puns a nil path to an empty path, like CLJS
  for (const item of iterable(path)) {
    entry = get(entry, item);
  }
  if (entry === undefined) return orElse;
  return entry;
}

export function update_in(coll, path, f, ...args) {
  f = __toFn(f);
  return assoc_in(coll, path, f(get_in(coll, path), ...args));
}

export function fnil(f, ...defaults) {
  f = __toFn(f);
  const n = defaults.length;
  return function (...args) {
    for (let i = 0; i < n; i++) {
      if (args[i] == null) args[i] = defaults[i];
    }
    return f(...args);
  };
}

export function every_QMARK_(pred, coll) {
  pred = __toFn(pred);
  if (Array.isArray(coll)) {
    for (let i = 0; i < coll.length; i++) if (!pred(coll[i])) return false;
    return true;
  }
  const next = chunkCursor(coll);
  let ch;
  while ((ch = next()) !== null) {
    for (let i = 0; i < ch.length; i++) if (!pred(ch[i])) return false;
  }
  return true;
}

export function not_every_QMARK_(pred, coll) {
  return !every_QMARK_(pred, coll);
}

function keep1(pred) {
  return transducer((rf) => (r, x) => {
    const v = pred(x);
    return v == null ? r : rf(r, v);
  });
}

export function keep(pred, coll) {
  pred = __toFn(pred);
  if (arguments.length === 1) return keep1(pred);
  return mapChunks(coll, (ch) => {
    const out = [];
    for (let i = 0; i < ch.length; i++) {
      const res = pred(ch[i]);
      if (truth_(res)) out.push(res);
    }
    return out;
  });
}

export function reverse(coll) {
  return toArray(coll).reverse();
}

export function reversible_QMARK_(x) {
  return isVectorArray(x) || (x != null && x[SORTED_TAG] === true);
}

export function rseq(x) {
  // vectors and sorted maps/sets are reversible, like CLJS
  if (isVectorArray(x)) {
    return x.length === 0 ? null : [...x].reverse();
  }
  if (x != null && x[SORTED_TAG] === true) {
    const xs = [...x].reverse();
    return xs.length === 0 ? null : xs;
  }
  throw new Error('rseq not supported on: ' + typeof x);
}

export function sort(f, coll) {
  if (arguments.length === 1) {
    coll = f;
    f = undefined;
  }
  f = __toFn(f);
  // need a copy since .sort works in place and .toSorted isn't on Node < 20
  const clone = toArray(coll);
  // result is guaranteed to be stable since ES2019, like CLJS
  return clone.sort(f || compare);
}

function fnToComparator(f) {
  if (f === compare) {
    return f;
  }
  return (x, y) => {
    const r = f(x, y);
    if (number_QMARK_(r)) {
      return r;
    }
    if (r) {
      return -1;
    }
    if (f(y, x)) {
      return 1;
    }
    return 0;
  };
}

export function sort_by(keyfn, comp, coll) {
  if (arguments.length === 2) {
    coll = comp;
    comp = compare;
  }
  keyfn = __toFn(keyfn);
  comp = __toFn(comp);
  return sort((x, y) => {
    const f = fnToComparator(comp);
    const kx = keyfn(x);
    const ky = keyfn(y);
    return f(kx, ky);
  }, coll);
}

export function shuffle(coll) {
  const result = toArray(coll);
  let remaining = result.length;
  while (remaining) {
    const i = Math.floor(Math.random() * remaining--);
    const tmp = result[remaining];
    result[remaining] = result[i];
    result[i] = tmp;
  }

  return result;
}

export function some(pred, coll) {
  pred = __toFn(pred);
  if (Array.isArray(coll)) {
    for (let i = 0; i < coll.length; i++) {
      const res = pred(coll[i]);
      if (truth_(res)) return res;
    }
    return undefined;
  }
  const next = chunkCursor(coll);
  let ch;
  while ((ch = next()) !== null) {
    for (let i = 0; i < ch.length; i++) {
      const res = pred(ch[i]);
      if (truth_(res)) return res;
    }
  }
  return undefined;
}

export function not_any_QMARK_(pred, coll) {
  pred = __toFn(pred);
  return !some(pred, coll);
}

export function replace(smap, coll) {
  const mapf = Array.isArray(coll) ? mapv : map;
  return mapf((x) => {
    const repl = smap[x];
    if (repl !== undefined) {
      return repl;
    } else {
      return x;
    }
  }, coll);
}

export function empty_QMARK_(coll) {
  return seq(coll) ? false : true;
}

export function rand(n) {
  if (undefined === n) {
    n = 1;
  }
  return Math.random() * n;
}

export function rand_int(n) {
  return Math.floor(Math.random() * n);
}

export function rand_nth(coll) {
  const ri = rand_int(count(coll));
  return nth(coll, ri);
}

function _repeatedly(f) {
  return lazy(function* () {
    while (true) yield f();
  });
}

export function repeatedly(n, f) {
  if (arguments.length === 1) {
    f = n;
    n = undefined;
  }
  const res = _repeatedly(f);
  if (n !== undefined) {
    if (typeof n !== 'number') {
      throw new Error('repeatedly: count must be a number, got: ' + str(n));
    }
    return take(n, res);
  } else {
    return res;
  }
}

export function update_BANG_(m, k, f, ...args) {
  f = __toFn(f);
  const v = get(m, k);
  return assoc_BANG_(m, k, f(v, ...args));
}

export function group_by(f, coll) {
  f = __toFn(f);
  const res = {};
  for (const o of iterable(coll)) {
    const key = f(o);
    update_BANG_(res, key, fnil(conj_BANG_, []), o);
  }
  return res;
}

export function frequencies(coll) {
  const res = {};
  const uf = fnil(inc, 0);
  for (const o of iterable(coll)) {
    update_BANG_(res, o, uf);
  }
  return res;
}

// The lazy-seq macro emits `new LazySeq(() => body)`: a LazyIterable whose step
// evaluates the body thunk on first force and reads it unchunked.
export class LazySeq extends LazyIterable {
  // Core fns build the base LazyIterable, not this subclass, so widen
  // instanceof to any lazy cell for `(instance? LazySeq x)` parity with CLJS.
  static [Symbol.hasInstance](x) {
    return x instanceof LazyIterable;
  }
  constructor(f) {
    super(() => unchunkedSteps(es6_iterator(iterable(f())))());
  }
}

export function butlast(coll) {
  const x = toArray(coll);
  x.pop();
  return x.length > 0 ? x : null;
}

export function drop_last(...args) {
  const [n, coll] = args.length > 1 ? args : [1, args[0]];
  return map((x, _) => x, coll, drop(n, coll));
}

export function split_at(n, coll) {
  return [take(n, coll), drop(n, coll)];
}

export function split_with(pred, coll) {
  return [take_while(pred, coll), drop_while(pred, coll)];
}

export function count(coll) {
  if (!coll) return 0;
  const len = coll.length || coll.size;
  if (typeof len === 'number') {
    return len;
  }
  if (coll[ICounted__count] !== undefined) return coll[ICounted__count](coll);
  // sum chunk lengths instead of counting elements one by one
  const next = chunkCursor(coll);
  let ret = 0;
  let ch;
  while ((ch = next()) !== null) ret += ch.length;
  return ret;
}

export function true_QMARK_(x) {
  return x === true;
}

export function false_QMARK_(x) {
  return x === false;
}

export function some_QMARK_(x) {
  return !(x === null || x === undefined);
}

export function boolean$(x) {
  return truth_(x);
}

export function parse_boolean(s) {
  if (typeof s !== 'string') {
    throw new Error('Argument must be a string');
  }
  if (s === 'true') return true;
  if (s === 'false') return false;
  return null;
}

export function zero_QMARK_(x) {
  return x === 0;
}

export function neg_QMARK_(x) {
  return x < 0;
}

export function pos_QMARK_(x) {
  return x > 0;
}

export function js_obj(...args) {
  let ctr = 0;
  const ret = {};
  for (;;) {
    if (ctr >= args.length) {
      break;
    }
    ret[args[ctr]] = args[ctr + 1];
    ctr = ctr + 2;
  }
  return ret;
}

export function alength(arr) {
  return arr.length;
}

export function aset(arr, idx, val, ...more) {
  if (more.length == 0) {
    arr[idx] = val;
    return val;
  } else {
    const path = [idx, val, ...more];
    const _val = path[path.length - 1];
    let innerArray = arr;
    let _idx = 0;
    const _pathLen = path.length - 2;
    for (; _idx < _pathLen; _idx++) {
      innerArray = innerArray[path[_idx]];
    }
    innerArray[path[_idx]] = _val;
    return val;
  }
}

export function dorun(x) {
  // only a lazy seq needs forcing; realized colls are walked natively
  if (x instanceof LazyIterable) {
    const next = chunkCursor(x);
    while (next() !== null);
    return null;
  }
  for (const _ of iterable(x)) {
    // nothing here, just consume for side effects
  }
  return null;
}

export function doall(x) {
  // realize a lazy seq and return it, like CLJS; anything else is
  // already concrete
  if (x != null && x[TYPE_TAG] === LAZY_ITERABLE_TYPE) for (const _ of x);
  return x;
}

export function aclone(arr) {
  const cloned = [...arr];
  return cloned;
}

function typed_array(sizeOrSeq, initValOrSeq) {
  if (initValOrSeq !== undefined) {
    const a = new Array(sizeOrSeq);
    if (typeof initValOrSeq === 'number') return a.fill(initValOrSeq);
    let i = 0;
    for (const x of iterable(initValOrSeq)) {
      if (i >= sizeOrSeq) break;
      a[i++] = x;
    }
    return a;
  }
  if (typeof sizeOrSeq === 'number') return new Array(sizeOrSeq).fill(null);
  return [...iterable(sizeOrSeq)];
}

// like CLJS: provided for compatibility, all make a plain array
export function int_array(sizeOrSeq, initValOrSeq) {
  return typed_array(sizeOrSeq, initValOrSeq);
}
export function long_array(sizeOrSeq, initValOrSeq) {
  return typed_array(sizeOrSeq, initValOrSeq);
}
export function float_array(sizeOrSeq, initValOrSeq) {
  return typed_array(sizeOrSeq, initValOrSeq);
}
export function double_array(sizeOrSeq, initValOrSeq) {
  return typed_array(sizeOrSeq, initValOrSeq);
}
export function object_array(sizeOrSeq, initValOrSeq) {
  return typed_array(sizeOrSeq, initValOrSeq);
}

export function add_watch(ref, key, fn) {
  if (ref?.[IWatchable__add_watch] !== undefined) {
    ref[IWatchable__add_watch](ref, key, fn);
    return ref;
  }
  nilImpl(_add_watch, 'IWatchable.-add-watch', ref)(ref, key, fn);
  return ref;
}

export function remove_watch(ref, key) {
  if (ref?.[IWatchable__remove_watch] !== undefined) {
    ref[IWatchable__remove_watch](ref, key);
    return ref;
  }
  nilImpl(_remove_watch, 'IWatchable.-remove-watch', ref)(ref, key);
  return ref;
}

export function reduce_kv(f, init, m) {
  if (!m) {
    return init;
  }
  if (m[IKVReduce__kv_reduce] !== undefined) return m[IKVReduce__kv_reduce](m, f, init);
  var ret = init;
  for (const o of iterable(m)) {
    ret = f(ret, o[0], o[1]);
  }
  return ret;
}

// CLJS reduce of (cond (NaN? x) x (NaN? y) y (> x y) x :else y): returns one of
// the values, so nil acts like zero, and a NaN propagates. NaN? is js/isNaN.
export function max(x, ...more) {
  let m = x;
  for (const y of more) m = isNaN(m) ? m : isNaN(y) ? y : m > y ? m : y;
  return m;
}

export function min(x, ...more) {
  let m = x;
  for (const y of more) m = isNaN(m) ? m : isNaN(y) ? y : m < y ? m : y;
  return m;
}

export function associative_QMARK_(x) {
  switch (typeConst(x)) {
    case MAP_TYPE:
    case ARRAY_TYPE:
    case OBJECT_TYPE:
      return true;
    case INSTANCE_TYPE:
      return x[IAssociative__assoc] !== undefined;
    default:
      return false;
  }
}

export function map_QMARK_(coll) {
  if (coll == null) return false;
  if (isObj(coll)) return true;
  if (coll instanceof Map) return true;
  if (coll[TYPE_TAG] === MAP_TYPE) return true;
  if (coll[IMap.__sym] !== undefined) return true;
  return false;
}

export function every_pred(...preds) {
  return (...args) => {
    for (const p of preds) {
      for (const a of args) {
        const res = p(a);
        if (!res) {
          return false;
        }
      }
    }
    return true;
  };
}

export function some_fn(...fns) {
  return (...args) => {
    let res;
    for (const f of fns) {
      for (const a of args) {
        res = f(a);
        // truth_, not JS truthiness: 0 and "" are truthy in CLJS
        if (truth_(res)) return res;
      }
    }
    return res;
  };
}

export function into_array(type, aseq) {
  const theSeq = aseq || type;
  return vec(theSeq);
}

export function iterate(f, x) {
  var current = x;
  return lazy(function* () {
    while (true) {
      yield current;
      current = f(current);
    }
  });
}

export function juxt(...fs) {
  fs = fs.map(__toFn);
  return (...args) => {
    const ret = [];
    for (const f of fs) {
      ret.push(f(...args));
    }
    return ret;
  };
}

export function next(x) {
  if (Array.isArray(x)) {
    const ret = x.slice(1);
    if (ret.length > 0) {
      return ret;
    } else {
      return null;
    }
  } else {
    return seq(rest(x));
  }
}

export function nnext(x) {
  return next(next(x));
}

export function nthnext(coll, n) {
  let xs = seq(coll);
  while (xs != null && n > 0) {
    xs = next(xs);
    n = n - 1;
  }
  return xs;
}

export function nthrest(coll, n) {
  let xs = coll;
  while (n > 0 && seq(xs) != null) {
    xs = rest(xs);
    n = n - 1;
  }
  return xs;
}

export function compare(x, y) {
  if (x === y) {
    return 0;
  } else {
    if (x == null) {
      return -1;
    }
    if (y == null) {
      return 1;
    }
    const tx = typeof x;
    const ty = typeof y;
    if ((tx === 'number' && ty === 'number') || (tx === 'string' && ty === 'string') ||
        (tx === 'boolean' && ty === 'boolean')) {
      if (x === y) {
        return 0;
      }
      if (x < y) {
        return -1;
      }
      return 1;
    } else if (Array.isArray(x) && Array.isArray(y)) {
      // Implemented like `APersistentVector.compareTo`.
      if (x.length < y.length) {
        return -1;
      } else if (x.length > y.length) {
        return 1;
      } else {
        for (let i = 0; i < x.length; i++) {
          const c = compare(x[i], y[i]);
          if (c != 0) {
            return c;
          }
        }
        return 0;
      }
    } else {
      throw new Error(`comparing ${tx} to ${ty}`);
    }
  }
}

export function to_array(aseq) {
  return into_array(aseq);
}

export function truth_(x) {
  return x != null && x !== false;
}

export const t = truth_; // backward compat, remove in 2025

export function subs(s, start, end) {
  return s.substring(start, end);
}

export function fn_QMARK_(x) {
  return 'function' === typeof x;
}

export function ifn_QMARK_(x) {
  return fn_QMARK_(x);
}

export function any_QMARK_(_x) {
  return true;
}

export function distinct_QMARK_(x, ...more) {
  if (more.length === 0) return true;
  const seen = [x];
  for (const y of more) {
    for (const s of seen) {
      if (truth_(_EQ_(s, y))) return false;
    }
    seen.push(y);
  }
  return true;
}

export function re_seq(re, s) {
  return lazy(function* () {
    while (true) {
      const matches = re.exec(s);
      if (matches) {
        const match_str = matches[0];
        const match_vals = matches.length === 1 ? match_str : vec(matches);
        yield match_vals;
        const post_idx = matches.index + max(1, match_str.length);
        if (post_idx > s.length) break;
        s = subs(s, post_idx);
      } else break;
    }
  });
}

export function NaN_QMARK_(x) {
  // coercing, like CLJS js/isNaN: (NaN? "foo") is true
  return isNaN(x);
}

export function number_QMARK_(x) {
  return typeof x == 'number';
}

export function keys(obj) {
  if (obj == null) return null;
  const t = typeConst(obj);
  switch (t) {
    case INSTANCE_TYPE:
      // Object.keys on an instance would leak internals: go through -kv-reduce
      if (obj[IKVReduce__kv_reduce] !== undefined) {
        const ks = obj[IKVReduce__kv_reduce](obj, (acc, k, _) => (acc.push(k), acc), []);
        if (ks.length) return ks;
        return;
      }
    // fall through
    case OBJECT_TYPE: {
      const ks = Object.keys(obj);
      if (ks.length) return ks;
      return;
    }
    case MAP_TYPE:
      if (obj.size) return Array.from(obj.keys());
      return;
  }
  // not a map: nil when empty, like seq in CLJS, else throw
  for (const _ of iterable(obj)) {
    throw new TypeError(obj + ' is not a map');
  }
  return null;
}

export function js_keys(obj) {
  return keys(obj) ?? [];
}

export function vals(obj) {
  if (obj == null) return null;
  const t = typeConst(obj);
  switch (t) {
    case INSTANCE_TYPE:
      if (obj[IKVReduce__kv_reduce] !== undefined) {
        const vs = obj[IKVReduce__kv_reduce](obj, (acc, _, v) => (acc.push(v), acc), []);
        if (vs.length) return vs;
        return;
      }
    // fall through
    case OBJECT_TYPE: {
      const vs = Object.values(obj);
      if (vs.length) return vs;
      return;
    }
    case MAP_TYPE:
      if (obj.size) return Array.from(obj.values());
      return;
  }
  // not a map: nil when empty, like seq in CLJS, else throw
  for (const _ of iterable(obj)) {
    throw new TypeError(obj + ' is not a map');
  }
  return null;
}

export function string_QMARK_(s) {
  return typeof s === 'string';
}

export function unchecked_int(x) {
  return Math.trunc(x);
}

export function unchecked_inc_int(x) {
  return x + 1;
}

export function unchecked_dec_int(x) {
  return x - 1;
}

export function unchecked_add_int(x, y) {
  return x + y;
}

// keywords/symbols are strings in squint; namespace is the part before "/"
export function namespace(x) {
  // keywords/symbols are strings in squint; namespace is the part before the
  // "/" ns separator (consistent with `name`, which returns the part after).
  // i >= 1 so a leading "/" yields a nil namespace rather than an empty one.
  const i = x.indexOf('/');
  return i >= 1 ? x.slice(0, i) : null;
}

// squint has no keyword type; keywords are strings, so keyword/keyword? are
// string-based. (keyword name) or (keyword ns name).
export function keyword(arg1, arg2) {
  if (arg2 !== undefined) {
    return (arg1 != null ? arg1 + '/' : '') + arg2;
  }
  return arg1;
}

export function symbol(arg1, arg2) {
  if (arg2 !== undefined) {
    return (arg1 != null ? arg1 + '/' : '') + arg2;
  }
  return arg1;
}

export function keyword_QMARK_(x) {
  return typeof x === 'string';
}

// squint has no symbol type either; symbols are strings
export function symbol_QMARK_(x) {
  return typeof x === 'string';
}

// squint has no first-class Var objects (#'x emits the value), so var? is false
export function var_QMARK_(_x) {
  return false;
}

export function simple_keyword_QMARK_(x) {
  return typeof x === 'string' && !x.includes('/');
}

export function qualified_keyword_QMARK_(x) {
  return typeof x === 'string' && x.includes('/');
}

export function ident_QMARK_(x) {
  return typeof x === 'string';
}

export function simple_ident_QMARK_(x) {
  return typeof x === 'string' && namespace(x) == null;
}

export function qualified_ident_QMARK_(x) {
  return typeof x === 'string' && namespace(x) != null;
}

export function simple_symbol_QMARK_(x) {
  return symbol_QMARK_(x) && namespace(x) == null;
}

export function qualified_symbol_QMARK_(x) {
  return symbol_QMARK_(x) && namespace(x) != null;
}

export function coll_QMARK_(coll) {
  return typeConst(coll) != undefined;
}

export function regexp_QMARK_(coll) {
  return coll instanceof RegExp;
}

class ExceptionInfo extends Error {
  constructor(message, data, cause) {
    super(message);
    this._data = data;
    this._cause = cause;
  }
}

export function ex_data(e) {
  if (e instanceof ExceptionInfo) return e._data;
  else return null;
}

export function ex_message(e) {
  if (e instanceof Error) return e.message;
  else return null;
}

export function ex_cause(e) {
  if (e instanceof ExceptionInfo) return e._cause;
  else return null;
}

export function ex_info(message, data, cause) {
  return new ExceptionInfo(message, data, cause);
}

export function int_QMARK_(x) {
  return Number.isInteger(x);
}

export function double_QMARK_(x) {
  return typeof x === 'number';
}

// like CLJS: every number is a float
export function float_QMARK_(x) {
  return double_QMARK_(x);
}

export const integer_QMARK_ = int_QMARK_;

export function pos_int_QMARK_(x) {
  return int_QMARK_(x) && x > 0;
}

export function nat_int_QMARK_(x) {
  return int_QMARK_(x) && x > -1;
}

export function neg_int_QMARK_(x) {
  return int_QMARK_(x) && x < 0;
}

export function meta(x) {
  if (x != null && x[IMeta__meta] !== undefined) {
    return x[IMeta__meta](x);
  }
  return null;
}

export function with_meta(x, m) {
  if (x != null && x[IWithMeta__with_meta] !== undefined) {
    return x[IWithMeta__with_meta](x, m);
  }
  return with_meta_copy(x, m);
}

// instance-level impls for a plain value; the meta lives in the closure
function installMeta(x, m) {
  x[IMeta__meta] = () => m;
  x[IWithMeta__with_meta] = with_meta_copy;
  return x;
}

function with_meta_copy(x, m) {
  // For functions, wrap in a new callable that forwards to the original
  // so fn? stays true and the original isn't mutated. copy() can't handle
  // functions - a {...x} spread loses the call signature.
  if (typeof x === 'function') {
    const wrapped = function (...args) { return x.apply(this, args); };
    return installMeta(wrapped, m);
  }
  // A lazy seq or cons is not copied element-wise: clone the head so the new
  // value carries its own metadata without forcing realization.
  if (x?.[TYPE_TAG] === LAZY_ITERABLE_TYPE) {
    const ret = Object.assign(Object.create(Object.getPrototypeOf(x)), x);
    return installMeta(ret, m);
  }
  return installMeta(copy(x), m);
}

export function vary_meta(x, f, ...args) {
  return with_meta(x, f(meta(x), ...args));
}

export function boolean_QMARK_(x) {
  return x === true || x === false;
}

export function counted_QMARK_(x) {
  const tc = typeConst(x);
  switch (tc) {
    case ARRAY_TYPE:
    case MAP_TYPE:
    case OBJECT_TYPE:
    case INSTANCE_TYPE:
    case LIST_TYPE:
    case SET_TYPE:
      return true;
  }
  return false;
}

export function bounded_count(n, coll) {
  if (counted_QMARK_(coll)) {
    return count(coll);
  } else {
    return count(take(n, coll));
  }
}

export function find(m, k) {
  const v = get(m, k);
  if (v !== undefined) {
    return tagMapEntry([k, v]);
  }
}

export function mod(x, y) {
  return ((x % y) + y) % y;
}

export function min_key(k, x, ...more) {
  k = __toFn(k);
  // pairwise (if (< (k x) (k y)) x y), like CLJS, so NaN propagates right
  let min = x;
  for (const y of more) {
    if (!(k(min) < k(y))) min = y;
  }
  return min;
}

export function max_key(k, x, ...more) {
  k = __toFn(k);
  // pairwise (if (> (k x) (k y)) x y), like CLJS, so NaN propagates right
  let max = x;
  for (const y of more) {
    if (!(k(max) > k(y))) max = y;
  }
  return max;
}

function parsing_err(x) {
  throw new Error(`Expected string, got: ${typeof x}`);
}

export function parse_long(s) {
  if (string_QMARK_(s)) {
    if (/^[+-]?\d+$/.test(s)) {
      const i = parseInt(s);
      if (Number.MIN_SAFE_INTEGER <= i && i <= Number.MAX_SAFE_INTEGER) {
        return i;
      }
    }
    return null;
  }
  return parsing_err(s);
}

export function parse_double(s) {
  if (string_QMARK_(s)) {
    // eslint-disable-next-line no-control-regex -- \x00-\x20 mirrors Java trim
    if (/^[\x00-\x20]*[+-]?NaN[\x00-\x20]*$/.test(s)) {
      return NaN;
    } else if (
      // eslint-disable-next-line no-control-regex -- \x00-\x20 mirrors Java trim
      /^[\x00-\x20]*[+-]?(Infinity|((\d+\.?\d*|\.\d+)([eE][+-]?\d+)?)[dDfF]?)[\x00-\x20]*$/.test(
        s
      )
    ) {
      return parseFloat(s);
    } else {
      return null;
    }
  } else {
    return parsing_err(s);
  }
}

function fix(q) {
  if (q >= 0) {
    return Math.floor(q);
  }
  return Math.ceil(q);
}

export function quot(n, d) {
  const rem = n % d;
  return fix((n - rem) / d);
}

export function trampoline(f, ...args) {
  if (args.length == 0) {
    // eslint-disable-next-line no-constant-condition
    while (true) {
      const ret = f();
      if (truth_(fn_QMARK_(ret))) {
        f = ret;
        continue;
      } else {
        return ret;
      }
    }
  } else {
    return trampoline(function () {
      return apply(f, args);
    });
  }
}

export function transduce(xform, ...args) {
  switch (args.length) {
    case 2: {
      const f = args[0];
      const coll = args[1];
      return transduce(xform, f, f(), coll);
    }
    default: {
      let f = args[0];
      const init = args[1];
      const coll = args[2];
      f = xform(f);
      const ret = reduce(f, init, coll);
      return f(ret);
    }
  }
}

export function zipmap(keys, vals) {
  const res = {};
  const keyIterator = iterable(keys)[Symbol.iterator]();
  const valIterator = iterable(vals)[Symbol.iterator]();
  let nextKey, nextVal;
  for (;;) {
    nextKey = keyIterator.next();
    if (nextKey.done) break;
    nextVal = valIterator.next();
    if (nextVal.done) break;
    res[nextKey.value] = nextVal.value;
  }
  return res;
}

export function not_empty(x) {
  const isSeq = seq(x);
  if (isSeq) {
    return x;
  } else return null;
}

export function tree_seq(isBranch, children, root) {
  const walk = function* (node) {
    yield node;
    if (truth_(isBranch(node))) {
      for (const c of iterable(children(node))) {
        yield* walk(c);
      }
    }
  };
  return lazy(function* () {
    yield* walk(root);
  });
}

export function flatten(x) {
  return filter(complement(sequential_QMARK_), rest(tree_seq(sequential_QMARK_, seq, x)));
}

export function transient$(x) {
  if (x != null && x[IEditableCollection__as_transient] !== undefined) return x[IEditableCollection__as_transient](x);
  return copy(x);
}

export function persistent_BANG_(x) {
  if (x != null && x[ITransientCollection__persistent_BANG_] !== undefined) return x[ITransientCollection__persistent_BANG_](x);
  // no Object.freeze: persistent structures stay extensible so symbol-keyed
  // metadata can be attached
  return x;
}

const SortedSet = defclass(
class SortedSet {
  constructor(xs, cmp) {
    this[TYPE_TAG] = SET_TYPE;
    this[SORTED_TAG] = true;
    this._cmp = cmp ?? xs?._cmp;
    const isSorted = xs instanceof SortedSet && xs._cmp === this._cmp;
    if (!isSorted) {
      xs = this._cmp ? sort(this._cmp, xs) : sort(xs);
    }
    const s = new Set(xs);
    // we don't re-use xs since xs can contain duplicates
    this._elts = [...s];
    this._set = s;
    this.size = this._elts.length;
  }
  add(x) {
    if (this._set.has(x)) return this;
    const xs = this._elts;
    const cmp = this._cmp;
    let added = false;
    for (let i = 0; i < xs.length; i++) {
      if ((cmp ? cmp(x, xs[i]) : compare(x, xs[i])) <= 0) {
        xs.splice(i, 0, x);
        added = true;
        break;
      }
    }
    if (!added) {
      xs.push(x);
      // in this case we can re-use the set, since we add the element last
      this._set.add(x);
    } else {
      this._set = new Set(xs);
    }
    this.size = xs.length;
    return this;
  }
  delete(x) {
    if (!this._set.has(x)) return this;
    const xs = this._elts;
    const idx = xs.indexOf(x);
    xs.splice(idx, 1);
    this._set = new Set(xs);
    this.size = xs.length;
    return this;
  }
  has(x) {
    return this._set.has(x);
  }
  keys() {
    return this.values();
  }
  values() {
    return this._elts[Symbol.iterator]();
  }
  entries() {
    return this._set.entries();
  }
  forEach(...xs) {
    return this.set.forEach(...xs);
  }
  clear() {
    this._elts = [];
    this._set = new Set(this._elts);
  }
  [Symbol.iterator]() {
    return this.keys();
  }
}
);

export function sorted_set(...xs) {
  return new SortedSet(xs);
}

export function sorted_set_by(cmp, ...xs) {
  return new SortedSet(xs, fnToComparator(__toFn(cmp)));
}

// A map that keeps its keys in sorted order. Backed by a sorted key array plus
// a plain Map for lookup, and branded MAP_TYPE so map ops dispatch normally.
const SortedMap = defclass(
class SortedMap {
  constructor(entries, cmp) {
    this[TYPE_TAG] = MAP_TYPE;
    this[SORTED_TAG] = true;
    this._cmp = cmp ?? entries?._cmp;
    this._map = new Map();
    this._keys = [];
    this.size = 0;
    if (entries) {
      for (const [k, v] of entries) this.set(k, v);
    }
  }
  set(k, v) {
    if (!this._map.has(k)) {
      const ks = this._keys;
      const cmp = this._cmp;
      let i = 0;
      while (i < ks.length && (cmp ? cmp(k, ks[i]) : compare(k, ks[i])) > 0) i++;
      ks.splice(i, 0, k);
    }
    this._map.set(k, v);
    this.size = this._map.size;
    return this;
  }
  get(k) {
    return this._map.get(k);
  }
  has(k) {
    return this._map.has(k);
  }
  delete(k) {
    if (this._map.delete(k)) {
      this._keys.splice(this._keys.indexOf(k), 1);
      this.size = this._map.size;
    }
    return this;
  }
  *keys() {
    yield* this._keys;
  }
  *values() {
    for (const k of this._keys) yield this._map.get(k);
  }
  *entries() {
    for (const k of this._keys) yield [k, this._map.get(k)];
  }
  forEach(f) {
    for (const k of this._keys) f(this._map.get(k), k, this);
  }
  clear() {
    this._map.clear();
    this._keys = [];
    this.size = 0;
  }
  [Symbol.iterator]() {
    return this.entries();
  }
}
);

export function sorted_map(...kvs) {
  const m = new SortedMap();
  for (let i = 0; i < kvs.length; i += 2) m.set(kvs[i], kvs[i + 1]);
  return m;
}

export function sorted_map_by(cmp, ...kvs) {
  const m = new SortedMap(null, fnToComparator(__toFn(cmp)));
  for (let i = 0; i < kvs.length; i += 2) m.set(kvs[i], kvs[i + 1]);
  return m;
}

function mkBoundFn(_sc, test, key) {
  return (e) => {
    return test(compare(e, key), 0);
  };
}

function indexFrom(sc, startKey, _asc = true) {
  let i = 0;
  for (; i < sc.length; i++) {
    if (!(compare(startKey, sc[i]) > 0)) {
      break;
    }
  }
  return i;
}

function subseq3([sc, test, key]) {
  const includeFn = mkBoundFn(sc, test, key);
  if (test === _GT_ || test === _GT__EQ_) {
    const seqFrom = [...sc];
    const startIdx = indexFrom(seqFrom, key, true);
    // delete startIdx items from the start;
    seqFrom.splice(0, startIdx);
    if (includeFn(seqFrom[0])) {
      return seqFrom;
    } else {
      // delete 1 item from the start;
      seqFrom.splice(0, 1);
      return seqFrom;
    }
  } else {
    return [...take_while(includeFn, sc)];
  }
}

function subseq5([sc, startTest, startKey, endTest, endKey]) {
  const seqFrom = [...sc];
  const startIdx = indexFrom(seqFrom, startKey, true);
  // delete startIdx items from the start
  seqFrom.splice(0, startIdx);
  const whileFn = mkBoundFn(sc, endTest, endKey);
  if (!mkBoundFn(sc, startTest, startKey)(seqFrom[0])) {
    // delete 1 item from the start
    seqFrom.splice(0, 1);
  }
  return [...take_while(whileFn, seqFrom)];
}

export function subseq(...xs) {
  if (xs.length === 3) {
    return subseq3(xs);
  }
  if (xs.length === 5) {
    return subseq5(xs);
  }
}

export function abs(x) {
  return Math.abs(x);
}

export function long$(x) {
  return fix(x);
}

export function float$(x) {
  return x;
}

export function double$(x) {
  return x;
}

export function num(x) {
  return x;
}

export function byte$(x) {
  return x;
}

export function short$(x) {
  return x;
}

export function type(x) {
  return x != null && x.constructor;
}

function preserving_reduced(rf) {
  return (a1, a2) => {
    const ret = rf(a1, a2);
    if (reduced_QMARK_(ret)) {
      return reduced(ret);
    } else return ret;
  };
}

export function cat(rf) {
  const rrf = preserving_reduced(rf);
  return (...args) => {
    switch (args.length) {
      case 0:
        return rf();
      case 1:
        return rf(args[0]);
      case 2:
        return reduce(rrf, args[0], args[1]);
    }
  };
}

export function rem(n, d) {
  const q = quot(n, d);
  return n - d * q;
}

export function memoize(f) {
  const cache = new Map();
  return (...xs) => {
    const path = [xs.length, ...xs];
    const res = get_in(cache, path);
    if (res === undefined) {
      const v = f(...xs);
      assoc_in_BANG_(cache, path, v);
      return v;
    } else return res;
  };
}

export function peek(vec) {
  if (vec == null) return null;
  // A list peeks at its front; squint lists are array-backed, so check list
  // before array to avoid returning the last element.
  if (list_QMARK_(vec)) {
    return first(vec);
  } else if (array_QMARK_(vec)) {
    return vec[vec.length - 1];
  } else if (vec[IStack__peek] !== undefined) {
    return vec[IStack__peek](vec);
  } else {
    throw missing_protocol('IStack.-peek', vec);
  }
}

export function pop(vec) {
  if (vec == null) return null;
  if (list_QMARK_(vec)) {
    if (vec.length === 0) throw new Error("Can't pop empty list");
    return rest(vec);
  } else if (array_QMARK_(vec)) {
    if (vec.length === 0) throw new Error("Can't pop empty vector");
    const ret = [...vec];
    ret.pop();
    return ret;
  } else if (vec[IStack__pop] !== undefined) {
    return vec[IStack__pop](vec);
  } else {
    throw missing_protocol('IStack.-pop', vec);
  }
}

export function pop_BANG_(v) {
  if (v != null && isVectorArray(v)) {
    if (v.length === 0) throw new Error("Can't pop empty vector");
    v.pop();
    return v;
  }
  if (v != null && v[ITransientVector__pop_BANG_] !== undefined) {
    return v[ITransientVector__pop_BANG_](v);
  }
  throw missing_protocol('ITransientVector.-pop!', v);
}

export function update_keys(m, f) {
  const m2 = empty(m);
  if (m2 != null && m2[IAssociative__assoc] !== undefined) {
    return reduce_kv((acc, k, v) => acc[IAssociative__assoc](acc, f(k), v), m2, m);
  }
  const assocFn = getAssocMut(m) || assoc_BANG_;
  reduce_kv(
    (acc, k, v) => {
      return assocFn(acc, f(k), v);
    },
    m2,
    m
  );
  return m2;
}

export function update_vals(m, f) {
  const m2 = empty(m);
  if (m2 != null && m2[IAssociative__assoc] !== undefined) {
    return reduce_kv((acc, k, v) => acc[IAssociative__assoc](acc, k, f(v)), m2, m);
  }
  const assocFn = getAssocMut(m) || assoc_BANG_;
  reduce_kv(
    (acc, k, v) => {
      return assocFn(acc, k, f(v));
    },
    m2,
    m
  );
  return m2;
}

export function random_uuid() {
  return new UUID(crypto.randomUUID());
}

export class UUID {
  constructor(uuid) {
    this.uuid = uuid;
  }
  toString() {
    return this.uuid;
  }
}

export function uuid(s) {
  // lowercased, like CLJS
  return new UUID(s.toLowerCase());
}

export function uuid_QMARK_(x) {
  return x instanceof UUID;
}

const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;

export function parse_uuid(s) {
  if (typeof s !== 'string') {
    throw new Error('Expected string, got: ' + (s === null ? 'nil' : typeof s));
  }
  return UUID_REGEX.test(s) ? uuid(s) : null;
}

export function inst_QMARK_(x) {
  return x instanceof Date;
}

const DELAY_DEREF = (self) => self._deref();

export class Delay {
  constructor(f) {
    this.f = f;
    this[IDEREF_SYM] = true;
    this[IDeref__deref] = DELAY_DEREF;
  }
  _deref() {
    if (this.realized) {
      return this.v;
    } else {
      this.v = this.f();
      this.realized = true;
      return this.v;
    }
  }
}

export function realized_QMARK_(x) {
  if (x instanceof Delay || x instanceof LazyIterable) {
    return x.realized === true;
  }
  throw new Error('realized? not supported on: ' + str(x));
}

export function force(x) {
  return x instanceof Delay ? x._deref() : x;
}

function clj__GT_js_(x, seen) {
  // we need to protect against circular objects
  if (seen.has(x)) return x;
  seen.add(x);
  if (x != null && x[IEncodeJS__clj__GT_js] !== undefined) {
    return x[IEncodeJS__clj__GT_js](x, (v) => clj__GT_js_(v, seen));
  }
  if (map_QMARK_(x)) {
    return update_vals(x, (x) => clj__GT_js_(x, seen));
  }

  const tc = typeConst(x);
  if (tc && tc != OBJECT_TYPE && tc != INSTANCE_TYPE) {
    return mapv((x) => clj__GT_js_(x, seen), x);
  }
  return x;
}

export function clj__GT_js(x) {
  return clj__GT_js_(x, new Set());
}

export function run_BANG_(proc, coll) {
  reduce((_, x) => proc(x), null, coll);
}

export function not_EQ_(...more) {
  return not(_EQ_(...more));
}

const VOLATILE_DEREF = (self) => self.v;

class Volatile {
  constructor(v) {
    this.v = v;
    this[IDeref__deref] = VOLATILE_DEREF;
  }
}

export function volatile_BANG_(x) {
  return new Volatile(x);
}

export function vreset_BANG_(vol, v) {
  vol.v = v;
  return v;
}

// readably false: strings unquoted
function toEDN(value, seen = new WeakSet(), readably = true) {
  if (value == null) return 'nil';
  if (typeof value === 'number') {
    if (value === Infinity) return '##Inf';
    if (value === -Infinity) return '##-Inf';
    if (Number.isNaN(value)) return '##NaN';
    return String(value);
  }
  if (typeof value === 'boolean') return String(value);
  if (typeof value === 'string') return readably ? JSON.stringify(value) : value;
  if (typeof value === 'bigint') return `${value}N`;

  if (typeof value === 'object') {
    if (value instanceof UUID) return readably ? `#uuid "${value.uuid}"` : value.uuid;
    // seen tracks the current ancestor path only. A shared reference that
    // appears under sibling branches is a DAG, not a cycle, so delete on exit.
    if (seen.has(value)) return '#object[circular]';
    seen.add(value);
    const T = typeConst(value);
    let keys, result;
    switch (T) {
      case ARRAY_TYPE:
        result = `[${value.map((v) => toEDN(v, seen, readably)).join(' ')}]`;
        break;
      case SET_TYPE:
        result = `#{${Array.from(value)
          .map((v) => toEDN(v, seen, readably))
          .join(' ')}}`;
        break;
      case MAP_TYPE:
        result = `#js/Map {${Array.from(value.entries())
          .map(([k, v]) => `${toEDN(k, seen, readably)} ${toEDN(v, seen, readably)}`)
          .join(', ')}}`;
        break;
      case LAZY_ITERABLE_TYPE:
      case LIST_TYPE:
        result = `(${mapv((v) => `${toEDN(v, seen, readably)}`, value).join(', ')})`;
        break;
      default:
        if (value[IPrintWithWriter__pr_writer] !== undefined) {
          let buf = '';
          const writer = { [IWriter__write]: (_w, s) => (buf += s), [IWriter.__sym]: true };
          value[IPrintWithWriter__pr_writer](value, writer, { readably: readably });
          result = buf;
          break;
        }
        if (value[IRecord.__sym] !== undefined) {
          keys = Object.keys(value);
          result = `#${value.constructor.name}{${keys.map((k) => `:${k} ${toEDN(value[k], seen, readably)}`).join(', ')}}`;
          break;
        }
        // Non-plain objects (Promise, Error, Date, class instances, ...) have a
        // constructor other than Object. Print them as #<Name> rather than {}
        // (which is what Object.keys would yield for opaque values like a
        // Promise).
        if (value.constructor && value.constructor !== Object) {
          seen.delete(value);
          return `#<${value.constructor.name}>`;
        }
        keys = Object.keys(value);
        result = `{${keys.map((k) => `:${k} ${toEDN(value[k], seen, readably)}`).join(', ')}}`;
    }
    seen.delete(value);
    return result;
  }

  return `#object[${value.constructor.name}]`;
}

export function pr_str(...xs) {
  return xs.map((v, _) => toEDN(v)).join(' ');
}

export function prn(...xs) {
  _STAR_print_fn_STAR_.val(pr_str(...xs));
  if (_STAR_print_newline_STAR_.val) _STAR_print_fn_STAR_.val('\n');
}

export function prn_str(...xs) {
  return pr_str(...xs) + '\n';
}