值语义与共享语义

本文档定义 Dujie 在语言层面对“赋值、传参、返回、修改、共享”的基本语义。

本文是类型系统总纲的专题文档,服务于 UI DSL 的当前语义设计,并为未来可能出现的响应式能力保留边界。它约束的是用户可见语义,而不是 Rust 后端的具体实现方式。

问题背景

当前文档中,曾经使用“值类型 / 引用类型”来解释语言行为,并把 stringlist<T>map<K, V>opt<T>iter<T>widgetstruct 等都归到“引用类型”里。

这种做法有几个问题:

它把用户可见语义和 Rust 后端实现强绑定

它会制造隐藏别名,降低局部可推理性

它不利于 UI DSL 的状态建模

它会让未来响应式系统和普通数据模型混在一起

对于 Dujie 这样的 UI DSL,普通数据和值的行为应尽量稳定、直接、可局部推导。共享、订阅、依赖传播等能力如果未来需要引入,也应由独立机制显式承担,而不是通过“所有容器类型和结构体默认共享可变状态”间接得到。

设计目标

让赋值、传参、返回值语义对用户可预测

让普通数据建模不依赖隐藏别名

为未来可能的响应式能力保留边界,但不通过普通容器类型偷带共享语义

保留后端使用结构共享、写时复制、引用计数等优化的空间

核心结论

1. Dujie 不对用户暴露通用“引用类型”分类

语言层不再把普通类型分为“值类型”和“引用类型”两大公开类别,并据此要求用户理解别名传播。

2. 普通 Dujie 值默认采用值语义

除非未来某类类型被明确指定为特殊引用型或响应式类型,否则普通 Dujie 值在以下场景都采用值语义:

  • 变量赋值
  • 函数传参
  • 函数返回
  • 容器和结构体字段传播

也就是说,这些操作得到的是“逻辑上独立的值”。后续对其中一个值的修改,不应影响另一个值。

3. 存储共享可以作为实现优化,但不能成为用户可观察语义

后端可以使用以下技术降低拷贝成本:

  • 结构共享
  • 写时复制(copy-on-write)
  • 引用计数
  • 持久化数据结构

但这些都属于实现策略,不应改变用户在语言层观察到的结果。

4. 未来若引入响应式共享,也不由普通类型承担

如果未来 Dujie 需要引入响应式系统,那么:

  • 依赖收集
  • 更新传播
  • 显式共享状态
  • 订阅关系

都应通过独立的响应式原语表达,而不是让 listmapstruct 默认共享底层可变状态。

术语

值语义

值语义指:

  • 赋值、传参、返回时得到逻辑上独立的值
  • 之后对其中一个值的修改,不影响另一个值

共享实现

共享实现指:

  • 编译器或运行时内部可以复用底层存储
  • 但用户不能通过普通语言操作观察到“隐藏别名”

可变绑定

var 表示该绑定可被更新。

这里的“更新”包括:

  • 重新赋值
  • 通过该绑定修改其当前值的可变部分,例如 list/map/struct

let 表示该绑定不可更新。

容器类型与结构体

本文中的“容器类型”专指:

  • list<T>
  • map<K, V>
  • opt<T>
  • iter<T>

struct 不归入容器类型。它是名义类型,但在值语义讨论中同样属于“可包含其他值的聚合数据”。

按类型分类的语义规则

1. 基础类型

intfloatboolrunestring 都按普通值处理。

其中:

  • string 是不可变值
  • rune 表示单个 Unicode 码点
  • 基础类型之间不存在“共享修改”的语言语义

string 不再被视为用户层面的“引用类型”。

2. 容器类型与结构体

list<T>map<K, V>struct 在语言层都视为普通数据值。

它们可以有更新操作,但这些更新只作用于当前值本身,不通过赋值关系隐式传播到其他变量。

例子:list

var a = [1, 2, 3];
var b = a;

b[0] = 10;

// a == [1, 2, 3]
// b == [10, 2, 3]

例子:map

var a = {"x": 1};
var b = a;

b["x"] = 2;

// a == {"x": 1}
// b == {"x": 2}

例子:struct

var a = Pair { key: "x", value: [1, 2, 3] };
var b = a;

b.value[0] = 10;

// a.value == [1, 2, 3]
// b.value == [10, 2, 3]

3. opt<T>

opt<T> 是值包装类型。

它只表达“有值 / 无值”,不额外引入共享语义。

其值语义由 T 的普通值语义继承而来。

4. widget

widget 表示 UI 组件值。

它在语言层应视为不可变值,而不是可共享可变对象。

原因:

  • UI 组件树更适合值式描述
  • 这更利于后续做 diff、重建和响应式更新
  • 这避免把“组件节点句柄”暴露成可变引用对象

5. iter<T>

iter<T> 不应再被归入“引用类型”讨论。

它更接近一种瞬时遍历对象,而不是持久数据容器。

当前只先约定:

  • iter<T> 不承载普通共享修改语义
  • 它不是用来表达共享可变状态的类型

关于“是否可重复消费、是否单次遍历、赋值后如何观察其状态”等问题,后续另开文档处理。

6. any

any 不改变值语义与共享语义的基本判断。

它只擦除静态类型信息,不自动把普通值变成共享引用对象。

函数传参与返回值

普通函数参数和返回值也采用值语义。

例子:函数内部修改不应外溢

func append_one(xs: list<int>) -> list<int> {
    var out = xs;
    out.push(1);
    return out;
}

var a = [1, 2];
var b = append_one(a);

// a == [1, 2]
// b == [1, 2, 1]

这意味着:

  • 普通参数不是隐式引用参数
  • 如果想表达“调用后外部状态被更新”,未来应使用显式机制,而不是依赖别名传播

对未来响应式系统的约束

本设计对未来响应式系统有一个明确要求:

普通数据值和响应式状态必须区分。

也就是说,未来响应式系统不应通过下面这种方式构建:

  • 把 list/map/struct 默认做成共享可变引用
  • 再依赖别名变化触发更新

更合理的方向是:

  • 普通值保持值语义
  • 响应式能力由专门的状态类型或响应式原语承担

这样可以避免“我只是传了一个值,为什么另一个地方的 UI 也被隐式改掉”这类问题。

允许的实现策略

本设计允许编译器或运行时采用多种内部策略实现值语义,包括:

  • 小对象直接拷贝
  • 聚合对象的结构共享
  • 写时复制
  • 引用计数包装
  • 针对 widget 的持久化树结构

但这些策略只能影响性能,不能改变语言层可见行为。

明确拒绝的设计

当前明确拒绝以下语言级设计:

1. 普通聚合类型默认共享可变状态

也就是拒绝把 listmapstruct 的普通赋值定义为“只复制引用”。

2. 用 Rust 后端表示直接反推语言语义

例如:

  • 因为后端用了 Arc<T>,所以语言里就认为它是引用类型
  • 因为后端用了 Vec<T>,所以语言里就必须暴露某种 Rust 风格移动规则

这些都不应成立。

3. 用普通值模型代替未来响应式模型

响应式系统需要显式设计,不应伪装成普通值赋值语义的一部分。

对现有文档的影响

00.guide/03.data-types

后续应重写其中“值类型 / 引用类型”的用户说明,不再把大量类型直接定义为共享引用。

01.design/06.struct-design

该文档目前更适合被视为“某种后端实现探索”,而不是结构体的最终语言语义定义。

widget 与未来响应式设计

后续讨论 widget 和响应式系统时,应默认以“组件值”而不是“组件引用对象”为前提。

未决问题

以下问题后续继续讨论:

iter<T> 的消费语义

list/map/struct 的更新操作是否都要求 var

结构体字段更新的完整规则

未来响应式原语的名称和基本模型