Notes To Myself

Notes to myself while designing this project and scheduling TODO entries, i.e., How To Implement Your Own Shitty Emacs.

1. Notes

1.1. Display Engine

There've been some discussions:

However, I don't think I should start designing it before fully understanding what redisplay does (maybe by digging into my centaur-tabs crashes).

Also: there are other Emacs re-implementations and the best we can get is to work together towards a new display backend that hopefully can also work on GNU Emacs.

1.1.2. Personal Preferences

Despite the usual complaints about how Electron bloats, I actually think it is good to simply embed the rendering engine of any browser:

  1. They should support BIDI out of the box. (Well, since Emacs also supports turning off BIDI, this might not be as good after all.)
  2. Decent performance.
  3. Multi-media support.

Any of the above will take significant effort if we are to implement them ourselves (correctly). Of course we can offload the work to some UI frameworks, but I believe BIDI support will rule out most of the so-called modern GPU-powered ones.

1.2. Obarrays

So I was testing on Emacs 29 because it was what the official Arch repo offers. Recently I have switched to Emacs 30 since it is what we aim to support, and it turns out… I should have switched much much earlier.

The single change: obarrays are now opaque types.

In Emacs 29, obarrays are vectors with symbols (or 0) as their elements. It is basically a hash table with the internal linked list stored directly in its symbols. So when we (aset ob1 0 (aref ob2 0)), I have no idea what might happen, but it is a behavior that we can observe and reproduce, and thus a thing that we need to support by making symbols part of the obarray, if we were targeting Emacs 29. With opaque obarrays, we can now make symbols immutable, and separate symbols and value containers.

So, thank goodnees for making more things opaque, Emacs developers!

1.3. Emacs MULE

1.3.1. Example 1: iso-2022-jp

I know little about the history behind Emacs. But according to XEmacs Lisp Reference Manual: 63. MULE and Frequently Asked Questions about Emacs - Question 27 (What variants of GNU Emacs exist?), MULE comes from Japanese forks of Emacs, which I do think are some Emacs variants (besides XEmacs) that should be remembered.

These days, Emacs can handle multilingual text and can (seemingly) losslessly convert between Unicode and other charsets, even for some charsets not yet mapped to Unicode. Emacs achieves this by extending the code point range of its internal UTF-8-like string representation to 0 - #x3FFFFF: The 0 - #x10FFFF range is for Unicode, and the remaining is for lossless storage of non-convertible characters.

The following example is inspired by Unicodeにマッピングされていない文字 - 清水 川のScrapbox ("Characters that are not yet mapped to Unicode" in English):

(let* (;; 洲﨑神社 if the text were convertible to Unicode
       (jis-encoded-bytes  "\x1b$B='yu?@<R\x1b(B")
       ;; Losslessly decoding to Emacs strings
       (lossless-string    (decode-coding-string jis-encoded-bytes 'iso-2022-jp))
       (convertible-char-1 (aref lossless-string 0))
       (non-convertible    (aref lossless-string 1))
       (re-encoded-bytes   (encode-coding-string lossless-string 'iso-2022-jp)))
  `(("Converted string"
     ;; Replace non-Unicode chars so that the outputs won't disrupt the coding of this file.
     ;; It did corrupted the encoding last time...
     ,(replace-regexp-in-string "[^\0-\U0010FFFF]" "?" lossless-string))
    ("Original bytes" ,(prin1-to-string jis-encoded-bytes))
    ("Re-encoded bytes" ,(prin1-to-string re-encoded-bytes))
    ("Convertible char" ,(format "%c (#x%06x < #x10FFFF)" convertible-char-1 convertible-char-1))
    ("Non-convertible"  ,(format "__ (#x%06x > #x10FFFF)" non-convertible))))
Converted string 洲?神社
Original bytes "$B='yu?@<R(B"
Re-encoded bytes "$B='yu?@<R(B"
Convertible char 洲 (#x006d32 < #x10FFFF)
Non-convertible __ (#x1420a4 > #x10FFFF)

1.3.2. Example 2: lisp/language/ethiopic.el

So awhile ago I started bootstraping files from emacs/lisp/language/*.el, and… there you go. Another example right in the source code:

(let ((non-unicode (with-temp-buffer
                     (insert-file (locate-file "ethiopic.el" load-path))
                     (replace-regexp "[\0-\U0010FFFF]+" "")
                     (buffer-string))))
  (format "#x%X > #x10FFFF" (aref non-unicode 0)))
#x1A01CA > #x10FFFF

There are some more files similar to ethiopic.el:

(defun file-contains-non-unicode? (file)
  (with-temp-buffer
    (insert-file file)
    (search-forward-regexp "[^\0-\U0010FFFF]" nil t)))

(let* ((language-dir (file-name-parent-directory (locate-file "ethiopic.el" load-path)))
       (files (append (directory-files language-dir t ".*\\.el$")
                      (directory-files (file-name-concat (file-name-parent-directory language-dir)
                                                         "international")
                                       t ".*\\.el$")))
       non-unicode-files)
  (dolist (file files)
    (if (file-contains-non-unicode? file)
        (push (list (file-name-nondirectory file)) non-unicode-files)))
  non-unicode-files)
titdic-cnv.el
tibetan.el
tibet-util.el
ind-util.el
ethiopic.el
ethio-util.el

1.4. Truffle Debugging

During performance testing, I found the following options (supplied to Context builders) quite useful:

.allowExperimentalOptions(true)
.option("engine.TraceCompilation", "true")
.option("engine.CompilationFailureReaction", "Diagnose")
.option("engine.SpecializationStatistics", "true")
.option("compiler.TraceMethodExpansion", "truffleTier")

To look into GraalVM compiler graphs, one may use Ideal Graph Visualizer or Seafoam.

  • SpecializationStatistics requires something like:

    tasks.withType(JavaCompile) {
        options.compilerArgs += '-Atruffle.dsl.GenerateSpecializationStatistics=true'
    }
    

1.5. Dynamic ELisp & Truffle

So special forms in Emacs Lisp is actually "special" ordinary functions, which means… one can overwrite them or assign them to other symbols. So this means you cannot convert a lisp cons (my-func (func?)) to an AST until you evaluate it.

  • For a normal function, the above may expand to FuncCallNode[my-func, FuncCallNode[func?]].
  • For special forms…, like (defalias 'my-func 'quote), it becomes FuncCallNode[my-func, LispObject[(func?)]].

It makes it hard to cache the CallTarget used by Truffle. Also, since a function can be a macro that yields dynamic AST, I don't think it is actually possible to Truffle-JIT this thing efficiently… So bytecode it is.

P.S. The current interpreted AST node ELispInterpretedNode.java somehow manages to produce an AST-ish thing for Truffle to JIT-compile. It is probably not that efficient though. Read the code and comments for more details.

1.5.1. Efficient local variable lookup

Most Truffle languages store local variables on frames (VirtualFrame). There are some optimizations with this approach:

  • Truffle can optimize many things for you by inlining and placing things on a real stack after JIT-compilation.
  • Nodes for reading and setting variables can cache the location of a specific variable, yielding better performance.
  • Lexical scopes are now materialized frames, which Truffle is aware and can potentially optimize/inline away the cost.

However, as always, Emacs Lisp brings some problems:

  • Most languages use static analysis to map variable names to their slot number. Static analysis ensures the slot numbers do not change between calls and is safe to cache.

    function f(
      a,  // slot#0
      b,  // slot#1
    ) {
      const c = 0;  // slot#2
      if (a) {
        let d = 1;  // slot#3
        b += d + a;
      }
      let e = c + b;  // slot#4 or slot#3 if reusing slots
      return e;
    }
    
  • As for ELisp, well, you cannot do static analysis with a dynamic AST.

    (defun f (a  ;; slot#0
              b  ;; slot#1
              )
      (some-macro
       ;; the slot of `c' depends on the expansion result of `some-macro'
       (let ((c 1)))))
    

    Alternatively, similar to how we handle things in ELispInterpretedNode.java, we may dynamically assign slot numbers and cache at runtime instead. However, Truffle FrameDescriptor requires a constant slot count at declaration time, which poses yet another challenge.

  1. Previous solution
    • Use a constant slot count (~32) for frame descriptors. And spill to a separate ArrayList if there are too many variables.
    • Keep static analysis stats on the stack frame:
      • Slot #0: LexicalFrame
        • Keeping track of occupied frame slots, symbol-to-slot-number mappings
          • Slot numbers: positive: local variable; zero or negative: argument
        • Parent frame reference
        • Whether the current frame is materialized: We can (only) reuse slots when the frame is not yet materialized (most helpful with let blocks in loops).
      • Slot #1: Frame spill ArrayList slot
      • Other slots: ordinary value slots
    • Future enhancements (or are they?):
      • Track required slot counts for each function and try to pre-allocate (or shrink stack size) for the following calls.
        • Using 8 instead of 32 as the slot count does seem to affect throughput though.
      • Benchmark to find more hotspots (possibly in assembly).
  2. Re-implementation

    It is too costly to maintain separate lexical contexts for every function call. Instead, we move the lexical context from stack frame to AST nodes with the following modification:

    • We never reuse stack slots. Since every slot has its own long/double specialization, reusing slots will risk invalidating the specializations.
    • Every variable is allocated a constant slot number.

    However, when users create closures in a loop, we might need to copy the frames (and the lexical context) for the closures. See ELispLexical.java for the implementation and BuiltInEvalTest.java for an example (testPerIterationScope).

    This improves the performance a little bit. However, after this, we changed ELispFrameSlotNode.java to use primitive frame slots whenever possible, which brings a ~3x performance boost and we run mandelbrot almost as fast as Java now.

  3. Re-re-implementation

    Now we follow what GraalJs does and implement dynamic variable slot allocation by creating a frame chain. It turns out that Graal/Truffle does an amazing job at optimizing materialized frames away.

1.5.2. The Graal Truffle tutorial series by Adam Ruka

var absFuncRootNode = new FunctionRootNode(
  this,
  AbsFunctionBodyExprNodeGen.create(new ReadFunctionArgExprNode(0)),
);
context.globalScopeObject.newConstant(
  "Math.abs",
  new FunctionObject(absFuncRootNode.getCallTarget()),
);

Basically:

  • FunctionCallExprNode evaluates all its children and dispatches the call with FunctionDispatchNode, which uses a DirectCallNode to call a FunctionObject.
  • Math.abs is a FunctionObject which is simply a wrapper for a CallTarget,
  • The CallTarget is obtained from a RootNode subclass (FunctionRootNode), which is wrapped around a function body node (AbsFunctionBodyExprNode extending EasyScriptExprNode).
  • AbsFunctionBodyExprNode uses ReadFunctionArgExprNode as a child node to read parameters from the VirtualFrame.
  • @GenerateNodeFactory is used to ease writing more built-in functions.

1.6. Truffle Function Calls

It is quite hard to gather all the details needed to build an efficient function call system in Truffle. Basically, we want:

  1. Built-in functions with less boilerplate code
  2. Support for user-defined functions
  3. Fixed args, optional args and varargs
  4. Efficiency

And we need the following mechanism:

  1. Global/local variable dereferencing for named function calls
  2. Vararg parameter passing
  3. Fixed arg parameter passing optimization

We will try to first follow a tutorial and then look into how some official implementations do this.

1.6.1. Lambda Reimplementation

So it seems I have mis-implemented closure creation: (cl-loop for i from 0 to 10 collect (lambda () i)) should let all these lambdas share their AST and internal FunctionRootNode so as to reduce costs. Otherwise, because nodes should only be created in interpreted mode, every time a new closure is created, the code will deopt, jumping from compiled code to interpreted execution, causing significant slowdown.

I will try to follow how GraalJs handles this. (TL;DR: Read JSArguments.java, JSFunctionObject.java and finally JSFunctionCallNode.java.)

  1. I was benchmarking (mapc ...), so let's take a look at Array.forEach in GraalJs, which seems to be in ArrayPrototypeBuiltins.java.
  2. The corresponding node implementation is JSArrayForEachNode, which depends on two other nodes:
    • ArrayForEachIndexCallOperation: Parent node
    • MaybeResultNode: Whether to continue execution, used by ArrayForEachIndexCallOperation node
  3. After basic type checking, the @Specialization method in JSArrayForEachNode eventually calls forEachIndexCall, which is implemented by ArrayForEachIndexCallOperation.
  4. ArrayForEachIndexCallOperation seems a tiny wrapper around ForEachIndexCallNode, located in ForEachIndexCallNode.java (which also contains the definition of MaybeResult).
  5. ForEachIndexCallNode:
    • executeForEachIndexFast
    • callback(index, value, target, callback, callbackThisArg, currentResult)
    • callbackNode.apply(index, value, target, callback, callbackThisArg, currentResult)
    • maybeResultNode.apply(index, value, callbackResult, currentResult)
  6. callbackNode is a DefaultCallbackNode passed from ArrayForEachIndexCallOperation.
  7. The callbackNode further wraps around JSFunctionCallNode and JSArguments: callNode.executeCall(JSArguments.create(callbackThisArg, callback, value, boxIndex(index), target)).

Looking at JSArguments and JSFunctionObject, we can take a glimpse of how GraalJs handles closures:

  • Functions created from a certain "closure creating node" share their RootNode.
  • JS Functions, when called, are passed two extra arguments: this and the function object itself.
  • Function objects store captured parent frames.

So, in order to follow suit, we need to:

  1. Change the current call convention
  2. Let function special form (and make-closure function) caches and reuse previous AST (RootNode).

1.6.2. SimpleLanguage - Official Implementation #1

Its approach is quite similar to the previous tutorial:

  • SLInvokeNode corresponds to function call AST node. It uses InteropLibrary to dispatch calls though.
  • SLFunctionRegistry: Maps function names to SLFunction objects.
  • SLFunction wraps a RootCallTarget.
  • SLPrintlnBuiltin extends SLBuiltinNode (which has the @GenerateNodeFactory annotation and extends SLExpressionNode).
  • Built-in functions are registered by SLContext::installBuiltins, which in turn calls SLLanguage::lookupBuiltin to setup SLReadArgumentNode and functions.
  • Notably, it seems to use a CyclicAssumption to detect call target changes.

1.6.3. GraalJs

  • JSFunctionCallNode has a internal function object cache.
  • No @GenerateNodeFactory is used. Global functions are setup with JSRealm::setupGlobals with hand-written function lists.
  • JSFunction wraps (deeply) a CallTarget.
  • Notably, JSFunctionCallNode implements a rather complex caching logic in its executeAndSpecialize function.

1.6.4. GraalPython

  • The built-in functions in GraalPython show-case an advanced (undocumented?) usage of @GenerateNodeFactory:

    It seems that, when annotating inner classes with @GenerateNodeFactory, the DSL processor will also generate a factory for the outer class, containing a getFactories() method returning all the inner factories. This can be extremely convenient for writing and loading built-in functions in batch.

2. Reading List

2.1. Emacs

There are quite a lot Emacs forks or re-implementations out there, with helpful comments and discussions.

  • CL-Emacs: “Various people have proposed an emacs-like editor written in Common Lisp. This page collects together a few possibilities.”
  • JEmacs: The Java/Scheme-based Emacs Text Editor (source @ GNU Kawa)
  • remacs: Emacs fork with some ELisp builtin functions rewritten in Rust
  • Rune: Emacs (mainly ELisp) reimplementation in Rust

2.3. GraalVM / Truffle

GraalVM: “An advanced JDK with ahead-of-time Native Image compilation”

2.4. Others

  • Speculation in JavaScriptCore | WebKit
  • Conflict-free Replicated Data Type

    More thoughts: To allow automatic concurrentization, we will need to deal with concurrent buffer access. Some ways I can think of:

    1. No handling at all. So, for example, when the user requests completion at prefix<cursor> and moves the cursor to elsewhere before the scripts actually inserts any content, this will most likely lead to the content inserted at a wrong position.
      • Can the user correctly interpret this behavior? Sometimes, if the delay is not too long.
      • Can undo help? Yes. If the user is not satisfied by the insersion, they can simply revert it.
      • Thread safety? If access to a buffer does not block (or leads to green threads switching contexts), yes.
    2. Use locks.
      • Caveat: It leads to automatic deadlocks.
    3. CRDT: After the reasoning above, CRDT actually seems the same (at least to the user): interpretable if the delay is short; the user should be able to undo; thread safe (albeit maybe safer). But it does allow the user to move the cursor elsewhere while waiting for completion.
    4. OT. Simpler than CRDT.

Emacs 31.0.50 (Org mode 9.7.11)