Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Res ipsa loquitur — Latin, meaning "the thing speaks for itself"

Largely following the steps noted down in pg's The Roots of Lisp. Blissfully ignoring the admonitions in this axis-of-eval blog post.

Example REPL session

>>> (car '(x))
x
>>> (eq 'foo (car '(foo)))
t
>>> ((lambda (x) (cons x '(b))) 'a)
(a b)
>>> (eval '((lambda (x) (cons x '(b))) 'a) '())
(a b)

Short-term plan

  • Make a REPL work in a web page
  • Change the 'eval' evaluator to compare values, not symbols
  • Change the 'eval' evaluator to pass function values, not lambda exprs
  • Change the 'eval' evaluator to accept varargs

Three surprises on the way to the interpreter

See docs/three-surprises.md.

Compiling to an intermediate representation

We take things in two big steps. The first, compiling to an intermediate representation. This makes some statically known or inferrable things clear in the code, and breaks it down into smaller steps. It's also straightforward to flatten the intermediate representation to bytecode later — that is the second step.

Variable lookup

A variable lookup in code has two parts:

  • The static part, where the variable lookup is resolved to the innermost binder which defines it. The resulting lookup information, if successful, is of the form "M steps out, slot number N".

  • The dynamic part, where a value is looked up using the "M steps out, slot number N" information, together with an environment, a linked list of at least N frames, the Nth of which has at least M slots.

In case the static lookup is successful, we generate (lookup M N).

In case it wasn't, we generate (error (concat "Variable " name " not found")).

Quote

Quotation proceeds through a nested list structure, in preorder.

  • Leaf nodes are either symbols or the empty list.
    • A symbol generates (symbol N), where N is a number (like a u64) indexing into a global symbol registry. If the symbol wasn't already in this registry, we add it and give it a fresh index.
    • The empty list generates (empty-list).
  • Internal nodes cons the results of their children together, right-to-left.
    • Start by generating (empty-list); call this L.
    • For each child node e, right-to-left:
      • Calculate (cons e L); call this new list L.
    • The end result is L.

Conditional

A conditional has this form:

(cond c1 e1
c2 e2
...
cN eN)

Equationally, this is equivalent to a simpler if:

(if c1 e1
(if c2 e2
...
(if cN eN
(error "Fell off cond"))))

Let's consider a single if:

(if c e-then e-else)

For the intermediate format, we use "label binders":

(fwd-label AFTER-IF
(fwd-label AFTER-THEN
(jump-unless-nil c AFTER-THEN)
e-then
(jump AFTER-IF))
e-else)

This is still a nested format, but it's much easier to generate linear bytecode from it, thanks to the named labels.

Function application

A function application looks like this:

(closure-expr a1 a2 ... aN)

There are two challenges here:

  • In order to maintain a "flat" intermediate representation, we need to compute all the arguments a1 a2 ... aN, and store them in temporary registers.

  • In order for the "call function" opcode to have bounded size, the call must only make use of indexed slots, both for the called closure and all the arguments.

The resulting intermediate code looks something like this:

(set-slot sc closure-expr)
(set-slot s1 a1)
(set-slot s2 a2)
...
(set-slot sN aN)
(call sc s1 s2 ... sN)

Lambda

A lambda generates a function value, but let's call it a closure for greater impact. A closure has two parts:

  1. The environment, supplied by the runtime. This is so that the function body can access variables declared outside the function.
  2. The code, consisting of two parts:
    • A parameter list, but this is really a nonnegative integer.
    • Instructions, the result of recursively compiling the function body.

Importantly, the code part is compiled/prepared once, and can then be re-used with a different environment in each created closure.

Generates as (closure c), where c is an index into a global code registry. Again, the environment is supplied by the runtime.

Label

An expression (label name expr) generates the following:

(rec expr)

Where expr has been compiled in a new scope that binds name to its single slot.

Intermediate format instructions

FormType
(lookup M N)IrLookup
(error msg)IrError
(symbol sy)IrSymbol
(empty-list)IrEmptyList
(cons e L)IrCons
(fwd-label lbl IR)IrFwdLabel
(jump-unless-nil e lbl)IrJumpUnlessNil
(jump lbl)IrJump
(set-slot r e)IrSetSlot
(call sc s1 s2 ... sN)IrCall
(closure c)IrClosure
(rec e)IrRec

Tail calls

In fact, there's an easy addition we might as well make:

FormType
......
(call sc s1 s2 ... sN)IrCall
(tail-call sc s1 s2 ... sN)IrTailCall
......

The tail-call opcode is then used in tail-call position, which is defined inductively as follows:

  • Expressions at the end of a function definitions are in tail-call position.
  • If an if expression is in tail-call position, then both its "then" branch expression and its "else" branch expression are also in tail-call position.

Semantically, a tail call eliminates the need to return to the current function activation after the function call to sf completes. Instead, we can return immediately to the current function's caller. In other words, tail calls can act more like "go to" instructions, because they give all the benefits of a function call but without growing the stack.

About

A metacircular Lisp in TypeScript

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages