@typesol/cli 0.17.0 → 0.19.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.
@@ -12,12 +12,36 @@ export declare abstract class ExpressionEmitter extends StorageEmitter {
12
12
  * check is a backstop: an empty option aborts with 5108.
13
13
  */
14
14
  protected unwrap(v: Value, node: ts.Node): Value;
15
+ /**
16
+ * `acct?.field` on an optional account, `x.home?.field` on an Option<struct> place: the field as an Option, empty
17
+ * when the account was left out / the option is empty. (A method call through `?.` is not supported.)
18
+ */
19
+ protected optionalChain(node: ts.PropertyAccessExpression): Value;
15
20
  protected literal(value: bigint, type: ValueType, node: ts.Node): Value;
16
21
  protected expr(node: ts.Expression, hint?: ValueType): Value;
17
22
  protected property(node: ts.PropertyAccessExpression): Value;
18
23
  /** Whether TypeScript sees this expression as a string (decides `+` between concatenation and arithmetic). */
19
24
  protected looksLikeString(node: ts.Expression): boolean;
20
25
  /** A string view of a string, integer or boolean (for concatenation and templates). */
26
+ /**
27
+ * A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
28
+ * TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
29
+ * first, as JS reads them then.
30
+ */
31
+ protected templatePieces(node: ts.TemplateExpression): {
32
+ text: string;
33
+ arr?: undefined;
34
+ n?: undefined;
35
+ } | {
36
+ text?: undefined;
37
+ arr: string;
38
+ n: number;
39
+ };
40
+ /**
41
+ * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
42
+ * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
43
+ */
44
+ protected constantText(e: ts.Expression): string | undefined;
21
45
  protected toText(node: ts.Expression): string;
22
46
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
23
47
  protected textOfInt(name: IntName, c: string, out: string): string;
@@ -1,6 +1,7 @@
1
1
  import ts from "typescript";
2
+ import { base58 } from "../../keys.js";
2
3
  import { calleeName, resolveValueType } from "../analyze.js";
3
- import { INT_INFO, unsignedC, sameType, typeName } from "../model.js";
4
+ import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.js";
4
5
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
5
6
  import { StorageEmitter } from "./storage.js";
6
7
  /**
@@ -52,6 +53,52 @@ export class ExpressionEmitter extends StorageEmitter {
52
53
  return { c: `(${t}.v != 0)`, type: inner };
53
54
  return { c: `${t}.v`, type: inner };
54
55
  }
56
+ /**
57
+ * `acct?.field` on an optional account, `x.home?.field` on an Option<struct> place: the field as an Option, empty
58
+ * when the account was left out / the option is empty. (A method call through `?.` is not supported.)
59
+ */
60
+ optionalChain(node) {
61
+ const base = node.expression;
62
+ const optAccount = ts.isIdentifier(base) ? this.accounts.get(base.text) : undefined;
63
+ let present;
64
+ if (optAccount?.optional)
65
+ present = `a_${optAccount.name} != NULL`;
66
+ else {
67
+ const r = this.ref(base);
68
+ if (!r || r.type.kind !== "option" || r.type.inner.kind !== "struct")
69
+ this.fail(node, "?. works on optional accounts and Option<struct> values (other values are never null)");
70
+ present = `${r.lv}.some`;
71
+ }
72
+ // The same access without `?.`, lowered where the base is known to be there.
73
+ const f = ts.factory;
74
+ const plain = f.createPropertyAccessExpression(base, node.name);
75
+ ts.setTextRange(plain, node);
76
+ plain.parent = node.parent;
77
+ const t = this.tmp("oc");
78
+ const declAt = this.lines.length;
79
+ this.line(`/* ${t} */`);
80
+ let inner;
81
+ this.line(`if (${present}) {`);
82
+ this.block(() => {
83
+ const v = this.expr(plain);
84
+ const typed = v.literal !== undefined ? this.literal(v.literal, U64, node) : v;
85
+ if (typed.type.kind === "option") {
86
+ inner = typed.type.inner;
87
+ this.line(`ts_memcpy(&${t}, &${typed.c}, sizeof(${t}));`);
88
+ return;
89
+ }
90
+ if (typed.type.kind !== "int" && typed.type.kind !== "bool" && typed.type.kind !== "pubkey" && typed.type.kind !== "enum")
91
+ this.fail(node, `?. gives an Option, so the field must be an integer, boolean, Pubkey or enum, not ${typeName(typed.type)}`);
92
+ inner = typed.type;
93
+ this.line(`${t}.some = 1;`);
94
+ this.line(inner.kind === "pubkey" ? `${t}.v = *(${typed.c});` : `${t}.v = ${typed.c};`);
95
+ });
96
+ this.line(`}`);
97
+ const scalar = inner;
98
+ this.model.options.set(optionKey(scalar), scalar);
99
+ this.lines[declAt] = `${" ".repeat(this.indent)}${optC(scalar)} ${t};\n${" ".repeat(this.indent)}ts_memzero(&${t}, sizeof(${t}));`;
100
+ return { c: t, type: { kind: "option", inner: scalar } };
101
+ }
55
102
  literal(value, type, node) {
56
103
  if (type.kind !== "int")
57
104
  this.fail(node, `expected ${typeName(type)}, got an integer literal`);
@@ -84,6 +131,11 @@ export class ExpressionEmitter extends StorageEmitter {
84
131
  this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
85
132
  return this.unwrap(v, node);
86
133
  }
134
+ if (ts.isPropertyAccessExpression(node) && node.questionDotToken)
135
+ return this.optionalChain(node);
136
+ const unbanged = this.withoutBang(node);
137
+ if (unbanged)
138
+ return this.expr(unbanged, hint);
87
139
  this.noOptionalChain(node);
88
140
  if (ts.isBigIntLiteral(node)) {
89
141
  const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
@@ -100,22 +152,14 @@ export class ExpressionEmitter extends StorageEmitter {
100
152
  return { c: `ts_str((const uint8_t *)${cString(node.text)}, ${len})`, type: STRING, stringLiteral: node.text };
101
153
  }
102
154
  if (ts.isTemplateExpression(node)) {
103
- // `a${x}b${y}c` -> concatenations on the heap
104
- let acc = this.tmp("tpl");
105
- const head = node.head.text;
106
- this.line(`TS_Str ${acc} = ts_str((const uint8_t *)${cString(head)}, ${Buffer.byteLength(head, "utf8")});`);
107
- for (const span of node.templateSpans) {
108
- for (const piece of [
109
- this.toText(span.expression),
110
- `ts_str((const uint8_t *)${cString(span.literal.text)}, ${Buffer.byteLength(span.literal.text, "utf8")})`,
111
- ]) {
112
- const next = this.tmp("tpl");
113
- this.line(`TS_Str ${next};`);
114
- this.line(`TS_TRY(ts_str_concat(${acc}, ${piece}, &${next}));`);
115
- acc = next;
116
- }
117
- }
118
- return { c: acc, type: STRING };
155
+ // `a${x}b${y}c`: one join on the heap (or a literal when every piece is known at compile time).
156
+ const pieces = this.templatePieces(node);
157
+ if (pieces.text !== undefined)
158
+ return { c: `ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})`, type: STRING, stringLiteral: pieces.text };
159
+ const t = this.tmp("tpl");
160
+ this.line(`TS_Str ${t};`);
161
+ this.line(`TS_TRY(ts_str_join(${pieces.arr}, ${pieces.n}, &${t}));`);
162
+ return { c: t, type: STRING };
119
163
  }
120
164
  if (node.kind === ts.SyntaxKind.NullKeyword)
121
165
  return { c: "", type: BOOL, isNull: true };
@@ -126,6 +170,8 @@ export class ExpressionEmitter extends StorageEmitter {
126
170
  if (ts.isIdentifier(node)) {
127
171
  const l = this.lookup(node.text);
128
172
  if (l) {
173
+ if (l.closure)
174
+ this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
129
175
  if (l.type.kind === "array")
130
176
  this.fail(node, `"${node.text}" is an array; use .get(i)`);
131
177
  if (l.type.kind === "remaining")
@@ -248,8 +294,11 @@ export class ExpressionEmitter extends StorageEmitter {
248
294
  if (ts.isIdentifier(node.expression)) {
249
295
  const a = this.accounts.get(node.expression.text);
250
296
  if (a) {
251
- this.assertUsable(a, node);
252
297
  const v = `a_${a.name}`;
298
+ // The address is known before an Init account is created (Anchor seeds use `mint.key()` that way).
299
+ if (field === "key" && !a.optional)
300
+ return { c: `&${v}->key`, type: PUBKEY };
301
+ this.assertUsable(a, node);
253
302
  if (field === "key")
254
303
  return { c: `&${v}->key`, type: PUBKEY };
255
304
  if (field === "lamports")
@@ -374,6 +423,68 @@ export class ExpressionEmitter extends StorageEmitter {
374
423
  return isText(this.checker.getTypeAtLocation(node));
375
424
  }
376
425
  /** A string view of a string, integer or boolean (for concatenation and templates). */
426
+ /**
427
+ * A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
428
+ * TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
429
+ * first, as JS reads them then.
430
+ */
431
+ templatePieces(node) {
432
+ const parts = [];
433
+ const lit = (text) => {
434
+ if (!text)
435
+ return;
436
+ const last = parts.at(-1);
437
+ if (last?.text !== undefined)
438
+ last.text += text;
439
+ else
440
+ parts.push({ text });
441
+ };
442
+ lit(node.head.text);
443
+ for (const span of node.templateSpans) {
444
+ const known = this.constantText(span.expression);
445
+ if (known !== undefined)
446
+ lit(known);
447
+ else {
448
+ const mark = this.lines.length;
449
+ const c = this.toText(span.expression);
450
+ for (const p of parts)
451
+ if (p.c !== undefined)
452
+ p.c = this.settle({ c: p.c, type: STRING }, mark).c;
453
+ parts.push({ c });
454
+ }
455
+ lit(span.literal.text);
456
+ }
457
+ if (parts.every((p) => p.text !== undefined))
458
+ return { text: parts.map((p) => p.text).join("") };
459
+ const strOf = (text) => `ts_str((const uint8_t *)${cString(text)}, ${Buffer.byteLength(text, "utf8")})`;
460
+ const arr = this.tmp("parts");
461
+ this.line(`TS_Str ${arr}[${parts.length}] = {${parts.map((p) => (p.text !== undefined ? strOf(p.text) : p.c)).join(", ")}};`);
462
+ return { arr, n: parts.length };
463
+ }
464
+ /**
465
+ * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
466
+ * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
467
+ */
468
+ constantText(e) {
469
+ if (ts.isParenthesizedExpression(e))
470
+ return this.constantText(e.expression);
471
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))
472
+ return e.text;
473
+ if (e.kind === ts.SyntaxKind.TrueKeyword)
474
+ return "true";
475
+ if (e.kind === ts.SyntaxKind.FalseKeyword)
476
+ return "false";
477
+ if (ts.isBigIntLiteral(e))
478
+ return BigInt(e.text.slice(0, -1).replace(/_/g, "")).toString();
479
+ if (ts.isIdentifier(e) && !this.lookup(e.text)) {
480
+ const k = this.model.constants.get(e.text);
481
+ if (k && typeof k.value === "string")
482
+ return k.value;
483
+ if (k && typeof k.value === "object")
484
+ return base58(Uint8Array.from(k.value.pubkey));
485
+ }
486
+ return undefined;
487
+ }
377
488
  toText(node) {
378
489
  const v = this.expr(node);
379
490
  if (v.type.kind === "string")
@@ -404,7 +515,14 @@ export class ExpressionEmitter extends StorageEmitter {
404
515
  this.line(this.textOfInt(typed.type.name, typed.c, t));
405
516
  return t;
406
517
  }
407
- this.fail(node, `only strings, integers and booleans can be joined into text, not ${typeName(typed.type)}`);
518
+ if (typed.type.kind === "pubkey") {
519
+ // A Pubkey (or Hash) as base58, as Rust's `{}` prints it.
520
+ const t = this.tmp("b58");
521
+ this.line(`TS_Str ${t};`);
522
+ this.line(`TS_TRY(ts_base58_32((const uint8_t *)(${typed.c}), &${t}));`);
523
+ return t;
524
+ }
525
+ this.fail(node, `only strings, integers, booleans and Pubkeys can be joined into text, not ${typeName(typed.type)}`);
408
526
  }
409
527
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
410
528
  textOfInt(name, c, out) {
@@ -2,7 +2,7 @@ import ts from "typescript";
2
2
  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
- import { borshMinSize } from "./borsh.js";
5
+ import { borshIdentical, borshMinSize } from "./borsh.js";
6
6
  import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature } from "./shared.js";
7
7
  import { StatementEmitter } from "./statements.js";
8
8
  /**
@@ -46,8 +46,11 @@ export class FunctionEmitter extends StatementEmitter {
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}));`);
49
- if (k.kind === "mint")
49
+ // A mint created by this instruction is checked where it is created.
50
+ if (k.kind === "mint" && !k.init)
50
51
  head.push(` TS_TRY(ts_check_mint(${v}));`);
52
+ if (k.kind === "info" && k.systemOwned)
53
+ head.push(` if (!ts_key_eq(&${v}->owner, &TS_SYSTEM_PROGRAM_ID)) TS_FAIL(TS_ERR_NOT_SYSTEM_OWNED, "TypeSOL: ${a.name} must be owned by the System Program");`);
51
54
  if (k.kind === "system")
52
55
  head.push(` TS_TRY(ts_check_address(${v}, &TS_SYSTEM_PROGRAM_ID));`);
53
56
  if (k.kind === "instructionsSysvar")
@@ -96,7 +99,7 @@ export class FunctionEmitter extends StatementEmitter {
96
99
  const S = k.struct.name;
97
100
  head.push(` TSS_${S} *d_${a.name} = NULL;`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&d_${a.name}));`);
98
101
  if (k.kind === "account")
99
- head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}));`);
102
+ head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}, NULL));`);
100
103
  }
101
104
  else if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
102
105
  head.push(` TSS_${k.struct.name} *d_${a.name} = (TSS_${k.struct.name} *)(${v}->data + 8);`);
@@ -200,6 +203,17 @@ export class FunctionEmitter extends StatementEmitter {
200
203
  }
201
204
  head.push(` }`);
202
205
  }
206
+ else if (a.type.kind === "vecArg" && a.type.elem.kind === "string") {
207
+ // Borsh Vec<String>: u32 count, then each string's u32 length and UTF-8 bytes. Kept as views of the
208
+ // instruction data in a heap array of TS_Str (each string at least 4 bytes, so the count is bounded first).
209
+ const v = `arg_${a.name}`;
210
+ const items = `vs_${a.name}`;
211
+ const i = `vi_${a.name}`;
212
+ need("4");
213
+ head.push(` TS_VecArg ${v};`, ` ${v}.len = (uint32_t)ts_read_le(${at}, 4);`, ` off += 4;`);
214
+ need(`(uint64_t)${v}.len * 4`);
215
+ head.push(` TS_Str *${items};`, ` TS_TRY(ts_alloc((uint64_t)${v}.len * sizeof(TS_Str), (uint8_t **)&${items}));`, ` for (uint64_t ${i} = 0; ${i} < ${v}.len; ${i}++) {`, ` if (params->data_len < off + 4) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`, ` uint32_t n = (uint32_t)ts_read_le(params->data + off, 4);`, ` off += 4;`, ` if (params->data_len - off < n) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`, ` if (!ts_utf8_valid(params->data + off, n)) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid UTF-8 in a string argument");`, ` ${items}[${i}] = ts_str(params->data + off, n);`, ` off += n;`, ` }`, ` ${v}.ptr = (const uint8_t *)${items};`);
216
+ }
203
217
  else if (a.type.kind === "vecArg") {
204
218
  const elem = a.type.elem;
205
219
  const size = this.fieldBytes(elem);
@@ -215,6 +229,21 @@ export class FunctionEmitter extends StatementEmitter {
215
229
  head.push(` for (uint64_t ${i} = 0; ${i} < arg_${a.name}.len; ${i}++) {`, ...checks, ` }`);
216
230
  }
217
231
  }
232
+ else if (a.type.kind === "structRef" && this.model.structs.get(a.type.struct).layout === "borsh") {
233
+ // A @layout borsh struct: Borsh-decoded into its fixed-layout struct, as its accounts are.
234
+ const S = a.type.struct;
235
+ const v = `arg_${a.name}`;
236
+ const used = `used_${a.name}`;
237
+ head.push(` TSS_${S} *${v};`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&${v}));`, ` uint64_t ${used} = 0;`, ` if (ts_unpack_${S}(${at}, params->data_len - off, ${v}, &${used}) != SUCCESS) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid ${S} argument");`, ` off += ${used};`);
238
+ // Strings from outside are checked as UTF-8 (account data was checked when it was written).
239
+ for (const f of this.model.structs.get(S).fields) {
240
+ const bad = `TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid UTF-8 in a string argument")`;
241
+ if (f.type.kind === "fstring")
242
+ head.push(` if (!ts_utf8_valid(${v}->${f.name}.bytes, ${v}->${f.name}.len)) ${bad};`);
243
+ else if (f.type.kind === "vec" && f.type.elem.kind === "fstring")
244
+ head.push(` for (uint64_t i = 0; i < ${v}->${f.name}.len; i++)`, ` if (!ts_utf8_valid(${v}->${f.name}.items[i].bytes, ${v}->${f.name}.items[i].len)) ${bad};`);
245
+ }
246
+ }
218
247
  else if (a.type.kind === "structRef" || a.type.kind === "arrayArg") {
219
248
  // Fixed layouts are Borsh-identical: copy to the heap, then check bools, enums, options and strings.
220
249
  const v = `arg_${a.name}`;
@@ -245,8 +274,17 @@ export class FunctionEmitter extends StatementEmitter {
245
274
  head.push(` if (!ts_utf8_valid(${lv}.ptr, ${lv}.len)) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid UTF-8 in a string argument");`);
246
275
  continue;
247
276
  }
277
+ if (!isScalar(f.type) && borshIdentical(f.type, this.model)) {
278
+ // A struct or array whose Borsh bytes are its fixed layout: copied, then checked (bools, enums).
279
+ const lv = `${v}.v.${variant.name}.${f.name}`;
280
+ const size = this.fieldBytes(f.type);
281
+ head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
282
+ head.push(` ts_memcpy(&${lv}, params->data + off, ${size});`, ` off += ${size};`);
283
+ head.push(...this.validation(lv, f.type, " "));
284
+ continue;
285
+ }
248
286
  if (!isScalar(f.type))
249
- this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys, enums and strings`);
287
+ this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys, enums, strings and fixed-size structs and arrays of those`);
250
288
  const size = scalarSize(f.type);
251
289
  head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
252
290
  head.push(` ts_memcpy(&${v}.v.${variant.name}.${f.name}, params->data + off, ${size});`, ` off += ${size};`);
@@ -284,7 +322,14 @@ export class FunctionEmitter extends StatementEmitter {
284
322
  this.reporter.guard(() => this.statement(stmt, true));
285
323
  for (const name of this.pendingInit) {
286
324
  const k = this.accounts.get(name).kind;
287
- const how = k.kind === "token" ? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer)` : `${name}.create(payer, seeds)`;
325
+ const a = this.accounts.get(name);
326
+ const how = k.kind === "token" && a.constraints.ata
327
+ ? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer)`
328
+ : k.kind === "token"
329
+ ? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer, seeds | null, mint, authority)`
330
+ : k.kind === "mint"
331
+ ? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer, seeds | null, decimals, authority, freezeAuthority | null)`
332
+ : `${name}.create(payer, seeds)`;
288
333
  this.reporter.error(this.accounts.get(name).node, `Init account "${name}" is never created; call ${how}`);
289
334
  }
290
335
  const end = ix.returns
@@ -303,7 +348,7 @@ export class FunctionEmitter extends StatementEmitter {
303
348
  .map((a) => {
304
349
  const v = `a_${a.name}`;
305
350
  const S = a.kind.struct.name;
306
- return `if (${a.optional ? `${v} != NULL && ` : ""}${v}->data_len >= 8 && ts_key_eq(&${v}->owner, params->program_id)) TS_TRY(ts_pack_${S}(d_${a.name}, ${v}->data + 8, ${v}->data_len - 8));`;
351
+ return `if (${a.optional ? `${v} != NULL && ` : ""}${v}->data_len >= 8 && ts_key_eq(&${v}->owner, params->program_id)) TS_TRY(ts_pack_${S}(d_${a.name}, ${v}->data + 8, ${v}->data_len - 8, NULL));`;
307
352
  });
308
353
  return [
309
354
  ...packs,
@@ -326,15 +371,17 @@ export class FunctionEmitter extends StatementEmitter {
326
371
  const k = a.kind.kind;
327
372
  if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
328
373
  this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
329
- for (const f of c.hasOne) {
374
+ for (const entry of c.hasOne) {
375
+ // "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
376
+ const [f, p = f] = entry.split("=");
330
377
  if (k === "token")
331
378
  out.push(f === "mint"
332
- ? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${f}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${f}");`
333
- : ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${f}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${f}");`);
379
+ ? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${p}");`
380
+ : ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${p}");`);
334
381
  else if (k === "mint") {
335
382
  const at = f === "mintAuthority" ? 0 : 46;
336
383
  const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
337
- out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${f}->key)) TS_FAIL(${code}, "TypeSOL: ${f} is not the ${f} of ${a.name}");`);
384
+ out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
338
385
  }
339
386
  else
340
387
  out.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
@@ -65,6 +65,14 @@ export interface Local {
65
65
  /** For struct / data-enum references: the storage paths they alias (e.g. "book.asks[]"), and the declaration. */
66
66
  paths?: string[];
67
67
  decl?: ts.VariableDeclaration;
68
+ /** A local closure (`const f = (…) => …` reading the enclosing function's variables), expanded where it is called. */
69
+ closure?: Closure;
70
+ }
71
+ /** A closure and the scopes it was declared in (its free names resolve there, wherever it is called). */
72
+ export interface Closure {
73
+ node: ts.ArrowFunction | ts.FunctionExpression;
74
+ name: string;
75
+ scopes: Map<string, Local>[];
68
76
  }
69
77
  export declare const cBytes: (bytes: number[]) => string;
70
78
  /** C expressions that nothing in a TypeSOL expression can change: temporaries, locals, arguments, account keys, constants. */
@@ -185,6 +185,9 @@ export function helperSignature(h) {
185
185
  params.push(`TsAccount *a_${p.name}`);
186
186
  if (p.account.kind === "account" && p.account.struct.layout === "borsh")
187
187
  params.push(`TSS_${p.account.struct.name} *d_${p.name}`);
188
+ // A writable account with a growable Vec: its length at instruction entry, for push / pop / clear.
189
+ if (p.account.kind === "account" && p.account.mut && p.account.struct.vec)
190
+ params.push(`uint64_t entry_${p.name}`);
188
191
  }
189
192
  else if (p.type.kind === "signerSeeds")
190
193
  params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
@@ -196,7 +199,7 @@ export function helperSignature(h) {
196
199
  params.push(`${valueC(p.type)} p_${p.name}`);
197
200
  }
198
201
  if (h.returns)
199
- params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : `${scalarC(h.returns)} *ret`);
202
+ params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : h.returns.kind === "string" ? `TS_Str *ret` : `${scalarC(h.returns)} *ret`);
200
203
  return `static uint64_t fn_${h.name}(${params.join(", ")})`;
201
204
  }
202
205
  /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
@@ -1,4 +1,5 @@
1
1
  import ts from "typescript";
2
+ import { type Value, type Closure } from "./shared.js";
2
3
  import { CpiEmitter } from "./cpi.js";
3
4
  /**
4
5
  * Statements: variables, assignment, control flow and return.
@@ -23,6 +24,26 @@ export declare abstract class StatementEmitter extends CpiEmitter {
23
24
  * inside the loop so its checked-arithmetic temporaries are recomputed every iteration.
24
25
  */
25
26
  protected loop(cond: ts.Expression | undefined, body: ts.Statement, step: ts.Expression | undefined): void;
27
+ /**
28
+ * Expand a closure where it is called: the arguments are evaluated here, in order, into the parameters (scalars,
29
+ * Pubkeys and strings by value, structs by reference, as JS passes objects); the body then runs with the names it
30
+ * was declared with, so a block around the call that declares the same name does not change what it reads, and
31
+ * writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
32
+ */
33
+ protected expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
34
+ /**
35
+ * `xs.forEach / some / every / find / findIndex / reduce / includes / indexOf` on a Vec, FixedArray, FixedVec or Vec
36
+ * argument: an index loop (the length read each step, as `for … of`), the callback (an arrow written at the call)
37
+ * expanded with each element as `xs.get(i)` gives it (structs by reference) and, if it takes one, the index.
38
+ * `find` gives an Option of the element (scalar elements), `findIndex` / `indexOf` an i64 (-1 when absent).
39
+ */
40
+ protected listMethod(node: ts.CallExpression, method: string): Value | undefined;
41
+ /** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
42
+ protected closureReturn(stmt: ts.ReturnStatement, frame: (typeof this.inlineFrames)[number]): void;
43
+ /** Store a closure's result (fixing its type on the first one) and jump past the rest of its body. */
44
+ protected closureResult(value: Value, at: ts.Expression, frame: (typeof this.inlineFrames)[number]): void;
45
+ /** `do { … } while (cond)`: the body once, then the test; `continue` goes to the test. */
46
+ protected doLoop(stmt: ts.DoStatement): void;
26
47
  protected body(stmt: ts.Statement): void;
27
48
  protected variable(list: ts.VariableDeclarationList, topLevel?: boolean): void;
28
49
  /**