惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
C
CXSECURITY Database RSS Feed - CXSecurity.com
V
Visual Studio Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
小众软件
小众软件
L
LangChain Blog
Cyberwarzone
Cyberwarzone
美团技术团队
The Register - Security
The Register - Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
V2EX
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 最新话题
Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
酷 壳 – CoolShell
酷 壳 – CoolShell
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
aimingoo的专栏
aimingoo的专栏
人人都是产品经理
人人都是产品经理
F
Full Disclosure
V2EX - 技术
V2EX - 技术
The Cloudflare Blog
博客园 - 叶小钗
T
Threat Research - Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
G
GRAHAM CLULEY
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Latest news
Latest news
S
Security @ Cisco Blogs
Spread Privacy
Spread Privacy
Project Zero
Project Zero
K
Kaspersky official blog
MyScale Blog
MyScale Blog
Attack and Defense Labs
Attack and Defense Labs
云风的 BLOG
云风的 BLOG
博客园 - 【当耐特】
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏
P
Privacy International News Feed
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
C
Cybersecurity and Infrastructure Security Agency CISA
Webroot Blog
Webroot Blog
罗磊的独立博客
Vercel News
Vercel News
N
News and Events Feed by Topic
A
Arctic Wolf
C
CERT Recently Published Vulnerability Notes

Hacker News

Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests Retrofitting JIT Compilers into C Interpreters IPv6 – Google The Accursèd Alphabetical Clock Cybersecurity Looks Like Proof of Work Now Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent When moving fast, talking is the first thing to break Too much Discussion of the XOR swap trick – Heather Cafe Introduction to Spherical Harmonics for Graphics Programmers The Grand Line Building a Z-Machine in the worst possible language High-Level Rust: Getting 80% of the Benefits with 20% of the Pain GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The air throughout our homes is infused with microplastics. But there are things you can do to breathe less of them The disturbing white paper Red Hat is trying to erase from the internet – OSnews The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong Artemis II crew splashes down near San Diego after historic moon mission We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Cooperative Vectors Introduction | Evolve Keeping a Postgres queue healthy — PlanetScale Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? The Zettelkasten Method in Obsidian: A Practical Setup Guide Artemis II Is Competency Porn and We Are Starving For It WeakC4 Flight Viz — Cockpit View A Mexican surveillance giant you’ve never heard of is now watching the U.S. border Surelock: Deadlock-Free Mutexes for Rust RISC-V 101 – what is it and what does it mean for Canonical? | Ubuntu The Problem That Built an Industry How Much Linear Memory Access Is Enough? | Solidean Investigating Split Locks on x86-64 Simplest hash functions Sybilproof reputation mechanisms (2005) [pdf] What is a property? How Complex is my Code? Static code analysis in Kotlin — tools overview Toffoli gates are all you need PGLite evangelism dcmake: a new CMake debugger UI Clojure on Fennel part one: Persistent Data Structures Fragments: April 2 Python Release Python install manager 26.1 The Life and Death of the Book Review - Liberties Introducing Database Traffic Control — PlanetScale Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Building slogbox Apple Silicon and Virtual Machines: Beating the 2 VM Limit Who was “Not Even Wrong” first? Pokemon Evolution Vs Darwinian Evolution The APL Programming Language Source Code
2.7 FEXPR vs. vtable
naver · 2026-06-24 · via Hacker News

FEXPR vs. vtable: how LispE evaluates instructions

The LispE paradox: it is an interpreter of compiled objects.

Ah!!! The famous opposition between interpreter and compiler. On one side, you keep the original code, which buys you flexibility and malleability, with little room for machine-level optimization. On the other, compilation hands you an efficient binary, but one horribly local to the machine it was compiled for. At least, you can usually optimize in every direction, though the code is forever frozen in its form. Except that for a Lisp, that's a bit of a shame. Building an ultra-optimized Lisp but losing homoiconicity along the way is not exactly optimal.

But LispE is a walking paradox, because LispE refuses this choice: it elects to be an interpreter of compiled objects. I know, put that way it sounds very hard-boiled. And yet... What LispE proposes is to turn each instruction of the language into a derived class that exposes an eval method. Every class has its own override of eval. And since all these classes derive from a single mother class with an original and evocative name, Element, you can build a C++ vector that holds them all. And that is where the miracle happens: the AST is kept alive, but each node is an object that can be optimized to the hilt with the full C++ arsenal.

The claim of this text is that this miracle rests on a single equivalence, which the rest of the argument makes precise and defends:

datum:        f()           ⟺ F.eval()
instruction:  f(a₁,..,aₙ)   ⟺ F(a₁,..,aₙ).eval()

where F is an instance of a class derived from a universal root class, holding its arguments as sub-nodes, and eval is a method without arguments, uniform across the whole hierarchy. A datum is the zero-arity case, an instruction with no sub-node whose eval returns itself. We will see that the equivalence holds only if F is immutable, and that everything LispE does with its instructions follows from this.

But first, let us go back in time, back to a far-distant era when people asked real questions about Lisp, back to the debate over FEXPRs.

1. What is a FEXPR?

In the earliest Lisps (Lisp 1.5, MacLisp), an operator could be of two kinds:

  • an EXPR: an ordinary function, whose arguments are evaluated before the call (applicative order);
  • a FEXPR: a function that receives its arguments unevaluated, the raw form, and that decides itself, at runtime, what to evaluate, calling eval back as needed.

A FEXPR is therefore a runtime mechanism: it is the operator itself that drives the evaluation of its operands. This is very expressive, you can write if, and, quote, loops… as plain FEXPRs, but this expressiveness comes at a prohibitive cost: opacity to the compiler.

;; The idea of a FEXPR: receives (test then else) UNevaluated
(fexpr my-if (test then else)
  (if (eval test) (eval then) (eval else)))

Because it is the FEXPR that decides at runtime which arguments to evaluate, and how, the static analyzer can know nothing about the form: no inlining, no optimization, no early resolution. Every call becomes a black box. This is Kent Pitman's central argument (Special Forms in Lisp, 1980), and the reason why historical Lisps moved away from user FEXPRs to preserve compilability. LispE answers the same constraint, but by a different route, and the route is the equivalence above.

2. The equivalence f(a₁,..,aₙ) ⟺ F(a₁,..,aₙ).eval()

The ordinary way to read function application is functional: f(a₁,..,aₙ) denotes the value obtained by applying f to its arguments. The object-oriented reading says the same thing differently: build an object F holding those arguments, and send it the message eval. The two are not analogous, they are the same act under two notations. Applying a function to its arguments is constructing an object from them and asking it to evaluate itself.

This reverses the usual hierarchy. We tend to think of objects as a way to implement functions, reifying f into an F so it can be carried around. The equivalence says the opposite: f(a₁,..,aₙ) is the surface notation, and F(a₁,..,aₙ).eval() is the underlying form, an object that carries its own evaluation rule. The (op . args) of Lisp, the F(args) of C++, and the f(args) of the mathematician are three spellings of Element(args).eval().

A subtlety must be stated, because it closes the model. A datum is not an object of a different nature than (+ a b). It is an instruction whose evaluation rule is the identity: X.eval() returns X. In the notation above, it is the zero-arity case, f() ⟺ F.eval(), an instruction with no sub-node whose evaluation produces the instance itself. The data is the degenerate case of the instruction, the fixed point of eval. This is what lets a single root class Element cover everything: a list whose evaluation descends, and a value whose evaluation returns itself, are two regimes of one type. An instruction is a std::vector<Element*>, each cell an Element that knows how to evaluate itself, leaf or sub-list alike. No tag distinguishes "code" cells from "data" cells, because there is none to make. This is homoiconicity realized in C++: not "code looks like data," but "data is the minimal code, the one whose evaluation is a fixed point."

Why the equivalence requires F to be immutable

The equivalence is exact only under one condition: F must be immutable. The reason is referential transparency. f(a₁,..,aₙ) when evaluated twice yields the same result, and you can substitute the value for the expression anywhere. Hence for F(a₁,..,aₙ).eval(), eval must not modify F. If evaluation mutated the object or its sub-nodes, the second call would see a different state from the first, and the equality would break on the object side. An eval that mutates is no longer a function but a side-effecting procedure, and the whole construction collapses back into the imperative.

One distinction must be drawn here: this purity is a property of the interpreter, not of the LispE programs it runs. The methods that evaluate the tree are pure functions while the program they execute may well be impure. There is no contradiction: a pure evaluator can run an impure language.

So immutability is not an implementation refinement, it is the precondition for eval to be a function at all. And it is what makes the equivalence richer than f(a₁,..,aₙ) rather than merely equal to it. Because F is immutable, the instance persists: the node (* a b) exists as a stable object in the AST, and a loop that runs it a thousand times sends eval to the same instance a thousand times. One instance, a thousand evaluations. A naive interpreter that re-decides the form on every pass cannot do this; here the form is decided once and frozen in the type, so re-execution changes nothing. The thousand-and-first eval starts from the same state as the first. What changes from one turn to the next is not F but the execution context: F is the invariant, the stack is the variant, and eval is the pure function that reads one and advances the other. The instruction itself is stateless, it holds no mutable state of its own; all the state lives in the stack, outside the instruction.

Note that eval itself takes no argument other than the interpreter. The arguments a₁,..,aₙ are not passed to eval; they are recorded in the instance at composition, as sub-nodes. F(a₁,..,aₙ).eval() is the accurate form: F holds its sub-nodes, each an immutable Element, and eval, uniform across the hierarchy, evaluates each according to its own regime, fixed point for a constant, resolution on the stack for an atom, descent for a sub-instruction. This uniformity of eval is precisely what makes dispatch by C++ vtable possible: one virtual method, one signature, overridden per class.

3. How LispE implements its instructions

The substrate follows directly. What LispE evaluates is an abstract syntax tree whose every node is an Element. On input, each form (op …) is compiled into an instance of a subclass of List chosen according to op:

(cons a b)   ->  instance of List_cons_eval
(+ a b)      ->  instance of List_plusn
(defun f …)  ->  calls to f compiled into List_function_eval
(defpat p …) ->  calls to p compiled into List_pattern_eval

Each such instance is immutable and carries, in its very type, its evaluation rule. Evaluating the tree is then a matter of sending eval to each node, which dispatches through the host's virtual method table to the right overridden eval. There is no central evaluator that inspects the form and branches; the tree evaluates itself, each node carrying its own rule. This is exactly what a vtable is, a dispatch distributed in the objects rather than centralized in a loop, and it is the precise opposite of the FEXPR, which is the archetype of centralized runtime evaluation.

evall_block makes the descent concrete. A block is a list of forms evaluated in sequence; its value is that of the last one:

Element* List::evall_block(LispE* lisp) {
    long listsize = liste.size();

    if (listsize == 2)
        return liste[1]->eval(lisp);

    Element* element = null_;

    for (long i = 1; i < listsize && element->type != l_return; i++) {
        element->release();
        element = liste[i]->eval(lisp);
    }

    return element;
}

The loop is the traversal itself: each liste[i]->eval(lisp) dispatches on its node's own type, no inspection needed on the parent side. Because the instances are immutable, this very block can be evaluated again and again, inside a loop, without reconstruction. Each intermediate result is freed in passing by element->release() as soon as the next supersedes it; the instructions themselves, being immutable, are never touched.

The argument that closes section 1 is now visible. The reproach made to the FEXPR was its opacity to the compiler: because it decides at runtime what it evaluates, no static optimization is possible. LispE takes the inverse path: because it decides at composition, freezing each instruction into a native C++ class, its code becomes ordinary code, exposed to the compiler like any other C++. Where the FEXPR makes optimization limited, the compilation of instructions allows it, for the same reason inverted, the moment the decision is made. The "interpreter of compiled objects" is exactly this: the instance is the immutable functional object F, and its class carries native C++ code, so that the same artifact is at once a pure functional term and an object optimized down to the silicon.

The same statelessness that lets a loop reuse one instance lets several threads share it. Since instructions hold no mutable state, the AST can be evaluated concurrently with no copying and no locks on the tree: give each thread its own stack and the immutable instructions are read-only, shared safely by construction. What would otherwise be the hard part of a parallel interpreter, protecting shared evaluation state, simply does not arise, because there is no shared mutable state to protect. Concurrency falls out of the same decision as purity and re-execution: the instruction is the invariant, the stack is the per-thread variant.

4. eval is only one facet

F(a₁,..,aₙ).eval() is one facet of the instance, not its definition. The same immutable object answers other messages. asString() produces its textual form; unify() matches it against another term as a pattern. Nothing forces these to be a single method, and nothing forces the instance to exist only to be evaluated. One C++ object, several behaviors, each selected by the method you send, all reading the same immutable data.

This is why the same Element hierarchy serves evaluation, serialization, and unification without three separate representations. A number knows how to evaluate itself (return itself), how to print itself, and how to unify with another term (match if equal). A List_cons_eval knows how to evaluate (build a pair), how to print ((cons a b)), and how to unify (match head against head, tail against tail). The instruction is not a procedure that evaluation runs; it is an object that happens to support evaluation among other things, the way it supports printing. eval is the facet this article is about, but it sits beside the others on equal footing, and they all rest on the same fact: the instance is immutable, so every facet reads it without fear of another having changed it.

This is, in effect, the same idea as Haskell's type classes. The root Element declares a set of methods that every derived class is expected to override:

u_ustring asUString(LispE* lisp);
Element* equal(LispE* lisp, Element* e);
Element* less(LispE* lisp, Element* e);
Element* lessorequal(LispE* lisp, Element* e);
Element* more(LispE* lisp, Element* e);
Element* moreorequal(LispE* lisp, Element* e);

(and there are others). asUString is Show, the way a value renders to text. equal is Eq. The four ordering methods are Ord. Where Haskell splits these contracts into separate classes that a type implements one by one, with instance Show, instance Eq, instance Ord, LispE folds them into the mother class and requires every derived class to fill them in. Same principle, a type supplying one behavior per operation, with a different grouping: type classes resolve the instance from the type, LispE resolves it through the vtable. And as before, the facets coexist only because the object underneath never changes.

//Comparing two elements
Element* List_eq_eval::eval(LispE* lisp) {
    Element* first_element = liste[1]->eval(lisp);
    Element* second_element = liste[2]->eval(lisp);

    //Same pointer 
    //or comparison according to first_element underlying implementation
    bool test = ( (first_element == second_element) || first_element->egal(second_element));

    second_element->release();        
    first_element->release();
    return booleans_[test];
}

5. The dynamic case: the explicit table evals[]

There remains the code built on the fly:

(eval (list 'cons 'a 'b))

Here, there is no List_cons_eval object: the list is built at runtime, its type cannot be known at compilation, and C++'s vtable, which assumes a static type, can therefore dispatch nothing. It is then List::eval that takes over, via an indexing table evals[] that LispE manages itself, indexed by the label of the instruction:

Element* List::eval(LispE* lisp) {
    Element* e = (this->*lisp->delegation->evals[lisp->checkState(this, liste.size())])(lisp);
    ...
}

checkState returns the first label of the instruction as a 16-bit integer; it also takes the opportunity to check the interpreter's state.

evals[t_atom]     = &List::eval_call_function;
evals[t_function] = &List::evalt_function;
evals[t_pattern]  = &List::evalt_pattern;
evals[t_lambda]   = &List::evalt_lambda;
...

The decisive point is that this is not a second, centralized mechanism opposed to the first. The vtable and the evals[] table do the same work, indexing a type to a method, under two different regimes. When the type is known at compilation, C++ provides its dispatch vtable implicitly. When the type is resolved only at runtime, this implicit mechanism no longer applies, and LispE provides the same table explicitly, evals[], indexed by hand. Dispatch remains distributed by type in both cases; only who holds the table changes. Actually, each instruction definition feeds the two mechanisms at the same time, which ensures that the evals[] table will be in sync with the class implementations.

And the explicit table converges toward the vtable. On this path, eval_list_instruction borrows, in the buffer-sharing sense without copy, a List_cons_eval object to evaluate the form:

Element* List::eval_list_instruction(LispE* lisp) {
    List* l = lisp->borrowing((Listincode*)this, size());
    Element* e;
    try { 
        e = l->eval(lisp); 
    }
    catch(Error* err) { 
        lisp->relax(l); 
        return lisp->check_error(this, err, -1); 
    }
    lisp->relax(l);
    return e;
}

The explicit table is therefore only a preamble that reconstructs, for the dynamic case, the specialized object that composition could not create, after which we rejoin the normal vtable regime. Functionally, this path receives the raw form and drives its evaluation: it is the behavior of a FEXPR, but confined to the dynamic case alone, not exposed as an extension mechanism, and which does not oppose the distributed dispatch but serves it, by bringing the on-the-fly code back into it. The table is a complete net rather than a catch-all: evals[] is initialized everywhere to eval_error, and only the valid entries are filled in, so a list head of a non-evaluable type falls back on a clean error, never on undefined behavior.

6. Conclusion

LispE rests on the equivalence f(a₁,..,aₙ) ⟺ F(a₁,..,aₙ).eval(), valid because F is immutable. From that single identity everything follows: each instruction is an immutable instance of a class derived from Element, carrying its own eval; the AST is kept alive as a tree of such instances and evaluates itself by virtual dispatch; an instance can be run a thousand times in a loop because nothing in it changes, only the surrounding context advancing; and where the type is not known until runtime, the same type-indexed dispatch is supplied explicitly by the evals[] table, which reconstructs the specialized object and rejoins the vtable. The FEXPR decided late and stayed opaque; LispE decides at composition and stays transparent to the compiler. That is why it can be, without paradox, an interpreter of compiled objects.