Ekaitz’s tech blog:I make stuff at ElenQ Technology and I talk about it
From the series: GNU Mes interpreter speedup
In the previous post I promised that I was going to talk about the module system. It’s time for that.
The module system
I didn’t pay any attention to the module system when I prepared the project. I thought that it was just going to work because it was all Scheme. Sometimes I’m just too smart.1
As I explained before, the interpreter runs the boot code to prepare the everything for user code to run, but what does that really mean?
When the module system is loaded, it takes control of almost everything. It’s like the runtime is hijacked by it:…
Ekaitz’s tech blog:I make stuff at ElenQ Technology and I talk about it
From the series: GNU Mes interpreter speedup
In the previous post I promised that I was going to talk about the module system. It’s time for that.
The module system
I didn’t pay any attention to the module system when I prepared the project. I thought that it was just going to work because it was all Scheme. Sometimes I’m just too smart.1
As I explained before, the interpreter runs the boot code to prepare the everything for user code to run, but what does that really mean?
When the module system is loaded, it takes control of almost everything. It’s like the runtime is hijacked by it: it loads new files, it resolves variables and, of course, affects macro expansion. All the boot process needs to do is make it prepare the module system and give it the control of the rest of the execution.
Understanding Guile modules
<janneke> so we’re really booting guile-modules, in a way
Mes uses Guile modules. The were copied from Guile 1.8 to Mes’ boot files with a few changes to make them compatible with Mes’ internals. This is one of the most important points of Guile compatibility in Mes.
The file mes/module/mes/guile-module.mes has the code for the module system and as you can see there is long, not very-well documented and it’s hard to follow because everything has several layers of indirection. I’m going to explain what I understood from it. Take this with a grain of salt.
In Guile, modules are stored in a global variable that is managed by the module system. Each module is a record.
The modules have a few fields:
- The
nameof the module. - The
kindof the module. - Something called
obarray, a hash-map-like structure that contains all the definitions that happen in the current module. - A collection of
usesthat contains all the modules used by this module.
But also a few very dynamic things that are available for the user to change:2
- A procedure that defines the strategy for looking up symbols in the module, called
eval-closure. - Procedures that help deal with duplicates, things that are imported from more than one module. If some identifier comes from two dependencies, which one should we resolve to?
- Some other stuff related with observers, which is not well described but they look like functions that are called when the module changes.
If you are familiar with Guile modules you probably know they can export things, but they can also keep things internal to the module. For that, Guile uses interfaces that are also modules but only expose the public interface of the module they are created from (interface is a module kind). This means each module is a module by itself, but it also has an interface that is what it exposes to the world. So, actually, a module would use interfaces, not just modules.
Apart from that, one can refer to any definition of a module, regardless if it is imported or not using @ and @@. The difference between them one can access to the private parts of a module, while the other can access only the ones that are exported by its interface.
All this is provided in a very flexible API. Modules can be defined declaratively using define-module but also in a more imperative way by using things like use-modules, export, define-public…
Now, search any mention to macros in the description I just gave you. There is none. Macros are treated like any other definition.
If none of the things I mentioned here made you feel uncomfortable I have to admit you are stronger than I am. These last two paragraphs still trigger something unpleasant inside me. Yes, after all this time.
Compiling, loading and interpreting modules
I’m not going to get into the details about how modules are loaded and when. I’ll just encourage you to try to exploit them and use some eval-whens here and there to see what happens. It’s cooler than it seems.
If you do, you’ll find Guile modules have to be loaded in different moments. Guile’s ahead of time compiler loads modules, but they are also loaded during evaluation time. The macro expander is no less and loads modules too, because they might define macros that we are importing from them.
In summary, the module system is involved in all possible steps of code processing. This is solved with a very precise use of eval-when in a few parts of the module system.
Booting
If you have the guts to read the guile-module.mes file or it’s equivalent part in Guile’s module/ice-9/boot-9.scm you’ll realize that it is quite a high-level scheme program. Booting it means everything it requires must be available at the point when we run it.
Of course when booting we don’t have access to a module system, but must be able to define things. Everything that is defined before the module system is booted is defined as a global, out of the module system, in what it’s called “initial module”. When the module system takes over, define will define things at module-level instead. Modules themselves are stored in a global.
This means we need to be able to check if the module system is loaded. That’s easy: if the current module is not false it means it has been already loaded. It is not rare to find that check in Mes’ core.
Compared to Guile, Mes defines many more things in Scheme, so we have some includeing and loading in order to separate the booting process in smaller files.
Scheme-C interaction
Now, the modules are written in Scheme but from the C part of the interpreter we need to interact with them.
The current module is an example of that. It is supposed to be a Scheme fluid, but in Mes it’s just stored in a register called M1. The interpreter provides the builtins that manipulate it.
In Guile a few more parts of the module system are actually written in C and quite a few C parts call the module system.
In Mes, calling a Scheme function from C is not that obvious, so a few things are written in a simplified way in C, avoiding to call the module system. This doesn’t comply totally with what Guile does, but it’s good enough. I’m talking about src/modules.c and how, for example, observers are not called.
Some other places, like my macro expander, prepare a piece of Scheme code, literally consing things together, and call apply (in the macro expander I wrote it’s called macro_apply) with it.
Not defined yet
One of the very interesting behaviors the module system is dealing with variables that are not defined yet. Consider this code:
(define-module (imported)
#:export (m))
(define-macro (m x)
(f x))
(define (f x)
(display x))
Imagine another module imports this one, and tries to use m. Something like this:
(define-module (other-module)
#:use-module (imported))
(m '(something here))
If the macro expander doesn’t help, this would fail saying f is not defined because it is defined after m is. Of course, in Guile this works.
Macro introduced names
The detail above is similar but simpler than the one that I’m going to explain here.
This example below shows how a name is introduced by the macro:
;; boot/thing.scm
(define-module (boot thing)
#:export (with-hi))
(define (hola) (display "HOLA"))
(define-macro (with-hi . exps)
`(begin (hola) ,@exps))
; ^^^^^^
;; boot/other.scm
(use-modules (boot thing))
(with-hi 1) ;; expands to => (begin (hola) 1)
;; ????
When using with-hi it introduces a name, hola, where the with-hi was. This is not in the module where hola was defined.
In that case, what should that hola be? It depends, if boot/other.scm has a (define hola 2) it will be 2. If it doesn’t have any (define hola ...) it will be unknown and the program execution will fail.
Fully qualified names
This case, though:
;; boot/thing.scm
(define-module (boot thing)
#:export (with-hi))
(define (hola) (display "HOLA"))
(define-macro (with-hi . exps)
`(begin ((@@ (boot thing) hola)) ,@exps))
; ^^^^^^^^^^^^^^^^^^^^^^^^
;; boot/other.scm
(use-modules (boot thing))
(with-hi 1) ;; expands to => (begin ((@@ (boot thing) hola)) 1)
The @@ here makes us know exactly what hola is, as it knows where it’s coming from and is completely independent of the context where it is used. It’s a fully qualified name:
[…] a fully qualified name is an unambiguous name that specifies which object, function, or variable a call refers to without regard to the context of the call.
— Wikipedia
But I don’t like having to do that manually. Wouldn’t it be cool if the macro system automatically did this expansion, using the current module so the hola name is perfectly under control?
And, for the previous “not defined yet” case, if the macro expander also was aware of all definitions, local or global, wouldn’t it be able to do the very same thing and expand the unknown names to point to the current module so they are known later?
That’s exactly what it tries to do.
If you tried the examples in this post you might be confused, because the “macro introduced names” doesn’t work. Guile doesn’t know what hola is. You have to manually specify, as I did in this subsection.
It turns out that in Lisp style macros (define-macro) the introduced names are not that easy to find3 and Guile applies this full name qualification only in hygienic macros:
;; TEST.scm
(define-module (TEST)
#:export (m))
(define-syntax m
(syntax-rules ()
((_ x)
(+ x y))))
(define y 100)
;; Now in the repl (guile -L .):
scheme@(guile-user)> (use-modules (TEST))
scheme@(guile-user)> (use-modules (language tree-il))
scheme@(guile-user)> (tree-il->scheme (macroexpand '(m 10)))
$1 = ((@@ (TEST) +) 10 (@@ (TEST) y))
There you go.
In summary
I needed to fight with all this specifics and many others, but it’s very interesting to see that when I was very worried about being unable to make the macro expander work with the module system, I wrote emails to a few people. One of them, Ludovic, Guile co-maintainer and Guix author, answered me with a very clean, straight to the point, description of Guile’s module system.
I’m not sure I can be of much help because I’m not familiar with how Mes does it, but I can talk about Guile’s module system.
Guile’s module system is almost unchanged since its inception in 1996! Modules exist at run-time (in 3.0 they also have an existence at compile-time) and they’re essentially a thin layer around a hash table mapping symbols (bindings) to variables (boxes around the values of those bindings). There’s also a hash table for imports.
Then there’s the
current-modulefluid that says what the current module is.The macro expander replaces references to free variables by their “fully qualified names”. For instance, if you have:
(define-module (x) #:use-module (ice-9 q)) (define (whatever) (make-q))… the reference to
make-qis turned into:(@ (ice-9 q) make-q)at macro-expansion time. To do that, the expander searches for free variables in the local or imported bindings of
(current-module), in this case(x). Here the value of(current-module)is consulted at macro-expansion time only; its value at run time doesn’t matter.The
(define-module (x) …)form expands to something that creates(x)and then does(set-current-module (resolve-module '(x))).That’s about all there is to the module system.
(In 3.0,
#:re-export-and-replacefor instance is a bit more sophisticated because it’s a compile-time feature.)HTH!
Ludo’.
At the moment when I got the email I didn’t really see how much wisdom the email contained and I was frustrated, because my specific concerns were unattended. He told me what I needed to know, not what I wanted to know.
I needed to fight with every single nitty-gritty detail to be able to understand the email properly, and that’s why I didn’t start the post with the email. I wanted to replicate a little part of my travel with you.
With time and distance, that’s all what is left: the very precise but high level description of the matter. But when you are stuck in the mud, you are just in a different conversation.4
In Mes
But you don’t read what I write looking for elegant descriptions, but the ugly corner cases and implementation details.
Let me then describe how all this applies to changes in the macro expander and my approach to the project.
Removing eval-when abuse
When I introduced eval-when in previous posts I didn’t explain a lot about how Guile’s behaviour is actually pretty weird with it. If you are compiling a Guile file Ahead of Time (calling guild compile) the behavior is different from what you’ll get if you just interpret the file directly.
Try to run this vs compile it.
(define a 10)
(define-macro (m) a)
(display (m))
Guile will complain about a being unbound but it will run and do what you expect. Compiling simply doesn’t work because a is not bound at compilation time.
This made me decide whether to implement everything that Guile does or to choose one of the two possible behaviors. In a previous blogpost I said I decided to make Mes act like Guile when it compiles files Ahead of Time but that made me pollute all the boot files with eval-whens and had a few other implications. What happens if the modules Mes uses are written in a way that is not compilable?
Thinking about it in simple terms, there’s no need to make Mes have an Ahead of Time compiler, and everything is simpler if we embrace the idea of compiling and evaluating expressions one by one (isn’t that what the REPL should do?). It simply covers more cases without making everything harder, but maybe making it simpler.
With that in mind I decided to change the architecture of the interpreter, and don’t make an Ahead of Time macro expansion, but iterate over the top-level expressions and expand them and evaluate them right after they are expanded. Later we would add a compilation step in between, and that’s all.
This doesn’t make me introduce eval-when‘s in every single top-level define that is used in the boot files, as I was doing before, and keeps everything clean and easier to reason about. Remember we have a lot of Scheme code for booting, while Guile has a lot of that written directly in C.
I still have the branch with all the eval-whens added to it and all but it isn’t the longest branch anymore. Let’s say it that way.
Macros are not global anymore
Mes implemented macros in a global variable inside the interpreter. They didn’t use the module system to be defined. It was simply bypassing the module system.
This point is extremely important for the macro system to get right, and is related with the things I shared before. This piece of code works with global macros but, according to what we read previously, it doesn’t work in Guile:
;; A.scm
(define-module (A)
#:export (m))
(define-macro (m x)
`(f ,x)) ;; Quoting is relevant here
(define-macro (f x)
(display x))
;; B.scm
(define-module (B)
#:use-module (A))
(m 10) ;; What should this expand to? `(f 10)`? What is `f` then?
If macros are global, there’s access for f in B, but if the module system is involved in the thing, f doesn’t exist. It’s the same thing that I described earlier in the post, but specific to macros. I have to fully qualify all the names inside of the macros if I want this to work.
Of course, if macros are global you don’t actually need to do any of this, and you can get away with just fully qualifying names in functions when you find them. That’s what Mes did: the simplest thing that could possibly work.
Now5, it’s not just that Mes will use the module system for macros, with all the benefits that brings, it will also fully qualify names during macro expansion, and not only for the (define (f ...) ...) case, like we described in the earlier blog post, but for everything.
This change uncovers a lot of warnings when you run the interpreter, that were not detected before6:
$ ./bin/mes
WARNING: (srfi srfi-39): imported module (srfi srfi-16) overrides core binding `case-lambda'
WARNING: (mes getopt-long): imported module (ice-9 optargs) overrides core binding `define*'
WARNING: (mes getopt-long): imported module (ice-9 optargs) overrides core binding `let-optional*'
WARNING: (mes getopt-long): imported module (ice-9 optargs) overrides core binding `let-keywords*'
WARNING: (mes getopt-long): imported module (srfi srfi-9) overrides core binding `define-record-type'
WARNING: (mes repl): imported module (mes mes-0) overrides core binding `mes-use-module'
WARNING: (mes main): imported module (mes mes-0) overrides core binding `mes-use-module'
GNU Mes git
Copyright (C) 2016,2017,2018,2019,2020,2021,2022,2023,2024,2025 Janneke Nieuwenhuizen <>
Copyright (C) 2019,2020,2021 Danny Milosavljevic <>
Copyright (C) 2021 Wladimir van der Laan <>
Copyright (C) 2022,2023 Timothy Sample <>
Copyright (C) 2022,2023,2025 Ekaitz Zarraga <>
Copyright (C) 2023,2025 Andrius Štikonas <>
and others.
GNU Mes comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.
Enter `,help' for help.
mes>
The warnings are the module system finding overrides. When everything was global there was no chance to find this kind of possible issues. This doesn’t mean they weren’t happening.
The macro expansion algorithm
We shared a prototype of the macro expander in the previous blog post, but it certainly has evolved a lot since.7
The macro expansion algorithm is a recursive process like the one I proposed in previous blog posts, but now it has gained some extra capabilities.
Now, if an expression is a top-level expression it is evaluated after expansion. This makes begin become a tricky business until you realize how easy it is: it’s a structure that doesn’t increase the level and its body is processed one by one, as if the expressions were independent. Simple, but difficult.
We already talked about scoped macros in previous episodes. We used a variable that tracked all the macros as a list of environments. Now we have another level of complexity here: when there are no local environments we have to use the module system and register the macro in the current module. But hey! If the module system is not loaded yet we should use the global environment instead. Thankfully, Mes has a function for that: lookup_variable.
If we are going to expand everything to the fully qualified name we need to keep track of local variables, so we can know if they are module-level, local or global. It’s not that different from what we just said for the macros, really: keep track of all definitions, uses that are not defined yet should be fully qualified pointing to the current module, hoping they will be defined later in the module; known definitions should be pointed to whatever their origin is. This requires more program analysis and understanding that our expansion does not return Scheme code anymore, but something else. Something better.
If we are fully qualifying names as we expand things, how should we treat this case?
(@@ (a module) a-name)
If this is just a regular macro, the recursive algorithm would try to fully qualify a-name, and if it is fully qualified by expanding to this structure, it would continue do that forever. In order to avoid that, I implemented both @ and @@ in C, as an special case in the macro expander so that doesn’t happen. What do they resolve to? They resolve to bindings!
As I introduced in the previous blog post, a binding is a type of value we don’t share with the Scheme programmer but is very useful for the interpreter. Bindings point to the location where values are or will be stored. In the case of a module, as the obarrays are hash-maps, the binding to a module variable would point to the handle to that location in the obarray.
In the case you didn’t know (I didn’t know), the handle is the key/value pair that contains the elements of a hash-map.
Hash tables are implemented as a vector indexed by a hash value formed from the key, with an association list of key/value pairs for each bucket in case distinct keys hash together. Direct access to the pairs in those lists is provided by the -handle- functions.
— The Guile Reference manual (6.6.22.2 Hash Table Reference)
Before I added this to the interpreter, the module system simply died even if the rest of the code looked like it was properly expanded.
After a #:replace was used in a module definition with basic scheme functions like member and filter the module system collapsed. The issue was, as they weren’t properly qualified, the module system’s use of those functions was also affected by the #:replace. As #:replace was removing the declaration to add it later, between one thing and the other the module system was unable to use them.
With functions that were defined on top of their simplified versions, which is common during the booting process, I experienced the effects of this too. During booting, map is defined in terms of map1, which is defined in terms of an earlier map. If things are not properly fully qualified that becomes a recursive definition and the stack overflows.8
Fully qualifying the names fixes all of these small but hard to catch issues. If only I had known!
Funny enough, the expand_variable function I introduced in the previous blog post, that was only called in the (define (whatever ...) ...) case, was enough to prevent many occurrences of this issues from happening. It was born as an optimization, but it happened to be a feature some other things relied upon.
Putting it all together
The macro expansion step has grown a lot since the blogpost where I introduced it, but it kind of works now!
Some of the fully qualifying issues are not totally solved, because they are not obvious. What should I do with things that are not yet defined but they are defined in one of the module uses? They might be overridden later in the file! Maybe the best is to make several passes to find out, but I’m doing everything as we go so I’m always choosing the best-effort approach.
As I said in the previous blog post, I still need to fix a couple of issues with the memory and some pointers being killed when the garbage collector triggers, but the algorithm works. So it looks like the best-effort approach was enough.
You can see the whole thing in the new modules branch, where I continued with the development:
https://codeberg.org/ekaitz-zarraga/mes/src/branch/modules/src/macro.c
I encourage you to read it because I think it’s quite easy to follow and it’s full of comments. A few functions have a weird name, I know9, but you’ll be fine.
Fully qualifying macro-introduced names
None of what I said until here fixes the full name qualifying issue for macro introduced variables.
When I introduced macro expansion for the first time I mentioned that I was going for define-macro only, and that we would implement define-syntax, syntax-case and the whole family on top of it.
In Mes define-syntax and syntax-rules are defined in mes/module/mes/syntax.scm. This file comes from one of the alternative implementation files in Scheme-48. It’s kind of a short file, but still… It is complex. Instead of trying to understand it, let me introduce how I realized many things here.
I found this piece of code in Nyacc failed when sx-match was imported and used in a different module:
(define-syntax sx-match
(syntax-rules ()
((_ e c ...)
(let ((v e)) (sx-match-1 v c ...)))))
(define-syntax sx-match-1
(syntax-rules ()
((_ v (pat exp ...) c1 ...)
(let ((kf (lambda () (sx-match-1 v c1 ...))))
(sxm-sexp v pat (begin (if #f #f) exp ...) (kf))))
((_ v) (error "sx-match: nothing matches"))))
You can see how sx-match expands to something that uses sx-match-1, that has to be re-expanded, using the macro below. I already introduced this issue before: if this module only exports sx-match, if it is used in another module, it will expand to some piece of code using sx-match-1, that it should expand further until no macros are left. But sx-match-1 is not exported, so it’s not known in the module where the expansion happens.
If all macros are global, this simply works.
If sx-match-1 was a local procedure that we were calling we could even know, as we could catch and fully qualify it when the original module is processed.
But this case is very weird: the code expands to something that contains it. In other words, sx-match-1 is quoted inside the underlying definition in Scheme code, behind that template-based definition you can see there, making it as hard as the case that Guile was unable to fully qualify.
Click here if you don’t believe its quoted, or if you want to see my GDB debugging skills.
;; (gdb) p display_ (get_macro(macros, cstring_to_symbol ("sx-match"))->cdr)
(((*circ* . sx-match args %program ...)) args
((lambda (%input %rename %compare)
((lambda (%tail)
(if #t
((lambda (test)
(if test
((lambda ()
test
((lambda (e)
((lambda (c)
((lambda ()
(#<binding cons> (%rename (quote let))
(#<binding cons>
(#<binding cons>
(#<binding cons> (%rename (quote v))
(#<binding cons> e (quote ())))
(quote ()))
(#<binding cons>
(#<binding cons> (%rename (quote sx-match-1))
;; ||||||||||
;; This is where sx-match-1 appears _________________________//////////
(#<binding cons> (%rename (quote v))
c))
(quote ())))))))
(#<binding cdr> %tail)))
(#<binding car> %tail))))
(if #t ((lambda (test)
(if test
((lambda ()
test (#<binding syntax-error> use of macro doesn't match definition %input)))
*unspecified*))
#<binding else>))))
((lambda (%temp)
(if (#<binding pair?> %temp)
(#<binding list?> (#<binding cdr> %temp))
#f))
%tail))))
(#<binding cdr> %input)))
(#<binding cons> (quote sx-match)
args)
(lambda (x0)
x0)
#<binding eq?>))
From our classic lisp-style macro expander, we shouldn’t manipulate quoted things really, because they can actually mean to be just a symbol with no further meaning than their name. We don’t know what kind of code we are generating. We can’t simply guess.
But we don’t have to be there in the lower-level. The templates give us a great chance to process the code in a way that is easier to know which things should be fully qualified. That’s how Guile is able to do it.
Reading the mes/module/mes/syntax.scm I found a few interesing pieces of code, but it’s still hard for me to know what’s going on. There’s also some comment in the file, in the end:
;; Kludge for Scheme48 linker.
;; `(cons ,(make-transformer rules)
;; ',(find-free-names-in-syntax-rules subkeywords rules))
It’s a shame that find-free-names-in-syntax-rules, even if its name tells us that’s the procedure we need, doesn’t exist. It’s nowhere10.
Also what’s that %rename procedure we see everywhere in the file?
If I could use them both, I’d probably be able to fully qualify all those free variables.
I have to admit I didn’t do that yet, because I still have a few more interesting things to do.11
I managed to skip this issue by just being dumb, because dumber is better. I found all the specific cases where this thing was happening and hardcoded them in a rename function that checked for them manually.
Click here to expand a piece of ugly code that shows what I did.
;; Rename a few specific cases that I know that fail
(define (rename x)
(if (member x (list 'sx-match-1 'sx-match-tail 'sx-match-1-tail 'sxm-sexp
'sxm-attr-tail 'sxm-tag 'sxm-tail 'sxm-node))
(make-binding
`(@@ (nyacc lang sx-util) ,x)
(module-variable (resolve-module `(nyacc lang sx-util)) x))
(if (member x (list 'pmatch 'ppat))
(make-binding
`(@@ (system base pmatch) ,x)
(module-variable (resolve-module `(system base pmatch)) x))
x)))
(define-macro (define-syntax macro-name transformer . stuff)
`(define-macro (,macro-name . args)
(,transformer (cons ',macro-name args)
rename ;; Use the rename procedure above
eq?)))
And it worked.
That was more than enough to let me know that I could do it, so I could focus on other things, as I said. I should come back to this later.
Conclusion
In this deep dive we mixed the concept of bindings, introduced in the previous blog post, with their usage in the macro expansion and how that relates with the module system. We described how the Guile module system works and how Mes tried to emulate it which led us to how my macro expander changed to include all this.
It has been a messy amalgamation of information coming from someone that had to have the miopic view of the one that is dealing with the specific issues, but hopefully it will become cleaner later, with the experience, distance and time real experts like Ludovic have.
For the moment, I’m slowly getting out of this swamp. To fall in the next one, probably.
It’s still a lot of fun to remember how macro expansion and the module system weren’t very important for me when I proposed the project but I’ve already spent more than the half of the project dealing with them and I didn’t really finish yet.
I don’t know how much you agree with this but I believe the outcome of this whole process is good. I think I managed to demonstrate here that my approach for macro expansion is good, and that Mes will benefit from it once I fix the couple of things that I have pending.
For me, the fact that the algorithm works is really an interesting side effect of the real deal: deeply understanding its inner workings.
Regardless of the impact this may have in my income, my deadline and all, I’m very happy with the work here. I would probably never learned this much in a project that I started myself. Here the goals are clear (Guile compatibility, running MesCC…) and most functionality is already implemented (booting, srfi, data-structures, gc…). I can just focus on learning and fixing what I think is not great. Rewriting things takes time but it’s not as hard as starting everything myself.
And of course, I have great hackers around that help me a lot. That’s the best way to learn.
I’m sure this whole thing would pay up in the long term, even if it already did in the short: it helped me understand how Guile actually works.
Hopefully it helped you, too.
Cheers
Of course, I’m just joking here. The reality is that I didn’t know how tricky it could get. If I knew everything I’d be doing something else. I’m not a kamikaze though, I try to account for this kind of deviations and opportunities for learning when I prepare a project. ↩ 1.
This is a significant detail. Calling Scheme code from C is not that obvious, specially if you are trying to isolate the core of the interpreter to replace it later. ↩ 1.
I tried myself, quite hard, and I didn’t manage to do it. ↩ 1.
This also happens to me a lot when I write blog posts like this one. I had to struggle with way more things, but my brain washed them out. I have to make a huge effort to describe them here the way I saw them before understanding them. It’s a very hard exercise. ↩ 1.
I mean, if I ever get to fix the memory management issue in the macro expander. ↩ 1.
Emails are redacted from the command output because I’m sick of AI scrapers. ↩ 1.
Basically because it was wrong. 🤡 ↩ 1.
This recursive definition was later removed by Janneke. ↩ 1.
Use git blame before coming at me with your complaints. ↩
1.
And believe me: I searched for it. ↩ 1.
Like writing this blog post and fix the memory errors I have. ↩