CS143 PA1:栈机器实现与 Docker 实验环境搭建
明确实验目标
这一个实验很简单,主要内容是通过Cool语言实现一个简单的Stack Machine,目的是辅助学生学习 Cool 语法。可以按照 视频学习 → PA1.pdf → cool-tour.pdf → 实验代码 的顺序进行完成
首先阅读 handout,获取需要实现的 Stack Machine 的具体细节:

其中,“d” 命令将从栈顶开始的栈中的内容以每行一个元素进行逐行输出; “e” 命令的行为取决于发出该命令时栈中的内容:
如果“+”符号位于栈顶,那么就将“+”符号从栈中弹出,接着弹出接下来的两个整数并进行相加操作,然后将结果再推回栈中。
如果栈顶是“s“字符,那么就将“s”字符弹出,然后将栈中的接下来的两个字符进行交换。
如果栈顶是整数或者栈为空,那么栈的结构将保持不变。
handout 中给出了具体的 example 供我们进一步理解 Stack Machine 的规则:

同时需要注意,Stack Machine 运行时,我们需要输出 ">" 来提示用户输入字符,并在用户输入 x 命令后静默返回(注意,不是调用 abort() 进行退出)
梳理实验思路
这个实验只是实现一个特定的 Stack Machine,并不是很难,因此不再这里赘述具体的实现思路,暂且贴一个伪代码以供参考。

搭建实验环境
项目仓库包含作业源码、Makefile 和课程工具,但不包含完整的操作系统与编译依赖。本机是 Apple Silicon(arm64)macOS,项目依赖的是 Linux/x86 环境,因此我选择建立 docker 容器进行环境配置。下面是我用AI总结的项目初始文件中缺失的环境及其作用,如果有读者具备 Linux 环境,可以单独下载缺失项:
| 环境或依赖 | 用途与初始问题 | 配置方式 |
|---|---|---|
| Linux/x86 运行环境 | bin/.i686/ 中的课程程序是 32 位 x86 Linux ELF 文件,macOS 无法原生运行 | 使用 linux/amd64 的 Ubuntu 容器,由 Docker Desktop 提供运行环境 |
| GNU C/C++ 工具链和 Make | 编译作业中的 C++ 源码;仓库本身不提供工具链 | 安装 build-essential、gcc、g++、make |
| Flex、Bison | 从词法和语法规则生成源码 | 安装 flex、bison |
| Flex 库与头文件 | 为使用 Flex 库的构建提供依赖 | 安装 libfl-dev、libfl2 |
| 32 位 Linux C 运行库 | spim 使用 /lib/ld-linux.so.2;仅有 64 位运行库时无法加载 | 安装 libc6-i386 |
| 脚本与辅助工具 | 支持课程脚本、诊断及辅助操作 | 安装 csh、perl、python3、file、ca-certificates |
Docker 环境配置流程
首先启动 Docker Desktop
docker run -d \
--name cs143-dev \
--platform linux/amd64 \
--mount type=bind,source=/Users/ziyoung/File/Study/CS143,target=/workspace/CS143 \
--workdir /workspace/CS143 \
ubuntu:22.04 sleep infinity为了方便本地和容器内的文件同步更新,这里将本地的项目绑定载到了 docker 容器内部的 /workspace/CS143 下。如果本地项目换了位置,需要相应调整
source=部分。
然后在容器中安装缺失的依赖:
docker exec cs143-dev apt-get update
docker exec cs143-dev apt-get install -y --no-install-recommends \
build-essential gcc g++ make flex bison libfl-dev libfl2 libc6-i386 \
ca-certificates file perl python3 csh至此我们就完成了实验环境的搭建
容器的基础使用以及实验运行指令
进入正在运行的容器:
docker exec -it cs143-dev bash如果容器已停止,先启动,再进入:
docker start cs143-dev
docker exec -it cs143-dev bash进入容器后,可切换到对应作业目录执行编译,例如:
cd /workspace/CS143/assignments/PA2
make lexer
./lexer test.clPA2 的 Makefile 默认目标是准备源码,显式执行 make lexer 才会构建词法分析器。其他作业按照各自 README 和 Makefile 中的目标操作。
也可以在本机终端直接调用容器中的命令,无需先进入交互 shell:
docker exec -w /workspace/CS143/assignments/PA2 cs143-dev make lexer退出 shell 使用 exit,容器会继续运行。需要停止容器时执行:
docker stop cs143-dev实现实验细节
这部分内容同样不用赘述,直接给出结果
class StackNode {
value : String;
next : StackNode;
init(input : String, next_node : StackNode) : SELF_TYPE {
{
value <- input;
next <- next_node;
self;
}
};
get_value() : String { value };
get_next() : StackNode { next };
};
class Stack {
top : StackNode;
is_empty() : Bool { isvoid top };
push(input : String) : SELF_TYPE {
{
top <- (new StackNode).init(input, top);
self;
}
};
pop() : String {
let result : String <- top.get_value() in
{
top <- top.get_next();
result;
}
};
peek() : String { top.get_value() };
get_top() : StackNode { top };
};
class StackCommand {
read(input : String, stack : Stack, io : IO) : Bool {
let command : StackCommand <-
if input = "e" then new EvaluateCommand else
if input = "d" then new DisplayCommand else
if input = "x" then new ExitCommand else
(new PushCommand).init(input)
fi fi fi
in command.execute(stack, io)
};
execute(stack : Stack, io : IO) : Bool { true };
};
class PushCommand inherits StackCommand {
value : String;
init(input : String) : SELF_TYPE {
{
value <- input;
self;
}
};
execute(stack : Stack, io : IO) : Bool {
{
stack.push(value);
true;
}
};
};
class EvaluateCommand inherits StackCommand {
execute(stack : Stack, io : IO) : Bool {
if stack.is_empty() then true else
let value : String <- stack.peek() in
if value = "+" then
(new AddCommand).execute(stack, io)
else if value = "s" then
(new SwapCommand).execute(stack, io)
else
true
fi fi
fi
};
};
class AddCommand inherits StackCommand {
execute(stack : Stack, io : IO) : Bool {
let converter : A2I <- new A2I in
{
stack.pop();
let first : Int <- converter.a2i(stack.pop()),
second : Int <- converter.a2i(stack.pop()) in
stack.push(converter.i2a(first + second));
true;
}
};
};
class SwapCommand inherits StackCommand {
execute(stack : Stack, io : IO) : Bool {
{
stack.pop();
let first : String <- stack.pop(),
second : String <- stack.pop() in
{
stack.push(first);
stack.push(second);
};
true;
}
};
};
class DisplayCommand inherits StackCommand {
execute(stack : Stack, io : IO) : Bool {
let current : StackNode <- stack.get_top() in
{
while not isvoid current loop
{
io.out_string(current.get_value());
io.out_string("\n");
current <- current.get_next();
}
pool;
true;
}
};
};
class ExitCommand inherits StackCommand {
execute(stack : Stack, io : IO) : Bool { false };
};
class Main inherits IO {
main() : Object {
let stack : Stack <- new Stack,
command : StackCommand <- new StackCommand,
keep_running : Bool <- true in
{
while keep_running loop
{
out_string(">");
keep_running <- command.read(in_string(), stack, self);
}
pool;
self;
}
};
};对于独立项目的实现(PA1也算是一个小项目了),这里有一条经验可供分享:
可以在项目开始之前先梳理一下程序大致的运行流程,以主要流程为主体,进一步梳理程序的主要结构,进而制定关键约束、边界处理,最终搭建起一个框架雏形。后续就只需要在这个框架雏形上进行修改即可。
总结实验收获
PA1 的目的是让我们了解 Cool 的语法,为后面的实验做准备,因此难度并不高,我在实现过程中也算是预料之中的顺利。但是我认为这个实验对我的意义不是一次简单的代码实践——对,这算我在AI爆发,全面投入vibe coding后,久违的代码实践环节。而是对于传统编程范式和如今 vibe coding 新范式之间的思考。
从20世纪50年代的人工智能萌芽(符号主义时代),到20世纪90年代的机器学习和连接主义崛起,再到如今的大模型和生成式AI时代。短短一甲子,AI就完成了从无到有再到如今繁盛的飞跃。AI 作为一种新型生产力,可以预见能够像电力、自动化以及互联网一般,为人类发展提供莫大助力。但是作为AI发展引起时代变革下的个体,我只感受到了无尽的迷茫和焦虑,以及对于人类未来的悲观预想。
人类的第一次工业革命发生在18世纪60年代,机器取代手工劳动,工厂制度诞生。社会生产力大幅提升,出现了现代意义上的工人阶级和资本家;第二次工业革命发生在下个世纪70年代,电气化与大规模标准化生产诞生,重工业成为国家发展主导,垄断资本主义形成,全球化贸易加速;第三次工业革命发生在20世纪40年代到21世纪初,信息技术的爆发,人类进入“信息时代”,自动化设备开始替代部分体力劳动和重复性脑力劳动。资本垄断愈加严峻。而如今,AI 浪潮涌动,第“第四次工业革命”爆发。
综合以往的事件,我们不乏总结出以下几个规律:
每次工业革命,都是新生产力替代旧的生产力。背后隐含着一批岗位的淘汰和另一批岗位的兴起。
每一次工业革命爆发后,资本都会更进一步流向老牌或新兴资本家
非资本家的劳动岗位和机会越来越少(先是从基层的纯手工艺人的淘汰开始,逐渐朝着脑力劳动的中层白领延伸...)
根据以上这些结论,我们不难得出一个悲观的想法:往后随着 AI 的愈加发展,会有越来越多岗位(包括之前难以替代的脑力劳作岗位)被 AI 所替代。
到那个时候,未来可能走向两种情形:
一种是理想的共产主义:AI 完全解放了人类生产力,劳作都派发给 AI,人们过上物质和精神都非常富足的生活。
另一种是极端化的赛博朋克世界:资本全部由老牌资本家所掌握,上升通道完全被垄断,底层人民只能艰难求生。
或许有人会反驳说,后者可能在西方资本主义社会上演,而我们国家大概率是前者。但我并不这么认为。不过这些不太适合在这里讨论,我就不再深入展开了。
我引出这么多文字,想表达的核心观点其实就一个:AI 迟早会替代一切岗位。而我们程序员虽然顶着“AI 创造者”的好听名头,但悲哀的是,却很可能是率先被淘汰的那一批人。
作为其中的个体,面对这样灰暗的未来,我们该采取怎样的行动去做准备?现在学习技术是否还有用?更现实一点,站在当下大学生的阶段,我们是否还要踏实下来学习这些技术,甚至更进一步,我们到底还该不该去手写代码呢?
这个问题其实早在一年前我大二的时候就已经有了结果,而在今天完成 CS143 Lab 1 时,更让我明确了这一点:我们一定要积极拥抱新兴技术,涌入新时代的潮流当中,但同时也不能丢掉以往积累的经验,不能完全随波逐流,还是要有自己的立足之处。
(这里让我感慨的是,这刚好契合了计算机科学中 trade-off 的思想)
具体来说应该怎样理解呢?新技术和新方法的诞生是时代发展的大趋势。作为宏大时代下的微小个体,我们是被时代推涌着向前的,如果不顺应时代,只会被落下、被淘汰,所以主动去了解、接纳、学习并运用这些新技术是非常必要的。
但拥抱新技术并不意味着一味追求前沿和热点,更不是去跟进世界上发生的每一件事并逐一尝试,那样只会分散你的精力和行动力。我们要做的应当是广泛了解,然后根据自己的实际情况,选择适合自己的方向去主动学习、提前投资。
另一方面,学习并拥抱新技术,并不意味着摒弃基础知识。就像建楼一样,拥有一台搭建钢筋、浇筑混凝土效率极高的机器,固然能让我们高效完成楼层的修建,但倘若没有扎实的地基,楼房终究难以垒成高楼;甚至因为地基虚浮,往上搭个一两层就会轰然倒塌。
这也非常契合我们现在的处境:AI 的发展极其迅猛,基础的编程工作基本都可以交给 AI 完成,甚至前沿模型已经能搞定一些复杂的项目工作,看似程序员已经没有必要再去学习底层原理、自己动手写代码了,但事实恰恰相反。但是事实恰恰相反,对底层技术原理的掌握在如今的情形下其实更加有必要,只是说一些重复性的代码编写场景可以适当减少。
为什么呢?因为现在 AI 固然可以帮你实现很多编程想法,但如果你对背后的底层原理并不清楚,AI 的创造力就只能停留在它预设的范围之内,你无法引导它去完成更高质量的工作。
我自己就亲身经历过:我想让 AI 帮我写一个个人博客网站,把想法告诉了它。但因为我没有了解过前后端技术栈,所以只是给出了一个整体目标就让它去做。最后做出来的效果并不完美,只能说符合基础要求、搭出了网站框架,但我预想中的网页布局、文字排版、动画效果以及后端信息收集,基本上都没达到预期。
这背后的原因就是我对相关知识了解太少,没办法引导它工作,只能任由它在庞大的参数中搜寻最基础的知识来应付任务。但倘若我了解前端的 VitePress 博客框架、JavaScript、前端动画以及后端的相关知识,我就可以引导 AI 在庞大的参数库中调取更深、更垂直的内容,把事情做得更好。
这是一个点:在如今有了 AI 之后,我们仍然要去了解一些技术的基本原理、基本知识和实现原理。
但由于 AI 很擅长做一些重复性的工作,所以我们也没有必要按照以往的经验去做重复练习。我们只需要知道这项技术背后关键的实现原理是什么、能够做什么,其实就已经足够了
另一个点就是,在现在更加难能可贵的品质是一些核心能力,比如说任务规划能力、逻辑思维能力、决策能力,以及个人的主观能动性等等。
现在有了 AI 之后,我们作为 AI 的使用者,更多时候不再像以前那样作为一个基础程序员去手动编写代码,而是作为项目的 leader,AI 则作为项目的 teammate。我们向它们派发任务,让它们实现具体的一些细节。
在这个过程中,你需要明确知道自己对这个项目的需求是怎样的。当 AI 反馈项目完成情况、给出建议的时候,你要有全局观和决策力去引导整个项目的走向,做出明确、明智且正确的判断,而不是被 AI 牵着鼻子走。这种情况是我在初次接触 AI 时经常发生的。
实际经验告诉我,一个有全局观、有良好决策力的人,在同样的项目下,往往能够比没有上述品质的人做出更好的结果。
最后让我们回到最开始我产生关于未来的悲观看法时,所举出的那个例子:
纵观人类经历的几次工业革命,人的生产力尽管被逐渐解放,但解放生产力带来的好处却没有惠及所有人,而是更多地积累在资本家手中,让贫富差距越来越大,底层人民的上升机会越来越少。
但这只是我站在个人视角下的悲观看法。如果将视野抬高,其实我们会总结出另外一个规律:从 18 世纪开始,人类社会的发展便伴随着不断的技术更迭。基本上每一代人都处于技术更迭的过渡期,面临着时代的转变,身处于机遇与挑战当中。
以往的人并没有被这些困难打倒,也有人不断从挑战中找到属于自己的机遇。因此,我们这一代人也不必过度担忧和苦恼未来世界的走向。我们只需活在当下,再适当地放眼未来,不要太焦虑,也不要太悲观,相信自己总能在未来获得属于自己的美好生活
附

实验手稿