@typesol/cli 0.17.0 → 0.18.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.
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { calleeName, resolveScalarType } from "../analyze.js";
2
+ import { calleeName, resolveScalarType, structNameOf } from "../analyze.js";
3
3
  import { isScalar, } from "../model.js";
4
4
  import { isStable, cString, valueC } from "./shared.js";
5
5
  /**
@@ -23,6 +23,10 @@ export class EmitterBase {
23
23
  pendingInit = new Set();
24
24
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
25
25
  bumps = new Map();
26
+ /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
27
+ inlineFrames = [];
28
+ /** Closures being expanded (a closure calling itself cannot be expanded). */
29
+ inlining = new Set();
26
30
  /** Enclosing loops, innermost last; each has a `continue` label. */
27
31
  loops = [];
28
32
  ix;
@@ -47,6 +51,7 @@ export class EmitterBase {
47
51
  options: analysis.model.options,
48
52
  dataEnums: analysis.model.dataEnums,
49
53
  typeArgs: "accounts" in target ? undefined : target.typeArgs,
54
+ structArgs: "accounts" in target ? undefined : target.structArgs,
50
55
  };
51
56
  this.checker = analysis.checker;
52
57
  this.analysis = analysis;
@@ -54,7 +59,7 @@ export class EmitterBase {
54
59
  this.ix = target;
55
60
  for (const a of target.accounts) {
56
61
  this.accounts.set(a.name, a);
57
- if (a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || (a.kind.kind === "token" && a.kind.init))
62
+ if (a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || ((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init))
58
63
  this.pendingInit.add(a.name);
59
64
  }
60
65
  for (const a of target.args)
@@ -86,23 +91,59 @@ export class EmitterBase {
86
91
  if (cached !== undefined)
87
92
  return cached ?? undefined;
88
93
  const callee = node.expression;
89
- const name = calleeName(callee, this.model.helperDecls, this.checker) ?? (this.lookup(callee.text) ? undefined : callee.text);
94
+ // A function parameter of this helper instance: the function it was given.
95
+ const passed = this.helper?.fnArgs?.get(callee.text);
96
+ const name = passed ?? calleeName(callee, this.model.helperDecls, this.checker) ?? (this.lookup(callee.text) ? undefined : callee.text);
90
97
  const g = name ? this.model.generics.get(name) : undefined;
91
98
  let h = name ? this.model.helpers.get(name) : undefined;
92
99
  if (g)
93
- h = this.analysis.instantiate(g, this.typeArgsOf(node, g));
100
+ h = this.analysis.instantiate(g, g.typeParams.length ? this.typeArgsOf(node, g) : [], this.fnArgsOf(node, g));
94
101
  else if (h && node.typeArguments)
95
102
  this.fail(node, `${h.display} takes no type arguments`);
96
103
  this.calleeCache.set(node, h ?? null);
97
104
  return h;
98
105
  }
106
+ /**
107
+ * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
108
+ * captures nothing). A closure can only run where it is declared, so it cannot be passed.
109
+ */
110
+ fnArgsOf(node, g) {
111
+ return g.fnParams.map((i) => {
112
+ const arg = node.arguments[i];
113
+ const pname = g.node.parameters[i].name.text;
114
+ if (!arg || !ts.isIdentifier(arg))
115
+ this.fail(arg ?? node, `pass ${g.display}'s ${pname} a named function (declare it: const f = (…) => …)`);
116
+ const passed = this.helper?.fnArgs?.get(arg.text);
117
+ if (passed)
118
+ return passed;
119
+ if (this.lookup(arg.text)?.closure)
120
+ this.fail(arg, `${arg.text} reads the variables of the function around it, so it can only be called there, not passed to ${g.display}`);
121
+ const name = calleeName(arg, this.model.helperDecls, this.checker) ?? arg.text;
122
+ if (this.model.generics.has(name))
123
+ this.fail(arg, `${arg.text} is generic or takes functions itself: pass a plain function`);
124
+ if (!this.model.helpers.has(name))
125
+ this.fail(arg, `${arg.text} is not a function of this program`);
126
+ return name;
127
+ });
128
+ }
129
+ /** A struct written as a type argument (`pick<Point>(…)`). */
130
+ structArg(t) {
131
+ const name = structNameOf(t, this.scope);
132
+ return name && this.model.structs.has(name) ? this.fixedStruct(name, t) : undefined;
133
+ }
134
+ fixedStruct(name, at) {
135
+ const s = this.model.structs.get(name);
136
+ if (s.size === undefined || s.vec)
137
+ this.fail(at, `${name} has variable-size fields; type parameters stand for fixed-size structs`);
138
+ return { kind: "struct", name };
139
+ }
99
140
  /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
100
141
  typeArgsOf(node, g) {
101
142
  const usage = `write ${g.display}<u64>(…) with the types`;
102
143
  if (node.typeArguments) {
103
144
  if (node.typeArguments.length !== g.typeParams.length)
104
145
  this.fail(node, `${g.display} takes ${g.typeParams.length} type argument(s)`);
105
- return node.typeArguments.map((t) => resolveScalarType(t, this.scope) ?? this.fail(t, "type arguments are integers, boolean, Pubkey or enums"));
146
+ return node.typeArguments.map((t) => resolveScalarType(t, this.scope) ?? this.structArg(t) ?? this.fail(t, "type arguments are integers, boolean, Pubkey, enums or fixed-size structs"));
106
147
  }
107
148
  return g.typeParams.map((tp) => {
108
149
  for (const [i, p] of g.node.parameters.entries()) {
@@ -116,10 +157,22 @@ export class EmitterBase {
116
157
  this.hoisted.length = saved.hoisted;
117
158
  this.temp = saved.temp;
118
159
  this.reporter.diagnostics.length = saved.diagnostics;
160
+ if (probe?.type.kind === "structRef")
161
+ return this.fixedStruct(probe.type.struct, arg);
162
+ if (!probe) {
163
+ // A struct place (`p.a`) is not a value: type it as a reference, again without emitting anything.
164
+ const r = this.reporter.guard(() => this.ref(arg));
165
+ this.lines.length = saved.lines;
166
+ this.hoisted.length = saved.hoisted;
167
+ this.temp = saved.temp;
168
+ this.reporter.diagnostics.length = saved.diagnostics;
169
+ if (r?.type.kind === "struct")
170
+ return this.fixedStruct(r.type.name, arg);
171
+ }
119
172
  if (!probe || probe.literal !== undefined || probe.isNull)
120
173
  continue;
121
174
  if (!isScalar(probe.type))
122
- this.fail(arg, `${tp} stands for an integer, boolean, Pubkey or enum, not ${probe.type.kind}`);
175
+ this.fail(arg, `${tp} stands for an integer, boolean, Pubkey, enum or struct, not ${probe.type.kind}`);
123
176
  const t = probe.type;
124
177
  return t.kind === "pubkey" ? (t.hash ? { kind: "pubkey", hash: true } : { kind: "pubkey" }) : t;
125
178
  }
@@ -196,7 +249,7 @@ export class EmitterBase {
196
249
  if ((ts.isPropertyAccessExpression(node) || ts.isCallExpression(node) || ts.isElementAccessExpression(node)) && node.questionDotToken)
197
250
  this.fail(node, "optional chaining (?.) is not supported; check `x !== null` first");
198
251
  if (ts.isNonNullExpression(node))
199
- this.fail(node, "the ! assertion works on Option values only; check `x !== null` so TypeScript narrows it");
252
+ this.fail(node, "the ! assertion works on Option values and optional accounts only; check `x !== null` so TypeScript narrows it");
200
253
  }
201
254
  lookup(name) {
202
255
  for (let i = this.scopes.length - 1; i >= 0; i--) {
@@ -219,7 +272,25 @@ export class EmitterBase {
219
272
  exportName(node) {
220
273
  return ts.isIdentifier(node) ? this.imports.get(node.text) : undefined;
221
274
  }
275
+ /**
276
+ * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
277
+ * failing with 3020 otherwise). Returns undefined for anything else.
278
+ */
279
+ withoutBang(node) {
280
+ if (!ts.isPropertyAccessExpression(node) || !ts.isNonNullExpression(node.expression))
281
+ return undefined;
282
+ const inner = node.expression.expression;
283
+ if (!ts.isIdentifier(inner) || !this.accounts.get(inner.text)?.optional)
284
+ return undefined;
285
+ const plain = ts.factory.createPropertyAccessExpression(inner, node.name);
286
+ ts.setTextRange(plain, node);
287
+ plain.parent = node.parent;
288
+ return plain;
289
+ }
222
290
  accountParam(node, what) {
291
+ // `acct!` on an optional account: the account (its use checks it was passed).
292
+ if (ts.isNonNullExpression(node) && ts.isIdentifier(node.expression) && this.accounts.get(node.expression.text)?.optional)
293
+ return this.accountParam(node.expression, what);
223
294
  if (!ts.isIdentifier(node))
224
295
  this.fail(node, `${what} must be an account parameter`);
225
296
  const a = this.accounts.get(node.text);
@@ -236,7 +307,7 @@ export class EmitterBase {
236
307
  ? "create"
237
308
  : a.kind.kind === "initIfNeeded"
238
309
  ? "createIfNeeded"
239
- : a.kind.kind === "token" && a.kind.init
310
+ : (a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init
240
311
  ? a.kind.init === "create"
241
312
  ? "create"
242
313
  : "createIfNeeded"
@@ -45,16 +45,16 @@ export function borshMinSize(s, model) {
45
45
  let n = 0;
46
46
  for (const f of s.fields) {
47
47
  const t = f.type;
48
- if (t.kind === "fstring" || t.kind === "vec")
48
+ if (t.kind === "fstring" || t.kind === "vec" || t.kind === "string")
49
49
  n += 4;
50
- else if (t.kind === "option")
50
+ else if (t.kind === "option" || t.kind === "dataEnum")
51
51
  n += 1;
52
52
  else
53
53
  n += fixedBytes(t, model);
54
54
  }
55
55
  return n;
56
56
  }
57
- const DESER = `TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account data is not a valid Borsh value")`;
57
+ const DESER = `TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: bytes are not a valid Borsh value")`;
58
58
  const SER = `TS_FAIL(TS_ERR_DID_NOT_SERIALIZE, "TypeSOL: account data too small for the value")`;
59
59
  /** C for `ts_unpack_<S>` and `ts_pack_<S>` of every `@layout borsh` struct. */
60
60
  export function emitBorshCodecs(model) {
@@ -84,7 +84,7 @@ export function emitBorshCodecs(model) {
84
84
  };
85
85
  // Bounds: the caller checked `len >= MIN` (the smallest Borsh form). Fixed-size pieces then always fit; only a
86
86
  // String / Vec / Option payload is checked, against what is left after the fields that follow it still need.
87
- const minOf = (t) => (t.kind === "fstring" || t.kind === "vec" ? 4 : t.kind === "option" ? 1 : fixedBytes(t, model));
87
+ const minOf = (t) => t.kind === "fstring" || t.kind === "vec" || t.kind === "string" ? 4 : t.kind === "option" || t.kind === "dataEnum" ? 1 : fixedBytes(t, model);
88
88
  const minAfter = s.fields.map((_, i) => s.fields.slice(i + 1).reduce((n, f) => n + minOf(f.type), 0));
89
89
  const need = (n, rest, pad) => `${pad}if (${n} > len - at - ${rest}) ${DESER};`;
90
90
  const room = (n, rest, pad) => `${pad}if (${n} > cap - at - ${rest}) ${SER};`;
@@ -149,6 +149,43 @@ export function emitBorshCodecs(model) {
149
149
  `${pad}}`,
150
150
  ];
151
151
  }
152
+ case "string": {
153
+ // A view of the bytes it was read from (arguments only), checked as UTF-8 as Borsh String deserialization does.
154
+ const l = tmp("n");
155
+ return [
156
+ `${pad}uint32_t ${l} = (uint32_t)ts_read_le(src + at, 4);`,
157
+ `${pad}at += 4;`,
158
+ need(l, rest, pad),
159
+ `${pad}if (!ts_utf8_valid(src + at, ${l})) ${DESER};`,
160
+ `${pad}${lv} = ts_str(src + at, ${l});`,
161
+ `${pad}at += ${l};`,
162
+ ];
163
+ }
164
+ case "dataEnum": {
165
+ const e = model.dataEnums.get(t.name);
166
+ const tag = tmp("tag");
167
+ const lines = [
168
+ `${pad}uint8_t ${tag} = src[at++];`,
169
+ `${pad}if (${tag} >= ${e.variants.length}) ${DESER};`,
170
+ `${pad}${lv}.tag = ${tag};`,
171
+ `${pad}switch (${tag}) {`,
172
+ ];
173
+ e.variants.forEach((v, i) => {
174
+ lines.push(`${pad}case ${i}: {`);
175
+ for (const f of v.fields) {
176
+ const flv = `${lv}.v.${v.name}.${f.name}`;
177
+ if (f.type.kind === "string") {
178
+ lines.push(`${pad} if (4 > len - at - ${rest}) ${DESER};`, ...unpackVar(f.type, flv, rest, `${pad} `));
179
+ continue;
180
+ }
181
+ const n = fixedBytes(f.type, model);
182
+ lines.push(need(String(n), rest, `${pad} `), `${pad} ts_memcpy(&${flv}, src + at, ${n});`, `${pad} at += ${n};`, ...validate(f.type, flv, `${pad} `));
183
+ }
184
+ lines.push(`${pad} break;`, `${pad}}`);
185
+ });
186
+ lines.push(`${pad}}`);
187
+ return lines;
188
+ }
152
189
  default:
153
190
  throw new Error(`unreachable: ${t.kind} in a Borsh account`);
154
191
  }
@@ -199,6 +236,35 @@ export function emitBorshCodecs(model) {
199
236
  `${pad}}`,
200
237
  ];
201
238
  }
239
+ case "string": {
240
+ const l = tmp("n");
241
+ return [
242
+ `${pad}uint32_t ${l} = (uint32_t)${lv}.len;`,
243
+ room(`4 + (uint64_t)${l}`, rest, pad),
244
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
245
+ `${pad}ts_memcpy(dst + at + 4, ${lv}.ptr, ${l});`,
246
+ `${pad}at += 4 + (uint64_t)${l};`,
247
+ ];
248
+ }
249
+ case "dataEnum": {
250
+ const e = model.dataEnums.get(t.name);
251
+ const lines = [room("1", rest, pad), `${pad}dst[at++] = ${lv}.tag;`, `${pad}switch (${lv}.tag) {`];
252
+ e.variants.forEach((v, i) => {
253
+ lines.push(`${pad}case ${i}: {`);
254
+ for (const f of v.fields) {
255
+ const flv = `${lv}.v.${v.name}.${f.name}`;
256
+ if (f.type.kind === "string") {
257
+ lines.push(...packVar(f.type, flv, rest, `${pad} `));
258
+ continue;
259
+ }
260
+ const n = fixedBytes(f.type, model);
261
+ lines.push(room(String(n), rest, `${pad} `), `${pad} ts_memcpy(dst + at, &${flv}, ${n});`, `${pad} at += ${n};`);
262
+ }
263
+ lines.push(`${pad} break;`, `${pad}}`);
264
+ });
265
+ lines.push(`${pad}}`);
266
+ return lines;
267
+ }
202
268
  default:
203
269
  throw new Error(`unreachable: ${t.kind} in a Borsh account`);
204
270
  }
@@ -226,7 +292,7 @@ export function emitBorshCodecs(model) {
226
292
  i++;
227
293
  }
228
294
  const min = borshMinSize(s, model);
229
- out.push(`/* ${s.name} as Borsh (Anchor's account layout) <-> its fixed-layout struct. */`, `__attribute__((noinline)) static uint64_t ts_unpack_${s.name}(const uint8_t *src, uint64_t len, TSS_${s.name} *out) {`, ` if (len < ${min}) ${DESER};`, ` uint64_t at = 0;`, ...unpackLines, ` return SUCCESS;`, `}`, `__attribute__((noinline)) static uint64_t ts_pack_${s.name}(const TSS_${s.name} *in, uint8_t *dst, uint64_t cap) {`, ` if (cap < ${min}) ${SER};`, ` uint64_t at = 0;`, ...packLines, ` return SUCCESS;`, `}`, "");
295
+ out.push(`/* ${s.name} as Borsh (Anchor's account layout) <-> its fixed-layout struct. */`, `__attribute__((noinline)) static uint64_t ts_unpack_${s.name}(const uint8_t *src, uint64_t len, TSS_${s.name} *out, uint64_t *used) {`, ` if (len < ${min}) ${DESER};`, ` uint64_t at = 0;`, ...unpackLines, ` if (used) *used = at;`, ` return SUCCESS;`, `}`, `__attribute__((noinline)) static uint64_t ts_pack_${s.name}(const TSS_${s.name} *in, uint8_t *dst, uint64_t cap, uint64_t *written) {`, ` if (cap < ${min}) ${SER};`, ` uint64_t at = 0;`, ...packLines, ` if (written) *written = at;`, ` return SUCCESS;`, `}`, "");
230
296
  }
231
297
  return out;
232
298
  }
@@ -2,9 +2,6 @@ import ts from "typescript";
2
2
  import { type ErrorDef, type FieldType, type Helper } from "../model.js";
3
3
  import { type Value, type Ref } from "./shared.js";
4
4
  import { ExpressionEmitter } from "./expressions.js";
5
- /**
6
- * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
7
- */
8
5
  export declare abstract class CallEmitter extends ExpressionEmitter {
9
6
  protected call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
10
7
  protected vecMethod(node: ts.CallExpression, method: string, v: Ref): Value | undefined;
@@ -32,6 +29,8 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
32
29
  protected loadForeign(node: ts.CallExpression): Value;
33
30
  /** `loadAccount<T>(acct)` / `loadAccountMut<T>(acct)`: checked view of a program account. */
34
31
  protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
32
+ /** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
33
+ private helperGrows;
35
34
  protected callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
36
35
  /** One helper argument: a C argument list fragment (accounts, seeds, references) or a value to settle. */
37
36
  protected helperArg(h: Helper, p: Helper["params"][number], n: ts.Expression): Value | string;
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { resolveScalarType } from "../analyze.js";
2
+ import { calleeName, resolveScalarType } from "../analyze.js";
3
3
  import { decodeAddress } from "../../keys.js";
4
4
  import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
5
5
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
@@ -7,6 +7,8 @@ import { ExpressionEmitter } from "./expressions.js";
7
7
  /**
8
8
  * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
9
9
  */
10
+ /** List methods taking a callback (or a value) that expand into a loop. */
11
+ const LIST_METHODS = new Set(["forEach", "some", "every", "find", "findIndex", "reduce", "includes", "indexOf"]);
10
12
  export class CallEmitter extends ExpressionEmitter {
11
13
  call(node, topLevel) {
12
14
  const callee = node.expression;
@@ -126,14 +128,26 @@ export class CallEmitter extends ExpressionEmitter {
126
128
  }
127
129
  case "mulDiv":
128
130
  case "mulDivCeil": {
129
- // a * b / c with a 128-bit intermediate (u64), rounding down or up; aborts if the result does not fit.
131
+ // a * b / c with a double-width intermediate (128-bit for u64, 256-bit for u128), rounding down or up;
132
+ // aborts if the result does not fit. All three are u64, or all u128 (literals take the others' type).
130
133
  if (args.length !== 3)
131
- this.fail(node, `${builtin}(a, b, c) takes three u64 values`);
132
- const [x, y, z] = this.lowerInOrder(args, [U64, U64, U64]);
134
+ this.fail(node, `${builtin}(a, b, c) takes three u64 or three u128 values`);
135
+ const vals = [];
136
+ for (const a of args) {
137
+ const mark = this.lines.length;
138
+ const v = this.expr(a);
139
+ for (let j = 0; j < vals.length; j++)
140
+ vals[j] = this.settle(vals[j], mark);
141
+ vals.push(v);
142
+ }
143
+ const typed = vals.find((v) => v.literal === undefined);
144
+ const wide = typed?.type.kind === "int" && typed.type.name === "u128";
145
+ const ty = wide ? { kind: "int", name: "u128" } : U64;
146
+ const [x, y, z] = vals.map((v, i) => this.expect(v.literal !== undefined ? this.literal(v.literal, ty, args[i]) : v, ty, args[i]));
133
147
  const t = this.tmp("md");
134
- this.line(`uint64_t ${t};`);
135
- this.line(`TS_TRY(ts_mul_div(${x.c}, ${y.c}, ${z.c}, ${builtin === "mulDivCeil"}, &${t}));`);
136
- return { c: t, type: U64 };
148
+ this.line(`${wide ? "unsigned __int128" : "uint64_t"} ${t};`);
149
+ this.line(`TS_TRY(ts_mul_div${wide ? "_u128" : ""}(${x.c}, ${y.c}, ${z.c}, ${builtin === "mulDivCeil"}, &${t}));`);
150
+ return { c: t, type: ty };
137
151
  }
138
152
  case "isqrt": {
139
153
  if (args.length !== 1)
@@ -219,7 +233,18 @@ export class CallEmitter extends ExpressionEmitter {
219
233
  this.line(`TS_TRY(ts_t22_transfer_fee(${m}, ${amt.c}, &${t}));`);
220
234
  return { c: t, type: U64 };
221
235
  }
236
+ case "isTransferring": {
237
+ // TransferHookAccount (15) on a Token-2022 token account: set only while Token-2022 runs a transfer of it.
238
+ if (args.length !== 1)
239
+ this.fail(node, "isTransferring(tokenAccount) takes the source token account");
240
+ const m = this.anyAccount(args[0], "the token account");
241
+ this.line(`TS_TRY(ts_t22_layout(${m}, 2));`);
242
+ const off = this.tmp("eo");
243
+ this.line(`uint64_t ${off}, ${off}l;`);
244
+ return { c: `(ts_t22_ext(${m}, 15, &${off}, &${off}l) && ${off}l >= 1 && ${m}->data[${off}] != 0)`, type: BOOL };
245
+ }
222
246
  case "transferHookProgram":
247
+ case "transferHookAuthority":
223
248
  case "permanentDelegate":
224
249
  case "mintCloseAuthority": {
225
250
  // Token-2022 mint extensions holding an optional Pubkey: TransferHook (14, program at +32),
@@ -228,7 +253,7 @@ export class CallEmitter extends ExpressionEmitter {
228
253
  this.fail(node, `${builtin}(mint) takes the mint`);
229
254
  const m = this.anyAccount(args[0], "the mint");
230
255
  this.line(`TS_TRY(ts_t22_layout(${m}, 1));`);
231
- const [type, field] = builtin === "transferHookProgram" ? [14, 32] : builtin === "permanentDelegate" ? [12, 0] : [3, 0];
256
+ const [type, field] = builtin === "transferHookProgram" ? [14, 32] : builtin === "transferHookAuthority" ? [14, 0] : builtin === "permanentDelegate" ? [12, 0] : [3, 0];
232
257
  this.model.options.set("pubkey", PUBKEY);
233
258
  const t = this.tmp("ext");
234
259
  this.line(`${optC(PUBKEY)} ${t};`);
@@ -434,11 +459,22 @@ export class CallEmitter extends ExpressionEmitter {
434
459
  }
435
460
  if (builtin)
436
461
  this.fail(callee, `${builtin} cannot be called here`);
462
+ if (ts.isIdentifier(callee)) {
463
+ const local = this.lookup(callee.text);
464
+ if (local?.closure)
465
+ return this.expandClosure(local.closure, node.arguments, node);
466
+ }
437
467
  const helper = this.helperOf(node);
438
468
  if (helper)
439
469
  return this.callHelper(node, helper);
440
470
  if (ts.isPropertyAccessExpression(callee)) {
441
471
  const method = callee.name.text;
472
+ // xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
473
+ if (LIST_METHODS.has(method)) {
474
+ const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
475
+ if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(callee.expression))
476
+ return this.listMethod(node, method);
477
+ }
442
478
  // Vec<T> instruction arguments are read-only views.
443
479
  if (ts.isIdentifier(callee.expression)) {
444
480
  const l = this.lookup(callee.expression.text);
@@ -446,6 +482,10 @@ export class CallEmitter extends ExpressionEmitter {
446
482
  if (method !== "get" || args.length !== 1)
447
483
  this.fail(node, "Vec arguments are read-only: use .get(i) and .length");
448
484
  const elem = l.type.elem;
485
+ if (elem.kind === "string") {
486
+ const idx = this.index(args[0], `(uint64_t)${l.c}.len`);
487
+ return { c: `(((const TS_Str *)${l.c}.ptr)[${idx}])`, type: STRING };
488
+ }
449
489
  if (!isScalar(elem))
450
490
  this.fail(node, `read a field of the element: ${callee.expression.text}.get(i).field`);
451
491
  const idx = this.index(args[0], `(uint64_t)${l.c}.len`);
@@ -506,8 +546,10 @@ export class CallEmitter extends ExpressionEmitter {
506
546
  // Program accounts used through their methods (systemProgram.transfer, tokenProgram.*…) may be optional.
507
547
  if (target?.optional)
508
548
  this.assertUsable(target, callee.expression);
509
- if (target?.kind.kind === "token" && target.kind.init && method === (target.kind.init === "create" ? "create" : "createIfNeeded"))
510
- return this.createAta(node, target, topLevel);
549
+ if ((target?.kind.kind === "token" || target?.kind.kind === "mint") &&
550
+ target.kind.init &&
551
+ method === (target.kind.init === "create" ? "create" : "createIfNeeded"))
552
+ return target.kind.kind === "token" && target.constraints.ata ? this.createAta(node, target, topLevel) : this.createToken(node, target, topLevel);
511
553
  if (target?.kind.kind === "init" && method === "create")
512
554
  return this.create(node, target, topLevel);
513
555
  if (target?.kind.kind === "initIfNeeded" && method === "createIfNeeded")
@@ -574,7 +616,7 @@ export class CallEmitter extends ExpressionEmitter {
574
616
  if (info.cap !== undefined)
575
617
  return; // a FixedVec has its room already
576
618
  if (!info.resizable || !v.account)
577
- this.fail(node, "only Vecs of declared Mut<...> / Init<...> accounts can grow or shrink");
619
+ this.fail(node, "only Vecs of Mut<...> / Init<...> accounts can grow or shrink");
578
620
  this.line(`TS_TRY(ts_resize(params, ${payer}, a_${v.account.name}, 8 + ${info.fixed} + (${newLen}) * ${info.elemSize}, entry_${v.account.name}));`);
579
621
  };
580
622
  switch (method) {
@@ -899,8 +941,37 @@ export class CallEmitter extends ExpressionEmitter {
899
941
  }
900
942
  return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
901
943
  }
944
+ /** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
945
+ helperGrows(body, seen = new Set()) {
946
+ if (seen.has(body))
947
+ return false;
948
+ seen.add(body);
949
+ let found = false;
950
+ const visit = (n) => {
951
+ if (found)
952
+ return;
953
+ if (ts.isCallExpression(n)) {
954
+ // Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
955
+ if (ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
956
+ found = true;
957
+ else if (ts.isIdentifier(n.expression)) {
958
+ const name = calleeName(n.expression, this.model.helperDecls, this.checker);
959
+ const callee = name ? (this.model.generics.get(name)?.node ?? this.model.helpers.get(name)?.node) : undefined;
960
+ if (callee && this.helperGrows(callee, seen))
961
+ found = true;
962
+ }
963
+ }
964
+ ts.forEachChild(n, visit);
965
+ };
966
+ visit(body);
967
+ return found;
968
+ }
902
969
  callHelper(node, h) {
903
- if (node.arguments.length !== h.params.length)
970
+ if (this.ix && !this.ix.accounts.some((a) => a.kind.kind === "system" && !a.optional) && h.params.some((p) => p.account) && this.helperGrows(h.node))
971
+ this.fail(node, `${h.display}() grows a Vec (push): the instruction needs a SystemProgram parameter to top up rent`);
972
+ // Function arguments were bound when the helper was instantiated: they have no C parameter.
973
+ const argNodes = h.fnArgIndexes ? node.arguments.filter((_, i) => !h.fnArgIndexes.includes(i)) : node.arguments;
974
+ if (argNodes.length !== h.params.length)
904
975
  this.fail(node, `${h.display}() takes ${h.params.length} argument(s)`);
905
976
  // Arguments are evaluated left to right: settle earlier ones when a later one runs statements.
906
977
  // A helper that takes accounts may change account data, so account text it gets is copied first.
@@ -908,7 +979,7 @@ export class CallEmitter extends ExpressionEmitter {
908
979
  const values = [];
909
980
  const pieces = [];
910
981
  h.params.forEach((p, i) => {
911
- const n = node.arguments[i];
982
+ const n = argNodes[i];
912
983
  const mark = this.lines.length;
913
984
  const raw = this.helperArg(h, p, n);
914
985
  for (let j = 0; j < values.length; j++)
@@ -922,8 +993,8 @@ export class CallEmitter extends ExpressionEmitter {
922
993
  });
923
994
  // A helper that takes a writable struct account may replace anything stored in it (see assertNotAliased).
924
995
  for (const [i, p] of h.params.entries())
925
- if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(node.arguments[i]))
926
- this.assertNotAliased([node.arguments[i].text], node, h.display);
996
+ if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(argNodes[i]))
997
+ this.assertNotAliased([argNodes[i].text], node, h.display);
927
998
  const args = ["params", ...pieces.map((x) => (typeof x === "number" ? (values[x].type.kind === "pubkey" ? `*(${values[x].c})` : values[x].c) : x))];
928
999
  if (!h.returns) {
929
1000
  this.line(`TS_TRY(fn_${h.name}(${args.join(", ")}));`);
@@ -937,6 +1008,11 @@ export class CallEmitter extends ExpressionEmitter {
937
1008
  this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
938
1009
  return { c: `(&${t})`, type: { kind: "structRef", struct: h.returns.name, writable: true } };
939
1010
  }
1011
+ if (h.returns.kind === "string") {
1012
+ this.line(`TS_Str ${t};`);
1013
+ this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
1014
+ return { c: t, type: STRING };
1015
+ }
940
1016
  this.line(`${scalarC(h.returns)} ${t};`);
941
1017
  this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
942
1018
  return h.returns.kind === "pubkey" ? { c: `(&${t})`, type: h.returns } : { c: t, type: h.returns };
@@ -951,6 +1027,8 @@ export class CallEmitter extends ExpressionEmitter {
951
1027
  // A @layout borsh account goes with the struct it is being worked on as.
952
1028
  if (p.account.kind === "account" && p.account.struct.layout === "borsh")
953
1029
  return `a_${acct.name}, d_${acct.name}`;
1030
+ if (p.account.kind === "account" && p.account.mut && p.account.struct.vec)
1031
+ return `a_${acct.name}, entry_${acct.name}`;
954
1032
  return `a_${acct.name}`;
955
1033
  }
956
1034
  switch (p.type.kind) {
@@ -26,6 +26,25 @@ export declare abstract class CpiEmitter extends CallEmitter {
26
26
  protected describeSeed(el: ts.Expression): IdlSeed | undefined;
27
27
  protected recordPda(account: AccountParam, idl: IdlSeed[] | undefined): void;
28
28
  protected create(node: ts.CallExpression, target: AccountParam, topLevel: boolean, ifNeeded?: boolean): Value;
29
+ /**
30
+ * `vault.create(payer, seeds | null, mint, authority)` on `Init<TokenAccount>` and `mint.create(payer, seeds | null,
31
+ * decimals, authority, freezeAuthority | null)` on `Init<Mint>` (Anchor's `init` with `token::` / `mint::`
32
+ * constraints): a PDA of this program (or, with `null`, a new keypair account that signs), created owned by the
33
+ * instruction's token program and initialized. `createIfNeeded` on `InitIfNeeded<…>` does that unless the account
34
+ * exists, and then checks it is that PDA, owned by the token program, and has that mint and owner / those
35
+ * authorities and decimals (Anchor's init_if_needed checks). create returns the bump (0 for a keypair),
36
+ * createIfNeeded whether it created the account.
37
+ */
38
+ protected createToken(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value;
39
+ /**
40
+ * The extensions in `mint.create(…, { … })` (Anchor's `extensions::…` constraints): their values are evaluated now,
41
+ * in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
42
+ * InitializeMint2) run after the account is created, sized for exactly these extensions.
43
+ */
44
+ protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string): {
45
+ space: number;
46
+ lines: string[];
47
+ };
29
48
  /**
30
49
  * `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
31
50
  * (Anchor's `init` / `init_if_needed` with `associated_token::…`). create always calls the ATA program's Create, which
@@ -47,6 +66,7 @@ export declare abstract class CpiEmitter extends CallEmitter {
47
66
  }, tail?: {
48
67
  from: string;
49
68
  to: string;
69
+ asPassed?: boolean;
50
70
  }): void;
51
71
  protected signerSeeds(node: ts.Expression | undefined, authority: AccountParam, at: ts.Node): {
52
72
  name: string;
@@ -87,6 +107,12 @@ export declare abstract class CpiEmitter extends CallEmitter {
87
107
  protected bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
88
108
  /** Bubblegum V2 (trees on mpl-account-compression with mpl-noop; optional Metaplex Core collections). */
89
109
  protected bubblegumV2(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
110
+ /** Object literal properties by name, refusing others. */
111
+ protected literalProps(o: ts.ObjectLiteralExpression, allowed: string[], what: string): Map<string, ts.Expression>;
112
+ /** Vec<Creator> (address, verified, share) from a literal list; shares add up to 100 when literal. */
113
+ protected bubblegumCreators(creators: ts.Expression | undefined): MplField[];
114
+ /** UpdateArgs: every field optional (left out or null: unchanged). */
115
+ protected bubblegumUpdateArgs(n: ts.Expression): MplField[];
90
116
  /** `acct.key` of an account parameter, as an expression to serialize. */
91
117
  protected keyOf(n: ts.Expression): ts.Expression;
92
118
  /**
@@ -117,6 +143,10 @@ export declare abstract class CpiEmitter extends CallEmitter {
117
143
  authority: string;
118
144
  program: string;
119
145
  }): undefined;
146
+ /** emit<T> of a @layout borsh struct: built like account data, then packed as Anchor serializes it. */
147
+ private emitBorshEvent;
148
+ /** Discriminator + `body`, logged (emit) or self-invoked (emitCpi). */
149
+ private sendEvent;
120
150
  /** Structs reachable through the fields of struct `name` (not including itself unless it recurses). */
121
151
  protected nestedStructs(name: string): Set<string>;
122
152
  /** Structs whose IDL description is the fixed layout: account data, arguments, and everything nested in them. */