☰
Carp 动态语义指南:作用域、求值规则与 VM 化动态求值器
2026/10/10 5:33:46 网站建设 项目流程
  • 编程语言
  • 编译器

【免费下载链接】Carp

A statically typed lisp, without a GC, for real-time applications.

项目地址:https://gitcode.com/gh_mirrors/ca/Carp
点击查看免费下载

导读

本文以 Carp 仓库中 docs/DynamicSemantics.md 为核心骨架,系统讲解 Carp 的“动态语言”部分:即解释执行、带垃圾回收、用于宏展开与编译期计算的那一层。文章覆盖动态/静态语义的术语体系、词法作用域与命名空间规则、全局与局部变量的可变性、各类定义形式(defndynamic/defdynamic/defmacro等)、求值规则、宏与准引用(quasiquotation)、REPL 的上下文切换,以及当前实现迁移到 VM 架构后的源码级现状。读完本文,你将掌握 Carp 动态层与静态层的边界、符号查找与set!赋值规则、闭包语义,并能理解src/Eval.hs、src/EvalVM.hs等核心模块的求值管线。

文档定位:历史目标与当前实现

docs/DynamicSemantics.md最初描述的是 Carp 动态求值器“重写”的历史目标:修复动态求值中发现的各类 bug、补齐一个动态 Lisp 应有的缺失特性,并让求值器在未来易于扩展。

该目标已实现——Carp 的动态求值器如今是VM 后端(VM-backed)架构。对于当前实现的架构总览,请阅读 docs/EvaluatorVM.md;本文在讲解语义规则的同时,也会同步标注当前实现的源码位置,使历史语义目标与实际代码一一对应。

0. 本文使用的基本术语

docs/DynamicSemantics.md定义了四个贯穿全文的基本术语:

术语含义
form任何以文本形式表示的合法 Carp 数据结构
top level未被嵌入其他 form 中的任何 form
Static CarpCarp 语言被编译(生成 C 代码)的那一部分
Dynamic CarpCarp 被解释执行、函数式、带 GC 的那一部分

理解“两层”的划分是读懂 Carp 的关键:静态层最终被编译为 C 并在运行时执行,动态层则运行在编译器中(宏展开、REPL、构建系统配置均发生在此层)。动态层与静态层共享模块,但动态查找只会找到动态函数,静态查找只会找到静态函数。

1. 作用域规则(Scoping Rules)

1.1 符号如何取值:词法作用域

Carp 采用词法作用域(lexical scoping):查找符号 X 的值时,先查当前作用域,再逐层向外,直到全局作用域。能够创建作用域的结构包括:

  • 函数定义(defn、defndynamic、fn)
  • let绑定
  • 模块(defmodule)

这意味着符号的可见性完全由源码的词法结构决定,不存在任何动态作用域。

1.2 如何给符号赋值:set!

赋值使用统一的set!形式:

(set! <symbol> <value>)

在源码层面,set!由specialCommandSet实现(见 src/Eval.hs),它会先在内部环境(局部)中查找符号,再回退到全局环境,并根据绑定是动态还是静态类型决定是否做类型检查;动态或无类型的绑定走handleUnTyped分支(不检查类型),静态绑定则会对新值做类型核对。

1.3 保留名(Reserved Names)

Carp 存在一组保留名(详见 Parsing 模块):

defn、def、do、while、fn、let、break、if、match、true、false、address、set!、the、ref、deref、with

此外,defmacro中的:resttoken 也是保留的。文档同时指出,“更多东西应该被移入保留列表”——即这份清单本身是演进中的。

1.4 关键字(Keywords)

动态语义的原始设计中**没有关键字(keyword)**这一概念;文档提及一种可能的宏实现方案(类似带冒号前缀的 token),但这属于历史探讨,当前仓库的宏系统并未引入关键字。

1.5 动态与静态是否使用不同的命名空间

动态 Carp 与静态 Carp共享同一套模块结构,区别只在于查找范围:动态查找只命中动态函数(defndynamic、宏展开期的命令等),静态查找只命中静态函数(defn等)。

1.6 全局变量

关于全局变量,docs/DynamicSemantics.md明确回答:

  • 全局变量可变:可以。
  • 如何修改、何时生效:用set!修改,内部基于 IORef,立即生效。
  • 作用域类型:词法作用域(任何东西都没有动态作用域)。

在测试中可以看到这一语义的直接验证:test/dynamic-closures.carp 中先(defdynamic x 400),闭包closure-one捕获x,随后顶层(set! x 1000),断言闭包读取到的是1000(“closures over global variables get the global variable's latest state”),印证了全局变量在闭包中读到的是最新状态。

1.7 局部变量与闭包

  • 局部变量可变:可以。
  • 进入/离开作用域时机:遵循词法作用域规则,函数与let创建新变量。
  • 闭包的重要规则:
    • 被捕获的变量不可变;
    • 动态 lambda 在(fn ...)形式被求值的时刻捕获整个环境。

test/dynamic-closures.carp 对闭包语义给出了系统化验证:

  • 闭包可以捕获全局变量、函数参数、let绑定;
  • 动态函数可以在其引用的全局变量尚未定义时先创建闭包(closure-four捕获后面才定义的y,最终读到500),说明闭包保存的是环境而非立即求值;
  • 嵌套闭包优先使用闭合的内部环境(closure-five中let的x=5优先于全局x);
  • 闭包可以通过set!更新全局环境(closure-six连续三次(set! z (inc z))后读到3)。

这些测试位于 test/dynamic-closures.carp,是理解动态闭包语义最直接的样例。

1.8 命名空间规则

  • 两个模块中出现同名符号a(Foo与Bar),用点号限定引用:Foo.a、Bar.a;用(use <module name>)可免去限定。
  • 多个模块被导入且含同名符号时:查找该符号会得到运行时错误——因为无法解析为单一值。
  • 若Foo.a与Bar.a都存在,裸符号a除非有且仅有一个模块被use,否则不指向任何一个;两个都被use时查找即报错。
  • 函数与变量位于同一命名空间;类型位于不同的命名空间。

注意这与静态类型的多态推断有所不同:在静态上下文中,多个use的模块出现同名符号时,类型推断器会尝试依据类型消歧(见 docs/LanguageGuide.md 的“Modules and Name Lookup”一节);而动态语义文档描述的规则是动态查找的基线行为。

1.9 定义形式(Definitions)

docs/DynamicSemantics.md将定义形式按上下文分为三类:

动态上下文:

  • defndynamic:定义动态函数(编译期/REPL 可调用)
  • defdynamic:定义动态全局变量
  • defmacro:定义宏

静态上下文:

  • defn:定义静态函数
  • def:定义静态全局变量
  • deftype:定义积类型(struct)与和类型(sumtype)
  • register:注册外部 C 函数/变量

所有上下文:

  • defmodule:组织模块

在源码中,这些形式分别由 src/Eval.hs 的primitiveDefdynamic、primitiveDefndynamic、primitiveDefmacro等 primitive 实现,例如defndynamic与defmacro都通过dynamicOrMacro构造形如(dynamic|macro name params body)的绑定。

2. 求值规则(Evaluation Rules)

docs/DynamicSemantics.md以问题清单的形式提出了求值规则的核心问题:宏何时求值、符号何时求值、form 何时求值、求值顺序是从左到右还是从右到左、错误报告如何工作。其中部分问题在当时尚无定论(对应章节留空),但当前的 VM 实现已经给出了明确答案,可对照 docs/EvaluatorVM.md 阅读。

2.1 三类可求值实体

当前实现将动态求值器面对的构造分为三类(详见 docs/Macros.md 的“Inner Workings”):

  • 特殊形式(special forms):在 AST 中有独立表示、由求值器直接处理的构造,如fn、the,不能被当作值传递;
  • 原语(primitives):不求值其参数的普通 Carp 形式,类似用 Haskell 实现的内置宏,如defmacro、defn、quote,主要定义在 src/Primitives.hs;
  • 命令(commands):求值其参数、行为类似内置函数的普通 Carp 形式,如Project.config、car、cons,定义在 src/Commands.hs。

2.2 求值管线的当前实现

docs/DynamicSemantics.md提出的“易于扩展”“修复动态求值 bug”的目标,最终落地为一条清晰的 VM 管线(docs/EvaluatorVM.md):

  1. XObj降级为EvalIR(lowerExpr,见 src/EvalIR.hs);
  2. EvalIR编译为字节码EvalCode(compileEvalIR,见 src/EvalVM.hs);
  3. 执行字节码(runEvalCode)。

可调用对象要么被预编译(VMPrecompiled),要么首次调用时编译并缓存(VMCompileOnCall)。

公开入口集中在 src/Eval.hs:

evalDynamic :: Context -> XObj -> IO (Context, Either EvalError XObj) evalStatic :: Context -> XObj -> IO (Context, Either EvalError XObj) eval :: Context -> XObj -> LookupPreference -> IO (Context, Either EvalError XObj)

其中evalDynamic以PreferDynamic模式执行,evalStatic以PreferGlobal模式执行,eval则接受显式的LookupPreference(定义见 src/EvalTypes.hs):

data LookupPreference = PreferDynamic | PreferGlobal | PreferLocal (Set.Set String) (Map.Map Int XObj) (Map.Map String Int) EvalExecMode

模式决定符号查找与可调用对象的编译策略:函数模式(PreferLocal ... ExecFunction)要求局部槽位存在并快速失败;动态与宏模式(ExecDynamic/ExecMacro)保持对动态友好的查找行为。字节码指令集包括栈/值操作(IPushConst、IMakeArray、IMakeStaticArray、IDrop)、控制流(IJumpIfFalseRel、IJumpRel、IHalt、ITrap)、符号/调用分发(IResolveSymbol、IExecCallSymbol、IExecCall)以及特殊形式执行指令(IExecLet、IExecFn、IExecWhile、IExecWith、IExecSet)。

2.3 求值顺序

Carp 的调用参数遵循从左到右的求值顺序(Lisp 惯例);宏则不求值其参数(宏参数以原始 form 传入宏体)。在 VM 中,IExecCallSymbol/IExecCall同时携带原始参数 IR 与预编译的参数代码,使非宏参数求值在热路径上避免递归重新进入 IR,从而兼顾顺序语义与性能。

2.4 符号解析模型

编译阶段为每个符号分配一个ResolverHandle:RHLocalSlot(局部槽位)、RHGlobal(全局)、RHDynamic(动态)、RHQualified(限定)、RHUnqualified(未限定)。opcode 循环先按 handle 形状解析,仅在语义需要时才回退到更宽的查找。符号解析缓存以(contextBindingEpoch, symbolId)为键,环境状态变化时通过contextBindingEpoch使缓存失效。

2.5 错误报告

EvalError在动态求值器遇到错误时产生,由错误消息与元信息(traceback 与源码位置)组成;Context中维护了求值帧历史用于生成 traceback。reportExecutionError(src/Eval.hs)会根据contextExecMode(Check/Repl/Build/BuildAndRun/Install)决定是打印错误还是抛出异常终止。

3. 宏系统:从“缺失特性”到现行实现

docs/DynamicSemantics.md的“Desired features (currently missing)”清单包含:动态语言与宏系统的使用文档、完整的宏设施(准引用、拼接、完整错误报告等)、动态堆栈跟踪、用户定义名的自动补全。其中的宏设施与文档需求已经实现,详见 docs/Macros.md 与 docs/Quasiquotation.md。

3.1 defmacro 与动态函数

宏用defmacro定义,参数在调用时不求值,而是以原始 form 传入:

(defmacro apply [f args] (cons f args)) (apply + (1 2)) ; => (+ 1 2) (apply Array.replicate (5 "hello")) ; => (Array.replicate 5 "hello")

对应的动态函数版本用defndynamic(参数会被求值,调用时需要手动 quote):

(defndynamic apply [f args] (cons f args)) (apply '+ '(1 2)) ; => (+ 1 2) (apply 'Array.replicate '(5 "hello")) ; => (Array.replicate 5 "hello")

宏还支持可变参数(rest arguments),以:rest声明“catch-all”尾参:

(defmacro apply-or-sym [head :rest tail] (if (= (length tail) 0) head (cons head tail))) (apply-or-sym *global*) ; => *global* (apply-or-sym + 1 2) ; => (+ 1 2)

3.2 准引用(Quasiquotation)

docs/Quasiquotation.md说明:准引用只在动态部分可用,它把列表的大部分保持为未求值,同时允许用unquote插入求值结果:

(defdynamic x 2) (quasiquote (+ (unquote x) 1)) ; => (+ 2 1) ; 字面量简写:quasiquote 为 `,unquote 为 % `(+ %x 1) ; => (+ 2 1)

拼接(splicing)用unquote-splicing(简写%@),将一个列表扁平展开:

(defdynamic x '(1 2)) (quasiquote (+ (unquote-splicing x))) ; => (+ 1 2) `(+ %@x) ; => (+ 1 2) ; unquote 变体内部可以使用任意表达式 `(+ %@(map inc [1 2])) ; => (+ 2 3)

准引用可以嵌套:每个quasiquote使嵌套层级 +1,每个unquote/unquote-splicing使层级 −1,只有降到 0 层的表达式被求值:

``(x %%(* 2 2)) ; => `(x %4) ``(x %(* 2 2)) ; => `(x %(* 2 2))

在unquote之外使用unquote会在宏展开期报错。注意docs/Macros.md的历史说明曾称“未实现 quasiquoting”,该功能随后在 core/Macros.carp 及求值器中落地,(info Dynamic)可以查看动态模块中的全部可用函数。

4. REPL:动态与静态上下文的切换

docs/DynamicSemantics.md提出的问题——REPL 如何知道在动态还是静态上下文求值、何时决定运行给定代码——由 src/Eval.hs 的executeStringAtLine/executeCommand回答:

  • REPL 逐条解析输入并执行命令;普通表达式默认走evalDynamic(动态求值),因此defndynamic/defdynamic/defmacro与car、cons等动态操作在 REPL 中直接可用;
  • 对于顶层的defn/def静态绑定,executeStringAtLine会先识别静态绑定集合(staticBindingName),若存在则走processFormsWithForwardRefs分支,以支持文件内跨定义顺序的正向引用与相互递归(参见 docs/LanguageGuide.md);
  • 求值main定义或静态函数调用时,executeCommand会构造“build + run”序列(withBuildAndRun),触发编译与运行(HasStaticCall时走callFromRepl);
  • 若求值结果非UnitTy,REPL 以=> <value>的形式打印(prettyDynamic还会识别动态 map 的桶列表并按{k v ...}格式化)。

动态/静态两层因此不是两套独立 REPL,而是同一 REPL 依据 form 形态自动分发的两条执行路径。

5. 类型(Types)

docs/DynamicSemantics.md第 3 节围绕类型提出问题:可用类型有哪些、form 何时被类型检查、类型是否是一等公民。

回答可归纳为:

  • 静态类型由类型推断得出,可用the显式标注((the Int x)),详见 docs/LanguageGuide.md;可用的类型字面量包括Int、Long、Float、Double、Byte、Bool、String、Pattern、Char、(Array t)、(Map k v)、(Fn [args...] ret)等;
  • 动态层独有的数据字面量是列表(1 2 3)与符号foo——它们只存在于非编译代码中,静态层没有;
  • 类型检查的时机:动态求值时,set!对静态绑定会调用typeCheckValueAgainstBinder做类型核对;对defn/def等静态绑定,则经过annotateWithinContext的完整标注流程(含宏展开、限定与推断)。也就是说,动态层本身不做类型推断,但负责把需要静态处理的 form 交给标注/推断管线;
  • 类型与值分属不同命名空间:函数与变量同在一个值命名空间,类型在独立的类型环境(contextTypeEnv)中,因此类型名称与值名称可以共存。docs/DynamicSemantics.md以“一等公民”提问,从当前实现看,类型作为编译期实体由deftype/类型环境管理,而非动态值。

6. 维护者视角:如何安全地改动求值器

docs/EvaluatorVM.md给出了与动态语义改动配套的工程准则,可作为实践补充:

  1. 先判定改动性质:是纯实现细节,还是有意变更语言语义;
  2. 保持外部形态:若是实现性改动,保留外部 form 形态与错误表面(动态/宏定义保持(dynamic|macro name params body)外层形态);
  3. 同步测试:扩展 test/TestEvalIR.hs、test/TestEvalSlotLowering.hs、test/TestEvalVM.hs、test/TestEvalVMCore.hs;
  4. 回归宏与闭包场景:运行 test/macros.carp 与 test/dynamic-closures.carp;
  5. 基准对比:运行 bench/run-evaluator-bench.sh,比较多次运行的中位数而非单次结果。

此外,docs/EvaluatorVM.md 明确指出:求值器 VM 是编译器的内部基础设施,不是用户运行时 VM,它不改变 Carp 已编译代码的执行模型——这与“Dynamic Carp 是编译期解释层”的历史定位完全一致。

小结

Carp 的动态语义可以浓缩为几条核心规则:词法作用域、无动态作用域;全局与局部变量均可变,set!立即生效;闭包捕获创建时的整个环境,且被捕获变量不可变;函数与变量同一命名空间、类型独立命名空间;动态查找与静态查找共享模块但互不越界;defndynamic/defdynamic/defmacro构成动态上下文,defn/def/deftype/register构成静态上下文。这些规则在 docs/DynamicSemantics.md 中以问答形式确立,并通过 VM 求值器(src/EvalVM.hs、src/EvalIR.hs、src/Eval.hs)落地为可运行的实现,辅以 test/dynamic-closures.carp 等测试对闭包、全局变量最新状态等语义做了精确验证。对编译期编程感兴趣的用户,可继续阅读 docs/Macros.md、docs/Quasiquotation.md 与 docs/EvaluatorVM.md。

  • 编程语言
  • 编译器

【免费下载链接】Carp

A statically typed lisp, without a GC, for real-time applications.

项目地址:https://gitcode.com/gh_mirrors/ca/Carp
点击查看免费下载
上一篇:LaserGRBL固件升级教程:保持你的雕刻机始终最新
下一篇:Texture 之 ASCellNode 完全指南:三种实现方式、占位符控制与 UITableViewCell 属性桥接

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询