Introducing calcit-js: toy language inspired by cljs

Although I don’t use parentheses in calcit, it’s still mostly inspired by ClojureScript. The project is called calcit-runner since at first it was only designed be an interpreter, now it also emits JavaScript.

Some demos

To give a demo, there’s a linux binary called cr_once released at http://bin.calcit-lang.org/ , which can be run like:

=>> cr_once -e:"range 10"
Running calcit runner(0.2.34) in CI mode
(0 1 2 3 4 5 6 7 8 9)
=>> cr_once -e:"->> (range 4) $ map $ fn (x) (* x x)"
Running calcit runner(0.2.34) in CI mode
(0 1 4 9)

you may find functions and macros familiar if you see the docs http://apis.calcit-lang.org/ . And this docs itself is built with calcit-js, roughly 5x the cost comparing its ClojureScript version.

A more complicated demo is to run a script. In calcit, it’s using a snapshot file, rather “files of source”. Just one snapshot containing multiple namespaces of this package, and using indentation based syntax:

{} (:package |app)
  :configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!)
  :files $ {}
    |app.main $ {}
      :ns $ quote
        ns app.main $ :require
      :defs $ {}
        |main! $ quote
          defn main! ()
            println "\"Loaded program!"
            ; try-fibo
            echo $ sieve-primes ([] 2 3 5 7 11 13) 17 400

        |reload! $ quote
          defn reload! () nil

        |sieve-primes $ quote
          defn sieve-primes (acc n limit)
            if (&> n limit) acc $ if
              every?
                fn (m)
                  &> (mod n m) 0
                , acc
              recur (conj acc n) (inc n) (, limit)
              recur acc (inc n) limit
=>> cr_once fibo.cirru
Running calcit runner(0.2.34) in CI mode
Runner: specifying filesfibo.cirru
Calcit runner version: 0.2.34
Loaded program!
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397)

Since Nim compiles to C for running, it boots quite fast:

=>> time cr_once fibo.cirru
Running calcit runner(0.2.34) in CI mode
Runner: specifying filesfibo.cirru
Calcit runner version: 0.2.34
Loaded program!
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397)

real	0m0.122s
user	0m0.109s
sys	0m0.010s

By default it uses the file called compact.cirru which is emitted from calcit-editor.

And to emit js:

cr_once --emit-js fibo.cirru

The code generated from the command-line looks like(I formatted it with Pretter, manually):

import * as $calcit from "./calcit.core";

export let sieve_DASH_primes = $calcit.wrapTailCall(function sieve_DASH_primes(
  acc,
  n,
  limit
) {
  return $calcit._AND__GT_(n, limit)
    ? acc
    : $calcit.every_QUES_(function f_PCT_(m) {
        return $calcit._AND__GT_($calcit.mod(n, m), 0.0);
      }, acc)
    ? $calcit.recur($calcit.conj(acc, n), $calcit.inc(n), limit)
    : $calcit.recur(acc, $calcit.inc(n), limit);
});
export function main_BANG_() {
  $calcit.println("Loaded program!");
  /* (try-fibo) */ null;
  return $calcit.echo(
    sieve_DASH_primes(
      $calcit._LIST_(2.0, 3.0, 5.0, 7.0, 11.0, 13.0),
      17.0,
      400.0
    )
  );
}
export function reload_BANG_() {
  return null;
}

Also notice that an extra calcit.core.js is generated containing some core functions. And an extra package @calcit/procs is required to run the code.

Maybe you could be familiar with macros like ->>:

defmacro ->> (base & xs)
  if (empty? xs)
    quote-replace ~base
    &let
      x0 (first xs)
      if (list? x0)
        recur (append x0 base) & (rest xs)
        recur ([] x0 base) & (rest xs)

The binaries are not quite ready. I develop it with Macos and it probably requires some Nim and Webpack knowledge to run it on other platforms.

Motivations and calcit-editor

It’s called “calcit-runner” because it based on and also cooperated with my previous project Calcit Editor which was used to generate ClojureScript:

Calcit Editor uses calcit.cirru as snapshot file, and now supports emitting compact.cirru and .compact-inc.cirru as well. calcit-runner makes use of these 2 files. File file .compact-inc.cirru is for handling incremental changes.

While it’s cool to have ClojureScript with immutable data and macros, I do want to narrow the gap between “immutable data”-macros combo and JavaScript ecosystem. I learnt a lot about ClojureScript in the past 5 years, however it still bothers me in some aspects:

  • JVM compiler starts slowly and eats lots of memory of my laptops. thheller helped a lot but JVM has its own limitations.
  • ClojureScript has its own semantics because Clojure has the semantics, then we can’t change that for the reason that JavaScript has its own ecosystem, an always changing ecosystem.

I’m still using ClojureScript. Just want to try more possibilities. And I always thought in calcit.cirru I already have a lot of information of the program, why do I have to run it by compiling to another language first. So calcit-runner was the project to run Calcit Editor’s snapshot file.

Calcit Runner features

To make it kind of compatible to ClojureScript, I need some basic features:

  • macros, then I can generate syntax like in Clojure,
  • persistent data, which is at Clojure’s core semantics,
  • hot code replacement, for fast feedback loops,
  • frequently used APIs like in ClojureScript.

and being a JavaScript programmer there are still many features I can’ handle. So some major features you see in Clojure are missing in calcit-runner. It does not have a REPL, it does not support threads, no try/catch, no async programming. Or it’s currently more like a calculator.

However since macros are supported, emitting JavaScript is a lot easier after all syntax sugars are expanded.

The experience using calcit-runner is like something in between shadow-cljs and webpack, I modify the code, code got replaced in a browser(pure functions and atom states), an after-load function triggered, I run snippets in Chrome Console and use js debugger.

Meanwhile I can use just use calcit-runner to run code on top of Nim, save snapshot file again, it replaces code, and trigger after-load function as well, only got logs of error messages for debugging.

How calcit-js is implemented

Similar to Clojure, when calcit-runner is interpreting a syntax tree, it,

  • first, run preprocess to resolve symbols and expand macros,
  • then code are turned into core syntax with platform functions,
  • then platform function are called:

those phases can be seen in some of the files:

Roughly, preprocessing(resolving and macro expansion) and evaluating are 2 steps. So the preprocessing step can also be used to support code emitting.

An feature in calcit-runner is, after preprocessing, the information of the program can be exported into a JSON file A example of `cr --emit-ir` · GitHub . Only a demo though.

Meanwhile, the information is just ready for code generation, generating JavaScript in this case: calcit-runner/emit_js.nim at master · calcit-lang/calcit-runner · GitHub

Since syntaxes are de-sugared, its core is quite tiny. Although currently calcit-js lacks many features of ClojureScript or JavaScript, it’s still powerful enough for running a virtual DOM library to render simple pages, such as the APIs index above.

At current, calcit-js only support 2 simple rules of importing npm(or other js modules): :as and :refer, not renaming or handing .default. It just emits files with import/export syntax, relies on a extra bundler for running.

For tail recursions, calcit-js uses an extra function for handling Recur type from runtime, so does calcit-runner. It works well but comparing to code optimized from ClojureScript which uses while(...){...}, it could be a lot slower. calcit-js has bad compiler optimizations, it’s let is implemented with function wrappers so it’s far from optimized.

Expectation of calcit-js

Most time in my jobs I have to deal with npm packages and mostly with Webpack. Since React apps relies on both immutable data and packages from npm, I do want my virtual DOM library to integrate into JavaScript system well.

This is how I want to use calcit-js:

  • it compiles to js files with ES6 import/export syntax,
  • a bundle probably Webpack, loads files and bundles it with npm dependencies.
  • when a file changed, the bundler processes hot module replacement like a normal js file.

Besides Webpack, it works with Vite too, so I can debug code with code that’s less complicated, since browsers can load each *.mjs file without bundling.

It may also work in Node.js without bundling since Node.js can load modules in ES6 import/export syntax. It’s not quite smooth since it required .mjs extensions at current, and calcit-js need to handle those file extensions specifically.

The core functions of the runtime(except that calcit.core.js is emitted together with the program), are maintained with 2 npm packages so that bundler can just recognize:

At first I thought a language with immutable data and macros can hardly be compiled to js, but WISP did, Bucklescript did, that are proved to be possible. It’s just possible. ClojureScript has too many factors preventing it from integrating into JavaScript ecosystem comparing to other compile-to-js languages.

After all calcit-js only experimented a small area of integrating persistent data and macros into npm ecosystem. I’m fully expecting ClojureScript to find its way to connect with ES modules as well.(well, just please make JVM a little faster in doing that).

Optimizations of persistent data in calcit-js

During development of calcit-runner, there are several bottle necks of performance(besides my poor experience on system programming):

  • macro expanding
  • persistent data
  • variadic arguments

Macro expanding is still slow in my current implementation. I’m not going to discuss it since it involved too many factors.

For persistent data, I posted an explanation before(related to List and Map, not Set yet):

To make it work in calcit-js, I finally turned in into a TypeScript project and trying carefully to reduce unnecessary memory usages:

It’s not very optimal but since it’s designed to be a shared list, it has some benefits:

  • just a List, not a List and a Vector. I don’t need to use into [] to convert type in my program.
  • slice, assoc, dissoc operation salso shares parts of the tree, that’s cheaper.(bad part is get got slower.)

Some other issues are related to the “variadic arguments” feature. In Clojure, we have “variadic functions”. But in JavaScript, it’s using “arguments spreading”. And in calcit-runner, it’s more like JavaScript:

defn f1 (x0 & body)
  ; TODO x0 body

println (f1 1 2 3 4 5)

; or
def ys ([] 1 2 3 4 5)
println (f1 & ys))

While it may feels more natural to us JavaScript programmers(lack of static analysis though), it does bring performance costs. I located the costs in Nim implementation of calcit-runner. And it also brought barriers in using List in calcit-js for arguments spreading.

So to optimize that, I decided to implement CrDataList in two modes, one with Array and one with TernaryTreeList. When a CrDataList is first initialized, it’s an Array, so it’s fast for spreading. In Array mode, get is just JavaScript array accessing, that’s fast. And for slice, there’s virtual slicing by sharing same reference of original Array, so it’s cheap. It’s only turned into TernaryTreeList(which is a tree structure of persistent data) when it has to:

class CrDataList {
  value: TernaryTreeList<CrDataValue>;
  // array mode store bare array for performance
  arrayValue: Array<CrDataValue>;
  arrayMode: boolean;
  arrayStart: number;
  arrayEnd: number;
  cachedHash: Hash;
  constructor(value: Array<CrDataValue> | TernaryTreeList<CrDataValue>) {
    if (Array.isArray(value)) {
      this.arrayMode = true;
      this.arrayValue = value;
      this.arrayStart = 0;
      this.arrayEnd = value.length;
      this.value = null;
    } else {
      this.arrayMode = false;
      this.value = value;
      this.arrayValue = [];
      this.arrayStart = null;
      this.arrayEnd = null;
    }
  }
  turnListMode() {
    if (this.arrayMode) {
      this.value = initTernaryTreeList(
        this.arrayValue.slice(this.arrayStart, this.arrayEnd)
      );
      this.arrayValue = null;
      this.arrayStart = null;
      this.arrayEnd = null;
      this.arrayMode = false;
    }
  }
  len() {
    if (this.arrayMode) {
      return this.arrayEnd - this.arrayStart;
    } else {
      return listLen(this.value);
    }
  }
  get(idx: number) {
    if (this.arrayMode) {
      return this.arrayValue[this.arrayStart + idx];
    } else {
      return listGet(this.value, idx);
    }
  }
 // more....

A similar trick is also added to merge function. When two Maps are being merged, they are not merged into a single TernaryTreeList directly, but instead saved in a linked list, until they have to be merged:

class CrDataMap {
  cachedHash: Hash;
  chain: MapChain;
  depth: number;
  skipValue: CrDataValue;
  constructor(value: TernaryTreeMap<CrDataValue, CrDataValue>) {
    this.chain = { value: value, next: null };
    this.depth = 1;
    this.skipValue = fakeUniqueSymbol;
  }
  turnSingleMap() {
    if (this.depth === 1) {
      return;
    }
    // squash down to a single level of map
    let ret = this.chain.value;
    let cursor = this.chain.next;
    while (cursor != null) {
      if (!isMapEmpty(cursor.value)) {
        ret = ternaryTree.mergeSkip(cursor.value, ret, this.skipValue);
      }
      cursor = cursor.next;
    }
    this.chain = {
      value: ret,
      next: null,
    };
    this.depth = 1;
  }
  len() {
    this.turnSingleMap();
    return mapLen(this.chain.value);
  }
  get(k: CrDataValue) {
    let cursor = this.chain;
    while (cursor != null) {
      let v = mapGet(cursor.value, k);
      if (v != null && v !== this.skipValue) {
        return v;
      } else {
        cursor = cursor.next;
      }
    }
    return null;
  }

  merge(ys: CrDataMap) {
    return this.mergeSkip(ys, fakeUniqueSymbol);
  }
  mergeSkip(ys: CrDataMap, v: CrDataValue) {
    if (!(ys instanceof CrDataMap)) {
      throw new Error("Expected map");
    }

    let result = new CrDataMap(null);
    result.skipValue = v;
    ys.turnSingleMap();
    result.chain = {
      value: ys.chain.value,
      next: this.chain,
    };
    result.depth = this.depth + 1;
    if (result.depth > 5) {
      // 5 by experience, limit to squash linked list to value
      result.turnSingleMap();
    }
    return result;
  }
 // more code...

The trick for List made significant performance boost, especially for arguments spreading in JavaScript. The benefit of the trick in merge is unclear. Accessing values of Maps is slower in this way, so this might not be a good solution.

There are also some other tricks(even dirty ones) for hash function to reduce the cost of persistent map. However I’m understanding hashing and RRB tree well and so I still need to investigate into that.

Other thoughts…

As I said my main purpose is to run calcit-editor created programs without compiling to another language first, meanwhile, to use tools from JavaScript ecosystem. At least calcit-js accomplished these goals now, despite of its lack of features comparing to cljs.

A milestone of the project is I migrated Respo, which is my own virtual DOM libray, into calcit-js. I tried, the calcit-js one is roughly 5x slower compare to the optimized cljs one, it’s not very significant slowness:

My gains are more familiar tools from JavaScript side and faster boot time. Debugging the code generated from calcit-js is easier than from cljs since it has similar semantics with JavaScript, but meanwhile, it lacks of source maps. Being an experiment, I think it’s good enough.

For modules system, it uses a local folder ~/.config/calcit/modules to locate a different package. Very poor solution, no version resolutions.

It actually recompiles and then to know where a js file is changed(macros involved everywhere, hum?). So if one day my project gets large, it would probably be slower than cljs.

I’m not into C programming so I have difficulties porting TCP or other servers. That made the project rather limited, just no C extension to use. Also no VMs are well. Might be lots of considerations in future.

1 Like

In the past weeks I also migrated my own projects:

to calcit-js, and luckily they both work quite well.(Not use in larger apps though…)

Also I added cr --emit-ir option to let calcit-runner to emit a program-ir.json file, which is the program data after macro expanding, but before js code emitting. Meanwhile I built a ir-viewer page with Respo.calcit, as a debugging tool for the IR. So here you go:

Updates on calcit-js:

The interpreter/codegen has been rewritten in Rust for the power of ADTs. Performance is roughly same.

1 Like

Some feedbacks after using Calcit-js for some months…

Advantages

Just imaging you are writing ClojureScript, 1k+ lines of code, compiled(cold startup) with <500ms. Calcit-js has fewer features and so a lot faster.

The compiled code are just modern JavaScript with import / export syntax, so that works with Webpack and Vite quite well, no worries. Just as planned.

In Calcit-js I write map ([] 1 2 3) inc rather than (map inc [1 2 3]), I found it quite easy to connect that in code -> xs (map inc) (nth 1) which Clojure would need as-> to do. Small change but looks not bad.

Downsides

Somehow I do miss features from shadow-cljs, compiling multiple targets in a single run… watching inlined assets, better warnings… and Calcit-js is rough and even kind like broken in such aspects. And I have to start both calcit-js compiler and Webpack(or Vite) in order to run the page. shadow-cljs is really simpler. And I also copied :require (... :default ...) from shadow-cljs to make some imports works.

ClojureScript’s hot code swapping is nice. When I switch back to Webpack and Vite for hot code swapping, function definitions are inside closures, not as easily replaced as in ClojureScript. That make me flurried for a while and at last I have to fix that with extra atoms, a dirty way. Plus that webpack HMR for Node.js side is so rough, never becoming as simple as shadow-cljs.

I implemented macros in Calcit-js so it generates core functions like in Clojure. Well, I only have very limited experience on this and turned out the macro system was quite unrobust. I fixed several bugs and it seemed to work. But it’s probably still very far from the power of Clojure macro.

For easiness of development, I only copied features I wanted mainly for using it in JavaScript environments. One great feature I didn’t try to copy was “multiple dispatch”, or “protocol”, something like that, features for polymorphism. As I used Rust to built the interpreter/codegen, I become more and more realized about the power of polymorphism, or “ad-hoc polymorphism” in this case.(for parametric polymorphism I can still use tagged unions to simulate). To be honest I don’t have a clear sight on how to implement ad-hoc polymorphism in Calcit-js being a dynamic typed language. But now I feel more and more that I would need it.

and…

still trying to make it better. and it’s fun to extend the tools itself for more abilities.

(->> [1 2 3] (map inc) (take 1))

All sequence functions take the sequence as the last argument, so if you keep to those you can thread them all with thread-last.

Otherwise I think it’s good you’re experimenting on your own little programming language, you’ve probably learned a lot along the way.

I mean in Calcit-js, all sequence functions take the sequence as the FIRST argument. Since may functions for operating collections, get nth assoc… take collections as first argument, now I can chain them together with a single ->.

Ya, I remember there being a reason why in Clojure the sequence functions take it last, where as collection functions take it first, but I don’t recall.

Edit: Found this: Rules of thumb for function arguments ordering in Clojure - Stack Overflow

https://clojure.org/guides/faq#arg_order

that would be more obvious reasons to put sequence at last in Haskell,

since functions are all curried, it’s a very natural pattern to use partial evaluation and reuse the arguments before the last one as a partial function, then partial functions can be composed:

myfunc2 = dropEvery 3 . map (`div` 2) . filter even

and they even use this pattern for lookup (our get in Clojure):

lookup :: Ord k => k -> Map k a -> Maybe a

And I thought, we are not curried in most functions. And we are less often using that . compose. So I prefer always using macros to compose.

Transducers may need that kind of composition over functions, but transducers are rarely used in my projects. Plus we always have macros to save us from the worst cases.

after another day of work… might be boring to read but here are 2 examples of code generated by calcit-js(after running Prettier).

before Prettier, the indentations layout was very ugly.

import * as $calcit from "./calcit.core";

import { _LT__GT_ } from "./respo.core";

import { _EQ__LT_ } from "./respo.comp.space";

import { _GT__GT_ } from "./respo.core";

import { comp_inspect } from "./respo.comp.inspect";

import { comp_task } from "./respo.app.comp.task";

import { comp_wrap } from "./respo.app.comp.wrap";

import { comp_zero } from "./respo.app.comp.zero";

import { div } from "./respo.core";

import { extract_effects_list } from "./respo.core";

import { hsl } from "./respo.util.format";

import { input } from "./respo.core";

import { list__GT_ } from "./respo.core";

import { memof_call } from "./memof.alias";

import * as $respo_DOT_app_DOT_style_DOT_widget from "./respo.app.style.widget";

import * as $respo_DOT_schema from "./respo.schema";

import { span } from "./respo.core";

import { text_width } from "./respo.util.dom";

import { time_BANG_ } from "./respo.util.dom";

export function effect_focus() {
  if (arguments.length !== 0) throw new Error("argument sizes do not match");
  return $calcit._AND__PCT__MAP_($respo_DOT_schema.Effect, $calcit.kwd("name"), $calcit.kwd("effect-focus"), $calcit.kwd("coord"), $calcit._LIST_(), $calcit.kwd("args"), $calcit._LIST_(), $calcit.kwd("method"), function f_PCT_(args__74, params__75) {
    if (arguments.length !== 2) throw new Error("argument sizes do not match");
    let v__76 = args__74;
    let v__77 = params__75;
    let action = $calcit._AND_list_COL_nth(v__77, 0);
    let parent = $calcit._AND_list_COL_nth(v__77, 1);
    let at_place_QUES_ = $calcit._AND_list_COL_nth(v__77, 2);
    return console.log("todolist effect:", action);
  });
}
export function number_order(a, b) {
  if (arguments.length !== 2) throw new Error("argument sizes do not match");

  if ($calcit._AND__LT_(a, b)) {
    return -1;
  } else if ($calcit._AND__GT_(a, b)) {
    return 1;
  } else {
    return 0;
  }
}
export function on_focus(e, dispatch_BANG_) {
  if (arguments.length !== 2) throw new Error("argument sizes do not match");
  console.log($calcit.printable("Just focused~"));
}
export function run_test_BANG_(dispatch_BANG_, acc) {
  if (arguments.length !== 2) throw new Error("argument sizes do not match");
  let started = time_BANG_();
  dispatch_BANG_($calcit.kwd("clear"), null);
  $calcit.apply(
    function generated_loop(x) {
      if (arguments.length !== 1) throw new Error("argument sizes do not match");

      let ret_AUTO_2 = null;
      let times_AUTO_3 = 0;
      while (true) {
        /* Tail Recursion */
        if (times_AUTO_3 > 10000) throw new Error("tail recursion not stopping");
        dispatch_BANG_($calcit.kwd("add"), "empty");

        if ($calcit._GT_(x, 0)) {
          ret_AUTO_2 = $calcit.recur($calcit.dec(x));
        }
        ret_AUTO_2 = null;

        if (ret_AUTO_2 instanceof $calcit.CalcitRecur) {
          if (ret_AUTO_2.args.length !== 1) throw new Error("argument sizes do not match");
          [x] = ret_AUTO_2.args;

          times_AUTO_3 += 1;
          continue;
        } else {
          return ret_AUTO_2;
        }
      }
    },

    $calcit._LIST_(20)
  );
  $calcit.apply(
    function generated_loop(x) {
      if (arguments.length !== 1) throw new Error("argument sizes do not match");

      let ret_AUTO_5 = null;
      let times_AUTO_6 = 0;
      while (true) {
        /* Tail Recursion */
        if (times_AUTO_6 > 10000) throw new Error("tail recursion not stopping");
        dispatch_BANG_($calcit.kwd("hit-first"), $calcit.rand());

        if ($calcit._GT_(x, 0)) {
          ret_AUTO_5 = $calcit.recur($calcit.dec(x));
        }
        ret_AUTO_5 = null;

        if (ret_AUTO_5 instanceof $calcit.CalcitRecur) {
          if (ret_AUTO_5.args.length !== 1) throw new Error("argument sizes do not match");
          [x] = ret_AUTO_5.args;

          times_AUTO_6 += 1;
          continue;
        } else {
          return ret_AUTO_5;
        }
      }
    },

    $calcit._LIST_(20)
  );
  dispatch_BANG_($calcit.kwd("clear"), null);
  $calcit.apply(
    function generated_loop(x) {
      if (arguments.length !== 1) throw new Error("argument sizes do not match");

      let ret_AUTO_8 = null;
      let times_AUTO_9 = 0;
      while (true) {
        /* Tail Recursion */
        if (times_AUTO_9 > 10000) throw new Error("tail recursion not stopping");
        dispatch_BANG_($calcit.kwd("add"), "only 10 items");

        if ($calcit._GT_(x, 0)) {
          ret_AUTO_8 = $calcit.recur($calcit.dec(x));
        }
        ret_AUTO_8 = null;

        if (ret_AUTO_8 instanceof $calcit.CalcitRecur) {
          if (ret_AUTO_8.args.length !== 1) throw new Error("argument sizes do not match");
          [x] = ret_AUTO_8.args;

          times_AUTO_9 += 1;
          continue;
        } else {
          return ret_AUTO_8;
        }
      }
    },

    $calcit._LIST_(10)
  );
  let cost = $calcit._SUB_(time_BANG_(), started);

  if ($calcit._LT_($calcit.count(acc), 40)) {
    return setTimeout(function f_PCT_() {
      if (arguments.length !== 0) throw new Error("argument sizes do not match");
      return run_test_BANG_(dispatch_BANG_, $calcit.conj(acc, cost));
    }, 0);
  } else {
    console.log($calcit.printable("result:", $calcit.sort(acc, number_order)));
  }
}
export function on_test(e, dispatch_BANG_) {
  if (arguments.length !== 2) throw new Error("argument sizes do not match");
  console.log($calcit.printable("trigger test!"));
  return run_test_BANG_(dispatch_BANG_, $calcit._LIST_());
}
export function comp_todolist(states, tasks) {
  if (arguments.length !== 2) throw new Error("argument sizes do not match");
  return extract_effects_list(
    $calcit._AND__PCT__MAP_(
      $respo_DOT_schema.Component,
      $calcit.kwd("effects"),
      $calcit._LIST_(),
      $calcit.kwd("name"),
      $calcit.kwd("comp-todolist"),
      $calcit.kwd("tree"),
      (function __fn__() {
        let cursor = (function __fn__() {
          let v1__72 = $calcit.get(states, $calcit.kwd("cursor"));

          if ($calcit.nil_QUES_(v1__72)) {
            return $calcit._LIST_();
          } else if ($calcit._EQ_(false, v1__72)) {
            return $calcit._LIST_();
          } else {
            return v1__72;
          }
        })();
        let state = (function __fn__() {
          let v1__73 = $calcit.get(states, $calcit.kwd("data"));

          if ($calcit.nil_QUES_(v1__73)) {
            return initial_state;
          } else if ($calcit._EQ_(false, v1__73)) {
            return initial_state;
          } else {
            return v1__73;
          }
        })();
        return $calcit._LIST_(
          effect_focus(),
          div(
            $calcit._AND__MAP_($calcit.kwd("style"), style_root),
            comp_inspect("States", state, $calcit._AND__MAP_($calcit.kwd("left"), "80px")),
            div(
              $calcit._AND__MAP_($calcit.kwd("style"), style_panel),
              input(
                $calcit._AND__MAP_(
                  $calcit.kwd("placeholder"),
                  "Text",
                  $calcit.kwd("value"),
                  $calcit.get(state, $calcit.kwd("draft")),
                  $calcit.kwd("style"),
                  $calcit.merge($respo_DOT_app_DOT_style_DOT_widget.input, $calcit._AND__MAP_($calcit.kwd("width"), $calcit._AND_max(200, $calcit._ADD_(24, text_width($calcit.get(state, $calcit.kwd("draft")), 16, "BlinkMacSystemFont"))))),
                  $calcit.kwd("on-input"),
                  function f_PCT_(e, d_BANG_) {
                    if (arguments.length !== 2) throw new Error("argument sizes do not match");
                    return d_BANG_(cursor, $calcit.assoc(state, $calcit.kwd("draft"), $calcit.get(e, $calcit.kwd("value"))));
                  },
                  $calcit.kwd("on-focus"),
                  on_focus
                )
              ),
              _EQ__LT_(8, null),
              span(
                $calcit._AND__MAP_($calcit.kwd("style"), $respo_DOT_app_DOT_style_DOT_widget.button, $calcit.kwd("on-click"), function f_PCT_(e, d_BANG_) {
                  if (arguments.length !== 2) throw new Error("argument sizes do not match");
                  d_BANG_($calcit.kwd("add"), $calcit.get(state, $calcit.kwd("draft")));
                  return d_BANG_(cursor, $calcit.assoc(state, $calcit.kwd("draft"), ""));
                }),
                span($calcit._AND__MAP_($calcit.kwd("on-click"), null, $calcit.kwd("inner-text"), "Add"))
              ),
              _EQ__LT_(8, null),
              span(
                $calcit._AND__MAP_($calcit.kwd("inner-text"), "Clear", $calcit.kwd("style"), $respo_DOT_app_DOT_style_DOT_widget.button, $calcit.kwd("on-click"), function f_PCT_(e, d_BANG_) {
                  if (arguments.length !== 2) throw new Error("argument sizes do not match");
                  return d_BANG_($calcit.kwd("clear"), null);
                })
              ),
              _EQ__LT_(8, null),
              div($calcit._AND__MAP_(), div($calcit._AND__MAP_($calcit.kwd("style"), $respo_DOT_app_DOT_style_DOT_widget.button, $calcit.kwd("on-click"), on_test), _LT__GT_("heavy tasks")))
            ),
            list__GT_(
              $calcit._AND__MAP_($calcit.kwd("class-name"), "task-list", $calcit.kwd("style"), style_list),
              $calcit.map(
                $calcit.reverse(
                  (function __fn__() {
                    let v1__80 = tasks;

                    if ($calcit.nil_QUES_(v1__80)) {
                      return $calcit._LIST_();
                    } else if ($calcit._EQ_(false, v1__80)) {
                      return $calcit._LIST_();
                    } else {
                      return v1__80;
                    }
                  })()
                ),
                function f_PCT_(task) {
                  if (arguments.length !== 1) throw new Error("argument sizes do not match");
                  let task_id = $calcit.get(task, $calcit.kwd("id"));
                  return $calcit._LIST_(task_id, memof_call(comp_task, _GT__GT_(states, task_id), task));
                }
              )
            ),
            $calcit._GT_($calcit.count(tasks), 0)
              ? div(
                  $calcit._AND__MAP_($calcit.kwd("spell-check"), true, $calcit.kwd("style"), style_toolbar),
                  div(
                    $calcit._AND__MAP_(
                      $calcit.kwd("style"),
                      $respo_DOT_app_DOT_style_DOT_widget.button,
                      $calcit.kwd("on-click"),
                      $calcit.not($calcit.get(state, $calcit.kwd("locked?")))
                        ? function f_PCT_(e, d_BANG_) {
                            if (arguments.length !== 2) throw new Error("argument sizes do not match");
                            return d_BANG_($calcit.kwd("clear"), null);
                          }
                        : null
                    ),
                    _LT__GT_("Clear2")
                  ),
                  _EQ__LT_(8, null),
                  div(
                    $calcit._AND__MAP_($calcit.kwd("style"), $respo_DOT_app_DOT_style_DOT_widget.button, $calcit.kwd("on-click"), function f_PCT_(e, d_BANG_) {
                      if (arguments.length !== 2) throw new Error("argument sizes do not match");
                      return d_BANG_(cursor, $calcit.update(state, $calcit.kwd("locked?"), $calcit.not));
                    }),
                    _LT__GT_($calcit._AND_str_concat("Lock?", $calcit._AND_str($calcit.get(state, $calcit.kwd("locked?")))))
                  ),
                  _EQ__LT_(8, null),
                  comp_wrap(comp_zero())
                )
              : null,
            comp_inspect("Tasks", tasks, $calcit._AND__MAP_($calcit.kwd("left"), 500, $calcit.kwd("top"), 20))
          )
        );
      })()
    )
  );
}

export var initial_state = $calcit._AND__MAP_($calcit.kwd("draft"), "", $calcit.kwd("locked?"), false);

export var style_list = $calcit._AND__MAP_($calcit.kwd("color"), $calcit.kwd("black"), $calcit.kwd("background-color"), hsl(120, 20, 98));

export var style_panel = $calcit._AND__MAP_($calcit.kwd("display"), $calcit.kwd("flex"), $calcit.kwd("margin-bottom"), 4);

export var style_root = $calcit._AND__MAP_($calcit.kwd("color"), $calcit.kwd("black"), $calcit.kwd("background-color"), hsl(120, 20, 98), $calcit.kwd("line-height"), "24px", "font-size", 16, $calcit.kwd("padding"), 10, $calcit.kwd("font-family"), '"微软雅黑", Verdana');

export var style_toolbar = $calcit._AND__MAP_($calcit.kwd("display"), $calcit.kwd("flex"), $calcit.kwd("flex-direction"), $calcit.kwd("row"), $calcit.kwd("justify-content"), $calcit.kwd("start"), $calcit.kwd("padding"), "4px 0", $calcit.kwd("white-space"), $calcit.kwd("nowrap"));
import * as $calcit from "./calcit.core";

import { collect_mounting } from "./respo.render.effect";

import { collect_unmounting } from "./respo.render.effect";

import { collect_updating } from "./respo.render.effect";

import { compare_xy } from "./respo.util.comparator";

import { component_QUES_ } from "./respo.util.detect";

import { element_QUES_ } from "./respo.util.detect";

import * as $respo_DOT_schema_DOT_op from "./respo.schema.op";

import { val_of_first } from "./respo.util.list";

export let find_props_diffs = function find_props_diffs(collect_BANG_, coord, n_coord, old_props, new_props) {
  if (arguments.length !== 5) throw new Error("argument sizes do not match");

  let ret_AUTO_2 = null;
  let times_AUTO_3 = 0;
  while (true) {
    /* Tail Recursion */
    if (times_AUTO_3 > 10000) throw new Error("tail recursion not stopping");
    let was_empty_QUES_ = $calcit.empty_QUES_(old_props);
    let now_empty_QUES_ = $calcit.empty_QUES_(new_props);

    if (was_empty_QUES_ ? (now_empty_QUES_ ? now_empty_QUES_ : false) : false) {
      ret_AUTO_2 = null;
    } else if (was_empty_QUES_ ? ($calcit.not(now_empty_QUES_) ? $calcit.not(now_empty_QUES_) : false) : false) {
      collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.add_prop, coord, n_coord, $calcit.first(new_props)));
      ret_AUTO_2 = $calcit.recur(collect_BANG_, coord, n_coord, old_props, $calcit.rest(new_props));
    } else if ($calcit.not(was_empty_QUES_) ? (now_empty_QUES_ ? now_empty_QUES_ : false) : false) {
      collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_prop, coord, n_coord, $calcit.first($calcit.first(old_props))));
      ret_AUTO_2 = $calcit.recur(collect_BANG_, coord, n_coord, $calcit.rest(old_props), new_props);
    } else if (true) {
      let old_pair = $calcit.first(old_props);
      let new_pair = $calcit.first(new_props);
      let old_k = $calcit.first(old_pair);
      let old_v = $calcit.last(old_pair);
      let new_k = $calcit.first(new_pair);
      let new_v = $calcit.last(new_pair);
      let old_follows = $calcit.rest(old_props);
      let new_follows = $calcit.rest(new_props);
      let v__54 = compare_xy(old_k, new_k);

      if ($calcit._AND__EQ_(v__54, -1)) {
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_prop, coord, n_coord, old_k));
        ret_AUTO_2 = $calcit.recur(collect_BANG_, coord, n_coord, old_follows, new_props);
      } else if ($calcit._AND__EQ_(v__54, 1)) {
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.add_prop, coord, n_coord, new_pair));
        ret_AUTO_2 = $calcit.recur(collect_BANG_, coord, n_coord, old_props, new_follows);
      } else if ($calcit._AND__EQ_(v__54, 0)) {
        $calcit._SLSH__EQ_(old_v, new_v) ? collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.replace_prop, coord, n_coord, new_pair)) : null;
        ret_AUTO_2 = $calcit.recur(collect_BANG_, coord, n_coord, old_follows, new_follows);
      } else {
        ret_AUTO_2 = null;
      }
    } else {
      ret_AUTO_2 = null;
    }

    if (ret_AUTO_2 instanceof $calcit.CalcitRecur) {
      if (ret_AUTO_2.args.length !== 5) throw new Error("argument sizes do not match");
      [collect_BANG_, coord, n_coord, old_props, new_props] = ret_AUTO_2.args;

      times_AUTO_3 += 1;
      continue;
    } else {
      return ret_AUTO_2;
    }
  }
};

export let find_style_diffs = function find_style_diffs(collect_BANG_, c_coord, coord, old_style, new_style) {
  if (arguments.length !== 5) throw new Error("argument sizes do not match");

  let ret_AUTO_5 = null;
  let times_AUTO_6 = 0;
  while (true) {
    /* Tail Recursion */
    if (times_AUTO_6 > 10000) throw new Error("tail recursion not stopping");
    let was_empty_QUES_ = $calcit.empty_QUES_(old_style);
    let now_empty_QUES_ = $calcit.empty_QUES_(new_style);

    if ($calcit.identical_QUES_(old_style, new_style)) {
      ret_AUTO_5 = null;
    } else if (was_empty_QUES_ ? (now_empty_QUES_ ? now_empty_QUES_ : false) : false) {
      ret_AUTO_5 = null;
    } else if (was_empty_QUES_ ? ($calcit.not(now_empty_QUES_) ? $calcit.not(now_empty_QUES_) : false) : false) {
      let entry = $calcit.first(new_style);
      let follows = $calcit.rest(new_style);
      collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.add_style, c_coord, coord, entry));
      ret_AUTO_5 = $calcit.recur(collect_BANG_, c_coord, coord, old_style, follows);
    } else if ($calcit.not(was_empty_QUES_) ? (now_empty_QUES_ ? now_empty_QUES_ : false) : false) {
      let entry = $calcit.first(old_style);
      let follows = $calcit.rest(old_style);
      collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_style, c_coord, coord, $calcit.first(entry)));
      ret_AUTO_5 = $calcit.recur(collect_BANG_, c_coord, coord, follows, new_style);
    } else if (true) {
      let old_entry = $calcit.first(old_style);
      let new_entry = $calcit.first(new_style);
      let old_follows = $calcit.rest(old_style);
      let new_follows = $calcit.rest(new_style);
      let v__55 = compare_xy($calcit.first(old_entry), $calcit.first(new_entry));

      if ($calcit._AND__EQ_(v__55, -1)) {
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_style, c_coord, coord, $calcit.first(old_entry)));
        ret_AUTO_5 = $calcit.recur(collect_BANG_, c_coord, coord, old_follows, new_style);
      } else if ($calcit._AND__EQ_(v__55, 1)) {
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.add_style, c_coord, coord, new_entry));
        ret_AUTO_5 = $calcit.recur(collect_BANG_, c_coord, coord, old_style, new_follows);
      } else if ($calcit._AND__EQ_(v__55, 0)) {
        $calcit.not($calcit.identical_QUES_($calcit.last(old_entry), $calcit.last(new_entry))) ? collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.replace_style, c_coord, coord, new_entry)) : null;
        ret_AUTO_5 = $calcit.recur(collect_BANG_, c_coord, coord, old_follows, new_follows);
      } else {
        ret_AUTO_5 = null;
      }
    } else {
      ret_AUTO_5 = null;
    }

    if (ret_AUTO_5 instanceof $calcit.CalcitRecur) {
      if (ret_AUTO_5.args.length !== 5) throw new Error("argument sizes do not match");
      [collect_BANG_, c_coord, coord, old_style, new_style] = ret_AUTO_5.args;

      times_AUTO_6 += 1;
      continue;
    } else {
      return ret_AUTO_5;
    }
  }
};

export let find_element_diffs = function find_element_diffs(collect_BANG_, coord, n_coord, old_tree, new_tree) {
  if (arguments.length !== 5) throw new Error("argument sizes do not match");

  let ret_AUTO_8 = null;
  let times_AUTO_9 = 0;
  while (true) {
    /* Tail Recursion */
    if (times_AUTO_9 > 10000) throw new Error("tail recursion not stopping");

    if ($calcit.identical_QUES_(old_tree, new_tree)) {
      ret_AUTO_8 = null;
    } else if (component_QUES_(old_tree) ? (component_QUES_(new_tree) ? component_QUES_(new_tree) : false) : false) {
      let next_coord = $calcit.conj(coord, $calcit.get(new_tree, $calcit.kwd("name")));

      if ($calcit._EQ_($calcit.get(old_tree, $calcit.kwd("name")), $calcit.get(new_tree, $calcit.kwd("name")))) {
        collect_updating(collect_BANG_, $calcit.kwd("before-update"), coord, n_coord, old_tree, new_tree);
        find_element_diffs(collect_BANG_, next_coord, n_coord, $calcit.get(old_tree, $calcit.kwd("tree")), $calcit.get(new_tree, $calcit.kwd("tree")));
        ret_AUTO_8 = collect_updating(collect_BANG_, $calcit.kwd("update"), coord, n_coord, old_tree, new_tree);
      } else {
        collect_unmounting(collect_BANG_, coord, n_coord, old_tree, true);
        find_element_diffs(collect_BANG_, next_coord, n_coord, $calcit.get(old_tree, $calcit.kwd("tree")), $calcit.get(new_tree, $calcit.kwd("tree")));
        ret_AUTO_8 = collect_mounting(collect_BANG_, coord, n_coord, new_tree, true);
      }
    } else if (component_QUES_(old_tree) ? (element_QUES_(new_tree) ? element_QUES_(new_tree) : false) : false) {
      collect_unmounting(collect_BANG_, $calcit.conj(coord, $calcit.get(old_tree, $calcit.kwd("name"))), n_coord, old_tree, true);
      ret_AUTO_8 = $calcit.recur(collect_BANG_, coord, n_coord, $calcit.get(old_tree, $calcit.kwd("tree")), new_tree);
    } else if (element_QUES_(old_tree) ? (component_QUES_(new_tree) ? component_QUES_(new_tree) : false) : false) {
      let new_coord = $calcit.conj(coord, $calcit.get(new_tree, $calcit.kwd("name")));
      find_element_diffs(collect_BANG_, new_coord, n_coord, old_tree, $calcit.get(new_tree, $calcit.kwd("tree")));
      ret_AUTO_8 = collect_mounting(collect_BANG_, coord, n_coord, new_tree, true);
    } else if (element_QUES_(old_tree) ? (element_QUES_(new_tree) ? element_QUES_(new_tree) : false) : false) {
      if ($calcit._SLSH__EQ_($calcit.get(old_tree, $calcit.kwd("name")), $calcit.get(new_tree, $calcit.kwd("name")))) {
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.replace_element, coord, n_coord, new_tree));
        ret_AUTO_8 = null;
      } else {
        find_props_diffs(collect_BANG_, coord, n_coord, $calcit.get(old_tree, $calcit.kwd("attrs")), $calcit.get(new_tree, $calcit.kwd("attrs")));
        (function __fn__() {
          let old_style = $calcit.get(old_tree, $calcit.kwd("style"));
          let new_style = $calcit.get(new_tree, $calcit.kwd("style"));

          if ($calcit._SLSH__EQ_(old_style, new_style)) {
            return find_style_diffs(collect_BANG_, coord, n_coord, old_style, new_style);
          }
          return null;
        })();
        (function __fn__() {
          let old_events = $calcit.keys_non_nil(
            (function __fn__() {
              let v1__56 = $calcit.get(old_tree, $calcit.kwd("event"));

              if ($calcit.nil_QUES_(v1__56)) {
                return $calcit._AND__MAP_();
              } else if ($calcit._EQ_(false, v1__56)) {
                return $calcit._AND__MAP_();
              } else {
                return v1__56;
              }
            })()
          );
          let new_events = $calcit.keys_non_nil(
            (function __fn__() {
              let v1__57 = $calcit.get(new_tree, $calcit.kwd("event"));

              if ($calcit.nil_QUES_(v1__57)) {
                return $calcit._AND__MAP_();
              } else if ($calcit._EQ_(false, v1__57)) {
                return $calcit._AND__MAP_();
              } else {
                return v1__57;
              }
            })()
          );

          if ($calcit._SLSH__EQ_(old_events, new_events)) {
            let added_events = $calcit.difference(new_events, old_events);
            let removed_events = $calcit.difference(old_events, new_events);
            $calcit.foldl(added_events, null, function doseq_fn_PCT_(_acc, event_name) {
              if (arguments.length !== 2) throw new Error("argument sizes do not match");
              return collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.set_event, coord, n_coord, event_name));
            });
            return $calcit.foldl(removed_events, null, function doseq_fn_PCT_(_acc, event_name) {
              if (arguments.length !== 2) throw new Error("argument sizes do not match");
              return collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_event, coord, n_coord, event_name));
            });
          }
          return null;
        })();
        let old_children = $calcit.get(old_tree, $calcit.kwd("children"));
        let new_children = $calcit.get(new_tree, $calcit.kwd("children"));
        ret_AUTO_8 = find_children_diffs(collect_BANG_, coord, n_coord, 0, old_children, new_children);
      }
    } else if (true) {
      ret_AUTO_8 = console.warn("Diffing unknown params", old_tree, new_tree);
    } else {
      ret_AUTO_8 = null;
    }

    if (ret_AUTO_8 instanceof $calcit.CalcitRecur) {
      if (ret_AUTO_8.args.length !== 5) throw new Error("argument sizes do not match");
      [collect_BANG_, coord, n_coord, old_tree, new_tree] = ret_AUTO_8.args;

      times_AUTO_9 += 1;
      continue;
    } else {
      return ret_AUTO_8;
    }
  }
};

export let find_children_diffs = function find_children_diffs(collect_BANG_, coord, n_coord, index, old_children, new_children) {
  if (arguments.length !== 6) throw new Error("argument sizes do not match");

  let ret_AUTO_11 = null;
  let times_AUTO_12 = 0;
  while (true) {
    /* Tail Recursion */
    if (times_AUTO_12 > 10000) throw new Error("tail recursion not stopping");
    let was_empty_QUES_ = $calcit.empty_QUES_(old_children);
    let now_empty_QUES_ = $calcit.empty_QUES_(new_children);

    if (was_empty_QUES_ ? (now_empty_QUES_ ? now_empty_QUES_ : false) : false) {
      ret_AUTO_11 = null;
    } else if (was_empty_QUES_ ? ($calcit.not(now_empty_QUES_) ? $calcit.not(now_empty_QUES_) : false) : false) {
      let pair = $calcit.first(new_children);
      let k = $calcit.first(pair);
      let element = $calcit.last(pair);
      let new_coord = $calcit.conj(coord, k);
      collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.append_element, new_coord, n_coord, element));
      collect_mounting(collect_BANG_, coord, $calcit.conj(n_coord, index), element, true);
      ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, $calcit.inc(index), $calcit._LIST_(), $calcit.rest(new_children));
    } else if ($calcit.not(was_empty_QUES_) ? (now_empty_QUES_ ? now_empty_QUES_ : false) : false) {
      let pair = $calcit.first(old_children);
      let k = $calcit.first(pair);
      let new_coord = $calcit.conj(coord, k);
      let new_n_coord = $calcit.conj(n_coord, index);
      collect_unmounting(collect_BANG_, coord, new_n_coord, $calcit.last(pair), true);
      collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_element, new_coord, new_n_coord, null));
      ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, index, $calcit.rest(old_children), $calcit._LIST_());
    } else if (true) {
      let old_keys = $calcit.map($calcit.take(old_children, 16), $calcit.first);
      let new_keys = $calcit.map($calcit.take(new_children, 16), $calcit.first);
      let x1 = $calcit.first(old_keys);
      let y1 = $calcit.first(new_keys);
      let match_x1 = function f_PCT_(x) {
        if (arguments.length !== 1) throw new Error("argument sizes do not match");
        return $calcit._EQ_(x, x1);
      };
      let match_y1 = function f_PCT_(x) {
        if (arguments.length !== 1) throw new Error("argument sizes do not match");
        return $calcit._EQ_(x, y1);
      };
      let x1_remains_QUES_ = $calcit.any_QUES_(new_keys, match_x1);
      let y1_existed_QUES_ = $calcit.any_QUES_(old_keys, match_y1);
      let old_follows = $calcit.rest(old_children);
      let new_follows = $calcit.rest(new_children);

      if ($calcit._EQ_(x1, y1)) {
        let old_element = val_of_first(old_children);
        let new_element = val_of_first(new_children);
        find_element_diffs(collect_BANG_, $calcit.conj(coord, x1), $calcit.conj(n_coord, index), old_element, new_element);
        ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, $calcit.inc(index), old_follows, new_follows);
      } else if (x1_remains_QUES_ ? ($calcit.not(y1_existed_QUES_) ? $calcit.not(y1_existed_QUES_) : false) : false) {
        let pair = $calcit.first(new_children);
        let k = $calcit.first(pair);
        let element = $calcit.last(pair);
        let new_coord = $calcit.conj(coord, k);
        let new_n_coord = $calcit.conj(n_coord, index);
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.add_element, new_coord, new_n_coord, element));
        collect_mounting(collect_BANG_, coord, new_n_coord, val_of_first(new_children), true);
        ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, $calcit.inc(index), old_children, new_follows);
      } else if ($calcit.not(x1_remains_QUES_) ? (y1_existed_QUES_ ? y1_existed_QUES_ : false) : false) {
        let pair = $calcit.first(old_children);
        let k = $calcit.first(pair);
        let new_coord = $calcit.conj(coord, k);
        let new_n_coord = $calcit.conj(n_coord, index);
        collect_unmounting(collect_BANG_, coord, new_n_coord, $calcit.last(pair), true);
        collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_element, new_coord, new_n_coord, null));
        ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, index, old_follows, new_children);
      } else if (true) {
        let xi = $calcit.index_of(new_keys, x1);
        let yi = $calcit.index_of(old_keys, y1);
        let first_old_entry = $calcit.first(old_children);
        let first_new_entry = $calcit.first(new_children);
        let new_n_coord = $calcit.conj(n_coord, index);

        if ($calcit._LT__EQ_(xi, yi)) {
          let new_element = val_of_first(new_children);
          let new_coord = $calcit.conj(coord, y1);
          collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.add_element, new_coord, new_n_coord, new_element));
          collect_mounting(collect_BANG_, coord, new_n_coord, new_element, true);
          ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, $calcit.inc(index), old_children, new_follows);
        } else {
          collect_unmounting(collect_BANG_, coord, new_n_coord, val_of_first(old_children), true);
          collect_BANG_($calcit._LIST_($respo_DOT_schema_DOT_op.rm_element, $calcit.conj(coord, x1), new_n_coord, null));
          ret_AUTO_11 = $calcit.recur(collect_BANG_, coord, n_coord, index, old_follows, new_children);
        }
      } else {
        ret_AUTO_11 = null;
      }
    } else {
      ret_AUTO_11 = null;
    }

    if (ret_AUTO_11 instanceof $calcit.CalcitRecur) {
      if (ret_AUTO_11.args.length !== 6) throw new Error("argument sizes do not match");
      [collect_BANG_, coord, n_coord, index, old_children, new_children] = ret_AUTO_11.args;

      times_AUTO_12 += 1;
      continue;
    } else {
      return ret_AUTO_11;
    }
  }
};

also to mention that a new CLI bundle_calcit was added weeks back. for people hate using calcit-editor, it’s still okay to write calcit-js, just losing hot code swapping.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.

add page: compare to Clojure:
From Clojure - Calcit Guide

Calcit is mostly a ClojureScript dialect. So it should also be considered a Clojure dialect.

There are some significant features Calcit is learning from Clojure,

  • Runtime persistent data by default, you can only simulate states with Refs.
  • Namespaces
  • Hygienic macros(although less powerful)
  • Higher order functions
  • Keywords
  • Compiles to JavaScript, interops
  • Hot code swapping while code modified, and trigger an on-reload function
  • HUD for JavaScript errors

Also there are some differences:

Feature Calcit Clojure
Host Language Rust, and use dylibs for extending Java/Clojure, import Mavan packages
Syntax Indentations / Syntax Tree Editor Parentheses
Persistent data unbalanced 2-3 Tree, with tricks from FingerTree HAMT / RRB-tree
Package manager git clone to a folder Clojars
bundle js modules ES Modules, with ESBuild/Vite Google Closure Compiler / Webpack
operand order at first at last
Polymorphism at runtime, slow (.map ([] 1 2 3) f) at compile time, also supports multi-arities
REPL only at command line: cr --eval "+ 1 2" a real REPL
[] syntax [] is a built-in function builtin syntax
{} syntax ({} (:a b)) is macro, expands to (&{} :a :b) builtin syntax

also Calcit is a one-person language, it has too few features compared to Clojure.