@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.
@@ -422,6 +422,32 @@ export function analyze(entry) {
422
422
  });
423
423
  return;
424
424
  }
425
+ // An arrow written as a helper's function argument (`apply((x: u64): u64 => x + 1n, v)`): a local function of
426
+ // its own, passed like a named one. Missing parameter and return types come from the helper's parameter type.
427
+ const target = (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && ts.isCallExpression(n.parent) ? fnParamType(n, checker) : undefined;
428
+ if (target) {
429
+ reporter.guard(() => {
430
+ if (generic)
431
+ reporter.fail(n, "arrow functions passed inside a generic helper are not supported; declare the function at the top level");
432
+ const lit = n;
433
+ lit.parameters.forEach((p, i) => {
434
+ const from = target.parameters[i]?.type;
435
+ if (!p.type && from)
436
+ p.type = from;
437
+ });
438
+ if (!lit.type)
439
+ lit.type = target.type;
440
+ let name = `${outer}__fn`;
441
+ for (let i = 2; taken.has(name); i++)
442
+ name = `${outer}__fn${i}`;
443
+ taken.add(name);
444
+ const src = { node: lit, name, display: "the function passed here", decl: lit, imports, outer: fn };
445
+ locals.push(src);
446
+ if (lit.body)
447
+ collectLocals(lit.body, name, imports, lit);
448
+ });
449
+ return;
450
+ }
425
451
  ts.forEachChild(n, visit);
426
452
  };
427
453
  visit(body);
@@ -525,7 +551,7 @@ export function analyze(entry) {
525
551
  collectStructs(a.type, structs, dataEnums, seen);
526
552
  for (const s of seen)
527
553
  if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
528
- reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; pass a Vec<T> argument instead`);
554
+ reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; make ${s} @layout borsh, or pass the list as its own FixedVec<T, N> argument`);
529
555
  }
530
556
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
531
557
  if (ix.returns?.kind === "struct")
@@ -580,6 +606,7 @@ export function analyze(entry) {
580
606
  argStructs,
581
607
  fstrings,
582
608
  fixedVecs,
609
+ localVecs: new Map(),
583
610
  },
584
611
  sourceFile,
585
612
  files: programFiles(program, entryPath).map((f) => f.fileName),
@@ -642,11 +669,15 @@ export function resolveArgAggregate(node, scope, structs) {
642
669
  return { kind: "structRef", struct: top.name, writable: false };
643
670
  if (top?.kind === "array")
644
671
  return { kind: "arrayArg", array: top };
645
- if (typesolName(node, scope.imports) === "Vec") {
646
- const e = node.typeArguments?.[0];
672
+ const list = typesolName(node, scope.imports);
673
+ if (list === "Vec" || list === "FixedVec") {
674
+ const [e, n] = node.typeArguments ?? [];
647
675
  const elem = e && fixed(e);
676
+ const cap = list === "FixedVec" ? numericLiteralArg(n) : undefined;
677
+ if (list === "FixedVec" && !(cap !== undefined && Number.isInteger(cap) && cap >= 1))
678
+ return undefined;
648
679
  if (elem && (elem.kind === "struct" || elem.kind === "array"))
649
- return { kind: "vecArg", elem };
680
+ return { kind: "vecArg", elem, ...(cap ? { cap } : {}) };
650
681
  }
651
682
  return undefined;
652
683
  }
@@ -714,6 +745,14 @@ export function resolveValueType(node, scope, allowVec = false) {
714
745
  if (elem)
715
746
  return { kind: "vecArg", elem };
716
747
  }
748
+ // FixedVec<T, N> as an argument: a Borsh Vec of at most N elements (more fail to deserialize), read like a Vec argument.
749
+ if (allowVec && typesolName(node, scope.imports) === "FixedVec") {
750
+ const [elemNode, n] = node.typeArguments ?? [];
751
+ const elem = elemNode && resolveScalarType(elemNode, scope);
752
+ const cap = numericLiteralArg(n);
753
+ if (elem && cap !== undefined && Number.isInteger(cap) && cap >= 1)
754
+ return { kind: "vecArg", elem, cap };
755
+ }
717
756
  return resolveScalarType(node, scope);
718
757
  }
719
758
  function numericLiteralArg(node) {
@@ -824,9 +863,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
824
863
  if (problem)
825
864
  reporter.fail(m.name, problem);
826
865
  const type = fieldType(m.type);
827
- if (type.kind === "vec" && type.cap === undefined && m !== decl.members[decl.members.length - 1])
828
- reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size); use FixedVec<T, N> for a list of at most N elsewhere");
829
- fields.push({ name, type });
866
+ fields.push({ name, type, node: m });
830
867
  }
831
868
  checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
832
869
  const name = decl.name.text;
@@ -835,9 +872,20 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
835
872
  const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
836
873
  if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
837
874
  reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
875
+ const growable = (f) => f.type.kind === "vec" && f.type.cap === undefined;
876
+ // Anchor's bytes keep the declared order, so there a growable Vec has to be last (the fields after it would move).
877
+ if (layoutText === "borsh")
878
+ for (const f of fields.slice(0, -1))
879
+ if (growable(f))
880
+ reporter.fail(f.node ?? decl, "in a @layout borsh struct a Vec<T> field must be the last field (Anchor's bytes keep the declared order); use FixedVec<T, N> for a list of at most N elsewhere");
881
+ // The fixed layout stores the growable Vecs after the fixed-size fields, in the order declared, so every other
882
+ // field keeps a fixed offset; the IDL lists the fields in this stored order.
883
+ const stored = layoutText === "borsh" ? fields : [...fields.filter((f) => !growable(f)), ...fields.filter(growable)];
884
+ for (const f of stored)
885
+ delete f.node;
838
886
  structs.set(name, {
839
887
  name,
840
- fields,
888
+ fields: stored,
841
889
  size: undefined,
842
890
  accountDiscriminator: discriminator(`account:${name}`),
843
891
  eventDiscriminator: discriminator(`event:${name}`),
@@ -987,13 +1035,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
987
1035
  s.size = total;
988
1036
  if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
989
1037
  reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
990
- const last = s.fields.at(-1);
991
- if (last?.type.kind === "vec" && last.type.cap === undefined) {
992
- const elemSize = sizeOf(last.type.elem, s.node);
993
- if (elemSize === undefined)
994
- reporter.fail(s.node, "Vec elements must have a fixed size");
995
- s.vec = { name: last.name, elem: last.type.elem, elemSize };
996
- }
1038
+ // Growable Vecs (stored last, see above): the first one's length and elements end the C struct; the others follow
1039
+ // it in the account, each a u32 length and its elements.
1040
+ for (const f of s.fields)
1041
+ if (f.type.kind === "vec" && f.type.cap === undefined) {
1042
+ const elemSize = sizeOf(f.type.elem, s.node);
1043
+ if (elemSize === undefined)
1044
+ reporter.fail(s.node, "Vec elements must have a fixed size");
1045
+ (s.vecs ??= []).push({ name: f.name, elem: f.type.elem, elemSize });
1046
+ }
1047
+ s.vec = s.vecs?.[0];
1048
+ // The C struct holds the first Vec's length; each further one adds its own when the account is created.
1049
+ if (s.vecs && s.size !== undefined)
1050
+ s.size -= 4 * (s.vecs.length - 1);
997
1051
  state.set(s.name, "done");
998
1052
  };
999
1053
  for (const s of structs.values())
@@ -1007,6 +1061,30 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
1007
1061
  }
1008
1062
  const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
1009
1063
  /** Whether a local function reads a variable or parameter of a function around it (a closure). */
1064
+ /**
1065
+ * The function type of the helper parameter an arrow is passed for (`f` in `function apply(f: (x: u64) => u64, …)`),
1066
+ * when the arrow is an argument of a call to a declared function or const arrow; undefined otherwise.
1067
+ */
1068
+ function fnParamType(arrow, checker) {
1069
+ const call = arrow.parent;
1070
+ const i = call.arguments.indexOf(arrow);
1071
+ if (i < 0 || !ts.isIdentifier(call.expression))
1072
+ return undefined;
1073
+ let sym = checker.getSymbolAtLocation(call.expression);
1074
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
1075
+ sym = checker.getAliasedSymbol(sym);
1076
+ for (const d of sym?.declarations ?? []) {
1077
+ const fn = ts.isFunctionDeclaration(d)
1078
+ ? d
1079
+ : ts.isVariableDeclaration(d) && d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
1080
+ ? d.initializer
1081
+ : undefined;
1082
+ const t = fn?.parameters[i]?.type;
1083
+ if (t && ts.isFunctionTypeNode(t))
1084
+ return t;
1085
+ }
1086
+ return undefined;
1087
+ }
1010
1088
  function captures(l, localDecls, checker) {
1011
1089
  const fn = l.node;
1012
1090
  let found = false;
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { type Analysis, type TypeScope } from "../analyze.js";
3
3
  import type { Reporter } from "../diagnostics.js";
4
- import { type AccountParam, type Helper, type IdlSeed, type Instruction, type ProgramModel, type ValueType } from "../model.js";
4
+ import { type AccountParam, type GenericHelper, type Helper, type IdlSeed, type Instruction, type ProgramModel, type ValueType } from "../model.js";
5
5
  import { type Value, type Ref, type Local, type Closure, type EmitOptions } from "./shared.js";
6
6
  /**
7
7
  * State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
@@ -50,6 +50,8 @@ export declare abstract class EmitterBase {
50
50
  constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
51
51
  /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
52
52
  protected helperOf(node: ts.CallExpression): Helper | undefined;
53
+ /** The helper a call runs, when it is given a closure for a function parameter (so it is expanded there). */
54
+ protected givenClosure(node: ts.CallExpression): GenericHelper | undefined;
53
55
  /**
54
56
  * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
55
57
  * captures nothing). A closure can only run where it is declared, so it cannot be passed.
@@ -116,4 +118,12 @@ export declare abstract class EmitterBase {
116
118
  protected abstract epilogue(): string[];
117
119
  protected abstract expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
118
120
  protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
121
+ protected abstract isPipeline(e: ts.Expression): e is ts.CallExpression;
122
+ protected abstract closureOf(arg: ts.Expression): Closure;
123
+ protected abstract eachElement(list: ts.Expression, at: ts.Node, names: {
124
+ index: string;
125
+ ci: string;
126
+ next?: string;
127
+ alwaysNext?: boolean;
128
+ }, body: (element: ts.Expression, index: ts.Expression) => void): void;
119
129
  }
@@ -87,6 +87,9 @@ export class EmitterBase {
87
87
  helperOf(node) {
88
88
  if (!ts.isIdentifier(node.expression))
89
89
  return undefined;
90
+ // Given a closure, the helper is expanded where it is called (helperWithClosure), not called.
91
+ if (this.givenClosure(node))
92
+ return undefined;
90
93
  const cached = this.calleeCache.get(node);
91
94
  if (cached !== undefined)
92
95
  return cached ?? undefined;
@@ -103,6 +106,17 @@ export class EmitterBase {
103
106
  this.calleeCache.set(node, h ?? null);
104
107
  return h;
105
108
  }
109
+ /** The helper a call runs, when it is given a closure for a function parameter (so it is expanded there). */
110
+ givenClosure(node) {
111
+ if (!ts.isIdentifier(node.expression))
112
+ return undefined;
113
+ const name = calleeName(node.expression, this.model.helperDecls, this.checker) ?? (this.lookup(node.expression.text) ? undefined : node.expression.text);
114
+ const g = name ? this.model.generics.get(name) : undefined;
115
+ if (!g?.fnParams.length)
116
+ return undefined;
117
+ const closureArg = (a) => !!a && (ts.isArrowFunction(a) || ts.isFunctionExpression(a) ? !this.model.helperDecls.has(a) : ts.isIdentifier(a) && !!this.lookup(a.text)?.closure);
118
+ return g.fnParams.some((i) => closureArg(node.arguments[i])) ? g : undefined;
119
+ }
106
120
  /**
107
121
  * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
108
122
  * captures nothing). A closure can only run where it is declared, so it cannot be passed.
@@ -111,6 +125,13 @@ export class EmitterBase {
111
125
  return g.fnParams.map((i) => {
112
126
  const arg = node.arguments[i];
113
127
  const pname = g.node.parameters[i].name.text;
128
+ // An arrow written here: lifted to a local function when it reads nothing around it.
129
+ if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))) {
130
+ const lifted = this.model.helperDecls.get(arg);
131
+ if (lifted)
132
+ return lifted;
133
+ this.fail(arg, `this function reads the variables of the function around it, so it can only run there, not be passed to ${g.display}; pass the values it needs as arguments`);
134
+ }
114
135
  if (!arg || !ts.isIdentifier(arg))
115
136
  this.fail(arg ?? node, `pass ${g.display}'s ${pname} a named function (declare it: const f = (…) => …)`);
116
137
  const passed = this.helper?.fnArgs?.get(arg.text);
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { type ErrorDef, type FieldType, type Helper } from "../model.js";
3
- import { type Value, type Ref } from "./shared.js";
3
+ import { type Value, type Ref, type Closure } from "./shared.js";
4
4
  import { ExpressionEmitter } from "./expressions.js";
5
5
  export declare abstract class CallEmitter extends ExpressionEmitter {
6
6
  protected call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
@@ -31,6 +31,16 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
31
31
  protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
32
32
  /** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
33
33
  private helperGrows;
34
+ /**
35
+ * A helper given a closure (a function that reads the caller's variables) for a function parameter: a closure only
36
+ * runs where it is declared, so the helper is expanded here, like a closure, with the closure bound to the
37
+ * parameter. Helpers expanded so take values (not accounts) and have no type parameters.
38
+ */
39
+ protected helperWithClosure(node: ts.CallExpression): {
40
+ value: Value | undefined;
41
+ } | undefined;
42
+ /** A function given for a function parameter of an expanded helper or closure, as something to expand. */
43
+ protected closureOf(arg: ts.Expression): Closure;
34
44
  protected callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
35
45
  /** One helper argument: a C argument list fragment (accounts, seeds, references) or a value to settle. */
36
46
  protected helperArg(h: Helper, p: Helper["params"][number], n: ts.Expression): Value | string;
@@ -1,9 +1,9 @@
1
1
  import ts from "typescript";
2
- import { calleeName, resolveScalarType } from "../analyze.js";
2
+ import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
3
3
  import { ANCHOR_ERRORS } from "../anchor-errors.js";
4
4
  import { decodeAddress } from "../../keys.js";
5
5
  import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
6
- import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
6
+ import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
7
7
  import { ExpressionEmitter } from "./expressions.js";
8
8
  /**
9
9
  * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
@@ -465,6 +465,9 @@ export class CallEmitter extends ExpressionEmitter {
465
465
  if (local?.closure)
466
466
  return this.expandClosure(local.closure, node.arguments, node);
467
467
  }
468
+ const inlined = this.helperWithClosure(node);
469
+ if (inlined)
470
+ return inlined.value;
468
471
  const helper = this.helperOf(node);
469
472
  if (helper)
470
473
  return this.callHelper(node, helper);
@@ -473,9 +476,12 @@ export class CallEmitter extends ExpressionEmitter {
473
476
  // xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
474
477
  if (LIST_METHODS.has(method)) {
475
478
  const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
476
- if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(callee.expression))
479
+ if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.isPipeline(callee.expression) || this.peekArray(callee.expression))
477
480
  return this.listMethod(node, method);
478
481
  }
482
+ // A map / filter chain is consumed by a list method, `.length` or for…of, or stored: const ys = xs.map(…).
483
+ if (this.isPipeline(node))
484
+ this.fail(node, `${method} builds a new list: store it (const ys = xs.${method}(…)), or use it right away with a list method, .length or for…of`);
479
485
  // Vec<T> instruction arguments are read-only views.
480
486
  if (ts.isIdentifier(callee.expression)) {
481
487
  const l = this.lookup(callee.expression.text);
@@ -620,6 +626,21 @@ export class CallEmitter extends ExpressionEmitter {
620
626
  this.fail(node, "only Vecs of Mut<...> / Init<...> accounts can grow or shrink");
621
627
  this.line(`TS_TRY(ts_resize(params, ${payer}, a_${v.account.name}, 8 + ${info.fixed} + (${newLen}) * ${info.elemSize}, entry_${v.account.name}));`);
622
628
  };
629
+ /**
630
+ * The growable Vecs stored after this one move by `by` bytes (+ grows, - shrinks): where they start and how many
631
+ * bytes they take, read before the resize; references into them cannot outlive this call.
632
+ */
633
+ const tail = info.tail;
634
+ const tailStart = () => {
635
+ if (!tail)
636
+ return undefined;
637
+ this.assertNotAliased(tail.paths, node);
638
+ const at = this.tmp("tail");
639
+ const n = this.tmp("tail_n");
640
+ this.line(`uint8_t *${at} = ${tail.end};`);
641
+ this.line(`uint64_t ${n} = (uint64_t)(${tail.dataEnd} - ${at});`);
642
+ return { at, n };
643
+ };
623
644
  switch (method) {
624
645
  case "set": {
625
646
  if (args.length !== 2)
@@ -655,8 +676,11 @@ export class CallEmitter extends ExpressionEmitter {
655
676
  const write = stageElem(args[0]);
656
677
  const n = this.tmp("n");
657
678
  this.line(`uint64_t ${n} = (uint64_t)${info.len};`);
658
- // Resizing does not move the data, so element pointers stay valid.
679
+ const t = tailStart();
680
+ // Resizing does not move the data, so element pointers stay valid; the Vecs after this one shift up.
659
681
  resize(`${n} + 1`, `a_${payer.name}`);
682
+ if (t)
683
+ this.line(`ts_memmove(${t.at} + ${info.elemSize}, ${t.at}, ${t.n});`);
660
684
  this.line(`${info.len} = (uint32_t)(${n} + 1);`);
661
685
  write(elemRef(n));
662
686
  return undefined;
@@ -686,6 +710,9 @@ export class CallEmitter extends ExpressionEmitter {
686
710
  this.line(`${valueC(st)} ${out} = ${val.c};`);
687
711
  result = st.kind === "pubkey" ? { c: `(&${out})`, type: st } : { c: out, type: st };
688
712
  }
713
+ const t = info.cap === undefined ? tailStart() : undefined;
714
+ if (t)
715
+ this.line(`ts_memmove(${t.at} - ${info.elemSize}, ${t.at}, ${t.n});`);
689
716
  this.line(`${info.len} = (uint32_t)(${n} - 1);`);
690
717
  resize(`${n} - 1`, "NULL");
691
718
  return result;
@@ -695,6 +722,9 @@ export class CallEmitter extends ExpressionEmitter {
695
722
  this.fail(node, "clear() takes no arguments");
696
723
  needWritable();
697
724
  this.assertNotAliased(elemPaths(), node);
725
+ const t = info.cap === undefined ? tailStart() : undefined;
726
+ if (t)
727
+ this.line(`ts_memmove((uint8_t *)${info.items}, ${t.at}, ${t.n});`);
698
728
  this.line(`${info.len} = 0;`);
699
729
  resize("0", "NULL");
700
730
  return undefined;
@@ -933,9 +963,10 @@ export class CallEmitter extends ExpressionEmitter {
933
963
  if (this.model.events.has(s.name) && s.fields.some((f) => f.type.kind === "option" || f.type.kind === "dataEnum"))
934
964
  this.fail(node, `${s.name} is emitted as an event and has Option or enum fields; declare a separate event struct`);
935
965
  this.model.accountStructs.add(s.name);
936
- this.line(`TS_TRY(ts_check_program_account(${h}, params->program_id, TS_DISC_${s.name}, sizeof(TSS_${s.name})));`);
966
+ this.line(`TS_TRY(ts_check_program_account(${h}, params->program_id, TS_DISC_${s.name}, ${emptySizeC(s)}));`);
937
967
  if (s.vec)
938
- this.line(`TS_TRY(ts_check_vec_len(${h}, sizeof(TSS_${s.name}), ((TSS_${s.name} *)(${h}->data + 8))->${s.vec.name}__len, TS_ELEM_${s.name}));`);
968
+ for (const l of vecLenCheck(s, h, `((TSS_${s.name} *)(${h}->data + 8))`, ""))
969
+ this.line(l);
939
970
  if (mut) {
940
971
  this.line(`TS_TRY(ts_check_writable(${h}));`);
941
972
  this.line(`TS_TRY(ts_check_unaliased(params, ${h}));`);
@@ -967,6 +998,37 @@ export class CallEmitter extends ExpressionEmitter {
967
998
  visit(body);
968
999
  return found;
969
1000
  }
1001
+ /**
1002
+ * A helper given a closure (a function that reads the caller's variables) for a function parameter: a closure only
1003
+ * runs where it is declared, so the helper is expanded here, like a closure, with the closure bound to the
1004
+ * parameter. Helpers expanded so take values (not accounts) and have no type parameters.
1005
+ */
1006
+ helperWithClosure(node) {
1007
+ const g = this.givenClosure(node);
1008
+ if (!g)
1009
+ return undefined;
1010
+ if (g.typeParams.length)
1011
+ this.fail(node, `${g.display} has type parameters, so it takes only functions that read nothing around them; pass the values the function needs instead`);
1012
+ for (const p of g.node.parameters)
1013
+ if (p.type && !ts.isFunctionTypeNode(p.type) && !resolveValueType(p.type, this.scope))
1014
+ this.fail(node, `${g.display} takes accounts or lists, so it cannot be expanded here for the function that reads the variables around it; pass the values it needs instead`);
1015
+ return { value: this.expandClosure({ node: g.node, name: g.display, scopes: [] }, node.arguments, node) };
1016
+ }
1017
+ /** A function given for a function parameter of an expanded helper or closure, as something to expand. */
1018
+ closureOf(arg) {
1019
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
1020
+ return { node: arg, name: "the function passed", scopes: [...this.scopes] };
1021
+ if (ts.isIdentifier(arg)) {
1022
+ const l = this.lookup(arg.text);
1023
+ if (l?.closure)
1024
+ return l.closure;
1025
+ const name = calleeName(arg, this.model.helperDecls, this.checker) ?? arg.text;
1026
+ const h = this.model.helpers.get(name) ?? this.model.generics.get(name);
1027
+ if (h)
1028
+ return { node: h.node, name: h.display, scopes: [] };
1029
+ }
1030
+ this.fail(arg, "pass a function: its name, or an arrow written here");
1031
+ }
970
1032
  callHelper(node, h) {
971
1033
  if (this.ix && !this.ix.accounts.some((a) => a.kind.kind === "system" && !a.optional) && h.params.some((p) => p.account) && this.helperGrows(h.node))
972
1034
  this.fail(node, `${h.display}() grows a Vec (push): the instruction needs a SystemProgram parameter to top up rent`);
@@ -2,7 +2,7 @@ import ts from "typescript";
2
2
  import { discriminator, snakeCase } from "../analyze.js";
3
3
  import { borshMinSize } from "./borsh.js";
4
4
  import { INT_INFO, isScalar, isWritable, optionKey, sameType, scalarSize, typeName, } from "../model.js";
5
- import { MAX_ACCOUNTS, MAX_SEEDS, MAX_CPI_DATA, MAX_EVENT, U64, U8, BOOL, PUBKEY, STRING, cBytes, isStable, cString, scalarStorageC, optC, valueC, } from "./shared.js";
5
+ import { MAX_ACCOUNTS, MAX_SEEDS, MAX_CPI_DATA, MAX_EVENT, U64, U8, BOOL, PUBKEY, STRING, cBytes, isStable, cString, scalarStorageC, optC, valueC, emptySizeC, fixedVecC, vecCount, } from "./shared.js";
6
6
  import { CallEmitter } from "./calls.js";
7
7
  /**
8
8
  * Bubblegum instructions that replace a leaf: discriminator, accounts in order ([label, writable, signer, optional,
@@ -321,7 +321,7 @@ export class CpiEmitter extends CallEmitter {
321
321
  const b = this.tmp("bump");
322
322
  // The account's size: the struct's (8 + its fixed size, Anchor's InitSpace for @layout borsh), or Anchor's
323
323
  // `space = …` when given, which must hold the struct.
324
- const need = `8 + sizeof(TSS_${s.name})`;
324
+ const need = s.layout === "borsh" ? `8 + sizeof(TSS_${s.name})` : `8 + ${emptySizeC(s)}`;
325
325
  let space = need;
326
326
  const spaceArg = node.arguments[2];
327
327
  if (spaceArg) {
@@ -2642,8 +2642,46 @@ export class CpiEmitter extends CallEmitter {
2642
2642
  this.fail(obj, `missing field "${f.name}" of ${s.name}`);
2643
2643
  for (const [name, e] of given) {
2644
2644
  const f = s.fields.find((x) => x.name === name);
2645
- if (f.type.kind === "vec")
2646
- this.fail(obj, "Vec fields are not supported in events yet");
2645
+ if (f.type.kind === "vec" && f.type.cap !== undefined) {
2646
+ // A FixedVec keeps its fixed layout, as in account data: u32 length, then room for N elements.
2647
+ const ft = f.type;
2648
+ const t = this.tmp("evv");
2649
+ this.line(`${fixedVecC(ft)} ${t};`);
2650
+ this.line(`ts_memzero(&${t}, sizeof(${t}));`);
2651
+ this.writeAggregate({ lv: t, type: ft, writable: true }, e);
2652
+ const n = this.fieldBytes(ft);
2653
+ staged.set(name, (put) => put(`&${t}`, String(n), n));
2654
+ continue;
2655
+ }
2656
+ if (f.type.kind === "vec") {
2657
+ // A Vec is Borsh: u32 length, then the elements, read from a Vec argument or account data when emitted.
2658
+ const l = ts.isIdentifier(e) ? this.lookup(e.text) : undefined;
2659
+ const r = l?.type.kind === "vecArg" ? undefined : this.ref(e);
2660
+ const elemSize = this.fieldBytes(f.type.elem);
2661
+ if (l?.type.kind === "vecArg") {
2662
+ if (!sameType(l.type.elem, f.type.elem))
2663
+ this.fail(e, `expected a ${typeName(f.type)}`);
2664
+ const c = l.c;
2665
+ staged.set(name, (put) => {
2666
+ const n = this.tmp("vl");
2667
+ this.line(`uint32_t ${n} = (uint32_t)${c}.len;`);
2668
+ put(`&${n}`, "4");
2669
+ put(`(const uint8_t *)${c}.ptr`, `(uint64_t)${n} * ${elemSize}`);
2670
+ });
2671
+ }
2672
+ else if (r?.vec && r.type.kind === "vec" && sameType(r.type.elem, f.type.elem)) {
2673
+ const info = r.vec;
2674
+ staged.set(name, (put) => {
2675
+ const n = this.tmp("vl");
2676
+ this.line(`uint32_t ${n} = (uint32_t)${vecCount(info)};`);
2677
+ put(`&${n}`, "4");
2678
+ put(`(const uint8_t *)${info.items}`, `(uint64_t)${n} * ${elemSize}`);
2679
+ });
2680
+ }
2681
+ else
2682
+ this.fail(e, `expected a ${typeName(f.type)}: a Vec argument or a list in account data`);
2683
+ continue;
2684
+ }
2647
2685
  if (f.type.kind === "option" && f.type.inner.kind === "struct")
2648
2686
  this.fail(obj, "Option<struct> fields are not supported in events yet");
2649
2687
  const vt = f.type.kind === "string" ? STRING : f.type.kind === "option" || isScalar(f.type) ? f.type : undefined;
@@ -174,6 +174,8 @@ export class ExpressionEmitter extends StorageEmitter {
174
174
  this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
175
175
  if (l.type.kind === "array")
176
176
  this.fail(node, `"${node.text}" is an array; use .get(i)`);
177
+ if (l.type.kind === "vec")
178
+ this.fail(node, `"${node.text}" is a list; use .get(i) and .length`);
177
179
  if (l.type.kind === "remaining")
178
180
  return { c: "", type: l.type };
179
181
  return l.type.kind === "pubkey" && !l.ptr ? { c: `(&${l.c})`, type: l.type } : { c: l.c, type: l.type };
@@ -261,6 +263,13 @@ export class ExpressionEmitter extends StorageEmitter {
261
263
  if (field === "length") {
262
264
  if (this.isRemaining(node.expression))
263
265
  return { c: `((uint64_t)(params->ka_num - ${this.ix.accounts.length}))`, type: U64 };
266
+ // The number of elements a map / filter chain gives: counted, nothing stored.
267
+ if (this.isPipeline(node.expression)) {
268
+ const n = this.tmp("count");
269
+ this.line(`uint64_t ${n} = 0;`);
270
+ this.eachElement(node.expression, node, { index: `__ln${this.temp++}`, ci: this.tmp("ln_i") }, () => this.line(`${n}++;`));
271
+ return { c: n, type: U64 };
272
+ }
264
273
  const arr = this.ref(node.expression);
265
274
  if (arr?.type.kind === "array")
266
275
  return { c: `${arr.type.len}ULL`, type: U64 };
@@ -3,7 +3,7 @@ import { snakeCase } from "../analyze.js";
3
3
  import { decodeAddress } from "../../keys.js";
4
4
  import { INT_INFO, isScalar, isWritable, scalarSize, typeName, } from "../model.js";
5
5
  import { borshIdentical, borshMinSize } from "./borsh.js";
6
- import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature } from "./shared.js";
6
+ import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature, emptySizeC, vecLenCheck } from "./shared.js";
7
7
  import { StatementEmitter } from "./statements.js";
8
8
  /**
9
9
  * One instruction or helper as a C function: the instruction prologue (account checks, constraints and
@@ -42,7 +42,7 @@ export class FunctionEmitter extends StatementEmitter {
42
42
  head.push(` TS_TRY(ts_check_writable(${v}));`);
43
43
  // A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
44
44
  if (k.kind === "account")
45
- head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : `sizeof(TSS_${k.struct.name})`}));`);
45
+ head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : emptySizeC(k.struct)}));`);
46
46
  // A token account created by this instruction is checked where it is created (create/createIfNeeded).
47
47
  if (k.kind === "token" && !k.init)
48
48
  head.push(` TS_TRY(ts_check_token_account(${v}));`);
@@ -106,7 +106,7 @@ export class FunctionEmitter extends StatementEmitter {
106
106
  if (k.struct.vec) {
107
107
  head.push(` uint64_t entry_${a.name} = ${v}->data_len;`, ` (void)entry_${a.name};`);
108
108
  if (k.kind === "account")
109
- head.push(` TS_TRY(ts_check_vec_len(${v}, sizeof(TSS_${k.struct.name}), d_${a.name}->${k.struct.vec.name}__len, TS_ELEM_${k.struct.name}));`);
109
+ head.push(...vecLenCheck(k.struct, v, `d_${a.name}`));
110
110
  }
111
111
  }
112
112
  // realloc(x, …) needs the size the account had when the instruction started (the 10 KiB growth limit).
@@ -219,6 +219,8 @@ export class FunctionEmitter extends StatementEmitter {
219
219
  const size = this.fieldBytes(elem);
220
220
  need("4");
221
221
  head.push(` TS_VecArg arg_${a.name};`, ` arg_${a.name}.len = (uint32_t)ts_read_le(${at}, 4);`, ` off += 4;`);
222
+ if (a.type.cap !== undefined)
223
+ head.push(` if (arg_${a.name}.len > ${a.type.cap}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: more elements than the FixedVec argument holds");`);
222
224
  need(`(uint64_t)arg_${a.name}.len * ${size}`);
223
225
  head.push(` arg_${a.name}.ptr = ${at};`, ` off += (uint64_t)arg_${a.name}.len * ${size};`);
224
226
  // Struct elements are read in place, so check them all now; scalars are checked when read.
@@ -1,4 +1,4 @@
1
- import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature, fixedVecC } from "./shared.js";
1
+ import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature, fixedVecC, laterVecC } from "./shared.js";
2
2
  import { FunctionEmitter } from "./function.js";
3
3
  import { emitBorshCodecs } from "./borsh.js";
4
4
  /**
@@ -96,9 +96,14 @@ export function emitProgram(analysis, opts = {}) {
96
96
  return;
97
97
  emitted.add(s.name);
98
98
  s.fields.forEach((f) => deps(f.type));
99
+ // Growable Vecs after the first live past the C struct (see StructDef.vecs): a header type each.
100
+ const later = new Set((s.vecs ?? []).slice(1).map((v) => v.name));
101
+ for (const v of (s.vecs ?? []).slice(1))
102
+ out.push(`typedef struct __attribute__((packed)) {`, ` uint32_t len;`, ` ${storageDecl(v.elem, "items[]")};`, `} ${laterVecC(s.name, v.name)};`);
99
103
  out.push(`typedef struct __attribute__((packed)) {`);
100
104
  for (const f of s.fields)
101
- out.push(` ${storageDecl(f.type, f.name)};`);
105
+ if (!later.has(f.name))
106
+ out.push(` ${storageDecl(f.type, f.name)};`);
102
107
  if (!s.fields.length)
103
108
  out.push(` uint8_t __empty[0];`); // a marker account (Anchor `pub struct NftAuthority {}`): discriminator only
104
109
  out.push(`} TSS_${s.name};`);
@@ -113,6 +118,8 @@ export function emitProgram(analysis, opts = {}) {
113
118
  emitStruct(s);
114
119
  for (const e of model.dataEnums.values())
115
120
  deps({ kind: "dataEnum", name: e.name });
121
+ for (const t of model.localVecs.values())
122
+ deps(t);
116
123
  for (const k of model.constants.values())
117
124
  if (typeof k.value === "object")
118
125
  out.push(`static const SolPubkey TSK_${k.name} = {${cBytes(k.value.pubkey)}};`);
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { type AccountParam, type FieldType, type Helper, type IntName, type ParamKind, type ScalarType, type ValueType } from "../model.js";
2
+ import { type AccountParam, type FieldType, type Helper, type IntName, type ParamKind, type ScalarType, type StructDef, type ValueType } from "../model.js";
3
3
  /** Maximum accounts the generated entrypoint deserializes. */
4
4
  export declare const MAX_ACCOUNTS = 32;
5
5
  export declare const MAX_SEEDS = 15;
@@ -46,16 +46,27 @@ export interface Ref {
46
46
  vec?: {
47
47
  len: string;
48
48
  items: string;
49
+ /** The account's size without this Vec's elements (so with `n` of them: 8 + fixed + n × elemSize). */
49
50
  fixed: string;
50
51
  elemSize: number;
51
52
  resizable: boolean;
52
53
  cap?: number;
54
+ /** Growable Vecs stored after this one move when it grows or shrinks: where its elements end, and their paths. */
55
+ tail?: {
56
+ end: string;
57
+ dataEnd: string;
58
+ paths: string[];
59
+ };
53
60
  };
54
61
  }
55
62
  export interface Local {
56
63
  c: string;
64
+ /** A value, or a local FixedArray / FixedVec (`fixedArray<T, N>(…)`, `const ys = xs.map(…)`) held in place. */
57
65
  type: ValueType | (FieldType & {
58
66
  kind: "array";
67
+ }) | (FieldType & {
68
+ kind: "vec";
69
+ cap: number;
59
70
  });
60
71
  mutable: boolean;
61
72
  /** Pubkey held as a `const SolPubkey *` (helper parameters) rather than a value. */
@@ -70,7 +81,8 @@ export interface Local {
70
81
  }
71
82
  /** A closure and the scopes it was declared in (its free names resolve there, wherever it is called). */
72
83
  export interface Closure {
73
- node: ts.ArrowFunction | ts.FunctionExpression;
84
+ /** The function expanded: a closure, or a helper expanded where it is called because it was given one. */
85
+ node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration;
74
86
  name: string;
75
87
  scopes: Map<string, Local>[];
76
88
  }
@@ -102,6 +114,24 @@ export interface EmitOptions {
102
114
  /** Log a marker and the remaining compute units before every statement (`typesol build --profile`). */
103
115
  profile?: boolean;
104
116
  }
117
+ /** C type of a growable Vec stored after the first one of struct `s`: `{ uint32_t len; T items[]; }`. */
118
+ export declare function laterVecC(s: string, field: string): string;
119
+ /** The account size of struct `s` with every growable Vec empty, as C (8-byte discriminator not included). */
120
+ export declare function emptySizeC(s: StructDef): string;
121
+ /**
122
+ * Where growable Vec `k` of a struct at `base` (a C lvalue of type TSS_<S>) starts, as C: its length and elements.
123
+ * The first is a member of the struct; each further one starts right after the previous one's elements.
124
+ */
125
+ export declare function vecPlace(s: StructDef, base: string, k: number): {
126
+ len: string;
127
+ items: string;
128
+ end: string;
129
+ };
130
+ /**
131
+ * C lines checking that an account's size matches its growable Vecs' lengths (`d`: its TSS_<S> pointer, `a`: its
132
+ * TsAccount). One Vec: ts_check_vec_len; more: each length read only once the bytes before it are known to exist.
133
+ */
134
+ export declare function vecLenCheck(s: StructDef, a: string, d: string, pad?: string): string[];
105
135
  /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
106
136
  export declare function fixedVecC(t: FieldType & {
107
137
  kind: "vec";