A journey through the maze of twisty passages

I’ve been playing around with Clojure for some time, but decided that over the holidays it would be good to delve more deeply into the internal workings of the system. Clojure is interesting in that the compiler is implemented in java, but a lot of the other code is implemented in Clojure itself; as we’ll see the system loads this code dynamically when the environment is set up.

It seemed that the best way to start to understand the system, would be to see how the REPL, the read-eval-print loop, is implemented in the system.

Well, first the Repl.java file contains a main method for running the main loop

public static void main(String[] args) throws Exception{

legacy_repl is defined elsewhere to be the code

public static void legacy_repl(String[] args) throws Exception{

The second statement, the invoke is just the standard way of calling into a Clojure defined Var which the system gets from the standard Clojure namespace.

final static private Var LEGACY_REPL = Var.intern(CLOJURE_MAIN_NS, Symbol.create("legacy-repl"));

This legacy-repl symbol is defined in main.clj

(defn- legacy-repl
  "Called by the clojure.lang.Repl.main stub to run a repl with args
  specified the old way"
  (let [[inits [sep & args]] (split-with (complement #{"–"}) args)]
    (repl-opt (concat ["-r"] args) (map vector (repeat "-i") inits))))

This eventually calls into the repl function that is defined in the same file, and then into the eval function that is defined in core.clj

(defn eval
  "Evaluates the form data structure (not text!) and returns the result."
  [form] (. clojure.lang.Compiler (eval form)))

which uses the eval method on the Compiler class. We’ll take a look at that later, but let’s first look at one of the other functions that were called earlier in the sequence.

What we need to look at is the call,

  final static private Var REQUIRE = Var.intern(RT.CLOJURE_NS, Symbol.create("require"));

Require is defined in core.clj as a call into the load-libs function which eventually goes through to the function load which wraps the runtime function
   (clojure.lang.RT/load  (.substring path 1))

This method loads a class corresponding to the module
        try {
                    RT.map(CURRENT_NS, CURRENT_NS.deref(),
                           WARN_ON_REFLECTION, WARN_ON_REFLECTION.deref()));
            loaded = (loadClassForName(scriptbase.replace(‘/’, ‘.’) + LOADER_SUFFIX) != null);
        finally {

Where did the compiled code from require come from? There are a number of class files that are generated as part of the build.
  <target name="compile-clojure" depends="compile-java"
          description="Compile Clojure sources.">
    <java classname="clojure.lang.Compile"
      <sysproperty key="clojure.compile.path" value="${build}"/>
      <arg value="clojure.core"/>
      <arg value="clojure.main"/>
      <arg value="clojure.set"/>
      <arg value="clojure.xml"/>
      <arg value="clojure.zip"/>
      <arg value="clojure.inspector"/>

Require is in core$$require__5049.class

The RT.java file contains the method doInit, which is responsible for loading much of the system into the memory.
static void doInit() throws Exception{
    load("clojure/zip", false);
    load("clojure/xml", false);
    load("clojure/set", false);

            RT.map(CURRENT_NS, CURRENT_NS.deref(),
                   WARN_ON_REFLECTION, WARN_ON_REFLECTION.deref()));
    try {
        Symbol USER = Symbol.create("user");
        Symbol CLOJURE = Symbol.create("clojure.core");

        Var in_ns = var("clojure.core", "in-ns");
        Var refer = var("clojure.core", "refer");
    finally {

If we run the system using the command line
C:UsersCliveDesktopclojure-1.0.0>java -cp classes clojure.lang.Repl
Exception in thread "main" java.lang.ExceptionInInitializerError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Unknown Source)
        at clojure.lang.RT.loadClassForName(RT.java:1499)
        at clojure.lang.RT.load(RT.java:385)
        at clojure.lang.RT.load(RT.java:367)
        at clojure.lang.RT.doInit(RT.java:402)
        at clojure.lang.RT.<clinit>(RT.java:288)
        at clojure.lang.Namespace.<init>(Namespace.java:32)
        at clojure.lang.Namespace.findOrCreate(Namespace.java:117)
        at clojure.main.<clinit>(main.java:21)
        at clojure.lang.Repl.main(Repl.java:20)
Caused by: java.lang.NullPointerException
        at java.util.Properties$LineReader.readLine(Unknown Source)
        at java.util.Properties.load0(Unknown Source)
        at java.util.Properties.load(Unknown Source)
        at clojure.core$fn__5774.invoke(core.clj:4092)
        at clojure.core__init.load(Unknown Source)
        at clojure.core__init.<clinit>(Unknown Source)
        … 11 more

we can see the doInit being used to start the system, called as a static constructor on the RT class.

Removing some jar files to allow us to play with the code
  java -cp classes;srcclj clojure.lang.Repl

I modified main.clj to see if I could get it to recompile…. that didn’t seem to happen. So running things under the java debugger: 

C:UsersCliveDesktopclojure-1.0.0>java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=4571 -cp classes;srcclj clojure.lang.Repl

C:UsersCliveDesktopclojure-1.0.0>"c:Program FilesJavajdk1.6.0_14binjdb.exe" -connect com.sun.jdi.SocketAttach:hostname=localhost,port=4571

main[1] stop in clojure.lang.RT.lastModified
main[1] use /users/clive/desktop/clojure-1.0.0/src/jvm
main[1] list

Stepping through, we can see that the code in
  static public void load(String scriptbase, boolean failIfNotFound) throws Exception{

loads the core.clj file as a resource script if it is modified later than the class file.

We can test this by adding an extra function at the end.
  (defn extra [] "boo")

C:UsersCliveDesktopclojure-1.0.0>java -cp classes;srcclj clojure.lang.Repl
Clojure 1.0.0-
user=> (extra)

The last modified is being tried on the init class corresponding to the file.

main[1] print url
url = "file:/C:/Users/Clive/Desktop/clojure-1.0.0/classes/clojure/core__init.class"

So we’ll have to compile up clojure.core manually.

C:UsersCliveDesktopclojure-1.0.0>java -Dclojure.compile.path=classes -cp classes;srcclj clojure.lang.Compile clojure.core
Compiling clojure.core to classes

The main method of Compile.java basically compiles using the

  private static final Var compile = RT.var("clojure.core", "compile");

This calls the
  public static Object compile(Reader rdr, String sourcePath, String sourceName) throws Exception{
in Compiler.java

Let’s debug into it. We can simply touch the core.clj file and then

main[1] stop in clojure.lang.Compiler.compile

We eventually come around to the code that does the compilation

        for(Object r = LispReader.read(pushbackReader, false, EOF, false); r != EOF;
            r = LispReader.read(pushbackReader, false, EOF, false))
            Expr expr = analyze(C.EVAL, r);
            fn.keywords = (IPersistentMap) KEYWORDS.deref();
            fn.vars = (IPersistentMap) VARS.deref();
            fn.constants = (PersistentVector) CONSTANTS.deref();
            expr.emit(C.EXPRESSION, fn, gen);

Inside the LispReader

Step completed: "thread=main", clojure.lang.LispReader.read(), line=122 bci=0
122                     int ch = r.read();

main[1] list
118     try
119             {
120             for(; 😉
121                     {
122 =>                  int ch = r.read();
124                     while(isWhitespace(ch))
125                             ch = r.read();
127                     if(ch == -1)
main[1] step

and eventually we go into code that looks up the macro characters

Step completed: "thread=main", clojure.lang.LispReader.read(), line=142 bci=73
142                     IFn macroFn = getMacro(ch);

main[1] list
138                                     return null;
139                             return n;
140                             }
142 =>                  IFn macroFn = getMacro(ch);
143                     if(macroFn != null)
144                             {
145                             Object ret = macroFn.invoke(r, (char) ch);
146                             if(RT.suppressRead())
147                                     return null;

The getMacro looks up the characters in the macro array in LispReader.

    macros[‘"’] = new StringReader();
    macros[‘;’] = new CommentReader();
    macros[”’] = new WrappingReader(QUOTE);
    macros[‘@’] = new WrappingReader(DEREF);//new DerefReader();
    macros[‘^’] = new WrappingReader(META);
    macros[‘`’] = new SyntaxQuoteReader();
    macros[‘~’] = new UnquoteReader();
    macros[‘(‘] = new ListReader();
    macros[‘)’] = new UnmatchedDelimiterReader();
    macros[‘[‘] = new VectorReader();
    macros[‘]’] = new UnmatchedDelimiterReader();
    macros[‘{’] = new MapReader();
    macros[‘}’] = new UnmatchedDelimiterReader();
    macros[‘\’] = new CharacterReader();
    macros[‘%’] = new ArgReader();
    macros[‘#’] = new DispatchReader();

The first non-comment form we read is

main[1] print r
r = "(ns clojure.core)"

This is passed into
  return analyzeSeq(context, (ISeq) form, name);

This form is macroexpanded
4,482 =>                Object me = macroexpand1(form);
4,483                   if(me != form)
4,484                           return analyze(context, me, name);

resulting in the form 

main[1] print me
me = "(do (clojure.core/in-ns (quote clojure.core)))"

and we go around the analyze/analyseSeq code again. This time, do isn’t a macro and hence we drop through into the code that checks for an inline function and then tries some other possibilities.

4,492                   IParser p;
4,493 =>                if(op.equals(FN))
4,494                           return FnExpr.parse(context, form, name);
4,495                   else if((p = (IParser) specials.valAt(op)) != null)
4,496                           return p.parse(context, form);
4,497                   else
4,498                           return InvokeExpr.parse(context, form);

specials contains the special forms, things that the compiler needs to know how to handle. This is the thing that has always impressed me about Lisp – the set of core functions that the compiler needs to handle is really small. Most control constructs can be implemented using macros and some underlying runtime functions.

static final public IPersistentMap specials = PersistentHashMap.create(
        DEF, new DefExpr.Parser(),
        LOOP, new LetExpr.Parser(),
        RECUR, new RecurExpr.Parser(),
        IF, new IfExpr.Parser(),
        LET, new LetExpr.Parser(),
        LETFN, new LetFnExpr.Parser(),
        DO, new BodyExpr.Parser(),
        FN, null,
        QUOTE, new ConstantExpr.Parser(),
        THE_VAR, new TheVarExpr.Parser(),
        DOT, new HostExpr.Parser(),
        ASSIGN, new AssignExpr.Parser(),
        TRY, new TryExpr.Parser(),
        THROW, new ThrowExpr.Parser(),
        MONITOR_ENTER, new MonitorEnterExpr.Parser(),
        MONITOR_EXIT, new MonitorExitExpr.Parser(),
        CATCH, null,
        FINALLY, null,
        NEW, new NewExpr.Parser(),
        _AMP_, null

In our case, Do is a special form that we need to handle,
4,496                           return p.parse(context, form);
main[1] print context
context = "EVAL"
main[1] print form
form = "(do (clojure.core/in-ns (quote clojure.core)))"
main[1] print p
p = "clojure.lang.Compiler$BodyExpr$Parser@1530551"
main[1] next
3,854           static class Parser implements IParser{
3,855                   public Expr parse(C context, Object frms) throws Exception{
3,856 =>                        ISeq forms = (ISeq) frms;
3,857                           if(Util.equals(RT.first(forms), DO))
3,858                                   forms = RT.next(forms);
3,859                           PersistentVector exprs = PersistentVector.EMPTY;
3,860                           for(; forms != null; forms = forms.next())
3,861                                   {

This causes us to go back around into analyze with
4,286           return analyze(context, form, null);
main[1] print context
context = "EVAL"
main[1] print form
form = "(clojure.core/in-ns (quote clojure.core))"

This time we need to invoke this as an InvokeExpr as it is simply a function call.
4,498                           return InvokeExpr.parse(context, form);

We need to analyse this and generate an InvokeExpr node
    static public Expr parse(C context, ISeq form) throws Exception{
        if(context != C.EVAL)
            context = C.EXPRESSION;
        Expr fexpr = analyze(context, form.first());
        PersistentVector args = PersistentVector.EMPTY;
        for(ISeq s = RT.seq(form.next()); s != null; s = s.next())
            args = args.cons(analyze(context, s.first()));

        return new InvokeExpr((String) SOURCE.deref(), (Integer) LINE.deref(), tagOf(form), fexpr, args);

We first analyze the function symbol

2,767                   Expr fexpr = analyze(context, form.first());

4,307                           return analyzeSymbol((Symbol) form);
main[1] print form
form = "clojure.core/in-ns"

The form gets resolved to a Var, which is registered and then returned as a VarExpr

4,605 =>        Object o = resolve(sym);
4,606           if(o instanceof Var)
4,607                   {
4,608                   Var v = (Var) o;
4,609                   if(isMacro(v) != null)
4,610                           throw new Exception("Can’t take value of a macro: " + v);
4,611                   registerVar(v);
4,612                   return new VarExpr(v, tag);
4,613                   }

We’ll need to look more at the context argument, but for now we have seen how a tree representation is built from the source.

After we have finished parsing, we get back up to the compile function.

main[1] list
4,948                           Expr expr = analyze(C.EVAL, r);
4,949                           fn.keywords = (IPersistentMap) KEYWORDS.deref();

4,950                           fn.vars = (IPersistentMap) VARS.deref();
4,951                           fn.constants = (PersistentVector) CONSTANTS.deref();
4,952 =>                        expr.emit(C.EXPRESSION, fn, gen);
4,953                           expr.eval();
4,954                           LINE_BEFORE.set(pushbackReader.getLineNumber());

4,955                           }
4,956                   //end of load
4,957                   gen.returnValue();
main[1] print fn.vars
fn.vars = "{#’clojure.core/in-ns 1, #’clojure.core/ns 0}"
main[1] print fn.constants
fn.constants = "[#’clojure.core/ns #’clojure.core/in-ns clojure.core]"
main[1] print fn.keywords
fn.keywords = "{}"

We now want to emit the code for the tree which is rooted at a BodyExpr. This will generate code emitting all but the last form in a STATEMENT context.

    public void emit(C context, FnExpr fn, GeneratorAdapter gen){
        for(int i = 0; i < exprs.count() – 1; i++)
            Expr e = (Expr) exprs.nth(i);
            e.emit(C.STATEMENT, fn, gen);
        Expr last = (Expr) exprs.nth(exprs.count() – 1);
        last.emit(context, fn, gen);

In our case, there is a single node, so we’ll step into that. It is of type InvokeExpr.

2,725           public void emit(C context, FnExpr fn, GeneratorAdapter gen){
2,726 =>                gen.visitLineNumber(line, gen.mark());
2,727                   fexpr.emit(C.EXPRESSION, fn, gen);
2,728                   gen.checkCast(IFN_TYPE);

We emit the fexpr, which is a VarExpr.

429     public void emit(C context, FnExpr fn, GeneratorAdapter gen){
430 =>          fn.emitVar(gen, var);
431             gen.invokeVirtual(VAR_TYPE, getMethod);

which emits the position of the Var in the vars map

3,525           public void emitVar(GeneratorAdapter gen, Var var){
3,526 =>                Integer i = (Integer) vars.valAt(var);
3,527                   emitConstant(gen, i);

main[1] print vars
vars = "{#’clojure.core/in-ns 1, #’clojure.core/ns 0}"

and the invokeVirtual is emitted which accesses the get() method

1,329           invokeInsn(Opcodes.INVOKEVIRTUAL, owner, method);
main[1] print owner
owner = "Lclojure/lang/Var;"
main[1] print method
method = "get()Ljava/lang/Object;"

We then generate the arguments and finally invoke the fn.
2,750                   gen.invokeInterface(IFN_TYPE, new Method("invoke", OBJECT_TYPE, ARG_TYPES[Math.min(MAX_POSITIONAL_ARITY + 1,

Back in the compile function we then need to evaluate to get any side effects to happen. We do this by evaluating the form in the current system. This is one difference between Clojure and Lisp systems I have worked on in the past. They would maintain a compile-time environment to avoid the need to side-effect the current environment. This had advantages in that it allowed cross-compilation of source code. Common Lisp has all sorts of interesting mechanisms in the eval-when special form to control what side-effects happen at which point.

4,953 =>                        expr.eval();

3,876                   Object ret = null;
3,877                   for(Object o : exprs)
3,878                           {
3,879                           Expr e = (Expr) o;
3,880 =>                        ret = e.eval();
3,881                           }
3,882                   return ret;

2,707           public Object eval() throws Exception{
2,708                   try
2,709                           {
2,710 =>                        IFn fn = (IFn) fexpr.eval();
2,711                           PersistentVector argvs = PersistentVector.EMPTY;
2,712                           for(int i = 0; i < args.count(); i++)
2,713                                   argvs = argvs.cons(((Expr) args.nth(i)).eval());
2,714                           return fn.applyTo(RT.seq(argvs));
2,715                           }

After looping around and code generating the rest of the forms, we end at code for generating the rest of the class initialization code. By this point, we have a large number of vars that need to be set up, as well as keywords and constants. These are referenced by the code generated above which is generated into a method named load.

The constants, including the vars that are referenced, are set up as static fields of the class. The static initializer then pushes the namespace, calls the load method, and then pops the thread bindings.

We’ll ignore the next forms in core.clj

4,948                           Expr expr = analyze(C.EVAL, r);
main[1] print r
r = "(def unquote)"

main[1] print r
r = "(def unquote-splicing)"

main[1] print r
r = "(def list (. clojure.lang.PersistentList creator))"

Defs are handled by a special parser

4,495                   else if((p = (IParser) specials.valAt(op)) != null)

main[1] print p
p = "clojure.lang.Compiler$DefExpr$Parser@ec0962"

This generates us a new DefExpr.

363                             }
364 =>                  IPersistentMap mm = sym.meta();
365                Object source_path = SOURCE_PATH.get();
366                source_path = source_path == null ? "NO_SOURCE_FILE" : source_path;
367                mm = (IPersistentMap) RT.assoc(mm, RT.LINE_KEY, LINE.get()).assoc(RT.FILE_KEY, source_path);
368                     Expr meta = analyze(context == C.EVAL ? context : C.EXPRESSION, mm);
369                     return new DefExpr((String) SOURCE.deref(), (Integer) LINE.deref(),

The debugger doesn’t show us the other important lines

                               v, analyze(context == C.EVAL ? context : C.EXPRESSION, RT.third(form), v.sym.name),
                               meta, RT.count(form) == 3);

which eventually generates a StaticFieldExpr for the body

749                                     return new StaticFieldExpr(line, c, sym.name);
main[1] print form
form = "(. clojure.lang.PersistentList creator)"

We’ll move on to emitting java code for the def when we get to the emit
4,952                           expr.emit(C.EXPRESSION, fn, gen);

which does
315     public void emit(C context, FnExpr fn, GeneratorAdapter gen){
316 =>          fn.emitVar(gen, var);
317             if(initProvided)
318                     {
319                     gen.dup();
320                     init.emit(C.EXPRESSION, fn, gen);
321                     gen.invokeVirtual(VAR_TYPE, bindRootMethod);

which then emits  a StaticFieldExpr
977     public void emit(C context, FnExpr fn, GeneratorAdapter gen){
978 =>          gen.visitLineNumber(line, gen.mark());
980             gen.getStatic(Type.getType(c), fieldName, Type.getType(field.getType()));
981             //if(context != C.STATEMENT)
982             HostExpr.emitBoxReturn(fn, gen, field.getType());
983             if(context == C.STATEMENT)

Now we need to look at how functions are emitted. Continuing for a bit we get to the compilation of
4,948                           Expr expr = analyze(C.EVAL, r);
main[1] print r
r = "(def cons (fn* cons [x seq] (. clojure.lang.RT (cons x seq))))"

The analyse code generates a DefExpr into which it puts the result of analysing

Step completed: "thread=main", clojure.lang.Compiler.analyze(), line=4,293 bci=0
4,293                   if(form instanceof LazySeq)
main[1] print form
form = "(fn* cons [x seq] (. clojure.lang.RT (cons x seq)))"

which we parse as an FnExpr
4,493                   if(op.equals(FN))
4,494 =>                        return FnExpr.parse(context, form, name);
4,495                   else if((p = (IParser) specials.valAt(op)) != null)
4,496                           return p.parse(context, form);
4,497                   else
4,498                           return InvokeExpr.parse(context, form);

The code generates a FnExpr and then processes each method

Step completed: "thread=main", clojure.lang.Compiler$FnExpr.parse(), line=2,963bci=420
2,963                                   FnMethod f = FnMethod.parse(fn, (ISeq) RT.first(s));
main[1] print s
s = "(([x seq] (. clojure.lang.RT (cons x seq))))"

This parse method
3,637           private static FnMethod parse(FnExpr fn, ISeq form) throws Exception{
3,638                   //([args] body…)
3,639 =>                IPersistentVector parms = (IPersistentVector) RT.first(form);
3,640                   ISeq body = RT.next(form);
3,641                   try
3,642                           {
3,643                           FnMethod method = new FnMethod(fn, (FnMethod) METHOD.deref());
3,644                           method.line = (Integer) LINE.deref();
main[1] print form
form = "([x seq] (. clojure.lang.RT (cons x seq)))"

generates a new FnMethod, sets up the argument variables and then parses the body.
3,695                           LOOP_LOCALS.set(argLocals);
3,696                           method.argLocals = argLocals;
3,697 =>                        method.body = (new BodyExpr.Parser()).parse(C.RETURN, body);
3,698                           return method;

This has the name

main[1] print fn.name
fn.name = "clojure.core$cons__4"

We can step through the compile method of this to see how the code is generated.

It is, however, easier to use a java decompiler to have a look at the generated code. We were compiling core.clj, and we saw that this would generate a class file core__init.class. Decompiling this using jd-gui we see the load method has the form.

public static void load()
    const__3.setMeta((IPersistentMap)RT.map(new Object[] { const__4, "clojure/core.clj", const__5, const__6 }));
    const__7.setMeta((IPersistentMap)RT.map(new Object[] { const__4, "clojure/core.clj", const__5, const__8 }));
    Var tmp100_97 = const__9; tmp100_97

      .setMeta((IPersistentMap)RT.map(new Object[] { const__4, "clojure/core.clj", const__5, const__10, const__11, const__12, const__13,
                           "Creates a new list containing the items." }));
    Var tmp173_170 = const__14;
    tmp173_170.bindRoot(new core.cons__2206());
    tmp173_170.setMeta((IPersistentMap)RT.map(new Object[] { const__4, "clojure/core.clj", const__5, const__15, const__11, const__16,
                                                                 const__13, "Returns a new seq where x is the first element and seq isn    the rest." }));

The code for cons__2206 is

public class core$cons__2206 extends AFunction
  public Object invoke(Object x, Object seq)
    throws Exception
    x = null; seq = null; return RT.cons(x, seq);

That code looks wrong of course… we don’t really blast the arguments before the call to the RT.cons function. The decompiler is not decompiling correctly. We can see this using javap, which shows that we load the incoming arguments on to the stack, before blasting the old values, and it is these stack values that are passed into the RT.cons method.

public java.lang.Object invoke(java.lang.Object, java.lang.Object)   throws java.lang.Exception;
   0:    aload_1
   1:    aload_2
   2:    aconst_null
   3:    astore_1
   4:    aconst_null
   5:    astore_2
   6:    invokestatic    #20; //Method clojure/lang/RT.cons:(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/ISeq;
   9:    areturn

jd cannot decompile the static constructor which has the following code for setting up const__14, which is then assigned to the Var, tmp173_170, into which an instance of the function class is assigned.

    //   246: ldc_w 6584
    //   249: ldc_w 6651
    //   252: invokestatic 6590    clojure/lang/RT:var    (Ljava/lang/String;Ljava/lang/String;)Lclojure/lang/Var;
    //   255: checkcast 12    clojure/lang/Var
    //   258: putstatic 89    clojure/core__init:const__14    Lclojure/lang/Var;

Using javap to decompile it,
  C:UsersCliveDesktopclojure-1.0.0classesclojure>"c:Program FilesJavajdk1.6.0_14binjavap.exe" -c core__init > foo.txt   

We see the following
   246:    ldc_w    #6584; //String clojure.core
   249:    ldc_w    #6651; //String cons
   252:    invokestatic    #6590; //Method clojure/lang/RT.var:(Ljava/lang/String;Ljava/lang/String;)Lclojure/lang/Var;
   255:    checkcast    #12; //class clojure/lang/Var
   258:    putstatic    #89; //Field const__14:Lclojure/lang/Var;

with the runtime method having the code
  static public Var var(String ns, String name){
      return Var.intern(Namespace.findOrCreate(Symbol.intern(null, ns)), Symbol.intern(null, name));

Hence, the instance of the class representing the function is placed into the Var that is found using this method in the runtime.

We need to have a look at nested lambda expressions, but there’s not time to do that now.

It was an interesting exercise, and I now have some idea of what is happening under the surface. Having worked on several Lisp systems in the past, it was slightly disappointing that more of the compiler wasn’t written in Clojure; keeping the architecture-specific code small in those systems, allowed the system to be ported much more easily to other platforms.

This entry was posted in Computers and Internet. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s