@typesol/cli 0.16.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.
@@ -12,6 +12,11 @@ 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;
@@ -1,6 +1,7 @@
1
1
  import ts from "typescript";
2
- import { INT_INFO, unsignedC, sameType, typeName } from "../model.js";
3
- import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC } from "./shared.js";
2
+ import { calleeName, resolveValueType } from "../analyze.js";
3
+ import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.js";
4
+ import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
4
5
  import { StorageEmitter } from "./storage.js";
5
6
  /**
6
7
  * Expressions: literals, operators with checked arithmetic, property reads, conditionals, text and casts.
@@ -51,6 +52,52 @@ export class ExpressionEmitter extends StorageEmitter {
51
52
  return { c: `(${t}.v != 0)`, type: inner };
52
53
  return { c: `${t}.v`, type: inner };
53
54
  }
55
+ /**
56
+ * `acct?.field` on an optional account, `x.home?.field` on an Option<struct> place: the field as an Option, empty
57
+ * when the account was left out / the option is empty. (A method call through `?.` is not supported.)
58
+ */
59
+ optionalChain(node) {
60
+ const base = node.expression;
61
+ const optAccount = ts.isIdentifier(base) ? this.accounts.get(base.text) : undefined;
62
+ let present;
63
+ if (optAccount?.optional)
64
+ present = `a_${optAccount.name} != NULL`;
65
+ else {
66
+ const r = this.ref(base);
67
+ if (!r || r.type.kind !== "option" || r.type.inner.kind !== "struct")
68
+ this.fail(node, "?. works on optional accounts and Option<struct> values (other values are never null)");
69
+ present = `${r.lv}.some`;
70
+ }
71
+ // The same access without `?.`, lowered where the base is known to be there.
72
+ const f = ts.factory;
73
+ const plain = f.createPropertyAccessExpression(base, node.name);
74
+ ts.setTextRange(plain, node);
75
+ plain.parent = node.parent;
76
+ const t = this.tmp("oc");
77
+ const declAt = this.lines.length;
78
+ this.line(`/* ${t} */`);
79
+ let inner;
80
+ this.line(`if (${present}) {`);
81
+ this.block(() => {
82
+ const v = this.expr(plain);
83
+ const typed = v.literal !== undefined ? this.literal(v.literal, U64, node) : v;
84
+ if (typed.type.kind === "option") {
85
+ inner = typed.type.inner;
86
+ this.line(`ts_memcpy(&${t}, &${typed.c}, sizeof(${t}));`);
87
+ return;
88
+ }
89
+ if (typed.type.kind !== "int" && typed.type.kind !== "bool" && typed.type.kind !== "pubkey" && typed.type.kind !== "enum")
90
+ this.fail(node, `?. gives an Option, so the field must be an integer, boolean, Pubkey or enum, not ${typeName(typed.type)}`);
91
+ inner = typed.type;
92
+ this.line(`${t}.some = 1;`);
93
+ this.line(inner.kind === "pubkey" ? `${t}.v = *(${typed.c});` : `${t}.v = ${typed.c};`);
94
+ });
95
+ this.line(`}`);
96
+ const scalar = inner;
97
+ this.model.options.set(optionKey(scalar), scalar);
98
+ this.lines[declAt] = `${" ".repeat(this.indent)}${optC(scalar)} ${t};\n${" ".repeat(this.indent)}ts_memzero(&${t}, sizeof(${t}));`;
99
+ return { c: t, type: { kind: "option", inner: scalar } };
100
+ }
54
101
  literal(value, type, node) {
55
102
  if (type.kind !== "int")
56
103
  this.fail(node, `expected ${typeName(type)}, got an integer literal`);
@@ -62,6 +109,20 @@ export class ExpressionEmitter extends StorageEmitter {
62
109
  expr(node, hint) {
63
110
  if (ts.isParenthesizedExpression(node))
64
111
  return this.expr(node.expression, hint);
112
+ // `x as T`: an annotation the value must already match (no conversion: use u8(x) etc. for that). Inside a
113
+ // generic helper it names the instance's type: `return (a + b) as T`.
114
+ if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
115
+ if (ts.isAsExpression(node) && ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName) && node.type.typeName.text === "const")
116
+ return this.expr(node.expression, hint);
117
+ const t = resolveValueType(node.type, this.scope);
118
+ if (!t || t.kind === "vecArg" || t.kind === "dataEnumArg")
119
+ this.fail(node.type, "`as` names an integer, boolean, Pubkey, enum, option or string type the value already has");
120
+ const v = this.expr(node.expression, t);
121
+ // Not a conversion: `x as u64 | null` does not make an Option of a u64.
122
+ if (v.literal === undefined && !v.isNull && !sameType(v.type, t))
123
+ this.fail(node, `this is a ${typeName(v.type)}, not a ${typeName(t)}: \`as\` does not convert (use u64(x) and the like for integers)`);
124
+ return this.expect(v, t, node.expression);
125
+ }
65
126
  // `x!` on an Option: its value, aborting with 5108 when it is empty (JS would carry the null on).
66
127
  if (ts.isNonNullExpression(node)) {
67
128
  const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
@@ -69,6 +130,11 @@ export class ExpressionEmitter extends StorageEmitter {
69
130
  this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
70
131
  return this.unwrap(v, node);
71
132
  }
133
+ if (ts.isPropertyAccessExpression(node) && node.questionDotToken)
134
+ return this.optionalChain(node);
135
+ const unbanged = this.withoutBang(node);
136
+ if (unbanged)
137
+ return this.expr(unbanged, hint);
72
138
  this.noOptionalChain(node);
73
139
  if (ts.isBigIntLiteral(node)) {
74
140
  const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
@@ -111,6 +177,8 @@ export class ExpressionEmitter extends StorageEmitter {
111
177
  if (ts.isIdentifier(node)) {
112
178
  const l = this.lookup(node.text);
113
179
  if (l) {
180
+ if (l.closure)
181
+ this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
114
182
  if (l.type.kind === "array")
115
183
  this.fail(node, `"${node.text}" is an array; use .get(i)`);
116
184
  if (l.type.kind === "remaining")
@@ -140,6 +208,8 @@ export class ExpressionEmitter extends StorageEmitter {
140
208
  }
141
209
  if (this.accounts.has(node.text))
142
210
  this.fail(node, `"${node.text}" is an account; use ${node.text}.key for its address`);
211
+ if (calleeName(node, this.model.helperDecls, this.checker) || this.model.helpers.has(node.text))
212
+ this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
143
213
  this.fail(node, `unknown name "${node.text}"`);
144
214
  }
145
215
  if (ts.isPropertyAccessExpression(node))
@@ -202,7 +272,7 @@ export class ExpressionEmitter extends StorageEmitter {
202
272
  if (arr?.type.kind === "array")
203
273
  return { c: `${arr.type.len}ULL`, type: U64 };
204
274
  if (arr?.type.kind === "vec")
205
- return { c: `((uint64_t)${arr.vec.len})`, type: U64 };
275
+ return { c: vecCount(arr.vec), type: U64 };
206
276
  if (ts.isIdentifier(node.expression)) {
207
277
  const l = this.lookup(node.expression.text);
208
278
  if (l?.type.kind === "vecArg")
@@ -231,8 +301,11 @@ export class ExpressionEmitter extends StorageEmitter {
231
301
  if (ts.isIdentifier(node.expression)) {
232
302
  const a = this.accounts.get(node.expression.text);
233
303
  if (a) {
234
- this.assertUsable(a, node);
235
304
  const v = `a_${a.name}`;
305
+ // The address is known before an Init account is created (Anchor seeds use `mint.key()` that way).
306
+ if (field === "key" && !a.optional)
307
+ return { c: `&${v}->key`, type: PUBKEY };
308
+ this.assertUsable(a, node);
236
309
  if (field === "key")
237
310
  return { c: `&${v}->key`, type: PUBKEY };
238
311
  if (field === "lamports")
@@ -387,7 +460,14 @@ export class ExpressionEmitter extends StorageEmitter {
387
460
  this.line(this.textOfInt(typed.type.name, typed.c, t));
388
461
  return t;
389
462
  }
390
- this.fail(node, `only strings, integers and booleans can be joined into text, not ${typeName(typed.type)}`);
463
+ if (typed.type.kind === "pubkey") {
464
+ // A Pubkey (or Hash) as base58, as Rust's `{}` prints it.
465
+ const t = this.tmp("b58");
466
+ this.line(`TS_Str ${t};`);
467
+ this.line(`TS_TRY(ts_base58_32((const uint8_t *)(${typed.c}), &${t}));`);
468
+ return t;
469
+ }
470
+ this.fail(node, `only strings, integers, booleans and Pubkeys can be joined into text, not ${typeName(typed.type)}`);
391
471
  }
392
472
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
393
473
  textOfInt(name, c, out) {
@@ -1,7 +1,8 @@
1
1
  import ts from "typescript";
2
2
  import { snakeCase } from "../analyze.js";
3
3
  import { decodeAddress } from "../../keys.js";
4
- import { INT_INFO, isScalar, isWritable, scalarSize, typeName } from "../model.js";
4
+ import { INT_INFO, isScalar, isWritable, scalarSize, typeName, } from "../model.js";
5
+ import { borshIdentical, borshMinSize } from "./borsh.js";
5
6
  import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature } from "./shared.js";
6
7
  import { StatementEmitter } from "./statements.js";
7
8
  /**
@@ -14,12 +15,12 @@ export class FunctionEmitter extends StatementEmitter {
14
15
  this.scopes = [new Map()];
15
16
  this.line(`(void)params;`);
16
17
  for (const p of h.params)
17
- if (p.account && p.account.kind === "account")
18
+ if (p.account && p.account.kind === "account" && p.account.struct.layout !== "borsh")
18
19
  this.line(`TSS_${p.account.struct.name} *d_${p.name} = (TSS_${p.account.struct.name} *)(a_${p.name}->data + 8);`);
19
- for (const stmt of h.node.body.statements)
20
+ for (const stmt of h.statements)
20
21
  this.reporter.guard(() => this.statement(stmt));
21
22
  // A helper with a result must return one; falling off the end would leave it unset.
22
- const end = h.returns ? ` TS_FAIL(TS_ERR_NO_RETURN, "TypeSOL: ${h.name} ended without returning a value");` : ` return SUCCESS;`;
23
+ const end = h.returns ? ` TS_FAIL(TS_ERR_NO_RETURN, "TypeSOL: ${h.display} ended without returning a value");` : ` return SUCCESS;`;
23
24
  return [`${helperSignature(h)} {`, ...this.hoisted, ...this.lines, end, `}`].join("\n");
24
25
  }
25
26
  emitInstruction() {
@@ -39,13 +40,17 @@ export class FunctionEmitter extends StatementEmitter {
39
40
  head.push(` TS_TRY(ts_check_signer(${v}));`);
40
41
  if (isWritable(k))
41
42
  head.push(` TS_TRY(ts_check_writable(${v}));`);
43
+ // A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
42
44
  if (k.kind === "account")
43
- head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, 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) : `sizeof(TSS_${k.struct.name})`}));`);
44
46
  // A token account created by this instruction is checked where it is created (create/createIfNeeded).
45
47
  if (k.kind === "token" && !k.init)
46
48
  head.push(` TS_TRY(ts_check_token_account(${v}));`);
47
- if (k.kind === "mint")
49
+ // A mint created by this instruction is checked where it is created.
50
+ if (k.kind === "mint" && !k.init)
48
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");`);
49
54
  if (k.kind === "system")
50
55
  head.push(` TS_TRY(ts_check_address(${v}, &TS_SYSTEM_PROGRAM_ID));`);
51
56
  if (k.kind === "instructionsSysvar")
@@ -87,7 +92,16 @@ export class FunctionEmitter extends StatementEmitter {
87
92
  head.push(` TS_TRY(ts_init_zeroed(${v}, params->program_id, TS_DISC_${k.struct.name}, sizeof(TSS_${k.struct.name})));`);
88
93
  if (k.kind === "initIfNeeded" && k.struct.vec)
89
94
  this.reporter.fail(a.node, "InitIfNeeded accounts cannot end in a Vec yet");
90
- if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
95
+ if ((k.kind === "account" || k.kind === "init" || k.kind === "initIfNeeded") && k.struct.layout === "borsh") {
96
+ // Worked on as its fixed-layout struct on the heap, packed back when the instruction returns. The heap is zero at
97
+ // entry and these are its first allocations (before the body uses it), so a string's padding and a list's
98
+ // unused slots start as zero: ts_unpack_* only writes what the Borsh bytes hold.
99
+ const S = k.struct.name;
100
+ head.push(` TSS_${S} *d_${a.name} = NULL;`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&d_${a.name}));`);
101
+ if (k.kind === "account")
102
+ head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}, NULL));`);
103
+ }
104
+ else if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
91
105
  head.push(` TSS_${k.struct.name} *d_${a.name} = (TSS_${k.struct.name} *)(${v}->data + 8);`);
92
106
  if (k.struct.vec) {
93
107
  head.push(` uint64_t entry_${a.name} = ${v}->data_len;`, ` (void)entry_${a.name};`);
@@ -189,6 +203,17 @@ export class FunctionEmitter extends StatementEmitter {
189
203
  }
190
204
  head.push(` }`);
191
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
+ }
192
217
  else if (a.type.kind === "vecArg") {
193
218
  const elem = a.type.elem;
194
219
  const size = this.fieldBytes(elem);
@@ -204,6 +229,21 @@ export class FunctionEmitter extends StatementEmitter {
204
229
  head.push(` for (uint64_t ${i} = 0; ${i} < arg_${a.name}.len; ${i}++) {`, ...checks, ` }`);
205
230
  }
206
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
+ }
207
247
  else if (a.type.kind === "structRef" || a.type.kind === "arrayArg") {
208
248
  // Fixed layouts are Borsh-identical: copy to the heap, then check bools, enums, options and strings.
209
249
  const v = `arg_${a.name}`;
@@ -234,8 +274,17 @@ export class FunctionEmitter extends StatementEmitter {
234
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");`);
235
275
  continue;
236
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
+ }
237
286
  if (!isScalar(f.type))
238
- 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`);
239
288
  const size = scalarSize(f.type);
240
289
  head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
241
290
  head.push(` ts_memcpy(&${v}.v.${variant.name}.${f.name}, params->data + off, ${size});`, ` off += ${size};`);
@@ -273,7 +322,14 @@ export class FunctionEmitter extends StatementEmitter {
273
322
  this.reporter.guard(() => this.statement(stmt, true));
274
323
  for (const name of this.pendingInit) {
275
324
  const k = this.accounts.get(name).kind;
276
- 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)`;
277
333
  this.reporter.error(this.accounts.get(name).node, `Init account "${name}" is never created; call ${how}`);
278
334
  }
279
335
  const end = ix.returns
@@ -285,13 +341,25 @@ export class FunctionEmitter extends StatementEmitter {
285
341
  epilogue() {
286
342
  if (!this.ix)
287
343
  return [];
288
- return this.ix.accounts
289
- .filter((a) => a.constraints.closeTo)
344
+ // @layout borsh accounts go back to their Borsh bytes (unless closed, or never created).
345
+ const packs = this.ix.accounts
346
+ .filter((a) => (a.kind.kind === "account" ? a.kind.mut : a.kind.kind === "init" || a.kind.kind === "initIfNeeded"))
347
+ .filter((a) => a.kind.struct.layout === "borsh")
290
348
  .map((a) => {
291
- const line = `TS_TRY(ts_close(a_${a.name}, a_${a.constraints.closeTo}));`;
292
- const optional = this.optionalIn(line);
293
- return optional.length ? `if (${optional.map((n) => `a_${n} != NULL`).join(" && ")}) ${line}` : line;
349
+ const v = `a_${a.name}`;
350
+ const S = a.kind.struct.name;
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));`;
294
352
  });
353
+ return [
354
+ ...packs,
355
+ ...this.ix.accounts
356
+ .filter((a) => a.constraints.closeTo)
357
+ .map((a) => {
358
+ const line = `TS_TRY(ts_close(a_${a.name}, a_${a.constraints.closeTo}));`;
359
+ const optional = this.optionalIn(line);
360
+ return optional.length ? `if (${optional.map((n) => `a_${n} != NULL`).join(" && ")}) ${line}` : line;
361
+ }),
362
+ ];
295
363
  }
296
364
  constraintChecks(ix) {
297
365
  const out = [];
@@ -1,5 +1,6 @@
1
- import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature } from "./shared.js";
1
+ import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature, fixedVecC } from "./shared.js";
2
2
  import { FunctionEmitter } from "./function.js";
3
+ import { emitBorshCodecs } from "./borsh.js";
3
4
  /**
4
5
  * The whole C translation unit: type declarations (structs, options, enums, events), every helper and instruction
5
6
  * (lowered by FunctionEmitter), and the entry point that dispatches on the instruction discriminator.
@@ -8,17 +9,26 @@ export function emitProgram(analysis, opts = {}) {
8
9
  const { model, reporter } = analysis;
9
10
  // Bodies first: they register events, string-literal errors and PDA descriptions.
10
11
  const helperBodies = [];
11
- for (const h of model.helpers.values()) {
12
- const body = reporter.guard(() => new FunctionEmitter(analysis, h, opts).emitHelper());
13
- if (body)
14
- helperBodies.push(body, "");
15
- }
12
+ const emittedHelpers = new Set();
13
+ // Generic helpers' instances appear while bodies are lowered: emit until none is left.
14
+ const emitHelpers = () => {
15
+ for (const h of model.helpers.values()) {
16
+ if (emittedHelpers.has(h.name))
17
+ continue;
18
+ emittedHelpers.add(h.name);
19
+ const body = reporter.guard(() => new FunctionEmitter(analysis, h, opts).emitHelper());
20
+ if (body)
21
+ helperBodies.push(body, "");
22
+ }
23
+ };
24
+ emitHelpers();
16
25
  const ixBodies = [];
17
26
  for (const ix of model.instructions) {
18
27
  const body = reporter.guard(() => new FunctionEmitter(analysis, ix, opts).emitInstruction());
19
28
  if (body)
20
29
  ixBodies.push(body, "");
21
30
  }
31
+ emitHelpers();
22
32
  const out = [];
23
33
  out.push(`/* Generated by TypeSOL from ${analysis.sourceFile.fileName.split("/").pop()}. Do not edit. */`);
24
34
  // `const HEAP_SIZE_KIB = 64n`: a larger heap, which clients request with ComputeBudget requestHeapFrame.
@@ -43,6 +53,13 @@ export function emitProgram(analysis, opts = {}) {
43
53
  emitStruct(model.structs.get(t.name));
44
54
  if (t.kind === "array" || t.kind === "vec")
45
55
  deps(t.elem);
56
+ if (t.kind === "vec" && t.cap !== undefined) {
57
+ const name = fixedVecC(t);
58
+ if (emitted.has(name))
59
+ return;
60
+ emitted.add(name);
61
+ out.push(`typedef struct __attribute__((packed)) {`, ` uint32_t len;`, ` ${storageDecl(t.elem, `items[${t.cap}]`)};`, `} ${name};`, "");
62
+ }
46
63
  if (t.kind === "option" && t.inner.kind === "struct") {
47
64
  const key = `opt:${t.inner.name}`;
48
65
  if (emitted.has(key))
@@ -99,6 +116,7 @@ export function emitProgram(analysis, opts = {}) {
99
116
  for (const k of model.constants.values())
100
117
  if (typeof k.value === "object")
101
118
  out.push(`static const SolPubkey TSK_${k.name} = {${cBytes(k.value.pubkey)}};`);
119
+ out.push(...emitBorshCodecs(model));
102
120
  const helpers = [...model.helpers.values()];
103
121
  for (const h of helpers)
104
122
  out.push(helperSignature(h) + ";");
@@ -39,11 +39,17 @@ export interface Ref {
39
39
  /** Field path from the account root, when it is a plain chain of fields. */
40
40
  path?: string[];
41
41
  /** For a Vec: the u32 length lvalue, the fixed-part size expression, and whether it may be resized here. */
42
+ /**
43
+ * A Vec place: `len` the u32 length (an lvalue), `items` the element array. A trailing Vec grows the account
44
+ * (`fixed` bytes before it); a FixedVec (`cap`) has room for `cap` elements and never resizes.
45
+ */
42
46
  vec?: {
43
47
  len: string;
48
+ items: string;
44
49
  fixed: string;
45
50
  elemSize: number;
46
51
  resizable: boolean;
52
+ cap?: number;
47
53
  };
48
54
  }
49
55
  export interface Local {
@@ -59,6 +65,14 @@ export interface Local {
59
65
  /** For struct / data-enum references: the storage paths they alias (e.g. "book.asks[]"), and the declaration. */
60
66
  paths?: string[];
61
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>[];
62
76
  }
63
77
  export declare const cBytes: (bytes: number[]) => string;
64
78
  /** C expressions that nothing in a TypeSOL expression can change: temporaries, locals, arguments, account keys, constants. */
@@ -88,3 +102,9 @@ export interface EmitOptions {
88
102
  /** Log a marker and the remaining compute units before every statement (`typesol build --profile`). */
89
103
  profile?: boolean;
90
104
  }
105
+ /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
106
+ export declare function fixedVecC(t: FieldType & {
107
+ kind: "vec";
108
+ }): string;
109
+ /** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
110
+ export declare function vecCount(v: NonNullable<Ref["vec"]>): string;
@@ -1,4 +1,4 @@
1
- import { INT_INFO, isWritable, optionKey, } from "../model.js";
1
+ import { INT_INFO, fixedVecName, isWritable, optionKey, } from "../model.js";
2
2
  // Types, limits and C type helpers shared by the emitter modules.
3
3
  /** Maximum accounts the generated entrypoint deserializes. */
4
4
  export const MAX_ACCOUNTS = 32;
@@ -89,6 +89,10 @@ export function fieldC(t) {
89
89
  case "pubkey":
90
90
  case "enum":
91
91
  return scalarStorageC(t);
92
+ case "vec":
93
+ if (t.cap !== undefined)
94
+ return fixedVecC(t);
95
+ throw new Error("unreachable: a Vec is never an element");
92
96
  default:
93
97
  throw new Error(`unreachable: no element C type for ${t.kind}`);
94
98
  }
@@ -115,6 +119,8 @@ export function storageDecl(t, name) {
115
119
  case "array":
116
120
  return storageDecl(t.elem, `${name}[${t.len}]`);
117
121
  case "vec":
122
+ if (t.cap !== undefined)
123
+ return `${fixedVecC(t)} ${name}`;
118
124
  // Borsh Vec as a trailing flexible array: u32 length, then the elements.
119
125
  return `uint32_t ${name}__len;
120
126
  ${storageDecl(t.elem, `${name}[]`)}`;
@@ -175,8 +181,14 @@ export function accountMismatch(have, want) {
175
181
  export function helperSignature(h) {
176
182
  const params = ["TsParams *params"];
177
183
  for (const p of h.params) {
178
- if (p.account)
184
+ if (p.account) {
179
185
  params.push(`TsAccount *a_${p.name}`);
186
+ if (p.account.kind === "account" && p.account.struct.layout === "borsh")
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}`);
191
+ }
180
192
  else if (p.type.kind === "signerSeeds")
181
193
  params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
182
194
  else if (p.type.kind === "pubkey")
@@ -187,6 +199,14 @@ export function helperSignature(h) {
187
199
  params.push(`${valueC(p.type)} p_${p.name}`);
188
200
  }
189
201
  if (h.returns)
190
- 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`);
191
203
  return `static uint64_t fn_${h.name}(${params.join(", ")})`;
192
204
  }
205
+ /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
206
+ export function fixedVecC(t) {
207
+ return `TSF_${fixedVecName(t)}`;
208
+ }
209
+ /** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
210
+ export function vecCount(v) {
211
+ return v.cap === undefined ? `((uint64_t)${v.len})` : `((uint64_t)(${v.len} < ${v.cap} ? ${v.len} : ${v.cap}))`;
212
+ }
@@ -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
  /**