- 示例工程
【免费下载链接】mal
mal - Make a Lisp
导读
本文围绕 mal(Make a Lisp)开源项目中 C++ 实现的编译与运行展开,系统讲解 impls/cpp/README.md 中记载的 macOS(Homebrew + g++)、Ubuntu(clang + libreadline-dev)与 Docker 容器三种构建路径,并深入 Makefile、Dockerfile、docker.sh 与源码入口(step0_repl.cpp 到 stepA_mal.cpp),帮你理解 C++ 版 mal 的依赖关系、构建产物与运行方式。读完本文,你可以在一台干净的机器上从零编译并运行完整的 stepA_mal REPL,也能独立编写脚本化构建流程。
一、项目背景:mal 的 C++ 实现是什么
mal 是一个以 "Make a Lisp" 为目标的系列教程项目,用上百种语言分别实现同一套 Lisp 解释器。C++ 实现位于 impls/cpp 目录,包含从step0_repl到stepA_mal的 11 个递进阶段(详见 process/guide.md),每个阶段都在前一个阶段基础上增加语言特性。
该实现的源码结构如下:
- 通用基础:
String.h/String.cpp(字符串封装与格式化)、MAL.h(全局类型声明与READ/EVAL/PRINT/rep接口) - 核心运行时:
Types.h/Types.cpp(mal 值类型体系)、Environment.h/Environment.cpp(词法环境)、Reader.cpp(读取器)、Core.cpp(内置函数)、ReadLine.cpp(交互行输入与历史记录) - 构建脚本:
Makefile、Dockerfile、docker.sh、run - 测试与辅助:
tests/目录下 1 个.mal测试文件、Debug.h、Validation.cpp/Validation.h(参数校验)
二、macOS 编译:g++ + GNU Readline(Homebrew)
根据 impls/cpp/README.md,该 C++ 实现最初在 Mac OS X Yosemite 上开发,使用系统自带的 g++ 编译器,唯一的额外依赖是 GNU Readline。
2.1 安装依赖
Readline 通过 Homebrew 安装:
brew install readline2.2 调整 Makefile 中的 READLINE 路径
README 明确指出:安装后可能需要编辑 Makefile 中的 READLINE 路径。查看 impls/cpp/Makefile 可以确认,Darwin 平台下的构建路径默认指向 Homebrew 的 readline 安装位置:
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') CXX=g++ ifeq ($(uname_S),Darwin) # Native build on yosemite. Requires: brew install readline READLINE=/usr/local/opt/readline INCPATHS=-I$(READLINE)/include LIBPATHS=-L$(READLINE)/lib endif如果你的 readline 安装在不同位置(例如 Apple Silicon Mac 上 Homebrew 前缀不同),需要同步修改READLINE变量,使-I指向其include目录、-L指向其lib目录。
2.3 编译参数要点
Makefile 中关键的编译与链接参数如下:
DEBUG=-ggdb CXXFLAGS=-O3 -Wall $(DEBUG) $(INCPATHS) -std=c++11 LDFLAGS=-O3 $(DEBUG) $(LIBPATHS) -L. -lreadline -lhistory-std=c++11:要求 C++11 标准支持;-O3 -Wall:开启最高优化与全部警告;-ggdb:生成 GDB 调试信息,方便用调试器跟踪 EVAL 执行;-lreadline -lhistory:链接 GNU Readline 及其历史记录库。
所有step*.cpp主程序通过通配符自动收集并逐一链接:
MAINS=$(wildcard step*.cpp) TARGETS=$(MAINS:%.cpp=%)编译全部目标只需:
makemake clean可清除.o、可执行文件、libmal.a、.deps与mal链接产物。
三、Ubuntu 编译:clang + libreadline-dev
在 Ubuntu 14.10/15.04 上,README 推荐安装以下三个软件包:
apt-get install clang-3.5 libreadline-dev makeclang-3.5:C++ 编译器(对应版本可替换为当前发行版提供的 clang);libreadline-dev:GNU Readline 的开发头文件与静态库,ReadLine.cpp中#include <readline/readline.h>、<readline/history.h>、<readline/tilde.h>均依赖它;make:构建工具。
注意:Linux 下 Makefile 走非 Darwin 分支,不设置READLINE路径,直接使用系统默认头文件路径。因此只要 libreadline-dev 安装到位,make即可完成构建。
四、Docker 构建:一条命令搞定环境
对于其他平台或希望隔离环境的用户,仓库提供了 impls/cpp/Dockerfile 与 impls/cpp/docker.sh,README 给出了完整的四步用法。
4.1 README 中的标准流程
# 构建 docker 镜像 ./docker build # 编译 MAL 二进制 ./docker make # 运行其中一个实现(如完整的 stepA_mal) ./docker run ./stepA_mal # 在 docker 容器内打开 shell ./docker run注意:README 中
./docker build是文档笔误,实际脚本名为docker.sh,正确命令为./docker.sh build、./docker.sh make、./docker.sh run。
4.2 docker.sh 的工作机制
查看 impls/cpp/docker.sh 源码可以了解其背后逻辑:
IMAGE_NAME=mal-cpp CONTAINER_NAME=mal-cpp-running run() { docker rm -f $CONTAINER_NAME > /dev/null 2>/dev/null docker run -v $PWD:/mal -ti --name $CONTAINER_NAME $IMAGE_NAME "$@" } case $1 in build) docker build -t $IMAGE_NAME . ;; run) shift run "$@" ;; make) shift run make "$@" ;; ...build:用当前目录的 Dockerfile 构建名为mal-cpp的镜像;make:在容器内执行make,编译全部 step 可执行文件;run:在容器内执行任意命令;核心技巧是通过-v $PWD:/mal把宿主当前目录挂载到容器/mal,因此编译产物会直接落在宿主的 impls/cpp 目录中,无需进入容器即可直接运行./stepA_mal;- 每次
run前会先删除同名旧容器mal-cpp-running,保证环境干净。
4.3 Dockerfile 环境内容
impls/cpp/Dockerfile 基于ubuntu:20.04,核心安装项为:
RUN apt-get -y update RUN apt-get -y install make python3 RUN ln -fs /usr/bin/python3 /usr/local/bin/python # Specific implementation requirements RUN apt-get -y install g++ libreadline-devg++与libreadline-dev:C++ 编译链与 readline 依赖;make:构建工具;python3(并软链为python):供仓库根目录的 runtest.py 测试驱动脚本使用;WORKDIR /mal:容器内工作目录与挂载点对应。
五、构建产物与运行方式
5.1 可执行文件清单
编译完成后,impls/cpp下会出现从step0_repl到stepA_mal共 11 个可执行文件。此外:
dist: mal mal: stepA_mal cp $< $@make mal会把stepA_mal复制为mal,便于统一命名调用。
5.2 通过 run 脚本选择步骤
仓库提供通用启动脚本 impls/cpp/run:
#!/usr/bin/env bash exec $(dirname $0)/${STEP:-stepA_mal} "${@}"默认运行stepA_mal,也可通过环境变量指定任意阶段,例如:
STEP=step4_if_fn_do ./run该脚本同时服务于仓库根目录的测试框架:STEP变量配合 tests 目录下的step0_repl.mal、stepA_mal.mal等测试文件,可逐阶段验证实现正确性(测试驱动见 runtest.py)。
5.3 运行 REPL 与脚本文件
直接运行:
./stepA_mal进入 REPL,提示符为user>。历史记录保存在~/.mal-history(见 ReadLine.cpp 中read_history/add_history/append_history的调用)。
带参数运行会加载并执行脚本(见 stepA_mal.cpp 的 main 逻辑):
if (argc > 1) { String filename = escape(argv[1]); safeRep(STRF("(load-file %s)", filename.c_str()), replEnv); return 0; }即./stepA_mal hello.mal等价于在 REPL 中执行(load-file "hello.mal");argv[2]起的参数会以字符串列表形式绑定到*ARGV*:
static void makeArgv(malEnvPtr env, int argc, char* argv[]) { malValueVec* args = new malValueVec(); for (int i = 0; i < argc; i++) { args->push_back(mal::string(argv[i])); } env->set("*ARGV*", mal::list(args)); }六、深入源码:C++ 实现的内部结构
结合编译配置,理解该实现的核心组件有助于排查构建与运行问题。
6.1 类型体系(Types.h)
impls/cpp/Types.h 以malValue为基类,定义了 mal 的全部数据类型:
malConstant(nil/true/false 单例)、malInteger(64 位整数)、malString/malKeyword/malSymbolmalSequence派生出的malList与malVectormalHash(基于std::map<String, malValuePtr>的哈希表)malApplicable派生的malBuiltIn(内置函数)与malLambda(用户函数,含宏标记)malAtom(原子类型,支持deref/reset)
所有对象均以RefCountedPtr(见 RefCountedPtr.h)管理生命周期,VALUE_CAST/DYNAMIC_CAST宏(定义于 Types.h 顶部)负责类型安全转换。
6.2 环境(Environment.h)
impls/cpp/Environment.h 提供malEnv,内部是一个std::map加指向外层环境的m_outer指针,实现词法作用域与闭包:
class malEnv : public RefCounted { public: malEnv(malEnvPtr outer = NULL); malValuePtr get(const String& symbol); malEnvPtr find(const String& symbol); malValuePtr set(const String& symbol, malValuePtr value); malEnvPtr getRoot(); private: Map m_map; malEnvPtr m_outer; };6.3 EVAL 与特殊形式
stepA_mal.cpp 中的EVAL采用「循环而非递归」的 TCO(尾调用优化)写法:if、let*、do、quasiquote、try*等特殊形式在修改ast/env后continue,避免 C++ 栈随深递归增长。同时支持DEBUG-EVAL调试开关:当环境中DEBUG-EVAL为真时,每次 EVAL 前打印当前 AST。
6.4 REPL 错误处理
safeRep统一捕获三类异常并格式化为Error: ...输出:
malEmptyInputException:空输入直接忽略;malValuePtr:mal 值类型的异常(如throw抛出的对象);String:普通字符串异常。
这保证了 REPL 遇到错误不会崩溃,可以持续交互。
七、常见问题排查
| 现象 | 可能原因与解决 |
|---|---|
macOS 编译报找不到readline/readline.h | brew install readline未执行,或 Makefile 中READLINE路径与安装位置不符,按 2.2 节调整 |
Linux 编译报readline链接错误 | 缺少libreadline-dev,执行apt-get install libreadline-dev |
| Docker 构建网络失败 | 需保证可访问 Ubuntu apt 源;也可改用本地已安装的 g++ 直接make |
运行./docker提示 command not found | 脚本名为docker.sh,应执行./docker.sh build等 |
| 输入命令无历史记录 | 检查~/.mal-history是否可写(ReadLine 每次append_history都会写入) |
八、结语
本文完整覆盖了 impls/cpp/README.md 中记载的三条构建路径:macOS 的 g++ + Homebrew Readline(需注意 Makefile 路径)、Ubuntu 的 clang + libreadline-dev、以及普适性最强的 Docker 方案(docker.sh的 build/make/run 三命令)。结合 Makefile、Dockerfile 与 stepA_mal.cpp 等源码,可清晰理解该实现的依赖结构、构建产物与运行机制。无论你是在本机学习 Lisp 解释器实现,还是在 CI 环境中自动化构建测试,上述路径都提供了开箱即用的方案。
- 示例工程
【免费下载链接】mal
mal - Make a Lisp
相关推荐
gRPC C++ 依赖接入与构建全指南:Bazel、CMake、pkg-config 三种集成路径详解
gRPC C++ 依赖接入与构建全指南:Bazel、CMake、pkg config 三种集成路径详解 导读 src/cpp/README.md 是 gRPC
后端RPC框架微服务通信VCClient 实时语音转换客户端部署指南:预编译包、Docker 与 Anaconda 三种运行方式详解
VCClient 实时语音转换客户端部署指南:预编译包、Docker 与 Anaconda 三种运行方式详解 导读 VCClient(Realtime Voic
人工智能语音模型推理服务深度学习V8 Builtins 架构详解:Torque、CSA、C++ 与汇编四种实现路径及加载机制
V8 Builtins 架构详解:Torque、CSA、C++ 与汇编四种实现路径及加载机制 V8 的 Builtins(内建函数)是预编译好的可执行代码,负责
语言运行时编译器JIT编译解释器内存管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考