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.Valueinterface for locals:ForwardedPerBufferBufferLocalForwardedPerFrame?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
IntegerSwitchnodes. - The graph contains many
Loopnodes. - 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 inunwind-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 generatedloaddefs.elseems to invokerxbefore its autoload definition. How does Emacs even support this?- Answer: Emacs has two bootstrapping phases:
pbootstrap: It interprets.elfiles and uses a tailoredldefs-boot.elthat avoid this kind of use-before-definition situations.pdump:pbootstrapwill compile (almost) all code into bytecode.elcfiles. Sincerxis a macro, it gets expanded during bytecode compilation. Then,pdumploads bytecode where macro-use-before-definition will never happen.
- Answer: Emacs has two bootstrapping phases:
[ ]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:
13.1. TODO Emacs Code Conversion Language (CCL)
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[ ]Bootstraploadup.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 onnativecomp?
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
- Types:
- 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 intoELispSymbol)[ ]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:
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
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.
- Text properties are mutable, and we hope to get an immutable tree for concurrent redisplay.
- 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-bytesandposition-bytes. - Truffle UTF-8 encoding have specialization for ASCII text so it is not that bad?
- We need to support
- 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.
- Now thinking of it, no, it is straight out impossible to do this. Consider
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-emacsas 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
TruffleStringnodes for some strictly UTF-8 string operations. For example, when reading from files, we can useTruffleString.FromByteArrayNodeto 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:
- polymorphism: you rarely get a "pure array"-like cons list. A single
setcdris enough to kill the performance. - 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
ELispConsArraycontainer 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 callsetcdrand 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:
pbootstrap:temacs->(load "loadup")->(dump-emacs)->bootstrap-emacsbootstrap-emacsis used to byte-compile files into.elc
pdump:pdumpexpects.elcfiles, but otherwise it has nothing to do withbootstrap-emacs. (I wrongly thoughtbootstrap-emacsis 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.
pdump
We have a pdumper now which simply serializes all data. It is quite slow (compare: running
loadup.elin 13 seconds, and loading.pdmpdata 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:
pbootstrappdump
- For example,
init_buffer_onceis only run on the initial run, whileinit_bufferis 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 inemacs.cduring initialization and also by other source files (mostly for assertions at a glance). - I've met some problems already:
define-categoryerrs if a category is already defined. However,characters.el(defining a bunch of categories) is always run duringloadup, throwing "category already defined" errors on the secondpdumprun. Why? How does Emacs bootstrap circumvent this?
- Emacs seems to require two rounds of dumping:
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 tonilbysubr.eland set to a valid value intop-level. This seems to be used by Emacsloadup.elbefore runningtop-levelwhennoninteractiveisnil.
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:
- Use the same stack frame, but use non-constant frame slot number for each
i. This is what we do now. - 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)andi = 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,
defunand 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
ELispObjectLiteralNodefor 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
- Multiple workers
- Multiple context objects
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 inELispGlobal[X]Symbols, generated forward value containers[X]Variousstaticfields inBuiltIn*classes
[X]Optimize- Cancelled: Use get contexts with
ELispContext.get(this): Too much work. [X]Read/write to global values with someReadGlobalNode[X]Read[X]Write? (setqonly;let/let*require too much changes)
[X]Cache constant globals[X]Keywords (turned into literal node)[X]Constants
[X]Maybe use aContextThreadLocalfor current buffer tracking- Moved: Automatically free container objects when the corresponding symbol goes out of scope
- Cancelled: Use get contexts with
[X]Make tests pass again
- Why sharing symbols & functions?
- Symbols
We heavily use
==comparisons for symbols in our builtin functions, and it would be a pain if we switch fromif (sym =EQ)= toif (isInterned(sym) && sym.name().equals(new MuleString("eq")))(and the latter also has poor performance). - Functions
A huge part of the ELisp we are familiar with are implemented in ELisp. For example, both
defunanddefmacroare written in ELisp code. If we don't share functions, each worker must runloadup.elindependently, 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.
- Symbols
- 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
See emacs-extractor.
16.16.1. DONE Initialization logic & ordering
The initialization logic of Emacs is quite complex. Basically:
- Every
.cfile may contain asyms_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.
- 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 β \\vertAlternation β \{m,n\}Postfix operator β \(...\)Capturing group β \(?:...\)Non-capturing β \(?num:...\)Explicitly numbered β \digitBack-reference β \wWord constituent β \WNon word β \scodeChar syntax β \ScodeNot char syntax β \ccodeChar category β \CcodeNot char category β \`Start of string \A\'End of string \z\=Buffer point β \bWord boundary β \BNot 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.NILtoBoolean.FALSE(andTtoBoolean.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
nullvalues 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]ConvertClassCastExceptionto 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.)
- JMH results:
- 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?)
- 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),varturnsnil, so that meansdefvaralways sets the value of the variable, right?Turns out it is not. It is because
elisp--eval-defun-1does extra work to reset variables fordefvarstatements anddefvaras 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.)