@rauchg: https://x.com/rauchg/status/2111285042165215580

X AI KOLs Following 工具

摘要

scriptc 发布实验性 API `@scriptc/experimental`,引入 `LifetimeRegion` 与 `ProgramLifetime` 契约,实现基于区域(region)的确定性内存释放,在 Node.js 下可无改动运行,并兼容常驻型编译器场景。

@hd_nvim https://t.co/MqsRp0aAPD
查看原文
查看缓存全文

缓存时间: 2026/10/17 02:47

@hd_nvim https://t.co/MqsRp0aAPD — # 实验性 API | scriptc 来源:https://scriptc.dev/docs/experimental @scriptc/experimental 包中包含仍在设计中的 API。每个导出都有对应的 Node.js 实现,因此使用它们的程序可以不加修改地在 Node.js 下运行,而 scriptc 会将它们编译进程序中。这些 API 的行为可能在版本之间发生变化。将 @scriptc/experimental 添加到项目的依赖中。 ProgramLifetime(https://scriptc.dev/docs/contracts#programlifetime)类的实例永远不会被释放,这适合那种构建一个语法树或符号表然后就退出的程序。对于长时间运行的程序,比如处于 watch 模式的编译器,每次重新构建时都会创建新一代的对象。LifetimeRegion 允许它在某一代不再需要时将其释放。

import type { ProgramLifetime } from "@scriptc/types";
import { LifetimeRegion } from "@scriptc/experimental";

class SyntaxNode implements ProgramLifetime {
  readonly children: SyntaxNode[] = [];
  parent: SyntaxNode | undefined = undefined;
}

function parse(depth: number): SyntaxNode {
  const node = new SyntaxNode();
  for (let i = 0; depth > 0 && i < 4; i++) {
    const child = parse(depth - 1);
    child.parent = node;
    node.children.push(child);
  }
  return node;
}

function count(node: SyntaxNode): number {
  return node.children.reduce((sum, child) => sum + count(child), 1);
}

let previous: LifetimeRegion | undefined;
for (let build = 1; build <= 3; build++) {
  const region = new LifetimeRegion();
  const tree = region.run(() => parse(8));
  console.log(`build ${build}: ${count(tree)} nodes`);
  // 上一次构建的语法树已不再被引用。
  previous?.release();
  previous = region;
}

在 Node.js 下,run 调用其回调函数,而 release 不执行任何操作:

$ node rebuild.ts
build 1: 87381 nodes
build 2: 87381 nodes
build 3: 87381 nodes

scriptc 可执行文件输出相同的结果。设置 SCRIPTC_REGION_STATS=1 后,它还会报告它释放的每个 region:

$ scriptc build rebuild.ts -o rebuild >/dev/null
$ SCRIPTC_REGION_STATS=1 ./rebuild
build 1: 87381 nodes
build 2: 87381 nodes
scriptc region 1: freed 87381 objects, 87381 owned objects, 2 chunks (4.0 MiB); cycles 0.0 ms, walk 14.9 ms, free 2.6 ms
build 3: 87381 nodes
scriptc region 2: freed 87381 objects, 87381 owned objects, 2 chunks (4.0 MiB); cycles 0.0 ms, walk 8.9 ms, free 2.4 ms
scriptc regions: 3 created, 2 released, 2 freed, 0 pinned, 0 kept
scriptc region heap passes: 0

API(https://scriptc.dev/docs/experimental#api)

new LifetimeRegion() 创建一个 region。创建 region 时还会重试那些尚未能被释放的已释放 region。

region.run(fn) 以该 region 作为当前 region 调用 fn 并返回其结果。调用期间创建的 ProgramLifetime 实例属于该 region。调用可以嵌套:回调内部的 run 会将另一个 region 设为当前 region,直到它返回,无论回调如何结束——包括抛出异常时——之前的 region 都会重新成为当前 region。

region.release() 请求释放该 region。scriptc 会在 region 外部没有任何东西引用其任何实例时立即释放它,这可能是立即发生、在后续的 release 或 region 创建时发生,或者永远不发生。当某个 region 上的 run 调用处于活跃状态时,该 region 永远不会被释放。调用 release() 之后,run 仍然会调用其回调函数,但它创建的实例将属于默认 region。

在所有 run 调用之外创建的 ProgramLifetime 实例属于默认 region,该默认 region 永远不会被释放。

普通对象(没有实现该契约的类的实例、数组、Map、闭包)仍然按以往进行引用计数;region 会释放那些仅被其自身实例引用的普通对象。

一个 region 何时被释放(https://scriptc.dev/docs/experimental#when-a-region-is-freed)

当一个 region 被释放后,scriptc 会检查对其实例的每个引用是否都来自该 region 本身:来自其自身的实例,或者来自仅被该 region 引用的普通对象。然后它会同时释放这些实例和那些普通对象,并移除以被释放实例为键的 WeakMap 和 WeakSet 条目。

一个已释放的 region 在以下情况下会保持已分配状态,其实例仍然可用:

  • 一个仍在运行的函数中的变量、模块级变量或局部变量;
  • 被 region 外部某物引用的普通对象,包括数组、Map 或 Set 的元素以及闭包捕获的变量;
  • 另一个 region 的实例,在那个 region 被释放之前;
  • 默认 region 的实例,其永远不会被释放;
  • 持有某个实例的 promise、生成器或 Error,在其不再被引用之前。

在回调函数内部 await 之后恢复的工作在 region 之外运行,因此它创建的实例属于默认 region,并且它引用的实例会保持已分配状态。

线程(https://scriptc.dev/docs/experimental#threads)

每个线程拥有自己的 region 和自己的默认 region。一个 region 属于创建它的线程:在另一个线程上,run 会将该线程的默认 region 设为当前 region,而 release 不执行任何操作。持有通过 @scriptc/threads(https://scriptc.dev/docs/threads)发布的实例的 region 永远不会被释放,因为其他线程可能引用它。

开销(https://scriptc.dev/docs/experimental#cost)

从未构建 LifetimeRegion 的程序编译结果与不使用该包时完全相同。在有构建的程序中,属于某个 region 的 ProgramLifetime 实例的每次 retain 和 release 都会更新其所在 region chunk 的计数。默认 region 的实例不参与计数,并且在程序创建第一个 region 之前,retain 或 release 只是测试计数是否已开始。

释放一个 region 会遍历它的每个实例以及它们拥有的普通对象。一个可能因检查而被保留的 region,可能仅被引用到该 region 内部的不可达循环所引用,因此在保留它之前,scriptc 会在线程的堆上运行循环收集器并再次检查。

SCRIPTC_REGION_STATS=1 会为每次释放尝试打印一行,标记为 freed、kept(附带检查无法解释的引用数量)或 pinned(已发布),并在退出时打印每个线程的汇总以及在线程堆上进行的循环收集器遍历次数。

在多个 worker 线程(https://scriptc.dev/docs/threads)上运行相同工作的程序往往会在每个线程上构建相同的缓存:驻留类型、字符串、已解析的模块。InternTable 是一个具名的表,存储已发布(https://scriptc.dev/docs/threads)的值,scriptc 程序的所有线程共享它,因此每个值只创建一次,所有线程读取同一个对象。

import { Worker, isMainThread, parentPort } from "node:worker_threads";
import { InternTable } from "@scriptc/experimental";

class Shape {
  readonly name: string;
  readonly parts: readonly Shape[];
  constructor(name: string, parts: readonly Shape[]) {
    this.name = name;
    this.parts = parts;
  }
}

// 模块作用域:每个执行到这一行的线程都打开同一个表。
const shapes = new InternTable("example.shapes");

function shape(name: string, parts: readonly Shape[] = []): Shape {
  return shapes.getOrInsertComputed(name, () => new Shape(name, parts));
}

function build(): string {
  const point = shape("point");
  const line = shape("line", [point, point]);
  const square = shape("square", [line, line, line, line]);
  return `${square.name}: ${square.parts.length} lines, ${shapes.size} shapes`;
}

if (isMainThread) {
  for (let i = 0; i < 2; i++) {
    new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text));
  }
} else {
  parentPort!.postMessage(`worker: ${build()}`);
}

每个执行 new InternTable(...) 的线程都打开同一个表。在 Node.js 下每个线程有自己的表,因此每个 worker 各自构建自己的三个形状;而在 scriptc 可执行文件中,两个 worker 使用同一个表并读取相同的形状对象。两者输出相同:

$ node intern.ts
worker: square: 4 lines, 3 shapes
worker: square: 4 lines, 3 shapes

$ scriptc build intern.ts -o intern >/dev/null
$ ./intern
worker: square: 4 lines, 3 shapes
worker: square: 4 lines, 3 shapes

方法(https://scriptc.dev/docs/experimental#methods)

new InternTable(name) 打开名为 name 的表。在同一线程内,每次以相同名称构建都指向同一个表。在 scriptc 构建产物中,进程的每个线程共享该表;在 Node.js 下,每个线程各自拥有一个表。

键可以是字符串、数字、数字数组或字符串数组。字符串和数字的比较方式与 Map 的键相同;数组按其元素以同样方式比较,最多可包含 65536 个元素。

table.get(key)、table.has(key)、table.size 读取表。

table.getOrInsert(key, value) 返回存储在 key 下的值。如果没有,则发布 value,存储它并返回。

table.getOrInsertComputed(key, callback) 返回存储在 key 下的值。如果没有,则调用 callback(key),发布其结果并存储,除非在此期间已有值被存储在 key 下(由回调函数本身存储,或在 scriptc 构建产物中由另一个线程存储),此时返回已存储的值。一个键的值一旦存储就永不改变,并且不存在 set 或 delete。在两种运行时中,存储的值及其可达的一切都是不可变的。

如果 publish 拒绝某个值,该调用会抛出相同的 TypeError 且不存储任何内容。

scriptc 要求名称必须是字符串字面量,并且同一个名称的所有构建都必须使用相同的类型参数,因为所有线程共享该表的布局。值可以是数字、布尔值、字符串、大整数、数组、Map、Set、对象和类实例。

线程行为(https://scriptc.dev/docs/experimental#thread-behavior)

回调运行期间不持有任何锁。两个线程同时错过同一个键时,可能都调用回调函数;其中一个结果被存储,两次调用都返回它。

当满足以下条件时,程序在 scriptc 和 Node.js 下行为相同:

  1. 存储的值不依赖于哪个线程创建了它——例如回调函数从键和其他已存储的值计算得出该值。
  2. 它不跨线程比较身份或 id。
  3. 一个线程不依赖另一个线程的插入:size 和 has 在 scriptc 下反映所有线程的插入,在 Node.js 下只反映调用线程的插入。

线程后续为共享值计算的状态(惰性解析的成员、缓存)应放在线程本地状态中,例如以该值为键的模块级 Map,或者以该值携带的 id 为索引的数组:在两种运行时中模块状态都是按线程隔离的。在 scriptc 中,以稠密 id 为索引的数组是其中最快的。

内存(https://scriptc.dev/docs/experimental#memory)

查找不加锁也不写入任何内容。插入会对表的 64 个分段之一加锁,进行一次探测和几次存储。存储的值在 scriptc 构建产物中是不朽的:它们一直保持已分配状态直到进程退出,包括回调函数为某个键计算但该键已被另一个线程先存储的值。

从未构建 InternTable 的程序不会运行它的任何代码。

相似文章

@rauchg:我们可以做得比 Rust 更好

X AI KOLs Following

Malte Ubl(cramforce)在回应 @rauchg 的一篇帖子时,讨论了 scriptc,这是一款 TypeScript 原生编译器,可通过计算异常行为来生成更优化的代码,是让编译后的 TS 比 Rust 更快这一努力的一部分。

MemScope:通过局部代码修复演进智能体记忆架构

arXiv cs.AI

MemScope 是一个由中国科学院提出的框架,通过工具使用的 Patch Agent 检查原始执行轨迹、运行时内存状态和现有架构代码,实现对智能体记忆架构的证据驱动、局部化原位进化,在 GAIA、xBench 和 ALFWorld 上均取得显著提升。

Valen 的内存安全:一种新型借用检查

Hacker News Top

Valen 的新文章介绍了一种更灵活的借用检查器(group borrowing),编译器会记住引用指向的位置,以实现比 Rust 更简单灵活的内存安全方案,并已被 Google Carbon 等语言采纳。