@calcit/procs 0.11.4 → 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) {
@@ -199,24 +227,37 @@ export let _$n_record_$o_get_name = (x) => {
199
227
  throw new Error("Expected a record");
200
228
  }
201
229
  };
230
+ export let _$n_record_$o_struct = (x) => {
231
+ if (x instanceof CalcitRecord) {
232
+ return x.structRef ?? null;
233
+ }
234
+ else {
235
+ throw new Error("Expected a record");
236
+ }
237
+ };
202
238
  export let _$n_record_$o_from_map = (proto, data) => {
203
- if (!(proto instanceof CalcitRecord))
204
- 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
+ }
205
246
  if (data instanceof CalcitRecord) {
206
- if (fieldsEqual(proto.fields, data.fields)) {
207
- 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);
208
249
  }
209
250
  else {
210
251
  let values = [];
211
- for (let i = 0; i < proto.fields.length; i++) {
212
- let field = proto.fields[i];
252
+ for (let i = 0; i < recordProto.fields.length; i++) {
253
+ let field = recordProto.fields[i];
213
254
  let idx = findInFields(data.fields, field);
214
255
  if (idx < 0) {
215
256
  throw new Error(`Cannot find field ${field} among ${data.fields}`);
216
257
  }
217
258
  values.push(data.values[idx]);
218
259
  }
219
- return new CalcitRecord(proto.name, proto.fields, values);
260
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
220
261
  }
221
262
  }
222
263
  else if (data instanceof CalcitMap || data instanceof CalcitSliceMap) {
@@ -230,8 +271,8 @@ export let _$n_record_$o_from_map = (proto, data) => {
230
271
  // mutable sort
231
272
  pairs_buffer.sort((pair1, pair2) => pair1[0].cmp(pair2[0]));
232
273
  let values = [];
233
- outerLoop: for (let i = 0; i < proto.fields.length; i++) {
234
- let field = proto.fields[i];
274
+ outerLoop: for (let i = 0; i < recordProto.fields.length; i++) {
275
+ let field = recordProto.fields[i];
235
276
  for (let idx = 0; idx < pairs_buffer.length; idx++) {
236
277
  let pair = pairs_buffer[idx];
237
278
  if (pair[0] === field) {
@@ -241,7 +282,7 @@ export let _$n_record_$o_from_map = (proto, data) => {
241
282
  }
242
283
  throw new Error(`Cannot find field ${field} among ${pairs_buffer}`);
243
284
  }
244
- return new CalcitRecord(proto.name, proto.fields, values);
285
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
245
286
  }
246
287
  else {
247
288
  throw new Error("Expected record or data for making a record");
@@ -260,16 +301,25 @@ export let _$n_record_$o_to_map = (x) => {
260
301
  }
261
302
  };
262
303
  export let _$n_record_$o_matches_$q_ = (x, y) => {
263
- if (!(x instanceof CalcitRecord)) {
264
- throw new Error("Expected first argument to be record");
304
+ let targetStruct;
305
+ if (y instanceof CalcitRecord) {
306
+ targetStruct = y.structRef;
265
307
  }
266
- if (!(y instanceof CalcitRecord)) {
267
- throw new Error("Expected second argument to be record");
308
+ else if (y instanceof CalcitStruct) {
309
+ targetStruct = y;
268
310
  }
269
- if (x.name !== y.name) {
270
- 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");
271
322
  }
272
- return fieldsEqual(x.fields, y.fields);
273
323
  };
274
324
  export function _$n_record_$o_extend_as(obj, new_name, new_key, new_value) {
275
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.4",
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.4",
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,23 +237,36 @@ export let _$n_record_$o_get_name = (x: CalcitRecord): CalcitTag => {
206
237
  }
207
238
  };
208
239
 
240
+ export let _$n_record_$o_struct = (x: CalcitValue): CalcitValue => {
241
+ if (x instanceof CalcitRecord) {
242
+ return x.structRef ?? null;
243
+ } else {
244
+ throw new Error("Expected a record");
245
+ }
246
+ };
247
+
209
248
  export let _$n_record_$o_from_map = (proto: CalcitValue, data: CalcitValue): CalcitValue => {
210
- 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
+ }
211
255
 
212
256
  if (data instanceof CalcitRecord) {
213
- if (fieldsEqual(proto.fields, data.fields)) {
214
- 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);
215
259
  } else {
216
260
  let values: Array<CalcitValue> = [];
217
- for (let i = 0; i < proto.fields.length; i++) {
218
- let field = proto.fields[i];
261
+ for (let i = 0; i < recordProto.fields.length; i++) {
262
+ let field = recordProto.fields[i];
219
263
  let idx = findInFields(data.fields, field);
220
264
  if (idx < 0) {
221
265
  throw new Error(`Cannot find field ${field} among ${data.fields}`);
222
266
  }
223
267
  values.push(data.values[idx]);
224
268
  }
225
- return new CalcitRecord(proto.name, proto.fields, values);
269
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
226
270
  }
227
271
  } else if (data instanceof CalcitMap || data instanceof CalcitSliceMap) {
228
272
  let pairs_buffer: Array<[CalcitTag, CalcitValue]> = [];
@@ -236,8 +280,8 @@ export let _$n_record_$o_from_map = (proto: CalcitValue, data: CalcitValue): Cal
236
280
  pairs_buffer.sort((pair1, pair2) => pair1[0].cmp(pair2[0]));
237
281
 
238
282
  let values: Array<CalcitValue> = [];
239
- outerLoop: for (let i = 0; i < proto.fields.length; i++) {
240
- let field = proto.fields[i];
283
+ outerLoop: for (let i = 0; i < recordProto.fields.length; i++) {
284
+ let field = recordProto.fields[i];
241
285
  for (let idx = 0; idx < pairs_buffer.length; idx++) {
242
286
  let pair = pairs_buffer[idx];
243
287
  if (pair[0] === field) {
@@ -247,7 +291,7 @@ export let _$n_record_$o_from_map = (proto: CalcitValue, data: CalcitValue): Cal
247
291
  }
248
292
  throw new Error(`Cannot find field ${field} among ${pairs_buffer}`);
249
293
  }
250
- return new CalcitRecord(proto.name, proto.fields, values);
294
+ return new CalcitRecord(recordProto.name, recordProto.fields, values, recordProto.structRef);
251
295
  } else {
252
296
  throw new Error("Expected record or data for making a record");
253
297
  }
@@ -266,17 +310,23 @@ export let _$n_record_$o_to_map = (x: CalcitValue): CalcitValue => {
266
310
  };
267
311
 
268
312
  export let _$n_record_$o_matches_$q_ = (x: CalcitValue, y: CalcitValue): boolean => {
269
- if (!(x instanceof CalcitRecord)) {
270
- throw new Error("Expected first argument to be record");
271
- }
272
- if (!(y instanceof CalcitRecord)) {
273
- 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");
274
320
  }
275
321
 
276
- if (x.name !== y.name) {
277
- 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");
278
329
  }
279
- return fieldsEqual(x.fields, y.fields);
280
330
  };
281
331
 
282
332
  export function _$n_record_$o_extend_as(obj: CalcitValue, new_name: CalcitValue, new_key: CalcitValue, new_value: CalcitValue) {