[{"content":"什么是 Overplus Overplus 是一个用 C++17 编写的轻量高性能代理服务器，支持 SOCKS5、HTTPS、Trojan 协议。\n亮点：\n极致性能：146 个并发连接仅占 25MB 内存，CPU \u0026lt; 1% 零依赖：静态编译，无需安装任何运行库 Windows GUI 客户端：开箱即用，一键连接 一键安装：服务端一条命令搞定 前置准备 你需要：\n一台境外 VPS（推荐 Ubuntu/Debian，2 核 2G 即可） 一个域名（可选，用于申请证书） SSH 登录 VPS 的终端 第一步：服务端安装 方式一：一键安装（推荐） SSH 登录 VPS 后，执行以下命令：\ncurl -O https://raw.githubusercontent.com/xyanrch1024/overplus/master/install.sh \u0026amp;\u0026amp; chmod +x install.sh \u0026amp;\u0026amp; sudo ./install.sh 脚本会引导你完成以下步骤：\n输入密码：设置你的代理连接密码 选择端口：默认 443，建议保持默认（伪装成正常 HTTPS 流量） 安装完成后，脚本会自动：\n生成 TLS 证书（有效期 10 年） 下载 Overplus 二进制文件 配置并启动 systemd 服务 验证服务是否正常运行：\nsystemctl status overplus 看到 active (running) 就说明成功了。\n方式二：手动安装 如果你更喜欢手动操作：\n# 1. 下载 wget https://github.com/xyanrch1024/overplus/releases/latest/download/overplus-linux-x86_64.zip unzip overplus-linux-x86_64.zip -d /tmp/overplus # 2. 安装二进制文件 cp /tmp/overplus/overplus /usr/bin/overplus chmod +x /usr/bin/overplus # 3. 生成 TLS 证书（需要已安装 certbot 或手动用 openssl） # 这里以自签证书为例： mkdir -p /etc/overplus openssl req -x509 -nodes -newkey ec:\u0026lt;(openssl ecparam -name prime256v1) \\ -keyout /etc/overplus/server.key \\ -out /etc/overplus/server.crt \\ -subj \u0026#34;/CN=your_server_ip\u0026#34; -days 3650 # 4. 创建配置文件 cat \u0026gt; /etc/overplus/server.json \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; { \u0026#34;run_type\u0026#34;: \u0026#34;server\u0026#34;, \u0026#34;local_addr\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;local_port\u0026#34;: \u0026#34;443\u0026#34;, \u0026#34;allowed_passwords\u0026#34;: [\u0026#34;your_password_here\u0026#34;], \u0026#34;log_level\u0026#34;: \u0026#34;NOTICE\u0026#34;, \u0026#34;log_dir\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;ssl\u0026#34;: { \u0026#34;cert\u0026#34;: \u0026#34;/etc/overplus/server.crt\u0026#34;, \u0026#34;key\u0026#34;: \u0026#34;/etc/overplus/server.key\u0026#34; }, \u0026#34;websocketEnabled\u0026#34;: false, \u0026#34;dns_cache_ttl\u0026#34;: 600, \u0026#34;dns_cleanup_interval\u0026#34;: 600 } EOF # 5. 创建 systemd 服务 cat \u0026gt; /etc/systemd/system/overplus.service \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [Unit] Description=overplus proxy After=network.target [Service] User=root ExecStart=/usr/bin/overplus -c /etc/overplus/server.json Restart=on-failure RestartSec=10s LimitNOFILE=infinity [Install] WantedBy=multi-user.target EOF # 6. 启动服务 systemctl daemon-reload systemctl start overplus systemctl enable overplus 开启 BBR 加速（强烈推荐） BBR 能显著提升 TCP 传输性能：\necho \u0026#34;net.core.default_qdisc=fq\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf echo \u0026#34;net.ipv4.tcp_congestion_control=bbr\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p 验证是否生效：\nsysctl net.ipv4.tcp_congestion_control # 输出 net.ipv4.tcp_congestion_control = bbr 第二步：Windows 客户端 下载 前往 Release 页面 下载 overplus-client-windows-x64.zip，解压到任意目录。\n配置 解压后会看到以下文件：\noverplus_client.exe # 主程序 Qt5Core.dll # Qt 运行库 Qt5Gui.dll Qt5Widgets.dll libcrypto-3-x64.dll # OpenSSL libssl-3-x64.dll plugins/ # Qt 插件 双击 overplus_client.exe 启动客户端，会看到一个简洁的 GUI 界面：\n在 Host Name 输入你的 VPS IP 地址 在 Host Port 输入端口号（默认 443） 在 Password 输入你设置的密码 点击 SAVE 保存配置 点击 CONNECT 连接 状态栏显示 CONNECTED 且托盘图标变绿，就说明连接成功了。\n配置系统代理 连接成功后，Overplus 会自动设置 Windows 系统 SOCKS 代理。关闭程序时会自动恢复。你也可以手动配置：\n打开 Windows 设置 → 网络和 Internet → 代理 手动设置代理服务器：127.0.0.1，端口 1080 使用其他客户端 Overplus 兼容 Trojan 协议，你也可以使用其他支持 Trojan 的客户端：\n客户端 平台 下载 Clash Windows/Mac/Linux GitHub Nekoray Windows/Mac/Linux GitHub Shadowrocket iOS App Store Quantumult X iOS App Store 配置示例（以 Clash 为例）：\nproxies: - name: \u0026#34;Overplus\u0026#34; type: trojan server: your_server_ip port: 443 password: your_password sni: your_server_ip skip-cert-verify: true 第三步：验证连接 检查服务端日志 # 查看 Overplus 日志 journalctl -u overplus -f # 或直接查看日志文件 cat /var/log/overplus.*.log 正常连接时会看到类似输出：\naccept incoming connection :1.2.3.4 connected to example.com:443 session destroyed 测试代理 连接成功后，访问 https://ipinfo.io 确认 IP 已切换到 VPS 的 IP。\n常见问题 连不上怎么办？ 检查防火墙：确保 VPS 的 443 端口开放\n# Ubuntu/Debian ufw allow 443/tcp # 或关闭防火墙 ufw disable 检查服务状态：\nsystemctl status overplus journalctl -u overplus -n 20 检查端口占用：443 端口不能被其他服务占用（如 nginx、apache）\n速度慢怎么办？ 确认已开启 BBR 尝试更换 VPS 的机房/线路 检查 VPS 的带宽和流量是否充足 如何更换密码？ 编辑服务端配置文件：\nnano /etc/overplus/server.json # 修改 allowed_passwords 字段 systemctl restart overplus 性能数据 在 2 核 Xeon / 2GB RAM / Ubuntu 24.04 上的实测数据：\n指标 数值 并发连接数 146 内存占用 25 MB 单连接内存 ~170 KB CPU 占用 \u0026lt; 1% TLS 重连 \u0026lt; 1ms 相关链接 GitHub: https://github.com/xyanrch1024/overplus Release 下载: https://github.com/xyanrch1024/overplus/releases Telegram 群: https://t.me/+JfKOqh2wH25kMWFl ","permalink":"https://xyanrch1024.github.io/zh/posts/overplus-getting-started/","summary":"手把手教你用 Overplus 搭建自己的代理服务器。一键安装脚本 + Windows GUI 客户端，全程 5 分钟搞定。","title":"Overplus 安装使用教程：5 分钟搭建你自己的代理服务"},{"content":"Overplus 是一个基于 C++17 和 Boost.Asio 的代理服务器，支持 SOCKS5、HTTPS、Trojan 和自定义 V-Protocol 协议。本文记录了对服务端进行的全链路性能优化过程。\n源代码：https://github.com/xyanrch1024/overplus.git\n优化前状态 指标 值 活跃连接 27 内存占用 5.6MB DNS 解析 每次连接重新解析 TLS 握手 每次完整握手 断连日志 ERROR 级别，大量刷屏 服务器配置：2 核 Intel Xeon Skylake，2GB RAM，Ubuntu 24.04。\n优化一：TCP DNS 全局缓存 问题 每个 TCP 连接（Session）建立时都要做一次 DNS 解析。对于频繁访问的网站（如 www.google.com），这是完全不必要的重复工作。\n错误尝试：per-session 缓存 最初的实现把 DNS 缓存放在 Session 类的成员变量里：\n// Session.h — 错误示范 struct TcpDnsCacheEntry { tcp::endpoint endpoint; time_t expire_time; }; std::unordered_map\u0026lt;std::string, TcpDnsCacheEntry\u0026gt; tcp_dns_cache_; 看起来没问题，但 Session 在 TCP 连接结束时就销毁了——缓存随 Session 一起销毁，根本没有跨连接复用的机会。每个 Session 只查一次 DNS，缓存刚写入就没了。\n正确方案：全局 DnsCacheManager 单例 把缓存提取为独立的全局单例，所有 Session 共享：\n// Shared/DnsCache.h class DnsCacheManager : private boost::noncopyable { public: struct TcpEntry { boost::asio::ip::tcp::endpoint endpoint; time_t expire_time; }; static DnsCacheManager\u0026amp; instance(); void set_default_ttl(time_t ttl); bool get_tcp(const std::string\u0026amp; key, boost::asio::ip::tcp::endpoint\u0026amp; ep); void put_tcp(const std::string\u0026amp; key, const boost::asio::ip::tcp::endpoint\u0026amp; ep); void cleanup_expired(); private: DnsCacheManager() = default; time_t default_ttl_ = 600; std::mutex mtx_; // 线程安全 std::unordered_map\u0026lt;std::string, TcpEntry\u0026gt; tcp_cache_; }; 使用时：\n// Session.cpp — do_resolve() std::string dns_key = remote_host + \u0026#34;:\u0026#34; + remote_port; tcp::endpoint cached_ep; if (DnsCacheManager::instance().get_tcp(dns_key, cached_ep)) { do_connect(cached_ep); // 命中缓存，跳过 DNS 解析 return; } // 未命中，异步解析后写入缓存 resolver_.async_resolve(remote_host, remote_port, ...); 关键设计：\nstd::mutex 保护并发访问（多线程 io_context） 惰性过期：查询时检查 TTL，过期则重新解析并覆盖 主动清理：steady_timer 每 600 秒扫描一次，删除过期条目 TTL 可配置：从 server.json 读取，不硬编码 定时清理器 // Service.cpp void Service::start_dns_cleanup_timer() { auto interval = ConfigManage::instance().server_cfg.dns_cleanup_interval; dns_cleanup_timer_.expires_after(std::chrono::seconds(interval)); dns_cleanup_timer_.async_wait([this](const boost::system::error_code\u0026amp; ec) { if (ec) return; DnsCacheManager::instance().cleanup_expired(); start_dns_cleanup_timer(); // 递归定时 }); } 配置 { \u0026#34;dns_cache_ttl\u0026#34;: 600, \u0026#34;dns_cleanup_interval\u0026#34;: 600 } 不配置时默认 600 秒（10 分钟），向后兼容。\n优化二：SSL Session 复用 问题 每次客户端连接都要做完整的 TLS 握手：证书交换 + 密钥协商，约 2-5ms。对于频繁重连的客户端（如浏览器），这是可避免的开销。\n方案 在 SSL context 初始化时启用服务端 session 缓存：\n// Service.cpp ctx.set_options( boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use); SSL_CTX_set_session_cache_mode(ctx.native_handle(), SSL_SESS_CACHE_SERVER); 原理 客户端首次 TLS 握手，服务端完成完整握手并缓存 session（session ID + 密钥参数） 客户端重连时在 ClientHello 里携带上次的 session ID 服务端识别后跳过完整握手，直接恢复加密通道（\u0026lt;1ms） OpenSSL 默认缓存 20480 个 session，对我们的并发量完全够用 客户端无需任何改动——TLS session 复用是协议标准行为，浏览器、curl、overplus_client 默认都支持。\n优化三：缓冲区调整 问题 每个 Session 分配 in_buf 和 out_buf 两个缓冲区用于读写。原始大小 32KB，在高吞吐场景下会导致更频繁的系统调用。\n方案 // Session.h static constexpr size_t MAX_BUFF_SIZE = 64 * 1024; // 32KB → 64KB 更大的缓冲区意味着每次 async_read_some / async_write 能处理更多数据，减少系统调用次数。TLS record 最大 16KB，64KB 缓冲区可以容纳多个 TLS record，减少加密解密的上下文切换。\n内存代价：每个 Session 多用 64KB。146 个连接 ≈ 18MB，对 2GB 服务器完全可以接受。\n优化四：热路径日志降级 问题 客户端断开连接时触发的错误日志（Connection reset by peer、stream truncated）在生产环境中是正常的、预期的行为，不应该用 ERROR 级别。高并发时这些日志大量刷屏，每次写入都要分配 std::ostringstream、格式化字符串、写文件——严重影响性能。\n方案 将断连相关日志从 ERROR_LOG 降级为 DEBUG_LOG：\n文件 行 改动 Server/Session.cpp:349 read from client ERROR → DEBUG Server/Session.cpp:367 read from downstream ERROR → DEBUG Server/Session.cpp:387 write to downstream ERROR → DEBUG Server/TlsSession.cpp:37 write to client (TCP) ERROR → DEBUG Server/TlsSession.cpp:55 write to client (UDP) NOTICE → DEBUG DEBUG_LOG 宏在日志级别高于 DEBUG 时完全不执行，零开销：\n#define DEBUG_LOG \\ if (logger::get_log_level() \u0026lt;= L_DEBUG) \\ logger(__FILE__, __func__, __LINE__, L_DEBUG).stream() 结果对比 指标 优化前 优化后 变化 活跃连接 27 146 +440% 内存占用 5.6MB 24.9MB +19.3MB（146×128KB buffer） CPU 正常 几乎为零 — DNS 解析 每次重做 缓存命中直接连接 省掉 DNS 往返 TLS 握手 每次完整握手 重连时 session 恢复 省掉 2-5ms ERROR 日志 大量断连刷屏 零 ERROR 日志安静 25MB 内存跑 146 个并发连接，CPU 几乎为零。\n总结 这次优化的核心思路：\n消除重复工作 — DNS 缓存避免重复解析，SSL Session 复用避免重复握手 全局共享 vs 局部缓存 — per-session 缓存在连接模型下是无效的，必须全局共享 线程安全 — 多线程 io_context 下共享缓存需要 std::mutex 保护 配置化 — TTL、清理间隔从 JSON 读取，不硬编码，方便调优 日志分级 — 断连是正常行为，不应该用 ERROR 级别污染日志 性能优化不是一蹴而就的——第一次把 DNS 缓存放在 Session 成员里是错的，后来才意识到 Session 销毁时缓存也跟着没了。先理解生命周期，再选择缓存位置，这是这次最大的教训。\n","permalink":"https://xyanrch1024.github.io/zh/posts/overplus-optimization/","summary":"对 Overplus（C++17 代理服务器）进行全链路性能优化：TCP DNS 全局缓存、SSL Session 复用、缓冲区调优、热路径日志降级，146 并发连接下内存仅 25MB。","title":"C++ 代理服务器性能优化实战：从 DNS 缓存到 TLS 复用的全链路调优"},{"content":"诸事不宜时总是想回避，但回避解决不了问题。\n行有不得，反求诸己——向内看，才是破局的路。\n","permalink":"https://xyanrch1024.github.io/zh/posts/fan-qiu-zhu-ji/","summary":"\u003cp\u003e诸事不宜时总是想回避，但回避解决不了问题。\u003c/p\u003e\n\u003cp\u003e行有不得，反求诸己——向内看，才是破局的路。\u003c/p\u003e","title":"行有不得，反求诸己"},{"content":"十年尘埃十年雪\n百年树木仍长青\n曾记年少青云志\n不负韶光不负卿\n","permalink":"https://xyanrch1024.github.io/zh/posts/random-thoughts/","summary":"十年尘埃十年雪，百年树木仍长青。曾记年少青云志，不负韶光不负卿。","title":"随想"},{"content":"REPL（Read-Eval-Print Loop）是任何交互式语言的门户。初学者用它来实验，开发者用它来调试代码片段，它让语言有了生命力。本文介绍 kai（一种运行在栈式虚拟机上的 Lua 风格脚本语言）的 REPL 设计。\n源代码：https://github.com/xyanrch1024/VM\n设计目标 多行输入——函数、if/while/repeat 块可以跨越多行 表达式自动打印——像 1 + 2 这样的裸表达式会自动输出结果 状态持久化——在一行中声明的变量在后续行中可用 优雅的错误恢复——错误不会让 REPL 崩溃，用户可以继续输入 架构：数据流 stdin → 行缓冲区 → isCompleteInput()? ├── 否 → \u0026#34;\u0026gt;\u0026gt; \u0026#34; 提示符 → 继续读取 └── 是 → parseQuiet() 测试 → 尝试 print() 包装 → runSource(accumulated) 核心函数位于 main.cpp：\n函数 作用 isCompleteInput(src) 括号 + 关键字平衡检测 runSource(src, vm) 解析 → 编译 → 解释执行 Parser::parseQuiet() 静默解析（不输出 stderr） 多行输入检测 isCompleteInput() 该函数通过扫描文本来判断语法完整性，检查三个维度：\n括号平衡 逐字符扫描追踪 ( )、[ ]、{ } 的深度，跳过字符串字面量（\u0026quot;...\u0026quot;）和行注释（--）。\n关键字平衡 转换为小写后扫描独立关键字：\n开启关键字（深度+1） 关闭关键字（深度-1） function, if, while, for, repeat, do end, until 关键字只有在前后都是非字母字符时才被认为是\u0026quot;独立\u0026quot;的——这样 endless 就不会错误地关闭一个块。\n完整性条件 bool complete = (parenDepth \u0026lt;= 0) \u0026amp;\u0026amp; (bracketDepth \u0026lt;= 0) \u0026amp;\u0026amp; (braceDepth \u0026lt;= 0) \u0026amp;\u0026amp; (blockDepth \u0026lt;= 0); 所有深度必须 ≤ 0。允许出现负值（例如孤立的 ) 没有匹配的 ( 不会欺骗启发式检测——虽然真正的解析器会在后续捕获它）。\n提示符显示 \u0026gt; 1 + 2 # 第一行的主提示符 \u0026gt;\u0026gt; if x then # 续行的副提示符 \u0026gt;\u0026gt; print(x) \u0026gt;\u0026gt; end 表达式自动打印 问题 在 kai 中，像 1 + 2 这样的纯表达式是有效的解析树，但编译器会以\u0026quot;表达式没有效果\u0026quot;为由拒绝它们作为语句。在 REPL 中，用户输入 1 + 2 期望看到 3 被打印出来。\n检测算法 当输入完整时，REPL 执行两步测试：\n直接尝试解析（使用 parseQuiet()）：\n如果产生有效语句 → 使用原始输入 处理：local x = 10、print(\u0026quot;hi\u0026quot;)、x = 5、if ... end 如果第 1 步失败，尝试包裹在 print(...) 中：\n如果 print(original) 能解析成功 → 使用包裹版本 处理：1+2、\u0026quot;hello\u0026quot;、x、f(3)——任何表达式 回退：如果都不行，使用原始输入（真实的错误消息会显示给用户）。\n静默解析 为了避免在测试解析时打印令人困惑的错误消息，我们将 stderr 重定向到 /dev/null：\nstd::vector\u0026lt;Stmt*\u0026gt; Parser::parseQuiet() { FILE* oldStderr = stderr; FILE* devnull = fopen(\u0026#34;/dev/null\u0026#34;, \u0026#34;w\u0026#34;); if (devnull) stderr = devnull; auto result = parse(); if (devnull) { stderr = oldStderr; fclose(devnull); } return result; } 错误消息只在 runSource() 中的真正编译期间显示给用户。\n状态持久化策略 REPL 将所有输入的源代码累加在一个 std::string accumulated 中。每次用户提交完整输入时，整个累加的源代码都会被重新编译并从头执行。\n第 1 行：local x = 10 → accumulated = \u0026#34;local x = 10\u0026#34; 第 2 行：print(x) → accumulated = \u0026#34;local x = 10\\nprint(x)\u0026#34; → 重新编译并运行全部 这种方法类似于早期 BASIC 的 REPL，其优缺点如下：\n优点 缺点 实现简单 每次新行都重新执行所有副作用 无需修改编译器 print() 输出会随着行数增长而重复 局部变量自然保持 性能随会话时间增长而下降 对于学习/调试环境，这种简单性值得付出代价。未来的改进可以采用增量编译和持久的符号表。\nREPL 循环伪代码 accumulated = \u0026#34;\u0026#34; // 持久化的源代码字符串 buffer = \u0026#34;\u0026#34; // 当前正在构建的输入 inMultiLine = false 循环： prompt = inMultiLine ? \u0026#34;\u0026gt;\u0026gt; \u0026#34; : \u0026#34;\u0026gt; \u0026#34; 打印 prompt line = readLine(stdin) 如果是 EOF： 如果 inMultiLine 且 buffer 不为空：runSource(buffer, vm) 退出 如果不是多行且 line 是 \u0026#34;exit\u0026#34; 或 \u0026#34;quit\u0026#34;：退出 buffer += \u0026#34;\\n\u0026#34; + line 如果 buffer 全是空白：继续 如果 isCompleteInput(buffer)： toRun = \u0026#34;\u0026#34; // 尝试原样解析 parser = Parser(buffer) stmts = parser.parseQuiet() 如果 stmts 不为空且无错误： toRun = buffer // 尝试包裹在 print() 中 如果 toRun 为空： parser = Parser(\u0026#34;print(\u0026#34; + buffer + \u0026#34;)\u0026#34;) stmts = parser.parseQuiet() 如果 stmts 不为空且无错误： toRun = \u0026#34;print(\u0026#34; + buffer + \u0026#34;)\u0026#34; // 回退 如果 toRun 为空： toRun = buffer accumulated += \u0026#34;\\n\u0026#34; + toRun runSource(accumulated, vm) buffer = \u0026#34;\u0026#34; inMultiLine = false 否则： inMultiLine = true 局限性及未来工作 当前局限性 局限 原因 影响 副作用重执行 状态持久化每次重新编译所有输入 print() 输出每次新行都重复 启发式完整性检查 基于文本，不基于语法 特殊语法可能误判 无历史记录 未集成 readline/libedit 无法用上箭头回忆之前的行 无 Tab 补全 未向 REPL 暴露符号表 只能手动输入 可能的改进 1. 增量编译 + 持久作用域 保持编译器的符号表跨行存活。每行编译为一个独立块，追加到同一函数中。需要大幅修改 CompileState 的生命周期。\n2. Readline 支持 链接 libreadline 或 libedit，获得行编辑、历史和 Tab 补全功能。用 readline() 替换 std::getline()。\n3. 语法感知的完整性检查 用真正的解析器替代启发式的 isCompleteInput()，使用特殊的\u0026quot;期待更多\u0026quot;错误模式。如果解析器因 unexpected EOF 失败，提示用户继续输入。需要修改 Lexer::next() 和 Parser::consume()。\n4. 持久化 REPL 命名空间 用 VM 中持久化的真实符号表替代累加源代码的方式。每行成为单独的函数，共享相同的顶层局部作用域，使用隐藏表存储 REPL 声明的变量。\n总结 kai REPL 是一个实用主义的最小设计，在实现成本与用户体验之间取得了平衡：\n多行输入——通过启发式的括号/关键字平衡检查器 表达式自动打印——通过静默解析和 print 包装 状态持久化——通过全源重新编译（简单但低效） 错误恢复——由循环结构自然处理 这不是一个生产级别的 REPL——没有 readline、没有 Tab 补全、没有增量编译——但它足以完成语言实验的工作。完整代码库（包括词法分析器、解析器、编译器和虚拟机）是开源的。\n完整源码：https://github.com/xyanrch1024/VM\n","permalink":"https://xyanrch1024.github.io/zh/posts/repl-design/","summary":"为 kai 脚本语言设计交互式 Read-Eval-Print Loop——多行输入、表达式自动打印、状态持久化和优雅的错误恢复。","title":"为脚本语言构建 REPL 交互环境"},{"content":"前端是语言实现的第一关：把源码字符串变成可执行的字节码。这篇文章总结我为栈式虚拟机写的 kai 语言前端——一个类 Lua 的脚本语言。\n项目代码：https://github.com/xyanrch1024/VM\n整体流程 源码 → 词法分析 (Lexer) → Token 流 → 语法分析 (Parser) → AST → 编译 (Compiler) → Bytecode → VM 三个模块分工明确：\n模块 输入 输出 行数 Lexer 源文件字符串 Token 流 ~200 Parser Token 流 AST ~600 Compiler AST Function (字节码) ~400 合计 ~1200 此外还需要 AST 节点定义 (~200 行) 和内置函数注册 (~100 行)，全前端约 1500 行。\n语言设计：精简版 Lua kai 是 Lua 的精简方言，保留核心哲学——简约、灵活、嵌入友好。\n特性 Lua kai 返回值 多返回值 单返回值 变量 默认全局 仅 local 元表/协程/goto 支持 暂不支持 泛型 for 支持 仅数值 for EBNF 语法（节选） program = { stat } stat = \u0026#39;local\u0026#39; name \u0026#39;=\u0026#39; expr | name \u0026#39;=\u0026#39; expr | \u0026#39;if\u0026#39; expr \u0026#39;then\u0026#39; block \u0026#39;end\u0026#39; | \u0026#39;while\u0026#39; expr \u0026#39;do\u0026#39; block \u0026#39;end\u0026#39; | \u0026#39;function\u0026#39; name \u0026#39;(\u0026#39; [ namelist ] \u0026#39;)\u0026#39; block \u0026#39;end\u0026#39; | \u0026#39;return\u0026#39; [ expr ] | functioncall expr = nil | true | false | NUMBER | STRING | functiondef | tableconstructor | prefixexpr | expr binop expr | unop expr 运算符优先级 1. () . [] -- 作用域/索引 2. # - not -- 一元 3. ^ -- 幂 (右结合) 4. * / % -- 乘法 5. + - -- 加法 6. .. -- 字符串拼接 7. \u0026lt; \u0026gt; \u0026lt;= \u0026gt;= == ~= -- 比较 8. and 9. or 词法分析 (Lexer) 手动实现的有限状态机，逐个字符扫描。\nToken Lexer::nextToken() { skipWhitespace(); switch (*cur) { case \u0026#39;+\u0026#39;: cur++; return {TK_PLUS, line}; case \u0026#39;-\u0026#39;: if (peek() == \u0026#39;-\u0026#39;) { skipComment(); return nextToken(); } cur++; return {TK_MINUS, line}; case \u0026#39;\u0026#34;\u0026#39;: return readString(); default: if (isdigit(*cur)) return readNumber(); if (isalpha(*cur) || *cur == \u0026#39;_\u0026#39;) return readNameOrKeyword(); error(\u0026#34;unexpected symbol \u0026#39;%c\u0026#39;\u0026#34;, *cur); } } 关键字表用 hash 映射，标识符查表决定是关键字还是普通 name。\n处理点：..（拼接）、...（变长参数）、== 和 = 的区分、注释跳过。\n语法分析 (Parser) 递归下降解析器，每个语法规则对应一个函数。\nExpr* Parser::parseExpr(int precedence) { Expr* expr = parsePrefix(); while (precedence \u0026lt; getPrecedence(current().type)) { TokenType op = current().type; advance(); Expr* rhs = parseExpr(getRightPrecedence(op)); expr = new BinaryExpr(expr, op, rhs); } return expr; } 左右递归通过运算符优先级表控制，避免手写层层嵌套。\n每个 parseStat 函数内嵌对应的语法规则，代码结构与 EBNF 基本一致：\n// \u0026#39;if\u0026#39; expr \u0026#39;then\u0026#39; block { \u0026#39;elseif\u0026#39; expr \u0026#39;then\u0026#39; block } [ \u0026#39;else\u0026#39; block ] \u0026#39;end\u0026#39; void Parser::parseIf() { std::vector\u0026lt;Expr*\u0026gt; conds; std::vector\u0026lt;Stmt*\u0026gt; bodies; conds.push_back(parseExpr()); expect(TK_THEN); bodies.push_back(parseBlock()); while (match(TK_ELSEIF)) { ... } if (match(TK_ELSE)) bodies.push_back(parseBlock()); expect(TK_END); new IfStmt(conds, bodies, elseBody); } AST 定义 使用带 union 的 C++ 结构体，避免虚函数开销。\nstruct Expr { ExprType type; int line; union { double numVal; // NUMBER const char* strVal; // STRING / NAME struct { TokenType op; Expr* rhs; } unary; // UNARY struct { TokenType op; Expr* l, *r; } bin; // BINARY struct { Expr* callee; vector\u0026lt;Expr*\u0026gt; args; } call; // CALL struct { Expr* obj; Expr* key; } index; // INDEX struct { vector\u0026lt;const char*\u0026gt; p; Stmt* b; } func; // FUNCDEF struct { vector\u0026lt;TableField\u0026gt; f; } table; // TABLE }; }; Stmt 同理。这种 tagged union 比继承多态更紧凑，遍历时 switch 分发即可。\n编译器 (AST → Bytecode) 核心是 compileExpr 和 compileStmt 两个递归函数。\n表达式编译表 源码 生成字节码 42 OP_CONSTANT \u0026lt;int:42\u0026gt; x OP_LOAD \u0026lt;slot\u0026gt; a + b a b OP_ADD a and b a OP_JZ →end OP_POP b (短路) f(x) x \u0026lt;funcIdx\u0026gt; OP_CALL 1 t[k] t k OP_GET_INDEX 短路逻辑 and 和 or 通过跳转实现短路：\na and b: \u0026lt;a\u0026gt; OP_JZ → end ; a 为假则跳过 b OP_POP \u0026lt;b\u0026gt; → end: a or b: \u0026lt;a\u0026gt; OP_JNZ → end ; a 为真则跳过 b OP_POP \u0026lt;b\u0026gt; → end: 作用域管理 编译器维护一个 Scope 栈：\nstruct Local { const char* name; int depth; int slot; }; vector\u0026lt;Scope\u0026gt; scopes; void pushScope() { scopes.push_back({scopeDepth++}); } void popScope() { // 发出 POP 清理超出作用域的变量 for (auto\u0026amp; local : scopes.back().locals) emitByte(OP_POP); scopes.pop_back(); } 变量查找：resolveLocal(name) 从内到外遍历作用域链，返回 slot 编号。\n控制流编译 if 语句：条件编译 → OP_JZ 跳分支 → 分支体 → OP_JMP 跳过剩余分支 → 回填跳转地址。\nwhile 循环：记录 loop start → 编译条件 → OP_JZ → 编译体 → OP_LOOP 回跳 → 回填。\nfor 循环：展开为 while，引入内部变量存储 limit 和 step。\n函数编译 function fact(n) if n \u0026lt;= 1 then return 1 end return n * fact(n - 1) end 编译过程：\n创建新的 Function 对象 参数 n 分配到 slot 0 编译函数体、递归调用自己 生成的字节码存入独立的 Chunk 外层发出 OP_CLOSURE \u0026lt;funcIdx\u0026gt; 编译示例完整追踪 function fact(n) if n \u0026lt;= 1 then return 1 end return n * fact(n - 1) end print(fact(5)) 生成字节码：\nmain 函数：\nOP_CLOSURE 0 ; 创建闭包 (fact) OP_STORE_0 ; fact = closure OP_POP OP_CONSTANT 0 ; push 5 OP_CONSTANT 1 ; push funcIdx(0) OP_CALL 1 ; fact(5) OP_PRINTLN OP_HALT fact 函数：\nOP_LOAD_0 ; push n OP_CONSTANT 0 ; push 1 OP_LE ; n \u0026lt;= 1? OP_JZ → 7 ; 假则跳过 return OP_CONSTANT 1 ; push 1 OP_RET OP_LOAD_0 ; push n OP_LOAD_0 ; push n OP_CONSTANT 2 ; push 1 OP_SUB ; n - 1 OP_CONSTANT 3 ; push funcIdx(1) OP_CALL 1 ; fact(n-1) OP_MUL ; n * fact(n-1) OP_RET VM 扩展 前端需要后端配合新增指令：\nOpcode 用途 OP_GET_INDEX t[k] 索引读取 OP_SET_INDEX t[k] = v 索引写入 OP_NEW_TABLE {} 创建表 OP_CLOSURE 创建闭包 OP_GET_UPVALUE 读取 upvalue OP_SET_UPVALUE 写入 upvalue Value 类型扩展 TABLE 和 CLOSURE，Table 实现为 unordered_map\u0026lt;Value, Value\u0026gt;。\n分阶段实现 Phase 内容 行数 测试 1 词法 + 语法 + 表达式 ~400 print(1 + 2 * 3) 2 控制流 + 变量 ~300 if/while/for/短路逻辑 3 函数 + 闭包 ~200 递归 factorial/fib 4 Table + 内置函数 ~200 表构造器/索引 每个阶段都有测试文件 + 预期输出，通过 diff 验证。\n测试策略 ./build/vm tests/arithmetic.kai \u0026gt; /tmp/out diff /tmp/out tests/arithmetic.expected 测试目录：\ntests/ ├── arithmetic.kai -- 算术运算 ├── variables.kai -- 局部变量 + 作用域 ├── if.kai -- 条件分支 ├── while.kai -- 循环 ├── for.kai -- 数值 for ├── recursion.kai -- 递归 ├── table.kai -- 表操作 └── ... 总结 这个前端从零构建了一个完整编程语言的第一步：词法分析 → 语法分析 → 字节码编译。\n词法分析器 ~200 行，手动状态机 解析器 ~600 行，递归下降，运算符优先级表 编译器 ~400 行，AST → 40 条指令的字节码 全前端 ~1500 行 C++17 加上之前实现的栈式虚拟机后端，已经能运行完整的程序了。\n完整源码：https://github.com/xyanrch1024/VM\n","permalink":"https://xyanrch1024.github.io/zh/posts/kai-lang-frontend/","summary":"从词法分析、语法分析到字节码编译，完整实现一个类 Lua 语言的前端。","title":"用 2000 行 C++ 写一个类 Lua 语言的前端"},{"content":"前段时间写了一个栈式虚拟机（Stack-based VM），整理了一份设计文档。本文是对这份文档的解读和总结。\n项目代码：https://github.com/xyanrch1024/VM\n为什么选栈式架构 虚拟机架构主要有两种：\n特性 栈式 寄存器式 指令长度 短（1-3 字节） 较长 编译器后端 极简 需要寄存器分配 解释执行 直观，Bug 少 指令数少约 30% JIT 适配 较难 天然适合 这个项目选择栈式，原因很简单：纯解释执行场景下，栈式实现简单、代码生成方便、不易出错。\n总体架构 VM 实例包含以下核心组件：\n操作数栈 — vector\u0026lt;Value\u0026gt;，同时存放局部变量和临时计算值 调用栈 — vector\u0026lt;CallFrame\u0026gt;，管理函数调用 字符串表 — intern 池，字符串比较退化为指针比较 函数表 — 所有已编译函数的集合 PC / FP — 程序计数器和帧指针 值系统 每个 Value 占 8 字节，用 1 字节类型标签 + 7 字节值：\n┌──────┬──────────────────────────────┐ │ type │ value │ │ 1B │ 7B │ ├──────┼──────────────────────────────┤ │ NIL │ padding │ │ BOOL │ bool │ │ INT │ int64_t │ │FLOAT │ double │ │STRING│ void* (→ string table) │ └──────┴──────────────────────────────┘ 类型提升规则：INT + FLOAT → FLOAT，字符串 + 走拼接。\n指令集（40 条） 指令编码格式：1 字节 opcode + 变长操作数。\n分为几大类：\n类别 数量 示例 常量压栈 5 OP_CONSTANT, OP_NIL 栈操作 5 OP_DUP, OP_SWAP, OP_ROT 局部变量 10 OP_LOAD, OP_STORE_0~3 算术运算 7 OP_ADD, OP_SUB, OP_MUL 比较运算 6 OP_EQ, OP_LT, OP_GE 位运算 6 OP_BIT_AND, OP_SHL 逻辑运算 1 OP_NOT 控制流 4 OP_JMP, OP_JZ, OP_LOOP 函数调用 2 OP_CALL, OP_RET 其他 4 OP_PRINT, OP_HALT 执行模型 操作数栈分为两个区域：\n┌───────────────────┐ │ 局部变量区域 │ [fp, fp+numLocals) │ slot 0,1,...,N │ LOAD/STORE 访问 ├───────────────────┤ │ 表达式栈 │ [fp+numLocals, sp] │ 临时计算值 │ PUSH/POP 操作 └───────────────────┘ 函数调用时：预分配局部变量 → 压入 CallFrame → 执行 → RET 弹出清栈。\n内存管理：字符串驻留 所有字符串在加载时放入 intern 表。每次遇到字符串常量，先查 hash map：\n已存在？→ 返回已有指针（O(1) 比较） 不存在？→ 创建新副本，加入表，返回稳定指针 编译示例 1 + 2 × 3 的字节码：\nOP_CONSTANT 0 ; push 1 OP_CONSTANT 1 ; push 2 OP_CONSTANT 2 ; push 3 OP_MUL ; 2 × 3 = 6 OP_ADD ; 1 + 6 = 7 OP_PRINTLN ; print 7 OP_HALT 递归阶乘 factorial(5)：\n与多数语言编译器的中间表示类似，函数体内通过 LOAD/STORE 访问参数和局部变量，CALL/RET 管理调用栈。\n测试覆盖 13 个测试覆盖了：算术、局部变量、条件分支、循环、递归、浮点、字符串、比较、位运算、栈操作、双递归调用（fibonacci）。\n优化方向（待做） Top-of-stack caching — 栈顶值缓存到寄存器 Computed goto — switch 替换为跳转表 内联缓存 — 频繁调用函数做内联 JIT 编译 — 热点字节码编译为本地机器码 结语 这份设计文档完整描述了一个可工作的栈式虚拟机。代码量不大（C++17，8 个文件），但涵盖了 VM 的核心设计范式。对编译器或语言实现感兴趣的同学可以参考。\n完整源码：https://github.com/xyanrch1024/VM\n","permalink":"https://xyanrch1024.github.io/zh/posts/stack-vm-design/","summary":"解读我实现的一个栈式虚拟机的设计文档，涵盖架构、指令集、执行模型和内存管理。","title":"从零实现一个栈式虚拟机：设计文档解读"},{"content":"我是一名程序员，热爱编程。\nGitHub xyanrch — 以前的账号（2FA 丢失，无法登录） xyanrch1024 — 当前活跃账号 ","permalink":"https://xyanrch1024.github.io/zh/about/","summary":"\u003cp\u003e我是一名程序员，热爱编程。\u003c/p\u003e\n\u003ch2 id=\"github\"\u003eGitHub\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/xyanrch\"\u003exyanrch\u003c/a\u003e — 以前的账号（2FA 丢失，无法登录）\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/xyanrch1024\"\u003exyanrch1024\u003c/a\u003e — 当前活跃账号\u003c/li\u003e\n\u003c/ul\u003e","title":"关于"},{"content":"一步步教你用 Hugo 搭建博客，通过 GitHub Actions 自动部署到 GitHub Pages。\n为什么选 Hugo + GitHub Pages Hugo：单二进制，构建极快，Go 语言生态 GitHub Pages：免费托管，HTTPS，支持自定义域名 GitHub Actions：零成本 CI/CD，推送即部署 1. 初始化 Hugo 站点 hugo new site blog --format yaml cd blog git init 添加主题（用 git submodule 管理）：\ngit submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod 编辑 hugo.yaml：\nbaseURL: https://xyanrch1024.github.io languageCode: zh-cn title: xyanrch 的博客 theme: PaperMod 2. 双仓库策略 仓库 用途 你操作 blog Hugo 源码（Markdown、主题、配置） 是 — 在这里写文章 xyanrch1024.github.io 构建后的静态文件（HTML/CSS/JS） 否 — Actions 自动管理 GitHub Pages 要求仓库名为 \u0026lt;用户名\u0026gt;.github.io，才能通过 https://用户名.github.io/ 访问。\n3. GitHub Actions 工作流 创建 .github/workflows/deploy.yml：\nname: Deploy to Pages on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - uses: peaceiris/actions-hugo@v3 - run: hugo --minify - uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.DEPLOY_TOKEN }} publish_dir: ./public external_repository: xyanrch1024/xyanrch1024.github.io publish_branch: main 关键点：\nsubmodules: true — 需要拉取 PaperMod 主题 personal_token — 跨仓库部署必须用 PAT；GITHUB_TOKEN 不能推送到外部仓库 external_repository — 目标仓库，即存放最终静态文件的仓库 4. Personal Access Token GITHUB_TOKEN（自动生成）只能推送同仓库。要部署到 xyanrch1024.github.io，需要创建一个 classic PAT（repo 权限）：\n打开 https://github.com/settings/tokens 生成 classic token，勾选 repo 权限 在 blog 仓库添加 secret 名为 DEPLOY_TOKEN： gh secret set DEPLOY_TOKEN --repo xyanrch1024/blog --body \u0026lt;你的token\u0026gt; 5. 启用 GitHub Pages 用户 Pages（\u0026lt;用户名\u0026gt;.github.io）本应自动启用，但有时需要推送一次才能触发首次构建。如果构建卡在 building，可以推送一个空提交：\ngit clone git@github.com:xyanrch1024/xyanrch1024.github.io.git cd xyanrch1024.github.io echo \u0026#34;trigger\u0026#34; \u0026gt;\u0026gt; trigger.txt git add . \u0026amp;\u0026amp; git commit -m \u0026#34;trigger rebuild\u0026#34; \u0026amp;\u0026amp; git push 构建完成后（status: built），站点就在 https://xyanrch1024.github.io/ 上线了。\n6. 写作工作流 # 创建新文章 hugo new posts/my-article.md # 本地预览 hugo server -D # 部署 git add . \u0026amp;\u0026amp; git commit -m \u0026#34;new article: xxx\u0026#34; \u0026amp;\u0026amp; git push 推送触发 GitHub Action，自动构建 Hugo 并部署到 Pages。\n总结 写文章 → 推送 → Actions 构建 → Pages 上线 本地：hugo new + hugo server -D 推送：git push origin main CI：Actions 自动构建和部署 托管：GitHub Pages 提供静态文件服务 访问：https://xyanrch1024.github.io/\n","permalink":"https://xyanrch1024.github.io/zh/posts/deploy-hugo-blog-to-github-pages/","summary":"一步步教你用 Hugo 搭建博客，通过 GitHub Actions 自动部署到 GitHub Pages。","title":"使用 Hugo + GitHub Pages 搭建博客"},{"content":"上有天堂，下有苏杭。杭州是一座融合了自然山水与历史文化的城市。\n必去景点 西湖 杭州的灵魂。推荐骑行或步行环湖，沿途经过断桥、白堤、苏堤。傍晚在湖边看日落，别有风味。\n灵隐寺 千年古刹，位于飞来峰下。寺内香火鼎盛，周围山林清幽，适合静心漫步。\n雷峰塔 登塔可俯瞰西湖全景。传说中白娘子的故事就发生在这里。\n清河坊历史街区 保留了大量清末民初的建筑，可以品尝杭州小吃、购买特色手工艺品。\n美食推荐 东坡肉 — 肥而不腻，入口即化 龙井虾仁 — 茶香与虾鲜的完美结合 片儿川 — 杭州人的日常面食，汤鲜味美 西湖醋鱼 — 酸甜可口，杭州名菜之首 旅游贴士 最佳季节：春季（3-5月）和秋季（9-11月），气候宜人 建议天数：2-3天 交通：杭州地铁覆盖主要景点，打车也很方便 住宿：推荐住在西湖附近或武林广场商圈 ","permalink":"https://xyanrch1024.github.io/zh/posts/hangzhou-travel-guide/","summary":"杭州旅游攻略，涵盖西湖、灵隐寺等必去景点和美食推荐。","title":"杭州旅游指南"},{"content":"这是我的第一篇博客文章，通过 Hugo + GitHub Pages 搭建。\n技术栈 Hugo - 静态站点生成器 PaperMod - 简洁的主题 GitHub Pages - 免费托管 GitHub Actions - 自动构建部署 ","permalink":"https://xyanrch1024.github.io/zh/posts/my-first-post/","summary":"我的第一篇博客，使用 Hugo + GitHub Pages 搭建。","title":"Hello World"},{"content":"页面未找到 返回首页\n","permalink":"https://xyanrch1024.github.io/zh/404/","summary":"\u003ch2 id=\"页面未找到\"\u003e页面未找到\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"/zh/\"\u003e返回首页\u003c/a\u003e\u003c/p\u003e\n\u003cscript src=\"https://volunteer.cdn-go.cn/404/latest/404.js\"\u003e\u003c/script\u003e","title":"404"}]