1. 先写 `diagnostic.ts` 的核心模块

Article detail

学习笔记

2026/4/19 · 15 分钟阅读

1. 先写 diagnostic.ts 的核心模块

对照你原来的 diagnostic.ts 和 issue 里的模板,你需要做以下几件事:


1. 先写 diagnostic.ts 的核心模块

issue 里引用了 diagnostic.infer_exprdiagnostic.context,说明你需要一个独立的诊断模块,负责遍历 AST 收集错误。对你的 utlc 项目来说,错误来源主要是 parse 阶段:

// diagnostic.ts
import { parser, syntax, token } from "../sexp";
import { exp } from "./parse_expr";

export type error = { range: range; message: string };
type position = { line: number; character: number };
type range = { start: position; end: position };

export type context = {
  report: error[];
  text: string;
};

const get_range = (syn: syntax): range => {
  // 和 parse_expr 里一样,从 syntax 节点取位置
  switch (syn.tag) {
    case syntax.tag.atom:
    case syntax.tag.lone: {
      const leaf = (syn as syntax.atom).leaf;
      return token.to_range(leaf);
    }
    case syntax.tag.group:
    case syntax.tag.mismatch: {
      const children = (syn as syntax.group).children;
      const first = children[0]! as syntax.atom;
      const last = children[children.length - 1]! as syntax.atom;
      return {
        start: token.to_start_position(first.leaf),
        end: token.to_end_position(last.leaf),
      };
    }
  }
};

export const infer_expr = (ctx: context, syn: syntax): exp | undefined => {
  try {
    return exp.parse(syn, ctx.text);
  } catch {
    ctx.report.push({ range: get_range(syn), message: "Parse error" });
    return undefined;
  }
};

2. 然后写 vscode_diagnostic.ts(或直接叫 diagnostic_provider.ts

直接套 issue 的模板,把 diagnostic.infer_expr 换成上面写的:

import { parser } from "../sexp";
import * as vscode from "vscode";
import { Diagnostic, Range, Position, DiagnosticSeverity as Severity } from "vscode";
import * as diagnostic from "./diagnostic";

export { make };

type Provider = (event: vscode.TextDocumentChangeEvent) => void;

enum Scheme {
  file = "file",
  output = "output",
}

type error = diagnostic.error;

const error: {
  readonly to_diagnostic: (self: error) => Diagnostic;
} = {
  to_diagnostic: self => {
    let { range: { start, end }, message } = self;
    const pstart = new Position(start.line, start.character);
    const pend = new Position(end.line, end.character);
    const range = new Range(pstart, pend);
    return new Diagnostic(range, message, Severity.Error);
  },
};

const handler = (
  out: vscode.OutputChannel,
  event: vscode.TextDocumentChangeEvent,
  dia: vscode.DiagnosticCollection,
): void => {
  const text = event.document.getText();
  const prs = parser.make(text);
  const syns = parser.parse(prs);
  const ctx: diagnostic.context = { report: [], text };

  if (syns.length > 0) {
    diagnostic.infer_expr(ctx, syns[0]!);
  }
  dia.set(event.document.uri, ctx.report.map(error.to_diagnostic));
};

const manager = vscode.languages.createDiagnosticCollection("utlc");

const make = (out: vscode.OutputChannel): Provider => {
  return e => {
    if (e.contentChanges.length === 0) return;
    if (e.document.uri.scheme === Scheme.output) return;
    if (e.document.languageId !== "utlc") return;
    handler(out, e, manager);
  };
};

3. 最后更新 extension.ts

照 issue 的模板直接加一行:

import * as diagnostic from "./vscode_diagnostic"; // 或你命名的文件

const activate = (context: vscode.ExtensionContext) => {
  let out = vscode.window.createOutputChannel("utlc", "log");
  // ... 已有的 cmpl 等
  let diag = vscode.workspace.onDidChangeTextDocument(diagnostic.make(out));
  context.subscriptions.push(cmpl, diag, out);
};

关键设计决策

你需要确认一点:错误粒度。现在的方案是整个表达式 parse 失败才报一个错,位置指向顶层节点。如果你希望做到子表达式级别的精确报错(比如 (lambda) 缺 body 时精确指向括号内部),那就需要把 parse_expr.ts 里的 impossible 改成带 range 的错误类型,然后在 infer_expr 里 catch 后取出精确位置。

你现在想要哪种粒度?

评论

动作测试