@calcit/procs 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,91 @@
1
+ # 2026-0223-0834 — 迁移到 defstruct 方案并实现 `%{}?` 可选字段 Record 宏
2
+
3
+ ## 背景
4
+
5
+ 将 Calcit Record 系统从旧的 `new-record`/`defrecord`/`defrecord!` 方案全面迁移到基于 `defstruct` 的新方案,同时新增 `%{}?` / `&%{}?` 支持可选字段初始化与更新。
6
+
7
+ ---
8
+
9
+ ## 知识点一:`%{}?` 与 `&%{}?` 的语义设计
10
+
11
+ `struct` 初始化为 record 时,部分字段在语义上是可选的,对应 TypeScript 中的 `{ a?: number; b?: number }`。原有的 `%{}` / `&%{}` 要求所有字段都必须显式传入。
12
+
13
+ ### `%{}?`(macro)
14
+
15
+ - 初始化 record 时允许省略字段,省略的字段自动填 `nil`。
16
+ - 用法:`%{}? MyRecord (:x 1)`
17
+
18
+ ### `&%{}?`(proc)
19
+
20
+ `call_record_partial` 的语义:
21
+
22
+ - **proto 是 Struct**:以全 `nil` 为基础 `values`,用传入的 k-v 覆盖对应位置。
23
+ - 传入未知字段 → 报错;传入重复字段 → 报错;`(args_size - 1) % 2 != 0` → 报错。
24
+
25
+ 关键实现细节:`CalcitStruct.fields` 元素类型是 `EdnTag`,比较时用 `f.ref_str()` 而非 `f.as_ref()`(后者无 `AsRef` impl)。
26
+
27
+ ### `%{}?` 宏定义
28
+
29
+ ```cirru
30
+ defmacro %{}? (R & xs)
31
+ if
32
+ not $ and (list? xs) (every? xs list?)
33
+ raise $ str-spaced "|%{}? expects field entries in list, got:" xs
34
+ &let
35
+ args $ &list:concat & xs
36
+ quasiquote $ &%{}? ~R ~@args
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 知识点二:Rust proc 注册流程
42
+
43
+ 新增一个内置 proc 需要同时修改四处:
44
+
45
+ | 文件 | 修改内容 |
46
+ | -------------------------- | -------------------------------------------------------- |
47
+ | `src/calcit/proc_name.rs` | 添加枚举变体 `NativeRecordPartial` + `ProcTypeSignature` |
48
+ | `src/builtins/records.rs` | 实现 `call_record_partial` 函数 |
49
+ | `src/builtins.rs` | 在 `match proc` 中添加分发分支 |
50
+ | `src/runner/preprocess.rs` | 将新 proc 加入"跳过 arity 检查"的 `matches!` 列表 |
51
+
52
+ ---
53
+
54
+ ## 知识点三:`%{}` 严格要求 Struct proto
55
+
56
+ - `call_record`(`%{}`):若 proto 为 `Calcit::Record`,直接返回错误提示改用 `defstruct`。
57
+ - `defstruct` 产生 `Calcit::Struct`(含 `field_types`),并作为 `%{}` / `%{}?` 的唯一原型来源。
58
+
59
+ ### `&record:get-name` / `&record:struct` 的参数约束
60
+
61
+ - 两者统一要求传入 `record`,避免 `struct` / `record` 混用语义。
62
+
63
+ ### `&record:matches?` 的参数约束
64
+
65
+ - 第一参数要求 `record`。
66
+ - 第二参数接受 `record` 或 `struct`(用于 `record-match` 的模式匹配场景)。
67
+
68
+ ---
69
+
70
+ ## 修改文件
71
+
72
+ | 文件 | 变更内容 |
73
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
74
+ | `src/calcit/proc_name.rs` | 新增 `NativeRecordPartial` 枚举变体与类型签名;移除 `NewRecord` 变体 |
75
+ | `src/builtins/records.rs` | 新增 `call_record_partial`(struct-only);`matches` 调整为 `(record, record/struct)`;移除 `new_record` 函数 |
76
+ | `src/builtins.rs` | 分发 `NativeRecordPartial`;移除 `NewRecord` 分发 |
77
+ | `src/runner/preprocess.rs` | arity 检查豁免新增 `NativeRecordPartial` |
78
+ | `src/cirru/calcit-core.cirru` | 新增 `%{}?` 宏定义与 `&%{}?` 文档条目;`defrecord`/`defrecord!` 改为 raise error;删除 `new-record` 定义 |
79
+ | `calcit/test-record.cirru` | Cat/BirdShape/Person/City/A/B/C/Demo 全部改为 `defstruct`;删除所有 `new-record` let 绑定;修复各测试函数体 |
80
+ | `docs/CalcitAgent.md` | 类型标注示例中 `new-record` 改为 `defstruct` |
81
+
82
+ ---
83
+
84
+ ## 验证
85
+
86
+ ```bash
87
+ cargo run --bin cr -- calcit/test-record.cirru -1
88
+ cargo clippy -- -D warnings
89
+ ```
90
+
91
+ 全部通过,无 warning。
package/lib/js-record.mjs CHANGED
@@ -169,6 +169,34 @@ export let _$n__PCT__$M_ = (proto, ...xs) => {
169
169
  return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
170
170
  }
171
171
  };
172
+ export let _$n__PCT__$M__$q_ = (proto, ...xs) => {
173
+ let recordProto;
174
+ let values;
175
+ if (proto instanceof CalcitStruct) {
176
+ recordProto = new CalcitRecord(proto.name, proto.fields, new Array(proto.fields.length).fill(null), proto);
177
+ values = recordProto.values.slice();
178
+ }
179
+ else {
180
+ throw new Error("Expected prototype to be a struct");
181
+ }
182
+ if (xs.length % 2 !== 0) {
183
+ throw new Error("Expected even number of key/value");
184
+ }
185
+ let touched = new Set();
186
+ for (let i = 0; i < xs.length; i += 2) {
187
+ let k = castTag(xs[i]);
188
+ let idx = findInFields(recordProto.fields, k);
189
+ if (idx < 0) {
190
+ throw new Error(`Cannot find field ${k} among ${recordProto.fields}`);
191
+ }
192
+ if (touched.has(idx)) {
193
+ throw new Error(`record field already has value, probably duplicated key: ${k}`);
194
+ }
195
+ touched.add(idx);
196
+ values[idx] = xs[i + 1];
197
+ }
198
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
199
+ };
172
200
  /// update record with new values
173
201
  export let _$n_record_$o_with = (proto, ...xs) => {
174
202
  if (proto instanceof CalcitRecord) {
@@ -208,23 +236,28 @@ export let _$n_record_$o_struct = (x) => {
208
236
  }
209
237
  };
210
238
  export let _$n_record_$o_from_map = (proto, data) => {
211
- if (!(proto instanceof CalcitRecord))
212
- throw new Error("Expected prototype to be record");
239
+ let recordProto;
240
+ if (proto instanceof CalcitStruct) {
241
+ recordProto = new CalcitRecord(proto.name, proto.fields, new Array(proto.fields.length).fill(null), proto);
242
+ }
243
+ else {
244
+ throw new Error("Expected prototype to be struct");
245
+ }
213
246
  if (data instanceof CalcitRecord) {
214
- if (fieldsEqual(proto.fields, data.fields)) {
215
- return new CalcitRecord(proto.name, proto.fields, data.values);
247
+ if (fieldsEqual(recordProto.fields, data.fields)) {
248
+ return new CalcitRecord(recordProto.name, recordProto.fields, data.values, recordProto.structRef);
216
249
  }
217
250
  else {
218
251
  let values = [];
219
- for (let i = 0; i < proto.fields.length; i++) {
220
- let field = proto.fields[i];
252
+ for (let i = 0; i < recordProto.fields.length; i++) {
253
+ let field = recordProto.fields[i];
221
254
  let idx = findInFields(data.fields, field);
222
255
  if (idx < 0) {
223
256
  throw new Error(`Cannot find field ${field} among ${data.fields}`);
224
257
  }
225
258
  values.push(data.values[idx]);
226
259
  }
227
- return new CalcitRecord(proto.name, proto.fields, values);
260
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
228
261
  }
229
262
  }
230
263
  else if (data instanceof CalcitMap || data instanceof CalcitSliceMap) {
@@ -238,8 +271,8 @@ export let _$n_record_$o_from_map = (proto, data) => {
238
271
  // mutable sort
239
272
  pairs_buffer.sort((pair1, pair2) => pair1[0].cmp(pair2[0]));
240
273
  let values = [];
241
- outerLoop: for (let i = 0; i < proto.fields.length; i++) {
242
- let field = proto.fields[i];
274
+ outerLoop: for (let i = 0; i < recordProto.fields.length; i++) {
275
+ let field = recordProto.fields[i];
243
276
  for (let idx = 0; idx < pairs_buffer.length; idx++) {
244
277
  let pair = pairs_buffer[idx];
245
278
  if (pair[0] === field) {
@@ -249,7 +282,7 @@ export let _$n_record_$o_from_map = (proto, data) => {
249
282
  }
250
283
  throw new Error(`Cannot find field ${field} among ${pairs_buffer}`);
251
284
  }
252
- return new CalcitRecord(proto.name, proto.fields, values);
285
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
253
286
  }
254
287
  else {
255
288
  throw new Error("Expected record or data for making a record");
@@ -268,16 +301,25 @@ export let _$n_record_$o_to_map = (x) => {
268
301
  }
269
302
  };
270
303
  export let _$n_record_$o_matches_$q_ = (x, y) => {
271
- if (!(x instanceof CalcitRecord)) {
272
- throw new Error("Expected first argument to be record");
304
+ let targetStruct;
305
+ if (y instanceof CalcitRecord) {
306
+ targetStruct = y.structRef;
273
307
  }
274
- if (!(y instanceof CalcitRecord)) {
275
- throw new Error("Expected second argument to be record");
308
+ else if (y instanceof CalcitStruct) {
309
+ targetStruct = y;
276
310
  }
277
- if (x.name !== y.name) {
278
- return false;
311
+ else {
312
+ throw new Error("Expected second argument to be record or struct");
313
+ }
314
+ if (x instanceof CalcitRecord) {
315
+ if (x.name !== targetStruct.name) {
316
+ return false;
317
+ }
318
+ return fieldsEqual(x.fields, targetStruct.fields);
319
+ }
320
+ else {
321
+ throw new Error("Expected first argument to be record");
279
322
  }
280
- return fieldsEqual(x.fields, y.fields);
281
323
  };
282
324
  export function _$n_record_$o_extend_as(obj, new_name, new_key, new_value) {
283
325
  if (arguments.length !== 4)
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.0.9",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.0.9",
@@ -176,6 +176,37 @@ export let _$n__PCT__$M_ = (proto: CalcitValue, ...xs: Array<CalcitValue>): Calc
176
176
  }
177
177
  };
178
178
 
179
+ export let _$n__PCT__$M__$q_ = (proto: CalcitValue, ...xs: Array<CalcitValue>): CalcitValue => {
180
+ let recordProto: CalcitRecord;
181
+ let values: Array<CalcitValue>;
182
+ if (proto instanceof CalcitStruct) {
183
+ recordProto = new CalcitRecord(proto.name, proto.fields, new Array(proto.fields.length).fill(null), proto);
184
+ values = recordProto.values.slice();
185
+ } else {
186
+ throw new Error("Expected prototype to be a struct");
187
+ }
188
+
189
+ if (xs.length % 2 !== 0) {
190
+ throw new Error("Expected even number of key/value");
191
+ }
192
+
193
+ let touched = new Set<number>();
194
+ for (let i = 0; i < xs.length; i += 2) {
195
+ let k = castTag(xs[i]);
196
+ let idx = findInFields(recordProto.fields, k);
197
+ if (idx < 0) {
198
+ throw new Error(`Cannot find field ${k} among ${recordProto.fields}`);
199
+ }
200
+ if (touched.has(idx)) {
201
+ throw new Error(`record field already has value, probably duplicated key: ${k}`);
202
+ }
203
+ touched.add(idx);
204
+ values[idx] = xs[i + 1];
205
+ }
206
+
207
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
208
+ };
209
+
179
210
  /// update record with new values
180
211
  export let _$n_record_$o_with = (proto: CalcitValue, ...xs: Array<CalcitValue>): CalcitValue => {
181
212
  if (proto instanceof CalcitRecord) {
@@ -198,7 +229,7 @@ export let _$n_record_$o_with = (proto: CalcitValue, ...xs: Array<CalcitValue>):
198
229
  }
199
230
  };
200
231
 
201
- export let _$n_record_$o_get_name = (x: CalcitRecord): CalcitTag => {
232
+ export let _$n_record_$o_get_name = (x: CalcitValue): CalcitTag => {
202
233
  if (x instanceof CalcitRecord) {
203
234
  return x.name;
204
235
  } else {
@@ -206,7 +237,7 @@ export let _$n_record_$o_get_name = (x: CalcitRecord): CalcitTag => {
206
237
  }
207
238
  };
208
239
 
209
- export let _$n_record_$o_struct = (x: CalcitRecord): CalcitValue => {
240
+ export let _$n_record_$o_struct = (x: CalcitValue): CalcitValue => {
210
241
  if (x instanceof CalcitRecord) {
211
242
  return x.structRef ?? null;
212
243
  } else {
@@ -215,22 +246,27 @@ export let _$n_record_$o_struct = (x: CalcitRecord): CalcitValue => {
215
246
  };
216
247
 
217
248
  export let _$n_record_$o_from_map = (proto: CalcitValue, data: CalcitValue): CalcitValue => {
218
- if (!(proto instanceof CalcitRecord)) throw new Error("Expected prototype to be record");
249
+ let recordProto: CalcitRecord;
250
+ if (proto instanceof CalcitStruct) {
251
+ recordProto = new CalcitRecord(proto.name, proto.fields, new Array(proto.fields.length).fill(null), proto);
252
+ } else {
253
+ throw new Error("Expected prototype to be struct");
254
+ }
219
255
 
220
256
  if (data instanceof CalcitRecord) {
221
- if (fieldsEqual(proto.fields, data.fields)) {
222
- return new CalcitRecord(proto.name, proto.fields, data.values);
257
+ if (fieldsEqual(recordProto.fields, data.fields)) {
258
+ return new CalcitRecord(recordProto.name, recordProto.fields, data.values, recordProto.structRef);
223
259
  } else {
224
260
  let values: Array<CalcitValue> = [];
225
- for (let i = 0; i < proto.fields.length; i++) {
226
- let field = proto.fields[i];
261
+ for (let i = 0; i < recordProto.fields.length; i++) {
262
+ let field = recordProto.fields[i];
227
263
  let idx = findInFields(data.fields, field);
228
264
  if (idx < 0) {
229
265
  throw new Error(`Cannot find field ${field} among ${data.fields}`);
230
266
  }
231
267
  values.push(data.values[idx]);
232
268
  }
233
- return new CalcitRecord(proto.name, proto.fields, values);
269
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
234
270
  }
235
271
  } else if (data instanceof CalcitMap || data instanceof CalcitSliceMap) {
236
272
  let pairs_buffer: Array<[CalcitTag, CalcitValue]> = [];
@@ -244,8 +280,8 @@ export let _$n_record_$o_from_map = (proto: CalcitValue, data: CalcitValue): Cal
244
280
  pairs_buffer.sort((pair1, pair2) => pair1[0].cmp(pair2[0]));
245
281
 
246
282
  let values: Array<CalcitValue> = [];
247
- outerLoop: for (let i = 0; i < proto.fields.length; i++) {
248
- let field = proto.fields[i];
283
+ outerLoop: for (let i = 0; i < recordProto.fields.length; i++) {
284
+ let field = recordProto.fields[i];
249
285
  for (let idx = 0; idx < pairs_buffer.length; idx++) {
250
286
  let pair = pairs_buffer[idx];
251
287
  if (pair[0] === field) {
@@ -255,7 +291,7 @@ export let _$n_record_$o_from_map = (proto: CalcitValue, data: CalcitValue): Cal
255
291
  }
256
292
  throw new Error(`Cannot find field ${field} among ${pairs_buffer}`);
257
293
  }
258
- return new CalcitRecord(proto.name, proto.fields, values);
294
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
259
295
  } else {
260
296
  throw new Error("Expected record or data for making a record");
261
297
  }
@@ -274,17 +310,23 @@ export let _$n_record_$o_to_map = (x: CalcitValue): CalcitValue => {
274
310
  };
275
311
 
276
312
  export let _$n_record_$o_matches_$q_ = (x: CalcitValue, y: CalcitValue): boolean => {
277
- if (!(x instanceof CalcitRecord)) {
278
- throw new Error("Expected first argument to be record");
279
- }
280
- if (!(y instanceof CalcitRecord)) {
281
- throw new Error("Expected second argument to be record");
313
+ let targetStruct: CalcitStruct;
314
+ if (y instanceof CalcitRecord) {
315
+ targetStruct = y.structRef;
316
+ } else if (y instanceof CalcitStruct) {
317
+ targetStruct = y;
318
+ } else {
319
+ throw new Error("Expected second argument to be record or struct");
282
320
  }
283
321
 
284
- if (x.name !== y.name) {
285
- return false;
322
+ if (x instanceof CalcitRecord) {
323
+ if (x.name !== targetStruct.name) {
324
+ return false;
325
+ }
326
+ return fieldsEqual(x.fields, targetStruct.fields);
327
+ } else {
328
+ throw new Error("Expected first argument to be record");
286
329
  }
287
- return fieldsEqual(x.fields, y.fields);
288
330
  };
289
331
 
290
332
  export function _$n_record_$o_extend_as(obj: CalcitValue, new_name: CalcitValue, new_key: CalcitValue, new_value: CalcitValue) {