@typesol/cli 0.21.0 → 0.23.0

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.
@@ -202,6 +202,49 @@ export function helperSignature(h) {
202
202
  params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : h.returns.kind === "string" ? `TS_Str *ret` : `${scalarC(h.returns)} *ret`);
203
203
  return `static uint64_t fn_${h.name}(${params.join(", ")})`;
204
204
  }
205
+ /** C type of a growable Vec stored after the first one of struct `s`: `{ uint32_t len; T items[]; }`. */
206
+ export function laterVecC(s, field) {
207
+ return `TSW_${s}__${field}`;
208
+ }
209
+ /** The account size of struct `s` with every growable Vec empty, as C (8-byte discriminator not included). */
210
+ export function emptySizeC(s) {
211
+ const n = s.vecs?.length ?? 0;
212
+ return n > 1 ? `(sizeof(TSS_${s.name}) + ${4 * (n - 1)})` : `sizeof(TSS_${s.name})`;
213
+ }
214
+ /**
215
+ * Where growable Vec `k` of a struct at `base` (a C lvalue of type TSS_<S>) starts, as C: its length and elements.
216
+ * The first is a member of the struct; each further one starts right after the previous one's elements.
217
+ */
218
+ export function vecPlace(s, base, k) {
219
+ const vs = s.vecs;
220
+ const first = vs[0];
221
+ let end = `((uint8_t *)${base}.${first.name} + (uint64_t)${base}.${first.name}__len * ${first.elemSize})`;
222
+ let place = { len: `${base}.${first.name}__len`, items: `${base}.${first.name}`, end };
223
+ for (let j = 1; j <= k; j++) {
224
+ const v = vs[j];
225
+ const h = `((${laterVecC(s.name, v.name)} *)${end})`;
226
+ end = `((uint8_t *)${h}->items + (uint64_t)${h}->len * ${v.elemSize})`;
227
+ place = { len: `${h}->len`, items: `${h}->items`, end };
228
+ }
229
+ return place;
230
+ }
231
+ /**
232
+ * C lines checking that an account's size matches its growable Vecs' lengths (`d`: its TSS_<S> pointer, `a`: its
233
+ * TsAccount). One Vec: ts_check_vec_len; more: each length read only once the bytes before it are known to exist.
234
+ */
235
+ export function vecLenCheck(s, a, d, pad = " ") {
236
+ const vs = s.vecs ?? [];
237
+ if (vs.length === 1)
238
+ return [`${pad}TS_TRY(ts_check_vec_len(${a}, sizeof(TSS_${s.name}), ${d}->${vs[0].name}__len, TS_ELEM_${s.name}));`];
239
+ const o = `${d.replace(/\W/g, "")}__vo`;
240
+ const lines = [`${pad}{`, `${pad} uint64_t ${o} = sizeof(TSS_${s.name}) + (uint64_t)${d}->${vs[0].name}__len * ${vs[0].elemSize};`];
241
+ for (const v of vs.slice(1)) {
242
+ lines.push(`${pad} if (${a}->data_len < 8 + ${o} + 4) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account size does not match its Vec lengths");`);
243
+ lines.push(`${pad} ${o} += 4 + (uint64_t)((${laterVecC(s.name, v.name)} *)(${a}->data + 8 + ${o}))->len * ${v.elemSize};`);
244
+ }
245
+ lines.push(`${pad} if (${a}->data_len != 8 + ${o}) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account size does not match its Vec lengths");`, `${pad}}`);
246
+ return lines;
247
+ }
205
248
  /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
206
249
  export function fixedVecC(t) {
207
250
  return `TSF_${fixedVecName(t)}`;
@@ -1,4 +1,5 @@
1
1
  import ts from "typescript";
2
+ import { type FieldType } from "../model.js";
2
3
  import { type Value, type Closure } from "./shared.js";
3
4
  import { CpiEmitter } from "./cpi.js";
4
5
  /**
@@ -17,6 +18,40 @@ export declare abstract class StatementEmitter extends CpiEmitter {
17
18
  * As in JS, the length is read again before every step, so a body that pushes or pops sees the new length.
18
19
  */
19
20
  protected forOf(stmt: ts.ForOfStatement): void;
21
+ /** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
22
+ protected isPipeline(e: ts.Expression): e is ts.CallExpression;
23
+ /** The list under a chain of map / filter calls, and the calls in the order they run. */
24
+ protected pipelineStages(e: ts.Expression): {
25
+ base: ts.Expression;
26
+ stages: {
27
+ kind: "map" | "filter";
28
+ fn: ts.ArrowFunction | ts.FunctionExpression;
29
+ }[];
30
+ };
31
+ /**
32
+ * Run `body` for each element of a list, or of a chain of `map` / `filter` calls on one (`xs.filter(f).map(g)`):
33
+ * one index loop over the list (its length read every step, as `for … of`), each callback expanded in it. A
34
+ * filter skips to the next element, a map binds its result, so a chain builds no list. `body` gets the element (as
35
+ * `list.get(i)` gives it, structs by reference, or the mapped value) and its index in the chain's output (after a
36
+ * filter, the count of elements it let through, as JS gives callbacks).
37
+ */
38
+ protected eachElement(list: ts.Expression, at: ts.Node, names: {
39
+ index: string;
40
+ ci: string;
41
+ next?: string;
42
+ alwaysNext?: boolean;
43
+ }, body: (element: ts.Expression, index: ts.Expression) => void): void;
44
+ /**
45
+ * `const ys = xs.map(…)` / `xs.filter(…)`: the chain's elements in a local FixedVec, as big as the list it starts
46
+ * from (a FixedArray's length, a FixedVec's capacity) or as annotated (`const ys: FixedVec<u64, 16> = …`, needed
47
+ * when it starts from a Vec). It lives on the stack, so it holds at most 2 KiB; one more element than it holds aborts.
48
+ */
49
+ protected localList(d: ts.VariableDeclaration, init: ts.CallExpression, c: string): FieldType & {
50
+ kind: "vec";
51
+ cap: number;
52
+ };
53
+ /** The element type of a Vec, FixedArray, FixedVec or Vec argument. */
54
+ protected listElem(list: ts.Expression): FieldType;
20
55
  /** Whether `e` is the remaining accounts (or a const bound to them). */
21
56
  protected isRemaining(e: ts.Expression): boolean;
22
57
  /**
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
- import { MAX_RETURN_DATA, resolveValueType, structNameOf } from "../analyze.js";
2
+ import { MAX_RETURN_DATA, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
3
3
  import { INT_INFO, isWritable, optionKey, scalarSize, typeName } from "../model.js";
4
- import { U64, BOOL, cString, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
4
+ import { U64, BOOL, cString, fixedVecC, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
5
5
  import { CpiEmitter } from "./cpi.js";
6
6
  /**
7
7
  * Statements: variables, assignment, control flow and return.
@@ -259,37 +259,196 @@ export class StatementEmitter extends CpiEmitter {
259
259
  n.parent = stmt;
260
260
  return n;
261
261
  };
262
- const indexId = place(f.createIdentifier(index));
263
- const length = place(f.createPropertyAccessExpression(stmt.expression, "length"));
264
- const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(stmt.expression, "get")), undefined, [indexId]));
265
- // The element's declaration: the source's own name (so aliasing checks find its uses) bound to list.get(i).
266
- const decl = f.createVariableDeclaration(d.name, undefined, undefined, get);
267
- ts.setTextRange(decl, d);
268
- decl.parent = list;
269
- const declList = { ...list, flags: ts.NodeFlags.Const, declarations: [decl] };
262
+ void place;
270
263
  const ci = this.tmp("of_i");
271
264
  const label = this.tmp("ts_continue_");
265
+ this.eachElement(stmt.expression, stmt, { index, ci, next: label, alwaysNext: true }, (element) => {
266
+ // The element's declaration: the source's own name (so aliasing checks find its uses) bound to the element.
267
+ const decl = f.createVariableDeclaration(d.name, undefined, undefined, element);
268
+ ts.setTextRange(decl, d);
269
+ decl.parent = list;
270
+ const declList = { ...list, flags: ts.NodeFlags.Const, declarations: [decl] };
271
+ this.variable(declList);
272
+ this.loops.push(label);
273
+ this.line(`{`);
274
+ this.block(() => this.body(stmt.statement));
275
+ this.line(`}`);
276
+ this.loops.pop();
277
+ });
278
+ }
279
+ /** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
280
+ isPipeline(e) {
281
+ return ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && (e.expression.name.text === "map" || e.expression.name.text === "filter");
282
+ }
283
+ /** The list under a chain of map / filter calls, and the calls in the order they run. */
284
+ pipelineStages(e) {
285
+ const stages = [];
286
+ let base = e;
287
+ while (this.isPipeline(base)) {
288
+ const access = base.expression;
289
+ const kind = access.name.text;
290
+ if (base.arguments.length !== 1)
291
+ this.fail(base, `${kind}() takes one argument: the callback`);
292
+ const fn = base.arguments[0];
293
+ if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))
294
+ this.fail(fn, `pass ${kind} an arrow function written here: xs.${kind}((x) => …)`);
295
+ if (fn.parameters.length > 2)
296
+ this.fail(fn.parameters[2], `${kind}'s callback takes the element and its index`);
297
+ stages.unshift({ kind, fn });
298
+ base = access.expression;
299
+ }
300
+ if (stages.length) {
301
+ const l = ts.isIdentifier(base) ? this.lookup(base.text) : undefined;
302
+ if (!(l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(base)))
303
+ this.fail(base, "map and filter work on a Vec, FixedArray or FixedVec");
304
+ }
305
+ return { base, stages };
306
+ }
307
+ /**
308
+ * Run `body` for each element of a list, or of a chain of `map` / `filter` calls on one (`xs.filter(f).map(g)`):
309
+ * one index loop over the list (its length read every step, as `for … of`), each callback expanded in it. A
310
+ * filter skips to the next element, a map binds its result, so a chain builds no list. `body` gets the element (as
311
+ * `list.get(i)` gives it, structs by reference, or the mapped value) and its index in the chain's output (after a
312
+ * filter, the count of elements it let through, as JS gives callbacks).
313
+ */
314
+ eachElement(list, at, names, body) {
315
+ const f = ts.factory;
316
+ const place = (n) => {
317
+ ts.setTextRange(n, at);
318
+ n.parent = at;
319
+ return n;
320
+ };
321
+ const { base, stages } = this.pipelineStages(list);
322
+ const { index, ci } = names;
323
+ const indexId = place(f.createIdentifier(index));
324
+ const length = place(f.createPropertyAccessExpression(base, "length"));
325
+ const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(base, "get")), undefined, [indexId]));
326
+ const next = names.next ?? (stages.some((s) => s.kind === "filter") ? this.tmp("ts_next_") : undefined);
272
327
  this.line(`{`);
273
328
  this.block(() => {
274
329
  this.line(`uint64_t ${ci} = 0;`);
275
330
  this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
331
+ const counters = stages
332
+ .filter((st) => st.kind === "filter")
333
+ .map(() => {
334
+ const k = this.tmp("lm_k");
335
+ this.line(`uint64_t ${k} = 0;`);
336
+ return k;
337
+ });
276
338
  this.line(`while (1) {`);
277
339
  this.block(() => {
278
- const n = this.expect(this.expr(length, U64), U64, stmt.expression);
340
+ const n = this.expect(this.expr(length, U64), U64, base);
279
341
  this.line(`if (!(${ci} < ${n.c})) break;`);
280
- this.variable(declList);
281
- this.loops.push(label);
282
- this.line(`{`);
283
- this.block(() => this.body(stmt.statement));
284
- this.line(`}`);
285
- this.loops.pop();
286
- this.line(`${label}:;`);
342
+ let element = get;
343
+ let idx = indexId;
344
+ let k = 0;
345
+ for (const st of stages) {
346
+ const closure = { node: st.fn, name: `${st.kind}'s callback`, scopes: [...this.scopes] };
347
+ const v = this.expandClosure(closure, [element, idx].slice(0, st.fn.parameters.length), st.fn);
348
+ if (st.kind === "filter") {
349
+ if (!v)
350
+ this.fail(st.fn, "filter's callback returns a boolean");
351
+ const b = this.expect(v, BOOL, st.fn);
352
+ this.line(`if (!(${b.c})) goto ${next};`);
353
+ const id = `__lk${this.temp++}`;
354
+ const ic = this.tmp("lm_ki");
355
+ this.line(`uint64_t ${ic} = ${counters[k++]}++;`);
356
+ this.scopes.at(-1).set(id, { c: ic, type: U64, mutable: false });
357
+ idx = place(f.createIdentifier(id));
358
+ }
359
+ else {
360
+ if (!v)
361
+ this.fail(st.fn, "map's callback returns the new element");
362
+ const mv = v.literal !== undefined ? this.literal(v.literal, U64, st.fn) : v;
363
+ if (!["int", "bool", "pubkey", "enum"].includes(mv.type.kind))
364
+ this.fail(st.fn, `map's callback returns integers, booleans, Pubkeys or enums (the new list's elements), not ${typeName(mv.type)}`);
365
+ const id = `__lv${this.temp++}`;
366
+ const mc = this.tmp("lm_v");
367
+ this.line(mv.type.kind === "pubkey" ? `SolPubkey ${mc} = *(${mv.c});` : `${valueC(mv.type)} ${mc} = ${mv.c};`);
368
+ this.scopes.at(-1).set(id, { c: mc, type: mv.type, mutable: false });
369
+ element = place(f.createIdentifier(id));
370
+ }
371
+ }
372
+ body(element, idx);
373
+ if (next && (names.alwaysNext || counters.length))
374
+ this.line(`${next}:;`);
287
375
  this.line(`${ci}++;`);
288
376
  });
289
377
  this.line(`}`);
290
378
  });
291
379
  this.line(`}`);
292
380
  }
381
+ /**
382
+ * `const ys = xs.map(…)` / `xs.filter(…)`: the chain's elements in a local FixedVec, as big as the list it starts
383
+ * from (a FixedArray's length, a FixedVec's capacity) or as annotated (`const ys: FixedVec<u64, 16> = …`, needed
384
+ * when it starts from a Vec). It lives on the stack, so it holds at most 2 KiB; one more element than it holds aborts.
385
+ */
386
+ localList(d, init, c) {
387
+ const { base, stages } = this.pipelineStages(init);
388
+ let cap;
389
+ let annotatedElem;
390
+ if (d.type) {
391
+ const t = d.type;
392
+ const args = ts.isTypeReferenceNode(t) ? (t.typeArguments ?? []) : [];
393
+ const n = args[1] && ts.isLiteralTypeNode(args[1]) && ts.isNumericLiteral(args[1].literal) ? Number(args[1].literal.text) : NaN;
394
+ const elemNode = args[0];
395
+ const sn = elemNode && structNameOf(elemNode, this.scope);
396
+ annotatedElem = elemNode
397
+ ? (resolveScalarType(elemNode, this.scope) ?? (sn && this.model.structs.has(sn) ? { kind: "struct", name: sn } : undefined))
398
+ : undefined;
399
+ if (!ts.isTypeReferenceNode(t) || typesolName(t, this.scope.imports) !== "FixedVec" || !annotatedElem || !Number.isInteger(n) || n < 1)
400
+ this.fail(t, "annotate the new list as FixedVec<T, N> (N a literal)");
401
+ cap = n;
402
+ }
403
+ else {
404
+ const r = this.reporter.guard(() => this.ref(base));
405
+ const l = ts.isIdentifier(base) ? this.lookup(base.text) : undefined;
406
+ if (r?.type.kind === "array")
407
+ cap = r.type.len;
408
+ else if (r?.type.kind === "vec" && r.type.cap !== undefined)
409
+ cap = r.type.cap;
410
+ if (cap === undefined || l?.type.kind === "vecArg")
411
+ this.fail(init, "this list has no fixed size: annotate the new one with its capacity, const ys: FixedVec<T, N> = …");
412
+ }
413
+ // The element type: the last map's result, else the list's own elements (known once the loop is emitted).
414
+ const declAt = this.lines.length;
415
+ this.line(`/* ${c} */`);
416
+ let elem = stages.some((st) => st.kind === "map") ? undefined : this.listElem(base);
417
+ const cap0 = cap;
418
+ this.eachElement(init, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element) => {
419
+ if (!elem) {
420
+ const v = this.expr(element);
421
+ elem = v.type;
422
+ }
423
+ const target = annotatedElem ?? elem;
424
+ const sub = { lv: `${c}.items[${c}.len]`, type: target, writable: true };
425
+ this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
426
+ const vt = this.storageValueType(target);
427
+ if (vt)
428
+ this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(element, vt), vt, element)).c) + ";");
429
+ else
430
+ this.writeAggregate(sub, element);
431
+ this.line(`${c}.len++;`);
432
+ });
433
+ const t = { kind: "vec", elem: annotatedElem ?? elem, cap: cap0 };
434
+ const bytes = 4 + this.fieldBytes(t.elem) * cap0;
435
+ if (bytes > 2048)
436
+ this.fail(init, `the new list takes ${bytes} bytes; locals live on the 4 KiB stack, so keep it under 2 KiB (a smaller FixedVec<T, N>)`);
437
+ this.model.localVecs.set(fixedVecC(t), t);
438
+ const pad = " ".repeat(this.indent);
439
+ this.lines[declAt] = `${pad}${fixedVecC(t)} ${c};\n${pad}ts_memzero(&${c}, sizeof(${c}));`;
440
+ return t;
441
+ }
442
+ /** The element type of a Vec, FixedArray, FixedVec or Vec argument. */
443
+ listElem(list) {
444
+ const l = ts.isIdentifier(list) ? this.lookup(list.text) : undefined;
445
+ if (l?.type.kind === "vecArg")
446
+ return l.type.elem;
447
+ const r = this.ref(list);
448
+ if (r && (r.type.kind === "array" || r.type.kind === "vec"))
449
+ return r.type.elem;
450
+ this.fail(list, "expected a Vec, FixedArray or FixedVec");
451
+ }
293
452
  /** Whether `e` is the remaining accounts (or a const bound to them). */
294
453
  isRemaining(e) {
295
454
  if (ts.isCallExpression(e) && this.exportName(e.expression) === "remainingAccounts")
@@ -337,6 +496,11 @@ export class StatementEmitter extends CpiEmitter {
337
496
  this.fail(p, "closure parameters are plain names");
338
497
  const arg = args[i];
339
498
  const name = p.name.text;
499
+ // A function parameter (of a helper expanded here): the function given, expanded where the body calls it.
500
+ if (p.type && ts.isFunctionTypeNode(p.type)) {
501
+ params.set(name, { c: "", type: BOOL, mutable: false, closure: this.closureOf(arg) });
502
+ return;
503
+ }
340
504
  const c2 = `l_${name}_${this.temp++}`;
341
505
  const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
342
506
  if (!annotated && this.looksLikeStructPlace(arg)) {
@@ -378,6 +542,8 @@ export class StatementEmitter extends CpiEmitter {
378
542
  this.line(`{`);
379
543
  this.block(() => {
380
544
  const body = fn.body;
545
+ if (!body)
546
+ this.fail(fn, `${c.name} has no body`);
381
547
  if (ts.isBlock(body))
382
548
  body.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
383
549
  else {
@@ -434,9 +600,7 @@ export class StatementEmitter extends CpiEmitter {
434
600
  this.fail(cb, `pass ${method} an arrow function written here: xs.${method}((x) => …)`);
435
601
  const fn = cb;
436
602
  const index = `__lm${this.temp++}`;
437
- const indexId = place(f.createIdentifier(index));
438
- const length = place(f.createPropertyAccessExpression(list, "length"));
439
- const element = place(f.createCallExpression(place(f.createPropertyAccessExpression(list, "get")), undefined, [indexId]));
603
+ void place;
440
604
  const I64 = { kind: "int", name: "i64" };
441
605
  // The result, declared before the loop.
442
606
  const res = this.tmp("lm");
@@ -482,69 +646,58 @@ export class StatementEmitter extends CpiEmitter {
482
646
  }
483
647
  const findDecl = method === "find" ? this.lines.length - 1 : -1;
484
648
  const ci = this.tmp("lm_i");
485
- this.line(`{`);
486
- this.block(() => {
487
- this.line(`uint64_t ${ci} = 0;`);
488
- this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
489
- this.line(`while (1) {`);
490
- this.block(() => {
491
- const n = this.expect(this.expr(length, U64), U64, list);
492
- this.line(`if (!(${ci} < ${n.c})) break;`);
493
- if (valueForm) {
494
- const eq = place(f.createBinaryExpression(element, ts.SyntaxKind.EqualsEqualsEqualsToken, place(f.createIdentifier(needle))));
495
- const hit = this.expect(this.expr(eq, BOOL), BOOL, node);
496
- this.line(`if (${hit.c}) {`);
497
- this.line(method === "includes" ? ` ${res} = true;` : ` ${res} = (int64_t)${ci};`);
498
- this.line(` break;`);
499
- this.line(`}`);
649
+ this.eachElement(list, node, { index, ci }, (element, indexId) => {
650
+ if (valueForm) {
651
+ const eq = place(f.createBinaryExpression(element, ts.SyntaxKind.EqualsEqualsEqualsToken, place(f.createIdentifier(needle))));
652
+ const hit = this.expect(this.expr(eq, BOOL), BOOL, node);
653
+ this.line(`if (${hit.c}) {`);
654
+ this.line(method === "includes" ? ` ${res} = true;` : ` ${res} = (int64_t)${this.expr(indexId, U64).c};`);
655
+ this.line(` break;`);
656
+ this.line(`}`);
657
+ }
658
+ else {
659
+ const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
660
+ const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
661
+ if (accId)
662
+ this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
663
+ const all = method === "reduce" ? [accId, element, indexId] : [element, indexId];
664
+ const v = this.expandClosure(closure, all.slice(0, fn.parameters.length), cb);
665
+ if (method === "forEach") {
666
+ if (v)
667
+ this.fail(cb, "forEach's callback returns nothing");
668
+ }
669
+ else if (method === "reduce") {
670
+ if (!v)
671
+ this.fail(cb, "reduce's callback returns the new accumulator");
672
+ const nv = this.expect(v, accType, cb);
673
+ this.line(accType.kind === "pubkey" ? `${res} = *(${nv.c});` : `${res} = ${nv.c};`);
500
674
  }
501
675
  else {
502
- const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
503
- const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
504
- if (accId)
505
- this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
506
- const all = method === "reduce" ? [accId, element, indexId] : [element, indexId];
507
- const v = this.expandClosure(closure, all.slice(0, fn.parameters.length), cb);
508
- if (method === "forEach") {
509
- if (v)
510
- this.fail(cb, "forEach's callback returns nothing");
511
- }
512
- else if (method === "reduce") {
513
- if (!v)
514
- this.fail(cb, "reduce's callback returns the new accumulator");
515
- const nv = this.expect(v, accType, cb);
516
- this.line(accType.kind === "pubkey" ? `${res} = *(${nv.c});` : `${res} = ${nv.c};`);
517
- }
518
- else {
519
- if (!v)
520
- this.fail(cb, `${method}'s callback returns a boolean`);
521
- const b = this.expect(v, BOOL, cb);
522
- if (method === "some")
523
- this.line(`if (${b.c}) { ${res} = true; break; }`);
524
- if (method === "every")
525
- this.line(`if (!(${b.c})) { ${res} = false; break; }`);
526
- if (method === "findIndex")
527
- this.line(`if (${b.c}) { ${res} = (int64_t)${ci}; break; }`);
528
- if (method === "find") {
529
- this.line(`if (${b.c}) {`);
530
- this.block(() => {
531
- const e = this.expr(element);
532
- elemType = e.type;
533
- if (!["int", "bool", "pubkey", "enum"].includes(e.type.kind))
534
- this.fail(node, "find returns an Option of the element, so the elements must be integers, booleans, Pubkeys or enums; use findIndex");
535
- this.line(`${res}.some = 1;`);
536
- this.line(e.type.kind === "pubkey" ? `${res}.v = *(${e.c});` : `${res}.v = ${e.c};`);
537
- this.line(`break;`);
538
- });
539
- this.line(`}`);
540
- }
676
+ if (!v)
677
+ this.fail(cb, `${method}'s callback returns a boolean`);
678
+ const b = this.expect(v, BOOL, cb);
679
+ if (method === "some")
680
+ this.line(`if (${b.c}) { ${res} = true; break; }`);
681
+ if (method === "every")
682
+ this.line(`if (!(${b.c})) { ${res} = false; break; }`);
683
+ if (method === "findIndex")
684
+ this.line(`if (${b.c}) { ${res} = (int64_t)${this.expr(indexId, U64).c}; break; }`);
685
+ if (method === "find") {
686
+ this.line(`if (${b.c}) {`);
687
+ this.block(() => {
688
+ const e = this.expr(element);
689
+ elemType = e.type;
690
+ if (!["int", "bool", "pubkey", "enum"].includes(e.type.kind))
691
+ this.fail(node, "find returns an Option of the element, so the elements must be integers, booleans, Pubkeys or enums; use findIndex");
692
+ this.line(`${res}.some = 1;`);
693
+ this.line(e.type.kind === "pubkey" ? `${res}.v = *(${e.c});` : `${res}.v = ${e.c};`);
694
+ this.line(`break;`);
695
+ });
696
+ this.line(`}`);
541
697
  }
542
698
  }
543
- this.line(`${ci}++;`);
544
- });
545
- this.line(`}`);
699
+ }
546
700
  });
547
- this.line(`}`);
548
701
  if (method === "find") {
549
702
  const inner = elemType;
550
703
  this.model.options.set(optionKey(inner), inner);
@@ -650,6 +803,11 @@ export class StatementEmitter extends CpiEmitter {
650
803
  this.scopes.at(-1).set(name, { c, type: t, mutable });
651
804
  continue;
652
805
  }
806
+ if (this.isPipeline(init)) {
807
+ const t = this.localList(d, init, c);
808
+ this.scopes.at(-1).set(name, { c, type: t, mutable });
809
+ continue;
810
+ }
653
811
  if (fn === "loadAccount" || fn === "loadAccountMut" || fn === "loadForeign") {
654
812
  if (mutable)
655
813
  this.fail(d, "bind loaded accounts with const");
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { resolveScalarType } from "../analyze.js";
3
3
  import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName } from "../model.js";
4
- import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount } from "./shared.js";
4
+ import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount, vecPlace, emptySizeC } from "./shared.js";
5
5
  import { EmitterBase } from "./base.js";
6
6
  /**
7
7
  * Places in account data and locals: references, reads and writes, Vec/array indexing, aliasing rules and
@@ -258,6 +258,8 @@ export class StorageEmitter extends EmitterBase {
258
258
  const l = this.lookup(node.text);
259
259
  if (l && l.type.kind === "array")
260
260
  return { lv: l.c, type: l.type, writable: !l.readonly };
261
+ if (l && l.type.kind === "vec")
262
+ return this.withFixedVec({ lv: l.c, type: l.type, writable: true });
261
263
  // A struct loaded from a remaining account.
262
264
  if (l && l.type.kind === "structRef")
263
265
  return { lv: `(*${l.c})`, type: { kind: "struct", name: l.type.struct }, writable: l.type.writable };
@@ -278,15 +280,35 @@ export class StorageEmitter extends EmitterBase {
278
280
  const r = { lv: `d_${a.name}->${f.name}`, type: f.type, writable: isWritable(a.kind), account: a, path: [f.name] };
279
281
  if (f.type.kind === "vec" && f.type.cap !== undefined)
280
282
  r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
281
- else if (f.type.kind === "vec")
283
+ else if (f.type.kind === "vec") {
282
284
  // Growing needs the account's length at instruction entry: instructions have it, helpers get it passed.
283
- r.vec = {
284
- len: `d_${a.name}->${f.name}__len`,
285
- items: r.lv,
286
- fixed: `sizeof(TSS_${a.kind.struct.name})`,
287
- elemSize: a.kind.struct.vec.elemSize,
288
- resizable: isWritable(a.kind),
289
- };
285
+ const s = a.kind.struct;
286
+ const k = s.vecs.findIndex((v) => v.name === f.name);
287
+ if (s.vecs.length === 1)
288
+ r.vec = {
289
+ len: `d_${a.name}->${f.name}__len`,
290
+ items: r.lv,
291
+ fixed: `sizeof(TSS_${s.name})`,
292
+ elemSize: s.vec.elemSize,
293
+ resizable: isWritable(a.kind),
294
+ };
295
+ else {
296
+ const base = `(*d_${a.name})`;
297
+ const at = vecPlace(s, base, k);
298
+ const others = s.vecs.map((v, j) => (j === k ? "" : ` + (uint64_t)${vecPlace(s, base, j).len} * ${v.elemSize}`)).join("");
299
+ r.lv = at.items;
300
+ r.vec = {
301
+ len: at.len,
302
+ items: at.items,
303
+ fixed: `(${emptySizeC(s)}${others})`,
304
+ elemSize: s.vecs[k].elemSize,
305
+ resizable: isWritable(a.kind),
306
+ tail: k < s.vecs.length - 1
307
+ ? { end: at.end, dataEnd: `(a_${a.name}->data + a_${a.name}->data_len)`, paths: s.vecs.slice(k + 1).map((v) => `${a.name}.${v.name}[]`) }
308
+ : undefined,
309
+ };
310
+ }
311
+ }
290
312
  return r;
291
313
  }
292
314
  }
@@ -330,8 +352,12 @@ export class StorageEmitter extends EmitterBase {
330
352
  this.fail(node.name, `${s.name} has no field "${field}"`);
331
353
  const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
332
354
  // Vecs of loaded accounts can be read and updated in place but not resized (no entry length known).
333
- if (f.type.kind === "vec" && f.type.cap === undefined)
334
- r.vec = { len: `${base.lv}.${f.name}__len`, items: r.lv, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
355
+ if (f.type.kind === "vec" && f.type.cap === undefined) {
356
+ const k = s.vecs.findIndex((v) => v.name === f.name);
357
+ const at = s.vecs.length === 1 ? { len: `${base.lv}.${f.name}__len`, items: r.lv } : vecPlace(s, base.lv, k);
358
+ r.lv = at.items;
359
+ r.vec = { len: at.len, items: at.items, fixed: emptySizeC(s), elemSize: s.vecs[k].elemSize, resizable: false };
360
+ }
335
361
  return this.withFixedVec(r);
336
362
  }
337
363
  return undefined;
@@ -671,8 +697,8 @@ export class StorageEmitter extends EmitterBase {
671
697
  const l = this.lookup(node.text);
672
698
  if (!l)
673
699
  this.fail(node, `"${node.text}" is not a local variable`);
674
- if (l.type.kind === "array")
675
- this.fail(node, `use ${node.text}.set(i, v) to change array elements`);
700
+ if (l.type.kind === "array" || l.type.kind === "vec")
701
+ this.fail(node, `use ${node.text}.set(i, v) to change list elements`);
676
702
  if (!l.mutable)
677
703
  this.fail(node, `"${node.text}" is const or an argument and cannot be reassigned`);
678
704
  if (l.type.kind === "pubkey")
@@ -38,9 +38,11 @@ export type ValueType = ScalarType | {
38
38
  inner: ScalarType;
39
39
  }
40
40
  /** `Vec<T>` instruction argument: a read-only view into instruction data. T is a scalar or a fixed-size struct. */
41
+ /** A Vec argument (Borsh), read in place; `FixedVec<T, N>` arguments are the same with at most `cap` elements. */
41
42
  | {
42
43
  kind: "vecArg";
43
44
  elem: FieldType;
45
+ cap?: number;
44
46
  }
45
47
  /** `FixedArray<T, N>` instruction argument (Borsh [T; N]), parsed into a read-only copy. */
46
48
  | {
@@ -119,6 +121,8 @@ export type FieldType = ScalarType | {
119
121
  export interface StructField {
120
122
  name: string;
121
123
  type: FieldType;
124
+ /** The declaration, while the struct is being analyzed (for messages). */
125
+ node?: ts.Node;
122
126
  }
123
127
  /** Every exported interface. Used as account data, nested in other structs, or emitted as an event. */
124
128
  export interface StructDef {
@@ -129,12 +133,21 @@ export interface StructDef {
129
133
  * or undefined when a field is a variable-length string (event-only structs).
130
134
  */
131
135
  size: number | undefined;
132
- /** The trailing Vec field, if any. Such structs can only be account data. */
136
+ /** The first growable Vec field, if any. Such structs can only be account data. */
133
137
  vec?: {
134
138
  name: string;
135
139
  elem: FieldType;
136
140
  elemSize: number;
137
141
  };
142
+ /**
143
+ * Every growable Vec, in stored order (after the fixed-size fields). The first ends the C struct (its u32 length,
144
+ * then its elements); each further one follows the previous one's elements: a u32 length, then its elements.
145
+ */
146
+ vecs?: {
147
+ name: string;
148
+ elem: FieldType;
149
+ elemSize: number;
150
+ }[];
138
151
  accountDiscriminator: number[];
139
152
  eventDiscriminator: number[];
140
153
  node: ts.InterfaceDeclaration;
@@ -436,6 +449,10 @@ export interface ProgramModel {
436
449
  fixedVecs: Map<string, FieldType & {
437
450
  kind: "vec";
438
451
  }>;
452
+ /** FixedVec types of local lists (`const ys = xs.map(…)`), filled in while emitting: C typedefs only, not the IDL. */
453
+ localVecs: Map<string, FieldType & {
454
+ kind: "vec";
455
+ }>;
439
456
  }
440
457
  export declare function isWritable(k: ParamKind): boolean;
441
458
  export declare function typeName(t: ValueType | FieldType): string;