Contributing
Thank you for your interest in Juicemacs! This document contains coding examples, common development pitfalls, and an introduction to some useful developer tools. Whether you are looking to contribute or just taking a peek, this guide should help you started with the current codebase.
Alternatively, you can also contribute by reporting bugs or discussing designs and features. We use GitHub as the issue tracker and CI/CD runner, and accept pull requests from both Codeberg and GitHub. Please feel free to open issues for anything related to Juicemacs. If you prefer something more conversational, we also have a (rather empty) Zulip server at juice.zulipchat.com.
For code contributors, there is also this legal notice boilerplate here:
When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence (GPLv3).
1. Background
Juicemacs builds upon other tools and frameworks and has its own code structure. For people with no prior experience of the dependencies, getting started can be difficult. This guide aims to ease the burden on new contributors and help them get familiar with the directory structure and the Truffle framework.
This guide assumes the following:
- The reader should have knowledge of Emacs Lisp, including its data types, language designs (i.e., Lisp-2, macros, lexical scoping, and dynamic scoping) and a bit of its internals (byte compilation and possibly native compilation).
- The reader should know Java and its basic syntax. In particular, there are a few new features from recent JDK versions that get used heavily in the codebase, including:
- No prior knowledge of the Truffle framework is required. The reader is not expected to know how to work with Truffle when implementing simple Lisp functions (but it would certainly help if they do). When talking about Truffle caveats that do need a basic understanding of Truffle/Graal, this guide will include links to materials that I find useful when getting started with Truffle.
2. Table Of Contents
This guide will talk about the following:
How Juicemacs uses code generation to ease developing:
To keep in sync with GNU Emacs, Juicemacs heavily relies on code generation (see emacs-extractor). This will keep the Lisp function signatures (extracted as Java method signatures) and documentation (extracted into JavaDoc) remain synchronized. The next section, 3, will cover how to work with the generated code.
What things you will need to take note of to make the Truffle JIT magic happen:
Juicemacs uses the Truffle framework for a JIT compiling ELisp interpreter. This framework, however, requires quite a few workarounds to make the JIT magic happen. We will document these caveats in dedicated sections. (If you encounter any new gotchas, you are welcome to add them here!)
- What to work on
3. Implementing Basic Built-in Functions
Like any language implementation, we need to implement a lot of built-in functions in Juicemacs. GNU Emacs has around 1800, and there's still a long long way to go. With code generation from emacs-extractor and the Truffle annotation processor, implementing a built-in Lisp subroutine is not that hard and can be a good way to get started with the codebase.
For example, we can get to implement the consp function in just two steps:
Locate the pre-generated placeholder implementation. In this case, the function is defined in
data.cin GNU Emacs, and the generated placeholder can be found inBuiltInData.java:@ELispBuiltIn(name = "consp", minArgs = 1, maxArgs = 1) @GenerateNodeFactory public abstract static class FConsp extends ELispBuiltInBaseNode { @Specialization public static Void consp(Object object) { throw new UnsupportedOperationException(); } }
Implement the function in two lines:
@ELispBuiltIn(name = "consp", minArgs = 1, maxArgs = 1) @GenerateNodeFactory public abstract static class FConsp extends ELispBuiltInBaseNode { @Specialization - public static Void consp(Object object) { - throw new UnsupportedOperationException(); + public static boolean consp(Object object) { + return object instanceof ELispCons; } }
Of course, most built-in functions will require more than two lines of code, but the above covers the gist of a built-in function definition in Juicemacs:
@ELispBuiltInmetadata@ELispBuiltInannotations contain basic function information, such as the function name and argument counts. They are used by the language context to register built-in functions. (SeeELispBuiltIns.javafor more implementation details.)@GenerateNodeFactory- This annotation asks Truffle to generate factory classes for this function and gather all the factories into a list.
abstract static class FConsp extends ELispBuiltInBaseNode- This class
definition is required for Truffle code generation to kick in:
abstract: Truffle will generate child classes.ELispBuiltInBaseNode: This class extends theNodeclass from Truffle and contains several abstractexecuteXXXmethods. Truffle will generate JIT-capable implementations for theseexecuteXXXmethods, which in turn will call@Specializationmethods for the actual work.
@SpecializationThis annotation marks the implementation of the
conspfunction. A class can contains several@Specializationmethods, each with different parameter types (and optionally different return types) but the same parameter count (similar to Java method overloading):@ELispBuiltIn(name = "length", minArgs = 1, maxArgs = 1) @GenerateNodeFactory public abstract static class FLength extends ELispBuiltInBaseNode { @Specialization public static long lengthCons(ELispCons sequence) { return sequence.size(); } @Specialization public static long lengthVector(ELispVectorLike<?> sequence) { return sequence.size(); } @Specialization public static long lengthString(ELispString sequence) { return sequence.length(); } }
Truffle will generate code to dispatch function calls to correponding specializations.
Most of the above should be kept as is, because they will be overwritten when we rerun the code generation to update JavaDoc and incorporate GNU Emacs changes. Therefore, for the code generator to work correctly, there are a few things to note:
- You can only place extra method / class definitions either inside the function
class, or at the top of the enclosing class (at the top of
BuiltInData.javain this case). - Do not modify the argument names, which are also used by the code generator and should already be in sync with the JavaDoc.
Sometimes the placeholder implementation is not yet generated. In that case, you will need to modify the emacs-extractor code to extract them from the GNU Emacs code.
3.1. Language Context: ELispContext
Language contexts are special objects for which Truffle can allocate dedicated
space in thread-local storage for quick retrieval. There are several way to
obtain an ELispContext reference:
ELispContext.get(<currentNode>):@ELispBuiltIn(name = "symbol-value", minArgs = 1, maxArgs = 1) @GenerateNodeFactory public abstract static class FSymbolValue extends ELispBuiltInBaseNode { @Specialization public Object symbolValue(ELispSymbol symbol) { ELispContext context = getContext(); // or ELispContext.get(this) return context.getStorage(symbol).getValue(symbol); } }
The
currentNodereference is usuallythis. It serves to help Truffle store theELispContextinstance inline.ELispBuiltInBaseNodechild classes have agetContextutility method for this.ELispContext.get(null): Used when noNodereference is within reach. Can be slower than the former case.
Currently, ELispContext provides access to:
- Global obarray
- Truffle environment
- Filesystem (with more IO policy enforced than plain
java.iocalls) - Symbol value storage (see 3.2.3)
3.2. Object Types
Most Lisp object implementations are in
party.iroiro.juicemacs.elisp.runtime.objects, with the exception of
ELispCons and ELispString. Other than primitive types, all of them implement
the ELispValue interface.
| Emacs Type | Juicemacs Type | Notes |
|---|---|---|
nil |
false or symbol NIL |
use isNil to check |
t |
true or symbol T |
use isT to check |
fixnum |
long or boxed Long |
avoid leaking boxed int |
float |
double or boxed Double |
|
bignum |
ELispBigNum |
|
symbol |
ELispSymbol |
immutable, see ValueStorage |
cons |
ELispCons |
with source location, by the way |
string |
ELispString |
see also TruffleString |
vector |
ELispVector |
|
record |
ELispRecord |
|
char-table |
ELispCharTable |
|
bool-vecotr |
ELispBoolVector |
3.2.1. Object Utilities
Most objects can be created with their constructors, and most utilities are
located directly within the object class, such as ELispCons.of(...) or
ELispCons.ListBuilder or consInstance.iterator().
For simple type checking, you may change the signature of @Specialization
methods, and Truffle will check the type for you. If a parameter can be of
multiple types, then you might need to perform manual type checking using
instanceof and Java pattern matching:
if (object instanceof ELispCons cons) { return cons.car(); }switch (object) { case ELispCons cons -> return cons.car(); }
ELispTypeSystem also has a few type-casting utilities:
isNil/isTnotNilOr: useful for optionalfixnumargumentsasLong/asInt/asRanged/asCharasCons/asConsIter/asVector/asSym/ …
3.2.2. Conses
Conses are simple pair-like constructs in Lisp. However, since lists and
ultimately Lisp source code are stored as conses, another field is packed into
our ELispCons object: source location encoded as a long. Truffle uses the
extra info in stack traces as well as Chrome Debugger protocol integration.
However, since most functions are byte-compiled after bootstrap, one may also
argue that this field can be removed.
Also, since cons lists can be circular, one is always recommended to use
ConsIterator (obtained via cons.iterator/listIterator()), which has cycle
detection baked in.
3.2.3. Symbols and Variables
Unlike GNU Emacs, which stores symbol values within symbol structs, we make
ELispSymbol immutable and store values inside ValueStorage and
FunctionStorage. Storage objects can be retrieved from the current language
context (ELispContext). (Note that lexical variables are implemented
differently. See ELispLexical.)
3.2.4. Strings and TruffleString
Emacs strings are … complicated. Here are some examples:
- Emacs codepoints go up to
#x3FFFFF, while Unicode spans only0to#x10FFFF. - Emacs has two kinds of strings:
unibyteandmultibyte."ascii string"is unibyte, as well as"ascii+\255"."🤔"is multibyte, and"🤗\255"is … multibyte (please read on). Also, both of them are mutable. - Multibyte strings can contain raw bytes, likely meaning bytes that failed to
decode, represented as codepoints between
#x3FFFF80and#x3FFFFF. - (And, yes. All above also applies to Emacs buffers.)
This means we cannot use java.lang.String or any string implementation that
expects only valid Unicode. The good news is that TruffleString support
UTF-32 and we can store those invalid Unicode codepoints with a bit of
tweaking. The bad news is, it still expects Unicode and will try to convert
invalid bytes into replacement characters from time to time. So, our final
answer is: we are to build our string type from scratch, directly on Java
byte[]. Now ELispString stores the following to maintain some compatibility
with GNU Emacs:
state- String state + cached hash code.
- States:
STATE_BYTESorSTATE_ASCIIisunibyte. AndSTATE_UTF_8/STATE_EMACSismultibyte. - Other states: A bit is used to represent whether the string is mutable.
- Hash: higher bits are used to store the hash code of the string, computed
lazily.
- Mutable strings does not have constant hashes, so we forbid computing hashes for mutable strings and mutating strings with computed hashes.
- States:
value- A
byte[]object inutf-8-emacsencoding (or raw bytes when the state isSTATE_BYTES). It should be able to support non-Unicode chars in#x110000 ~ #x3FFFFF.- We plan to extend this value field to support lazy values, like maybe
LazyConcat, so that we can reduce allocation for nestedformat/concatforms. (TruffleStringalso does this, and we should follow what they do.)
- We plan to extend this value field to support lazy values, like maybe
intervals- Emacs string properties
Utilities for strings are currently scattered everywhere, but mainly lie in the
:commons:mule-utils sub-project and the StringSupport class.
3.3. Linting
- Run
./gradlew :elisp:emacsGen. This will check if all function signations match. - Run
./gradlew :elisp:pmdMain. This will check against common pitfalls. - Run
./gradlew :app:nativeCompile. This will reveal many, but not all, incorrect uses of Truffle nodes.
3.4. Testing
In addition to writing manual unit tests, there are two other way to test your function implementation:
ElSemiFuzzTest.java- It runs
el-semi-fuzz.elfrom emacs-extractor to generate a bunch of test cases from GNU Emacs behavior, and then tests them against Juicemacs. It is best suited for context-independent pure functions like arithmetic. ELispLanguageTest- This test tries to bootstrap Juicemacs with pdump.
ELispRegressionTest- It loads pdump products and runs some ERT tests. I've added some tests from the GNU Emacs codebase, with many of them failing. Free free to add more!
To run the two test suites above, simply modify the Java code to include or generate test cases for your functions. During development, you might also want to exclude tests for other functions to save time, since these tests typically take quite some time.
5. Taming Native Images
One of the main challenges for working with Truffle is that it (and the Graal compiler) can be quite picky about the interpreter code. Truffle even has a method blocklist to prevent compilation of methods that are hard to partial-evaluate.
We have a few PMD rules to detect code that might cause trouble (see elisp/scripts/pmd-elisp-cautions.xml and elisp/scripts/pmd-truffle-practices.xml) and you may also find other pitfalls by trying to generating native images for Juicemacs.
This section tries to summarize these pitfalls.
- Recursive methods cause trouble because Truffle tries to inline everything,
leading to exploding code size. Annotate them with
@TruffleBoundaryto disallow partial evaluation. - Truffle uses
VirtualFrameto track variables. When passing the frame instance to a method, the method should not be used in a loop, unless the loop is mark for@ExplodeLoop. (Otherwise the method will fail to compile.) - When calling an polymorphic method (e.g.,
Object::toString), Truffle (during native image compilation) will mark all implementing methods for compilation.
The above leads to the following:
- Highly complicated classes (usually with high recursive methods) should only
be used inside
@TruffleBoundaryfunctions. This includesBigInteger,HashMapand a thousand others. - Locale related operations involves HashMap, and should be avoided. This means,
all
java.lang.Stringconcatenations must also happen inside@TruffleBoundarybecause they somehow calls formatting methods which can be locale dependent at times. - Working around string concatenation is painful, not only because we need a
dedicated
@TruffleBoundaryutility method, but also because it forbids us from using any method (in external libraries or Java standard library) that does string concatenation in partial evaluated code. - When calling polymorphic methods, any of them failing the compilation will
make the whole compilation fail. Since most Java libraries (standard or
external) uses
java.lang.Stringfor errors, this basically means: no polymorphic methods unless the interface is yours. When you see compilation errors from strange classes, it's very likely that it is from an unnoticed implementation of a polymorphic method.
There are more. Ways to discover them are:
- Try
./gradlew :app:nativeCompile. - Enable
engine.TraceCompilationand setengine.CompilationFailureActiontoDiagnose. (SeeELispLanguageTest.java.)
5.1. List of Random Caveats
- Truffle (or Graal) does not expect
Throwable::addSuppressedduring native image initialization. The method call is automatically generated byjavacfor all try-with-resource statements, and that means we must use try-finally instead. - Methods that perform string concatenation and thus must be used within
@TruffleBoundaryincludes:ByteBuffer.allocate(i)/byteBuffer.get(i)/byteBuffer.limit(i): exception message constructionSourceBuilder::build(Truffle objects tracking source info)
- Common methods (or types) that are highly polymorphic:
Object::toString/Object::hashCodeList::<any_method>Iterator::<any_method>Number::<any_method>Exception::getMessage
- Others
FileTime::toInstant(see JavaDoc inTruffleUtils.java)
6. Debugging
Using a Java debugger should be enough most of the time. See TestingUtils.java
for some useful Truffle options when debugging:
engine.Compilation: It seems sometimes Truffle can corrupt the stack info, making Java debuggers basically useless. If the bug is reproducible, you may try turning off compilation.inspect: Enable you to use Chrome debugger to debug Lisp code since we don't have edebug here. (It's not working very well though.)engine.CompilationFailureAction/engine.TraceCompilation: Useful when you are diagnosing performance issues.
7. What To Work On?
Places marked with
UnsupportedOperationExceptionorTODOcomments:We mark out unimplemented features in the codebase with
UnsupportedOperationExceptionorTODOcomments. And grepping for them are a good way to find things to work on.Failing conformance tests:
All those
el-semi-fuzzand ERT tests do reveal conformance issues in Juicemacs. Currently, Juicemacs does not pass all the ERT tests and occasionally failsel-semi-fuzztests (because of the randomness). Looking into these failing cases can be a good way to learn about the internals of both Juicemacs and GNU Emacs.Adding more tests:
There are quite a lot of ERT tests in the GNU Emacs codebase. However, currently we only run a few of them in our unit test (
ELispRegressionTest.java). Getting more of them to run can be quite beneficial too.Performance engineering:
There is a TODO list here at docs/TODO.html. The reason I chose to use a text file instead of issue trackers was because they were mostly about the internals of Juicemacs and might require some Truffle hacking, and there were simply too many things to track and learn about. There are still some unsolved issues (and one might add more), and you are welcome to look into them if you want to learn more about Truffle the framework.