课题7:编译器日志增强器

难度:低 | 类型:项目实战 | 源文件scratchv/utils/logger.py | 行数:~300 状态:✅ 已完成


概述

为编译器的各个阶段(解析、IR生成、优化、代码生成)添加分级、带颜色的日志输出,支持--log-level--log-file命令行参数。


理解背景

是什么?

日志增强器(logger.py)为 ScratchV 编译器提供了统一的结构化日志系统,替代散落在各处的 print() 调用。

核心功能: - 彩色终端输出:不同级别用不同颜色(DEBUG 灰、INFO 绿、WARNING 黄、ERROR 红) - 级别过滤:开发时看 DEBUG,正式跑只看 INFO - 文件输出:同时写日志文件(无颜色,纯文本) - 阶段计时:自动记录每个编译阶段的耗时 - 进度显示:长任务显示 [当前/总数] 百分比

为什么?

编译器是一个多阶段流水线(解析 → 优化 → 代码生成 → ...)。每个阶段都可能出问题。如果只是到处 print("ok")print("error")

  1. 找不到问题:不知道哪个阶段报的错、花了多少时间
  2. 日志太多:正式使用时无法关闭调试输出
  3. 格式不统一:每个人加的 print 格式都不一样

统一日志系统让调试效率大幅提升。

核心概念

日志级别(从低到高)
级别 用途 颜色 什么时候用
DEBUG 详细追踪信息 灰色 开发调试时
INFO 正常操作消息 绿色 报告进度
WARNING 非关键问题 黄色 可恢复的异常
ERROR 严重错误 红色 编译失败
CRITICAL 致命错误 粗体红 必须立即停止

log_phase 上下文管理器

自动记录一个阶段的开始和结束时间:

with log_phase("parse", "Parsing DSL input"):
    program = parser.parse(source)
# 自动输出:
# 12:34:56 INFO     [scratchv.parse] Parsing DSL input...
# 12:34:56 INFO     [scratchv.parse] Parsing DSL input... done (0.032s)

如果阶段抛异常,会自动输出 FAILED


详细任务
  1. 使用Python logging模块,创建多个logger(按模块)。
  2. 添加argparse参数:--log-level {DEBUG,INFO,WARN,ERROR}--log-file FILE
  3. 替换现有print语句为logger.infologger.debug
  4. 为关键操作添加日志:开始解析、优化Pass应用、指令数统计、代码生成完成。
  5. 实现彩色输出(使用colorlog或ANSI码),不同级别不同颜色。
  6. 支持同时输出到控制台和文件(文件可保留DEBUG级别)。
  7. 确保高日志级别时低级别字符串不会被构造(使用logger.isEnabledFor)。

交付产物
  • 修改后的编译器主文件和各个模块
  • 使用示例:python main.py test.dsl --log-level DEBUG --log-file build.log
  • 文档:日志级别含义、如何为新增模块添加日志

代码走读

一步步使用

Step 1: 初始化日志系统
from scratchv.utils.logger import init_logger, get_logger

# 初始化(通常在编译器入口点调用一次)
init_logger(
    level="DEBUG",           # 显示所有级别的日志
    log_file="build.log",    # 同时写入文件
    use_color=True,          # 终端彩色输出
)

Step 2: 获取日志器并使用
log = get_logger("scratchv.parser")  # 按模块命名

log.debug("Tokenizing line %d...", 42)
log.info("Parsed %d nodes", 150)
log.warning("Unknown attribute: %s", attr_name)
log.error("Syntax error at line %d: %s", line, msg)

Step 3: 使用阶段计时
from scratchv.utils.logger import log_phase

with log_phase("optimize", "Running optimization passes"):
    for pass_name in ["constant_folding", "dead_code", "peephole"]:
        run_pass(pass_name)

# 输出:
# INFO [scratchv.optimize] Running optimization passes...
# INFO [scratchv.optimize] Running optimization passes... done (0.156s)

Step 4: 运行时切换日志级别
from scratchv.utils.logger import set_level

set_level("WARNING")   # 只看警告和错误
set_level("DEBUG")     # 恢复详细输出

核心设计:单例 + 层级命名
_root_logger: Optional[logging.Logger] = None  # 全局根日志器

def init_logger(level, log_file, use_color):
    _root_logger = logging.getLogger("scratchv")     # 根
    _root_logger.addHandler(console_handler)          # → 终端
    if log_file:
        _root_logger.addHandler(file_handler)         # → 文件

def get_logger(name: str):
    if not name.startswith("scratchv"):
        name = f"scratchv.{name}"
    return logging.getLogger(name)   # Python logging 自动继承层级

所有 scratchv.xxx 日志器都继承根日志器的 handlers 和级别。

彩色格式化器
class _ColorFormatter(logging.Formatter):
    def format(self, record):
        color = _COLORS.get(record.levelname, "")
        return f"{color}{record.levelname:<8}\033[0m [{record.name}] {record.getMessage()}"

核心就是用 ANSI 转义码包裹日志级别名。\033[32m = 绿色开始,\033[0m = 重置。


动手练习

练习 1: 给一个模块加日志

找一个你正在开发的模块(比如优化 pass),把里面的 print() 替换成 log.debug() / log.info()

练习 2: 比较不同日志级别

分别用 DEBUGINFOWARNING 级别运行同一个编译任务,观察输出量的差异。

练习 3: 添加 log_phase

给编译器的某个阶段加上 log_phase,看看这个阶段实际耗时多少。


常见坑
说明
忘记 init_logger get_logger() 会自动用默认参数初始化,但建议显式调用 init_logger() 以获得预期行为
日志太多 默认级别是 INFO,如果你调成 DEBUG,可能输出大量内部信息
文件 handler 总是 DEBUG 文件输出固定为 DEBUG 级别(file_handler.setLevel(logging.DEBUG)),确保文件里有完整记录
彩色输出在管道中 如果输出被管道到文件(python compile.py > out.txt),ANSI 颜色码会变成乱码。用 use_color=False 关闭

进阶阅读

12周每周目标
  • W1:学习logging模块基础(Logger、Handler、Formatter、级别)。编写demo。
  • W2:分析编译器现有print语句,规划哪些应转为日志,划分级别。
  • W3:在main.py中初始化logging,添加控制台Handler,设置格式%(asctime)s - %(name)s - %(levelname)s - %(message)s
  • W4:替换前端(解析)中的printlogger.info/debug,添加--log-level参数。
  • W5:替换IR生成和优化阶段的print,为每个阶段创建子logger(如logger = logging.getLogger('ir'))。
  • W6:替换后端代码生成的print,确保所有输出通过日志。
  • W7:安装colorlog,根据级别设置颜色(ERROR红色,WARNING黄色,INFO绿色,DEBUG灰色)。
  • W8:添加--log-file参数,将日志同时写入文件。
  • W9:优化日志信息,避免噪音,关键步骤输出简洁的统计信息。
  • W10:测试不同级别和参数组合,确保性能影响小(使用if logger.isEnabledFor)。
  • W11:添加进度指示(如"Parsing... Done in 0.02s")。
  • W12:撰写文档:日志级别说明、配置方法、常见使用场景。