Skip to content

CS143 PA1:栈机器实现与 Docker 实验环境搭建 ​

明确实验目标 ​

这一个实验很简单,主要内容是通过Cool语言实现一个简单的Stack Machine,目的是辅助学生学习 Cool 语法。可以按照 视频学习 → PA1.pdf → cool-tour.pdf → 实验代码 的顺序进行完成

PA1.pdf

首先阅读 handout,获取需要实现的 Stack Machine 的具体细节:

image.png

其中,“d” 命令将从栈顶开始的栈中的内容以每行一个元素进行逐行输出; “e” 命令的行为取决于发出该命令时栈中的内容:

  • 如果“+”符号位于栈顶,那么就将“+”符号从栈中弹出,接着弹出接下来的两个整数并进行相加操作,然后将结果再推回栈中。

  • 如果栈顶是“s“字符,那么就将“s”字符弹出,然后将栈中的接下来的两个字符进行交换。

  • 如果栈顶是整数或者栈为空,那么栈的结构将保持不变。

handout 中给出了具体的 example 供我们进一步理解 Stack Machine 的规则:

image.png

同时需要注意,Stack Machine 运行时,我们需要输出 ">" 来提示用户输入字符,并在用户输入 x 命令后静默返回(注意,不是调用 abort() 进行退出)

梳理实验思路 ​

这个实验只是实现一个特定的 Stack Machine,并不是很难,因此不再这里赘述具体的实现思路,暂且贴一个伪代码以供参考。

image.png

搭建实验环境 ​

项目仓库包含作业源码、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

text
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=部分。

然后在容器中安装缺失的依赖:

text
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

至此我们就完成了实验环境的搭建

容器的基础使用以及实验运行指令 ​

进入正在运行的容器:

bash
docker exec -it cs143-dev bash

如果容器已停止,先启动,再进入:

bash
docker start cs143-dev
docker exec -it cs143-dev bash

进入容器后,可切换到对应作业目录执行编译,例如:

bash
cd /workspace/CS143/assignments/PA2
make lexer
./lexer test.cl

PA2 的 Makefile 默认目标是准备源码,显式执行 make lexer 才会构建词法分析器。其他作业按照各自 README 和 Makefile 中的目标操作。

也可以在本机终端直接调用容器中的命令,无需先进入交互 shell:

bash
docker exec -w /workspace/CS143/assignments/PA2 cs143-dev make lexer

退出 shell 使用 exit,容器会继续运行。需要停止容器时执行:

bash
docker stop cs143-dev

实现实验细节 ​

这部分内容同样不用赘述,直接给出结果

text
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 浪潮涌动,第“第四次工业革命”爆发。

综合以往的事件,我们不乏总结出以下几个规律:

  1. 每次工业革命,都是新生产力替代旧的生产力。背后隐含着一批岗位的淘汰和另一批岗位的兴起。

  2. 每一次工业革命爆发后,资本都会更进一步流向老牌或新兴资本家

  3. 非资本家的劳动岗位和机会越来越少(先是从基层的纯手工艺人的淘汰开始,逐渐朝着脑力劳动的中层白领延伸...)

根据以上这些结论,我们不难得出一个悲观的想法:往后随着 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 世纪开始,人类社会的发展便伴随着不断的技术更迭。基本上每一代人都处于技术更迭的过渡期,面临着时代的转变,身处于机遇与挑战当中。

以往的人并没有被这些困难打倒,也有人不断从挑战中找到属于自己的机遇。因此,我们这一代人也不必过度担忧和苦恼未来世界的走向。我们只需活在当下,再适当地放眼未来,不要太焦虑,也不要太悲观,相信自己总能在未来获得属于自己的美好生活

附 ​

e90f79949a20bcd22e752bbd37f9e5bd.jpg

实验手稿

最近更新: