TODO

1. TODO let/let* Buffer Local Logic

So, old Emacs had a bug:

(let ((buffer-file-name "/home/rms/.emacs"))
  ;; ...
  (set-buffer other-buffer)
  ;; ...
  )

It seems… we also have this bug.

1.1. Thread-local buffer-locals

Currently thread-local buffer-locals are treated as thread-locals. But they should be buffer-local thread-locals ;-) Buffalo buffalo buffalo.

1.2. Other local-locals

So, other than buffer-locals (and our thread-locals), Emacs also has "frame-locals" and "terminal-locals". I think we might need to unify the API a bit to make things easier to implement. Currently, we do have this ValueStorage.Value interface though, so maybe:

  • ThreadLocalStorage: We treat this specially now, because probably we could use a bit of performance for them.
  • Value interface for locals:
    • ForwardedPerBuffer
    • BufferLocal
    • ForwardedPerFrame?
    • ForwardedPerTerminal?

2. TODO Byte compiler warnings

(require 'bytecomp) throws a bunch of errors, but (byte-compile '(lambda () nil)) runs fine otherwise. I suspect that it comes from discrepancies with cconv.el.

3. TODO Frames, Windows, etc.

So we just have a few lines to go before fully bootstrapping loadup.el. However, these lines calls top-level and runs startup.el, which seems to require a fully functioning Emacs to work: it expects Emacs frames.

Since Emacs supports batch mode, I guess we can go with a dummy frame. But, hey, we will need a UI eventually and it will heavily influence our designs. Let's build a GUI client and a C/S protocol (like neovim) before we go down the wrong way.

4. TODO Optimize string operations

There are a few optimizations for string operations:

4.1. Lazy data

Lazy data for concat, useful for nested concat/format/... calls.

4.2. JIT-capable format

format function calls very often comes with constant format strings. It might help if we can turn (format "example: %d" var) into (concat "example" (format-%d var)) during JIT.

5. TODO Regexp redesign

So Emacs regexps can change depending on their contexts (e.g., the case tables and syntax tables of the current buffer), which makes it a no-go for NFA or DFA based implementations… Or does it?

The TRegex library from Truffle has a DFA implementation and falls back to a backtracking one for certain regexps. We do not use it because we need to support buffers, while TRegex requires TruffleString, but we can follow what it does and have two regexp engines.

However, if we are to handle more than some simple regexps, we might have some problems. The main problem is that regexps depends on (at least?) three char tables and an external variable:

  • Syntax tables
  • Category tables
  • Case tables and case-fold-search

For case-fold-search, Emacs has a canon table to normalize chars and we might do that beforehand and use an Assumption to track table changes. But for other char tables, I don't know… maybe we should just declare defeat? (Otherwise we need a way to include char-table-based state changes in NFAs and find a way to convert them into DFAs.)

5.1. Static Backtracking

PCRE-sljit seems to use a technique called "static backtracking". The basic idea is to represent success/failures with control flows instead of boolean values, which is only achievable under JIT with unstructured control flow. (For example, the patten /p/ checks the current char, if it matches p, it jumps to some ACCEPT path or otherwise a BACKTRACK path.)

I tried to re-implement the engine this way, but find the performance quite bad. One explanation may be: Graal/Truffle does not have very good support for unstructured control flow. Quoting from GraalVM Slack:

Josef Haider (on Dec 2019): MERGEEXPLODE supports unstructured control flow by falling back to an explicit state machine in the compiled code.

Chris Seaton (on Oct 2022): I wrote a blog post about irreducible loops, and yes Graal only supports them with a custom duplication phase, purely for the reason of supporting Kotlin's co-routines.

David Leopoldseder (on Oct 2022): … The optimizer is written based on our structured loop representation in loop closed SSA form, really supporting only reducible CF. … We do a limited form of support for detecting some common irreducible loops during PE and rewrite them to a switch pattern…

The quotes above explains several things in the IGV graphs of the static backtracking implementation:

  • There are several huge IntegerSwitch nodes.
  • The graph contains many Loop nodes.
  • The whole graph is just super complicated.

And I guess it also explains the performance. Anyway, I guess we will need to base our engine on boolean values and proper loops then. (The implementation is in the git logs in case any one is interested.)

5.2. Backtracking Regexps

  • pcomplete.el

    "\\(?:.\\|\n\\)*?\\(\\(?:.\\|\n\\)*\\)\x3FFF7F\\(?:.\\|\n\\)*\\1\\'"
    
  • dirtrack.el

    "\\`\\(.*\\)\\(?:/.*\\)?\n\\(.*/\\)\\1\\(?:/.*\\)?\\'"
    
  • diff-mode.el

    (concat
     "\\`\\(.*?\\)\\(.*\\)\\(.*\\)\n"
     "\\1\\(.*\\)\\3\n"
     "\\(.*\\(\\2\\).*\\)\\'")
    

6. TODO Implement placeholder functions

I've made lots of functions no-op in order to bootstrap ert.el. And we should probably return to them some time:

  • [X] decode-coding-string
  • [X] kill-emacs: This actually will not run code in unwind-protect, so a special exception probably will not do. See graal/truffle/docs/Exit.md.
  • [ ] framep, processp, windowp, etc.
  • [ ] Buffer keymaps
  • [-] Syntax-related functions
    • [X] skip-chars-backward, skip-chars-forward
    • [ ] skip-syntax-forward
    • [ ] forward-comment
    • [ ] scan-sexps: Currently we use our ELisp parser for this. But this actually scans "lisp-like" s-exps according to the syntax table?
    • [ ] backward-prefix-chars
  • [X] format-time-string

7. TODO Support native-comp

So this is what the bytecode compiler does:

(lisp-code) –> (lap) –> (bytecode)

And this is what the native compiler does:

(lisp-code) –> (lap) –> (limple) –> (gcc-ir) –> (native-code)

| these parts implemented in emacs lisp ||| these are in C |

It should be possible that we intercept the calls from comp--final1 and replace everything on the C end for our own implementation:

(lisp-code) –> (lap) –> (limple) -//-> (truffle-bytecode-dsl)

Also, Truffle bytecode DSL supports serialization, which means we might even be able to dump the bytecode into .eln files and simulate exactly native-comp behaviors.

7.1. Advantages

8. TODO add-variable-watcher

Currently I've only seen this used to redisplay things when certain variables change. So we can live without it for a while.

9. TODO Automatically free container objects when the corresponding symbol goes out of scope

Currently we follow TruffleRuby to store symbol values similar to global variables. However, symbols can go out of scope and their corresponding values should be reclaimed. Our Map<ELispSymbol, Integer> mapping prevents this and should probably get replaced by a weaker map.

However, I don't think many people are using non-interned symbols as a value container. So it is probably fine.

10. TODO Understanding Non-Bare Symbols

According to Symbols with Position (GNU Emacs Lisp Reference Manual), it seems only used in bytecode functions for debugging info. But I still hope we can make all symbols bare.

Update: emacs-devel is currently discussing alternative strategies for keeping positions in bytecode compilation (2025-10/msg00668). There are some ideas suggesting removing symbol-with-pos altogether from Emacs. We might as well wait a little bit and see where that discussion leads.

11. TODO Autoload

So basically one can have autoload functions as well as autoload strings (docstrings). What else?

  • [X] So we now handles autoload functions. However, the generated loaddefs.el seems to invoke rx before its autoload definition. How does Emacs even support this?
    • Answer: Emacs has two bootstrapping phases:
      1. pbootstrap: It interprets .el files and uses a tailored ldefs-boot.el that avoid this kind of use-before-definition situations.
      2. pdump: pbootstrap will compile (almost) all code into bytecode .elc files. Since rx is a macro, it gets expanded during bytecode compilation. Then, pdump loads bytecode where macro-use-before-definition will never happen.
  • [ ] Emacs autoload reads and sets several variables and supports undoing the loaded changes… How? (It seems related to feature unloading.)

12. TODO Threading preparations

Use a custom scheduler by using reflection. Also, Truffle has a bunch of thread-local fields that need to be initialized with TruffleLanguage.Env#newTruffleThreadBuilder. We will see if we can do this with our own scheduler.

13. TODO Charsets & Coding

Emacs MULE.

The most significant properties of charsets seem to be:

  • Mapping between byte-sequence and character code
  • Mapping between character code and Unicode codepoint

The mappings are stored in files generated from glibc charset data files.

See also:

14. TODO Keymaps

I now know there are sparse maps and dense ones, and they nest. And keymaps are index by character codes most of the time. However, I have seen it indexed with [t]. No idea what it is all about.

Edit: See comments in BuiltInKeymap.java for what keymap is about.

  • [ ] Default values
  • [ ] Auto-convert a sparse one to a dense one like Emacs (when?)
  • [-] map-keymap
    • [X] map-char-table

15. Trackers

15.1. Language [4/6]

  • [X] Emacs Lisp reader (lexer & parser)
  • [X]

    Buffer-local variables & scoping

    Basically, in addition to buffer-local variables, forwarded variables, etc., we want to add transparent "thread-local" variables, so that:

    • Dynamically bound variables are thread-local, during the lifetime of which other threads sees the original value.
    • Some specific variables must be thread-local to make transparent concurrentization work.
    • Also, lexical scopes are always thread-local.
    • [X] Handle default values
  • [X] All special forms
  • [ ] Bootstrap loadup.el
  • [X] A fallback, feature-complete regex engine -> no
  • [-] Emacs Lisp byte-code interpreter in Truffle
    • [X] ELispBytecodeFallbackNode: a fallback bytecode interpreter with not-bad JIT performance
    • [ ] A bytecode interpreter based on nativecomp?

15.2. Types [5/6]

Hopefully we don't need to take too much effort to implement these tons of types. We might need some boilerplate code for strings / integers for Truffle interop, but otherwise simply using some classes with public member should do.

  • [X] Lisp_Symbol
    • Constant marker
    • Intern state
    • Special?
    • Name
    • Value (cache)
      • Types:
        • Plain var
        • Varalias
        • Localized var (buffer local variables)
        • Forwarding variable
    • Function value (cache)
    • Property list
  • [X] Lisp_Int*
  • [X] Lisp_String
  • [-] Lisp_Vectorlike [13/36]
    • [X] PVEC_NORMAL_VECTOR
    • [ ] PVEC_FREE
    • [X] PVEC_BIGNUM
    • [X] PVEC_MARKER
    • [ ] PVEC_OVERLAY
    • [ ] PVEC_FINALIZER
    • [X] PVEC_SYMBOL_WITH_POS (maybe integrate into ELispSymbol)
    • [ ] PVEC_MISC_PTR
    • [ ] PVEC_USER_PTR
    • [ ] PVEC_PROCESS
    • [ ] PVEC_FRAME
    • [ ] PVEC_WINDOW
    • [X] PVEC_BOOL_VECTOR
    • [X] PVEC_BUFFER
    • [X] PVEC_HASH_TABLE
    • [X] PVEC_OBARRAY
    • [ ] PVEC_TERMINAL
    • [ ] PVEC_WINDOW_CONFIGURATION
    • [X] PVEC_SUBR
    • [ ] PVEC_XWIDGET
    • [ ] PVEC_XWIDGET_VIEW
    • [ ] PVEC_THREAD
    • [ ] PVEC_MUTEX
    • [ ] PVEC_CONDVAR
    • [ ] PVEC_MODULE_FUNCTION
    • [ ] PVEC_NATIVE_COMP_UNIT
    • [ ] PVEC_TS_PARSER
    • [ ] PVEC_TS_NODE
    • [ ] PVEC_TS_COMPILED_QUERY
    • [ ] PVEC_SQLITE
    • [X] PVEC_CLOSURE
    • [X] PVEC_CHAR_TABLE
    • [X] PVEC_SUB_CHAR_TABLE
    • [X] PVEC_RECORD
    • [ ] PVEC_FONT
    • [ ] PVEC_TAG_MAX
  • [X] Lisp_Cons
  • [X] Lisp_Float

16. Considered Done (For Now)

16.1. CANCELLED [#A] Start-up Performance Regression?

In the past few months I've done quite some things to the codebase, including adding array cons and removing it, moving from mule-string to truffle strings, etc. Now, ELispLanguageTest has become very slow. I suspect that it is due to string-related commits, leading to increasing buffer load/parse time.

16.2. DONE Buffer & string redesign

  • Rationale #1: Personally I would like the renderer to be in Rust (because of the vast number of UI libraries there). However, there is a fundamental problem here: most Rust libraries (including std ones) assume UTF-8 coding, while we currently use a compressed UTF-32-like coding, not to mention our code points can go up to #x3FFFFF.
    • Edit: I don't think most of the Rust libraries are that good though. We want out of box support for:

      • Platform-native font fallback
      • BIDI display
      • IME support
      • Glyph level manipulation
      • Good accessibility

      I don't think these requirements are nitpicking, but, I mean, not even GTK supports them all (see A 2025 Survey of Rust GUI Libraries (13 Apr 2025)). Just WTF.

    • Another problem is that we need to pass strings to Rust via UTF-8 (or Java via UTF-16), and there will be a huge performance penalty if we are to encode strings every single time we redisplay things.
  • Rationale #2: Both Emacs buffers and strings have text properties attached to them, and when a string gets inserted into a buffer, so does its properties. So we definitely do not want two separate interval tree implementation for buffers and strings.
  • Rationale #3: We have a tree for our ropes, and we want another tree for intervals and maybe one more for our marks. What if we merge them?

16.2.1. CANCELLED Problems

  1. Since redisplay relies on text properties, I want to make the fields that it relies on "special", like:

    record Interval(@Nullable boolean fontified,
                    @Nullable Object font,
                    /* ... */
                    ELispCons otherProperties
                    ) {
    }
    

    However, this can add quite some cost to intervals in strings.

  2. Text properties are mutable, and we hope to get an immutable tree for concurrent redisplay.
  3. Performant regexps mandates quick access to buffer texts and string characters. Since we are hoping to JIT-compile regexps, we should also provide inlinable nodes for string/buffer access. Currently there is none and the thing is highly polymorphic and inefficient.

16.2.2. Thoughts on strings

  • Maybe use UTF-8 after all.
    • We need to support string-bytes and position-bytes.
    • Truffle UTF-8 encoding have specialization for ASCII text so it is not that bad?
  • You don't need an actually-immutable tree. Instead you can have a mostly immutable tree where things used in redisplay are immutable (or volatile).
  • Besides redisplay, we also want to ensure we can have concurrent thread (that are guaranteed to never be parallel) editing the same buffer, with the redisplay engine rendering their merged results.
    • Now thinking of it, no, it is straight out impossible to do this. Consider buffer-undo-list: nothing can save you if you are fully exposing your undo data.

16.2.3. Update on strings

Emacs 31.1 is restricting modifications to strings: Incompatible Lisp Changes in Emacs 31.1:

'aset' on unibyte strings now requires the new character to be a single byte (0-255). On multibyte strings the new character and the character being replaced must both be ASCII (0-127).

I do think we are good to use utf-8-emacs and ditch TruffleString now.

16.2.4. Plan

I'll start with a third refactoring of strings… A bit of history: we started with TruffleString, replaced it with our MuleString (with a auto-compacting UTF-32 encoding) to handle utf-8-emacs encoding, and switched back to TruffleString for better JIT support. However, it turns out that TruffleString is really cumbersome to use: it doesn't handle utf-8-emacs and we have to do a bunch of nasty things; it hides its internals really well, meaning that we are limited by what its API exposes, which is not much considering the flexibility of Emacs.

So here is the plan:

  • Use utf-8-emacs as the encoding.

    We will no longer use our auto-compacting UTF-32 coding, but instead switch to the coding used by GNU Emacs. This gives us the following benefits:

    • Copy-only or even copy-free string creation is now possible if the text (or probably files) are encoded in UTF-8, which is very likely.
    • Some Emacs API can be easier to implement, like string-bytes.
    • We can still use TruffleString nodes for some strictly UTF-8 string operations. For example, when reading from files, we can use TruffleString.FromByteArrayNode to test if the string is in UTF-8, so as to use the vectorized intrinsics provided by Truffle/Graal.
  • We will have lazy string creation/concatenation nodes, just like TruffleString, but much simpler.

    Looking at some ELisp code, it is really common to have non-trivial string creation. And this should also help with (insert (format "...")).

16.2.5. DONE Object Design

  • Specialized data
    • Bytes
    • Lazy nodes (todo)
      • Lazy concat
      • Lazy integer
      • Lazy repeat
      • Lazy substring
    • Off-heap memory (todo)
  • Hash caching
  • State: ASCII, UTF-8, utf-8-emacs
    • Maybe we should merge hash & state into a single field.
  • Length (long)

16.3. DONE Use ert.el

I have stopped writing tests for newly implemented subroutines. We should definitely try to load ert.el before continue to bootstrap loadup.el.

16.4. DONE cl--generic-cyclic-definition

There is definitely a bug, but it is rather unreproducible. Can't deterministically trigger it. Have no idea what could be the reason (maybe something depending on hash table iteration order?).

(cl--generic-cyclic-definition cl-generic-generalizers)

Probably fixed: this seems related to wrong defvar semantics. I haven't seen the error ever since.

16.5. CANCELLED [#B] CDR coding or alternatives

16.5.1. Cancelled

So I had a ELispConsArray implemented, and the performance is quite bad. It is probably due to:

  1. polymorphism: you rarely get a "pure array"-like cons list. A single setcdr is enough to kill the performance.
  2. Graal/Truffle is not brave enough to treat an array-cons as a real array, incurring indirection costs. Judging from IGV graphs, the compiled code dereferences the ELispConsArray container and checks the type of the inner array every single time, which is not very efficient. It turns out to be the correct thing to do: any inner function call may call setcdr and deopt the whole thing, and what else can we do?

In conclusion, no, I cannot produce an efficient implementation (but maybe some Truffle expert can?). The code is committed and then reverted to leave traces in git logs in case anyone is interested.

16.5.2. Thoughts on conses

I am quite envious about how JS gets to optimize their internal array representations. In Lisp dialects that expose cons structure, I don't think it is possible without tons of workarounds. But… yes, let's see if we can work around that.

The basic idea is to use an unrolled linked list, treating conses as mere list iterators.

16.5.3. Workarounds

Function Cons Operation Deque Operation Frequent
cons (cons item nil) List.of(item) 🟒
  (cons item list) list.push(item) 🟒
  (cons obj1 obj2) invalid 🟒
car (car list) list.get(0) 🟒
cdr (cdr list) list.subList(1) 🟒
setcar (setcar list item) list.set(0, item) 🟒
setcdr (setcdr list nil) list.split()? ❓
  (setcdr list-end list2) list.extend(list2) 🟒
  (setcdr list obj) invalid ❓
append     🟒
nconc     🟒
memq     🟒
remq     🟒
assq     🟒
assq-delete-all     ❓
sort (sort list) list.stream().sort() 🟒
nreverse     🟒

16.6. DONE Bytecode Interpreter & Bootstrapping

Now that Truffle has a bytecode DSL, we can probably implement a bytecode interpreter more easily. However, I don't think anything (Juicemacs currently can run) in Emacs actually requires a working bytecode interpreter. Since our interpreter is more or less on par with nativecomp when fully warmed up, we can continue with our AST interpreter until we run into some real bottlenecks.

Edit: I was benchmarking using floats, which Emacs is known to be bad at. In integer-based tests our interpreter is significantly slower than nativecomp.

Edit: No, bytecomp.el requires a working bytecode interpreter.

Edit: No, I misunderstood how Emacs bootstraps so the following sub-sections are speaking nonsense. It does dump twice, but the two dumps are rather independent:

  1. pbootstrap:
    • temacs -> (load "loadup") -> (dump-emacs) -> bootstrap-emacs
    • bootstrap-emacs is used to byte-compile files into .elc
  2. pdump:
    • pdump expects .elc files, but otherwise it has nothing to do with bootstrap-emacs. (I wrongly thought bootstrap-emacs is used to dump a second time.)
    • pdump -> (load "loadup") (.elc) -> (dump-emacs) -> emacs

16.6.1. Bootstrapping

So we have a bytecode interpreter now. However, compiled Emacs .elc files seem to assume a bootstrapped environment, meaning that it expects at least autoload definitions from loaddefs.el even before loaddefs.el is loaded.

Emacs achieves this by somehow dumping its heap (approximately) and always restarts from this heap snapshot. I don't know how we are going to deal with though.

Also, currently Juicemacs load ~90% of loadup.el in around 13 seconds on my machine, which is not very acceptable. So we do need something similar to pdump.

  1. pdump

    We have a pdumper now which simply serializes all data. It is quite slow (compare: running loadup.el in 13 seconds, and loading .pdmp data in 6 seconds).

    But anyway, we can continue bootstrapping in this way (and consider accelerating data loading with native compilation after all these settle). The one thing here is that Emacs has complex bootstrapping logic:

    • Emacs seems to require two rounds of dumping:
      • pbootstrap
      • pdump
    • For example, init_buffer_once is only run on the initial run, while init_buffer is always run whether or not the current session is loaded from .pdmp.
    • The above depends on a dumped C variable bool initialized. This variable is used extensively in emacs.c during initialization and also by other source files (mostly for assertions at a glance).
    • I've met some problems already:
      • define-category errs if a category is already defined. However, characters.el (defining a bunch of categories) is always run during loadup, throwing "category already defined" errors on the second pdump run. Why? How does Emacs bootstrap circumvent this?

16.6.2. noninteractive

Currently, we set noninteractive to t in our tests and REPL to avoid running code expecting a bootstrapped environment. (We could also set dump-mode, but it seems to skip too many things.)

This includes:

  • user-emacs-directory: Set to nil by subr.el and set to a valid value in top-level. This seems to be used by Emacs loadup.el before running top-level when noninteractive is nil.

16.7. CANCELLED [#B] Make functions mutable

EDIT: This is cancelled. Let's simply try to align with the Emacs bytecode compiler: this is unsupported.

Currently in GNU Emacs 29, a function may be a lambda/closure cons:

;;; -*- lexical-binding: t -*-
(defalias 'my-func #'(lambda () 42))
(symbol-function 'my-func)
(lambda nil 42)

With lexical binding, you will get (closure (t) nil 42) instead of (lambda nil 42).

Since it is a cons, you may modify it to change the function definition on the fly. For the GNU Emacs interpreter, since it literally evaluates the cons, it is fine. But for Truffle interpreters, which usually wants a static AST for better JIT performance, this is bad news.

Currently, we don't bother checking whether each AST node needs update and the function may be considered immutable once the whole AST is constructed. (One exception is that we check at each cons node (maybe-function arg1 ...) whether the target function is changed so as to handle subroutines, functions as well as macros.) Also, with our interpreter, (function (lambda ())) produces <a closure object> instead of a cons, which is another behavioral difference.

The current performance is pretty good and we might consider adding more checks to ensure a consistent behavior. But we will need some benchmark to ensure it does not slow down too much.

16.8. DONE Re-implement lexical scoping

Current implementation of lexical scoping spends too much time book-keeping. For example, for (while ... (let ((x (fun))) ... )), we want it to be compiled to something like:

//while block start
////let block start
frame.setSlot(xSlot, resultOfFun); // xSlot: CompilationConstant
//...
////let block end
//while block end

That is, we want each let clause to be compiled to a single stack frame assignment instruction. However, our current implementation is compiled to:

//...
////let block start
ELispLexical lexical = ELispLexical.getLexical(frame);
int xSlot = lexical.addVariable(X_SYMBOL);
frame.setSlot(xSlot, resultOfFun);
////let block end

This is because we need to support per iteration scope in case of closure creation inside a loop:

(while (some-condition)
  (let ((i (some-value)))
    (push (lambda () i) closures)))

Note that for each different (lambda () i), i is a totally different variable. As far as I know, there are two ways to handle this:

  1. Use the same stack frame, but use non-constant frame slot number for each i. This is what we do now.
  2. Use constant frame slot for every variable, and copy the whole frame when needed (i.e., when there is lambda creation in a loop). This is what GraalJS does.

The latter approach seems significantly more complex. (Considering GraalJS can know if a node needsPerIterationScope at parse time, while we have to do this at runtime, it only gets worse.) But it is more performance in that it should compile normal let clauses (without closure creation) into a single instruction.

16.8.1. Design

I guess we still need a ELispLexical instance. But instead of one instance per call, we can have an instance per RootNode, which is only queried/updated when a let node or symbol dereferencing node is first executed.

When a function node is evaluated (with (lexical-binding . t)), it should invalidate some perIterationScope assumption in all its while parent nodes, after which the while nodes will be responsible for replacing the current frame with a new copy for each loop.

16.8.2. GraalJS ForNode

void executeVoid(VirtualFrame frame) {
    FrameIterationScopeNode copy = this.copy;
    VirtualFrame prevFrame = copy.execute(frame);
    while (true) {
        VirtualFrame prevFrameInner = copy.execute(frame);
        if (!executeCondition(frame)) {
            break;
        }
        executeBody(frame);
        copy.executeCopy(frame, prevFrame);
    }
    copy.exitScope(frame, prevFrame);
}

16.8.3. Re-design

See this blog post: Writing a Lisp JIT Interpreter with GraalVM Truffle. Basically, because Graal/Truffle seems to inline MaterializedFrame just fine (provided that it is used correctly and can be virtualized), we can construct a dynamically expanding "frame chain", making space for new variables by adding a new linked frame.

16.9. DONE module-info.java

16.10. DONE Buffer interval properties & markers & overlays

Also, currently our markers does not move when texts get inserted/deleted.

Sidenote: I think we do not need those fany CRDTs or OTs. It seems that we cal simply keep some special thread-local marks/properties to make edits in a threaded context invisible to other threads. So each thread enjoys their own buffer, while the UI can "redisplay" changes by all threads (or changes done prior to redisplay).

16.10.1. DONE Buffer & string redesign (part 1): marks and intervals

Re-implement buffer markers and string/buffer properties to make insert/delete operations more efficient and correct.

16.11. DONE Support load-source-file-function

The C implementation of load is quite simplistic: similar to internal-make-interpreted-closure-function, Emacs relies on load-source-file-function to handle more complex scenarios and file encoding.

16.11.1. Actually support Emacs encodings when loading elisp files

emacs/lisp/language/ethiopic.el is encoded with utf-8-emacs, containing a non-Unicode character. Currently we just treat these characters as white spaces. Also, ethiopic.el uses CCL, so it is probably time for yet another bytecode interpreter.

16.11.2. DONE Fix stack-trace source position

After switching to using load-source-file-function, the stack-trace for root nodes seems to miss source location info, probably caused by eval-buffer.

at <elisp> /.../Juicemacs/elisp/emacs/lisp/electric.el(Unknown)
  vs
at <elisp> loadup.el(emacs/lisp/loadup.el:393:0)

16.11.3. Is concurrent load possible?

Parsing huge files can be costly. And yet most of the operations have nothing to do with the current context: we can offload the job to other threads.

For the following snippet:

(load "a.el")
(load "b.el")

We want to silently turn it into something like:

(concurrent
 (parse-cache "a.el")
 (parse-cache "b.el"))

(run-cached "a.el")
(run-cached "b.el")

What we can do is to have the parser detect require/load (under "safe" conditions like during loadup) and parse them in the background.

However, since we are yet to fully bootstrap loadup.el, we might end up pre-parsing all bunch of things that will not get loaded.

With load-source-file-function, things are much more complex now.

16.12. DONE Use Truffle FileSystems

FileSystem is used by users. And I assume, as a language implementer, we should use the methonds in TruffleLanguage.Env instead.

16.13. DONE Cache function storage in function call nodes

So a previous commit (3465a76 perf: use assumptions for frame materialized top tracking) introduced a bug: let/let* statements should have N + 1 assumptions instead of only one, since the value branches can also modify the stack and introduce more variables.

This is not revealed until we implement this function storage caching.

(BTW, the cache brings current loadup.el execution from ~7s to ~4s. Hopefully we are not getting things seriously wrong here.)

16.14. DONE Reminder: Trivial things

  • [X] Avoid several duplicate allocations, esp. new Object[] for function arguments (incomplete optimizations done)
  • [X] (let/let* () ...) is equivalent to (progn ...).
  • [X]

    Concurrent class loading

    Built-in function initialization loads thousands of classes. Since each factory is independent of each other, we can make them concurrent. (The init function used to take ~0.5s, and now it takes ~0.3s. Not much, but still good to have and fun to concurrentize things.)

16.15. DONE Re-consider whether to stay static everywhere

Currently, we heavily use static variables and basically everything is global. This is the Emacs way: single threaded, globally dynamically bound. However, this has already posed a few challenges:

  • Truffle assumes the language allows several parallel contexts, that is, we can execute i = 1; print(i) and i = 2; print(i) concurrently without them interfering with each other. Apparently, our "global state for everything" approach can be problematic.
    • Previously, before we auto-gen the giant mess of initialization code, since JUnit tests are not concurrent, we are mostly fine, as long as we clean things up when creating a new context.
    • Now that we have convoluted init logic, with global variables scattered around the place, it becomes harder to properly "clean things up". (The tests are now failing with tons of (fatal) errors, probably due to charset initialization.)
  • Although we plan to follow JavaScript's model of concurrency: single-OS-threaded green threads, it will be nice to have real thread APIs, similar to the Web Worker API. Then, it will be necessary to separate the dynamic variable scope of different "workers".

16.15.1. Considerations

  • Web Worker API: The more intuitive way is to have multiple instances of the ELisp interpreter. However, we need to think twice before doing so:

    We want, for example, defun and others to be available to the worker. If we use multiple interpreters, we will need to (load "loadup.el") every time a worker is created. This won't be realistic before we can do pdump in Java.

  • Sharing anything between workers: unwise. Consider the following function:

    (defun self-modifying-f (value)
      (let ((inner '(nil)))
        (setcar inner value)))
    

    Concurrent calls to it will results in race conditions. Rune handles it by copying and marking the whole AST immutable, which results in behavioural discrepancies.

  • Actually, if we ignore the rare case that the AST is directly mutated, we might be able to use a auto-copying ELispObjectLiteralNode for this: when it is accessed, it copies the original value and use it for further access for the current OS thread.

    If we are to prevent the user from modifying the AST, we can also copy the AST tree for internal usage, and ignore any modification to the original object.

16.15.2. Multi-context, multi-worker, multi-threaded ELisp refactor

Scopes:

  • TruffleLanguage
    • Multiple context objects
      • Multiple workers
        • Multiple virtual threads mounted on a single carrier thread

Shared objects:

  • Symbols: shared across contexts, stored in ELispLanguage, following what TruffleRuby is doing.
  • Functions: across workers, but not contexts. Shallow-copied when shared.
  • Values: across workers, but not contexts. Deep-copied when shared.

Since symbols are shared across contexts, to get its corresponding value/function, we must look it up in a context/worker-local map. This has notable performance penalty: we don't want to look up a map for every variable access. A usual mitigation to this is to cache the value container objects, but, …, it can be hard to do so since we have multiple workers sharing the same function.

16.15.3. DONE Progress [3/3]

  • [X] Move global things to fields in ELispGlobal
    • [X] Symbols, generated forward value containers
    • [X] Various static fields in BuiltIn* classes
  • [X] Optimize
    • Cancelled: Use get contexts with ELispContext.get(this): Too much work.
    • [X] Read/write to global values with some ReadGlobalNode
      • [X] Read
      • [X] Write? (setq only; let/let* require too much changes)
    • [X] Cache constant globals
      • [X] Keywords (turned into literal node)
      • [X] Constants
    • [X] Maybe use a ContextThreadLocal for current buffer tracking
    • Moved: Automatically free container objects when the corresponding symbol goes out of scope
  • [X] Make tests pass again
  1. Why sharing symbols & functions?
    1. Symbols

      We heavily use == comparisons for symbols in our builtin functions, and it would be a pain if we switch from if (sym = EQ)= to if (isInterned(sym) && sym.name().equals(new MuleString("eq"))) (and the latter also has poor performance).

    2. Functions

      A huge part of the ELisp we are familiar with are implemented in ELisp. For example, both defun and defmacro are written in ELisp code. If we don't share functions, each worker must run loadup.el independently, which is slow.

      Also, under Truffle, since functions are JIT-compiled, starting anew means having to JIT-compile the functions again every time a new worker is started.

      An alternative to consider is to not support workers at all, but it is really nice-to-have.

  2. CANCELLED Value container caching in shared functions

    To make shared functions possible, we need automatical deep-copies of shared values, including function objects as well as all kinds of global objects and literals.

16.16. DONE More robust code generation

16.16.1. DONE Initialization logic & ordering

The initialization logic of Emacs is quite complex. Basically:

  • Every .c file may contain a syms_of_<part> that contains the definition of the symbols, variable initialization logic that this file manages.
  • Some may contain an init_<part> function that is called initialize… things.
  • Some may contain multiple init_<part>_<wut> functions that contains initialization instructions that must be run separately due to interdependencies between all those initialization logic.

The main function in emacs.c contains a dazzling amount of initialization code. (Search for init_alloc_once to start reading.) Thank you, Emacs developers for all those helpful comments around the init function calls. But, no, I would really want to avoid all these complex dependencies between code "modules".

The question is: is simplifying all this "mess" ever possible or the Emacs main initialization is the best we can get?

(One thing that comes to mind is dependency injection or inversion of control. But I'm afraid that the dependencies are not that simple or that OOP-friendly.)

Edit: Now that we generate all these logic directly from Emacs source code. Things should be good (for now).

16.17. DONE Optimize lexical variable access

16.17.1. Step One: Use assumptions instead of checking at every access

For simple functions, the stack slot number assign to each variable is constant and we do not need to check for changes every time.

The step introduces for each root node an assumption that stays true as long as the materialized top is not changed: slot numbers are indead constant.

16.17.2. Step Two: Optimize conditions for assumption invalidation

It is possible that a portion of the stack is materialized and yet the slot numbers do not change:

(let ((a 1)                ; a: slot #1
      (f #'(lambda () a))) ; <- frame materialized as the lexical context, f: slot #2
  (while (< 0 (funcall f))
    (let ((b -1))          ; even when the stack is materialized, b is always at slot #3
      (setq a b))))

So we want to differentiate the case above with the following:

(dolist (v '(1 2 3))
  (let ((vv (* v v)))  ; <- we have three "vv"s, at slot #1, #2 and #3
    #'(lambda () vv)))

The changes required are actually quite simple: instead of root nodes, we introduces assumptions at each let/let* scope.

The assumption is invalidated when:

  • The frame is materialized.
  • The scope is entered the second time, with a different materializedTop.

The changes bring down the execution time of mandelbrotNestedLets from 2.2s to 0.5s (i.e. from 55x Java to 14x Java). (BTW, mandelbrot (with a single huge let) is around 3x Java.)

16.18. DONE Strings & Buffers (Step 1)

For string processing in any language, I suppose there are always at least two kinds of offsets: char offsets & code point offsets. Emacs basically uses byte offsets and codepoint offsets, but Java uses UTF-16 char offsets and codepoint offsets. So to correctly handle strings in Juicemacs, we need to incorporate all these three kinds of offsets.

It could be easier if we could just follow Emacs. (And Truffle actually provides a byte-offset based TruffleString to free us of the trouble!) But, unfortunately, we can't. There is one indispensible Java API we need to use: Java Pattern regex, which, of course, is based on UTF-16 char offsets.

Edit: In order to support the widened code point range in Emacs, we are now rolling out our own string implementation. The good news is that, many Emacs string operations actually involves case tables and all, requiring a reimplementation of most of the java.lang.String API anyway. So why not?

16.18.1. DONE RegExp

Well, the conclusion is that we need to implement a new RegExp runtime. :) See ELispRegExp.java.

  1. Thoughts

    Truffle also provides a JIT-compiling RegExp runtime (TRegex). But we still need to experiment whether it is compatible with Emacs Lisp RegExps.

    RegExp Features ELisp Java TRegex
    Named capture No Yes  
    . Codepoint Codepoint  
    *, *? well-known βœ…  
    +, +? well-known βœ…  
    ?, ?? well-known βœ…  
    [...], [^...] Char classes βœ…  
    [:char-class:] Named char classes \p{class}  
    ^ Start of line βœ…  
    $ End of line βœ…  
    \\vert Alternation βœ…  
    \{m,n\} Postfix operator βœ…  
    \(...\) Capturing group βœ…  
    \(?:...\) Non-capturing βœ…  
    \(?num:...\) Explicitly numbered ❌  
    \digit Back-reference βœ…  
    \w Word constituent βœ…  
    \W Non word βœ…  
    \scode Char syntax ❌  
    \Scode Not char syntax ❌  
    \ccode Char category ❌  
    \Ccode Not char category ❌  
    \` Start of string \A  
    \' End of string \z  
    \= Buffer point ❌  
    \b Word boundary βœ…  
    \B Not word boundary βœ…  
    \< Start of word ❌  
    \> End of word ❌  
    \_< Start of symbol ❌  
    \_> End of symbol ❌  

    Now that we have our own MuleString, I don't think using TRegex is worth the cost any more.

16.19. DONE obarray

Oh no. It looks like a data structure with its internal structure fully exposed as a vector. It probably means we will have to exactly follow the GNU Emacs implementation.

Edit: No, it is not fully exposed. (See keymaps for what is fully exposed… Sigh.) It is a hash-table-ish thing. And actually, I do think we can make all hashes zeros and use our own HashMap instead.

16.20. CANCELLED Syntax tables & case tables

It seems basically a char table. However, in order to use it with RegExp, we will need to maintain a character set for each syntax class, which might take quite some memory if unoptimized.

Edit: task cancelled now that we have a dedicated regex engine.

16.21. DONE Code conventions

  • Maybe set ELispContext.NIL to Boolean.FALSE (and T to Boolean.TRUE).
  • [X] elisp/scripts/extract-emacs-src.py
    • Auto-detect types
    • Avoid java keywords
    • Auto-update existing ones
    • Set generated return types to Void
  • Analyze null values during initialization

16.22. DONE Signals

A central mechanism to handle exceptions.

I guess I should start implementing it before I litter IllegalArgumentException everywhere.

  • [X] New exception types & utility methods
  • [X] signal/error/condition-case
    • [X] Implementation
    • [X] Error groups
    • [X] Convert ClassCastException to errors
  • [X] catch/throw
  • [X] Stack traces
    • [X] Store debug info into cons nodes.
    • [X]

      So we want function names in stack traces, but all interpreted functions are just lambdas in Emacs: (defalias 'a-symbol #'(lambda () ...)). Maybe we can try to assign a lambda a name when it is first bound to a symbol? (<- chose this approach)

      I will need to check out how GraalJs handles lambdas. Too lazy to do that.

16.23. DONE Replace lexical scope maps with Truffle frames

  • Threefold speed-up: (fib 35) went from ~5s to 1.7s. At least we are not slower than interpreted GNU Emacs now (~3s).
    • Any other languages I tested takes less than an instant.
    • Python 3.12 takes ~0.6s. So it is quite embarrassing that a JIT implementation cannot beat an interpreted language.
      • JMH results: ~0.4 s/op, probably jacoco is interfering with previous results.
      • But… (mandelbrot 750) takes around 5.5s while Python uses only a second. So there is definitely space for improvement. (Emacs: ~30s.)
  • Writing a Language in Truffle. Part 3: Making my Language (Much) Faster

16.23.1. Reusing frame slots

Basically, each Truffle function automatically gets its own VirtualFrame, and for each lexical scope (either in a function or a let/let* scope), we manually assign a ELispLexical scope.

Lexical scopes are append-only and keeps track of mappings between variables in the current scope and their frame slots. When the current lexical scope is materialized (when a lambda function is created inside it, for example), it marks the corresponding frame materialized. However, instead of treating all frame slots as not reusable slots, it makes use of a materializedTop slot to track what slots that lambda function might have access to, allowing slots beyond those slots to be reused.

16.24. DONE Undertanding Lisp_Symbol (Variables)

I really doubt I get the implementation of ELispSymbol wrong (to some degree). Basically, a symbol can:

  • Contain a lisp value (plain value symbol)
  • Point to a field in a global C struct (forward symbol)
  • Point to a field in a buffer struct (buffer-local symbol)
  • Contain a user-defined buffer-local symbol (buffer-local symbol)
  • Point to another symbol (aliased symbol)

Also, similar to Java, lisp functions and values are in different "namespaces". So in the function namespace, a symbol can:

  • Point to a C function
  • Point to a lisp function
  • Point to another symbol (aliased function)
  • Other special values:
    • Macros
    • Autoload functions
    • Wait, what? A keymap?
  • Other values set by defalias

I have no idea how all these things interacts. (For example, what happens when you try to set the buffer-local default value for a plain value symbol? What if it is lexically bound? What behaviors may change if a symbol is lexically bound?)

(Did I forget to mention that symbols like :keyword are automatically constant? Or are they? They also seems to evaluate to themselves.)

16.24.1. Lexical Scoping

Note that unlike dynamic variables which are tied to the symbol object itself, the relationship between lexical variables and symbols is only present in the interpreter (or compiler). Therefore, functions which take a symbol argument (like β€˜symbol-value’, β€˜boundp’, and β€˜set’) can only retrieve or modify a variable’s dynamic binding (i.e., the contents of its symbol’s value cell).

C-h i g (elisp) Lexical Binding

Oh. Great.

16.24.2. DONE defvar

If INITVALUE is missing, the form marks the variable "special" locally (i.e., within the current lexical scope, or the current file, if the form is at top-level).

Mind-boggling. No idea. (And why? Is it just fun to change the whole semantics depending on a single missing parameter?)

  1. Semantics

    Uhh. So I have always used the *scratch* buffer to test elisp semantics. So when I first eval (setq var 1) and then (defvar var nil), var turns nil, so that means defvar always sets the value of the variable, right?

    Turns out it is not. It is because elisp--eval-defun-1 does extra work to reset variables for defvar statements and defvar as is described by the manual does not:

    But if symbol is not void, defvar does not evaluate value, and leaves symbol’s value unchanged.

16.24.3. DONE let and let*

Dynamic binding not handled yet. Also, still need to handle "special == true" symbols under lexical scoping.

Wait. Does "special == true" also applies to function arguments? (No.)

Emacs 31.0.50 (Org mode 9.7.11)