@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.
@@ -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,6 @@
1
1
  import ts from "typescript";
2
2
  import { calleeName, resolveValueType } from "../analyze.js";
3
- import { INT_INFO, unsignedC, sameType, typeName } from "../model.js";
3
+ import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.js";
4
4
  import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
5
5
  import { StorageEmitter } from "./storage.js";
6
6
  /**
@@ -52,6 +52,52 @@ export class ExpressionEmitter extends StorageEmitter {
52
52
  return { c: `(${t}.v != 0)`, type: inner };
53
53
  return { c: `${t}.v`, type: inner };
54
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
+ }
55
101
  literal(value, type, node) {
56
102
  if (type.kind !== "int")
57
103
  this.fail(node, `expected ${typeName(type)}, got an integer literal`);
@@ -84,6 +130,11 @@ export class ExpressionEmitter extends StorageEmitter {
84
130
  this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
85
131
  return this.unwrap(v, node);
86
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);
87
138
  this.noOptionalChain(node);
88
139
  if (ts.isBigIntLiteral(node)) {
89
140
  const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
@@ -126,6 +177,8 @@ export class ExpressionEmitter extends StorageEmitter {
126
177
  if (ts.isIdentifier(node)) {
127
178
  const l = this.lookup(node.text);
128
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`);
129
182
  if (l.type.kind === "array")
130
183
  this.fail(node, `"${node.text}" is an array; use .get(i)`);
131
184
  if (l.type.kind === "remaining")
@@ -248,8 +301,11 @@ export class ExpressionEmitter extends StorageEmitter {
248
301
  if (ts.isIdentifier(node.expression)) {
249
302
  const a = this.accounts.get(node.expression.text);
250
303
  if (a) {
251
- this.assertUsable(a, node);
252
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);
253
309
  if (field === "key")
254
310
  return { c: `&${v}->key`, type: PUBKEY };
255
311
  if (field === "lamports")
@@ -404,7 +460,14 @@ export class ExpressionEmitter extends StorageEmitter {
404
460
  this.line(this.textOfInt(typed.type.name, typed.c, t));
405
461
  return t;
406
462
  }
407
- 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)}`);
408
471
  }
409
472
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
410
473
  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,
@@ -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
  /**