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:
- https://lists.gnu.org/archive/html/emacs-devel/2021-12/msg02652.html
- https://lists.gnu.org/archive/html/emacs-devel/2024-12/msg00455.html
- https://github.com/CeleritasCelery/rune/issues/61
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.1. Reading List: Rendering
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:
- They should support BIDI out of the box. (Well, since Emacs also supports turning off BIDI, this might not be as good after all.)
- Decent performance.
- 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.
SpecializationStatisticsrequires 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 becomesFuncCallNode[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, TruffleFrameDescriptorrequires a constant slot count at declaration time, which poses yet another challenge.
- Previous solution
- Use a constant slot count (
~32) for frame descriptors. And spill to a separateArrayListif 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
letblocks in loops).
- Keeping track of occupied frame slots, symbol-to-slot-number mappings
- Slot #1: Frame spill
ArrayListslot - Other slots: ordinary value slots
- Slot #0:
- 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
8instead of32as the slot count does seem to affect throughput though.
- Using
- Benchmark to find more hotspots (possibly in assembly).
- Track required slot counts for each function and try to pre-allocate (or
shrink stack size) for the following calls.
- Use a constant slot count (
- 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/doublespecialization, 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.javafor the implementation andBuiltInEvalTest.javafor an example (testPerIterationScope).This improves the performance a little bit. However, after this, we changed
ELispFrameSlotNode.javato use primitive frame slots whenever possible, which brings a ~3x performance boost and we runmandelbrotalmost as fast as Java now. - We never reuse stack slots. Since every slot has its own
- 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:
FunctionCallExprNodeevaluates all its children and dispatches the call withFunctionDispatchNode, which uses aDirectCallNodeto call aFunctionObject.Math.absis aFunctionObjectwhich is simply a wrapper for aCallTarget,- The
CallTargetis obtained from aRootNodesubclass (FunctionRootNode), which is wrapped around a function body node (AbsFunctionBodyExprNodeextendingEasyScriptExprNode). AbsFunctionBodyExprNodeusesReadFunctionArgExprNodeas a child node to read parameters from theVirtualFrame.@GenerateNodeFactoryis 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:
- Built-in functions with less boilerplate code
- Support for user-defined functions
- Fixed args, optional args and varargs
- Efficiency
And we need the following mechanism:
- Global/local variable dereferencing for named function calls
- Vararg parameter passing
- 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.)
- I was benchmarking
(mapc ...), so let's take a look atArray.forEachin GraalJs, which seems to be inArrayPrototypeBuiltins.java. - The corresponding node implementation is
JSArrayForEachNode, which depends on two other nodes:ArrayForEachIndexCallOperation: Parent nodeMaybeResultNode: Whether to continue execution, used byArrayForEachIndexCallOperationnode
- After basic type checking, the
@Specializationmethod inJSArrayForEachNodeeventually callsforEachIndexCall, which is implemented byArrayForEachIndexCallOperation. ArrayForEachIndexCallOperationseems a tiny wrapper aroundForEachIndexCallNode, located inForEachIndexCallNode.java(which also contains the definition ofMaybeResult).ForEachIndexCallNode:executeForEachIndexFastcallback(index, value, target, callback, callbackThisArg, currentResult)callbackNode.apply(index, value, target, callback, callbackThisArg, currentResult)maybeResultNode.apply(index, value, callbackResult, currentResult)
callbackNodeis aDefaultCallbackNodepassed fromArrayForEachIndexCallOperation.- The
callbackNodefurther wraps aroundJSFunctionCallNodeandJSArguments: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:
thisand the function object itself. - Function objects store captured parent frames.
So, in order to follow suit, we need to:
- Change the current call convention
- Let
functionspecial form (andmake-closurefunction) caches and reuse previous AST (RootNode).
1.6.2. SimpleLanguage - Official Implementation #1
Its approach is quite similar to the previous tutorial:
SLInvokeNodecorresponds to function call AST node. It usesInteropLibraryto dispatch calls though.SLFunctionRegistry: Maps function names toSLFunctionobjects.SLFunctionwraps aRootCallTarget.SLPrintlnBuiltinextendsSLBuiltinNode(which has the@GenerateNodeFactoryannotation and extendsSLExpressionNode).- Built-in functions are registered by
SLContext::installBuiltins, which in turn callsSLLanguage::lookupBuiltinto setupSLReadArgumentNodeand functions. - Notably, it seems to use a
CyclicAssumptionto detect call target changes.
1.6.3. GraalJs
JSFunctionCallNodehas a internal function object cache.- No
@GenerateNodeFactoryis used. Global functions are setup withJSRealm::setupGlobalswith hand-written function lists. JSFunctionwraps (deeply) aCallTarget.- Notably,
JSFunctionCallNodeimplements a rather complex caching logic in itsexecuteAndSpecializefunction.
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 agetFactories()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.1.1. Emacs Lisp
- EmacsWiki: Hacker Guide
- Emacs Types - remacs Wiki
- emacs 源码分析(二): Contains a pretty diagram for some structs
- Emacs internal string encoding - remacs Issue #499
- read0 @ src/lread.c: The Emacs Lisp S-expr lexer & parser
- [PDF] Conceptual Views of EMACS's Architecture - chrismennie.ca!
- bytecode.rs @ rune
- [PDF] GNU Emacs Lisp Bytecode Reference Manual (TeX source here)
2.2. Regular Expressions
- The must-read series by Russ Cox
- Linear Matching of JavaScript Regular Expressions
- An ECMAScript 2015-Compliant Automata-based Regular Expression Engine for Graal.js (PDF) (the TRegex engine provided by Truffle)
- Extending the PCRE Library with Static Backtracking Based Just-in-Time Compilation Support
2.3. GraalVM / Truffle
GraalVM: “An advanced JDK with ahead-of-time Native Image compilation”
- GraalPy, the GraalVM Implementation of Python
- What To Read: Excellent (as well as performance-centric) usage of
@GenerateNodeFactory. - BuiltinCallNode.java
- BuiltinFunctionRootNode.java
- What To Read: Excellent (as well as performance-centric) usage of
- Espresso - Java On Truffle
- What To Read: How to write a byte-code interpreter efficiently with Truffle
- On-Stack Replacement (OSR)
- Mumble
- Graal Truffle tutorial part 0 – what is Truffle?: A series of Truffle tutorial on a JS-like language
- Truffle ISLISP: A Lisp-variant language implemented with Truffle
2.4. Others
- Speculation in JavaScriptCore | WebKit
Conflict-free Replicated Data Type
- Peritext - A CRDT for Rich-Text Collaboration
- Thoughts: This might help if we are to enable transparent parallelization for Emacs Lisp. For simple variables, we can do pretty well with a Copy-on-Write global environment. However, we will need a concrete way to handle parallelized buffer edits.
- Collaborative Text Editing with Eg-walker: Better, Faster, Smaller
More thoughts: To allow automatic concurrentization, we will need to deal with concurrent buffer access. Some ways I can think of:
- 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.
- Use locks.
- Caveat: It leads to automatic deadlocks.
- 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.
- OT. Simpler than CRDT.
- Peritext - A CRDT for Rich-Text Collaboration