@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.
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { resolveScalarType } from "../analyze.js";
3
3
  import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName } from "../model.js";
4
- import { U64, BOOL, PUBKEY, STRING, optC, fieldC, storageDecl } from "./shared.js";
4
+ import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount } from "./shared.js";
5
5
  import { EmitterBase } from "./base.js";
6
6
  /**
7
7
  * Places in account data and locals: references, reads and writes, Vec/array indexing, aliasing rules and
@@ -242,11 +242,14 @@ export class StorageEmitter extends EmitterBase {
242
242
  ref(node) {
243
243
  if (ts.isParenthesizedExpression(node))
244
244
  return this.ref(node.expression);
245
+ const unbanged = this.withoutBang(node);
246
+ if (unbanged)
247
+ return this.ref(unbanged);
245
248
  this.noOptionalChain(node);
246
249
  // A helper's struct result: a local of the caller's, read and copied like any struct.
247
250
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
248
- const h = this.model.helpers.get(node.expression.text);
249
- if (h?.returns?.kind === "struct" && !this.lookup(node.expression.text)) {
251
+ const h = this.helperOf(node);
252
+ if (h?.returns?.kind === "struct") {
250
253
  const v = this.callHelper(node, h);
251
254
  return { lv: `(*${v.c})`, type: { kind: "struct", name: h.returns.name }, writable: true };
252
255
  }
@@ -273,13 +276,16 @@ export class StorageEmitter extends EmitterBase {
273
276
  return undefined;
274
277
  this.assertUsable(a, node);
275
278
  const r = { lv: `d_${a.name}->${f.name}`, type: f.type, writable: isWritable(a.kind), account: a, path: [f.name] };
276
- if (f.type.kind === "vec")
277
- // Growing needs the account's length at instruction entry: only instructions know it.
279
+ if (f.type.kind === "vec" && f.type.cap !== undefined)
280
+ r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
281
+ else if (f.type.kind === "vec")
282
+ // Growing needs the account's length at instruction entry: instructions have it, helpers get it passed.
278
283
  r.vec = {
279
284
  len: `d_${a.name}->${f.name}__len`,
285
+ items: r.lv,
280
286
  fixed: `sizeof(TSS_${a.kind.struct.name})`,
281
287
  elemSize: a.kind.struct.vec.elemSize,
282
- resizable: isWritable(a.kind) && !!this.ix,
288
+ resizable: isWritable(a.kind),
283
289
  };
284
290
  return r;
285
291
  }
@@ -307,7 +313,7 @@ export class StorageEmitter extends EmitterBase {
307
313
  // TypeScript proved the variant set; this runtime check is the backstop.
308
314
  const cond = located.map((l) => `${base.lv}.tag != ${l.vi}`).join(" && ");
309
315
  this.line(`if (${cond}) TS_FAIL(TS_ERR_ENUM_VARIANT, "TypeSOL: ${e.name} is not ${names.join("/")}");`);
310
- return { ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined };
316
+ return this.withFixedVec({ ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined });
311
317
  }
312
318
  if (base && base.type.kind === "option" && base.type.inner.kind === "struct") {
313
319
  const s = this.model.structs.get(base.type.inner.name);
@@ -315,7 +321,7 @@ export class StorageEmitter extends EmitterBase {
315
321
  if (!f)
316
322
  this.fail(node.name, `${s.name} has no field "${field}"`);
317
323
  this.line(`if (!${base.lv}.some) TS_FAIL(TS_ERR_UNWRAP, "TypeSOL: option is empty");`);
318
- return { ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined };
324
+ return this.withFixedVec({ ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined });
319
325
  }
320
326
  if (base && base.type.kind === "struct") {
321
327
  const s = this.model.structs.get(base.type.name);
@@ -324,9 +330,9 @@ export class StorageEmitter extends EmitterBase {
324
330
  this.fail(node.name, `${s.name} has no field "${field}"`);
325
331
  const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
326
332
  // Vecs of loaded accounts can be read and updated in place but not resized (no entry length known).
327
- if (f.type.kind === "vec")
328
- r.vec = { len: `${base.lv}.${f.name}__len`, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
329
- return r;
333
+ if (f.type.kind === "vec" && f.type.cap === undefined)
334
+ r.vec = { len: `${base.lv}.${f.name}__len`, items: r.lv, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
335
+ return this.withFixedVec(r);
330
336
  }
331
337
  return undefined;
332
338
  }
@@ -334,7 +340,7 @@ export class StorageEmitter extends EmitterBase {
334
340
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "get") {
335
341
  const owner = node.expression.expression;
336
342
  const l = ts.isIdentifier(owner) ? this.lookup(owner.text) : undefined;
337
- if (l?.type.kind === "vecArg" && !isScalar(l.type.elem)) {
343
+ if (l?.type.kind === "vecArg" && !isScalar(l.type.elem) && l.type.elem.kind !== "string") {
338
344
  // A Vec<Struct> argument: elements are read in place (validated when the instruction started).
339
345
  if (node.arguments.length !== 1)
340
346
  this.fail(node, "get(index) takes one argument");
@@ -345,8 +351,9 @@ export class StorageEmitter extends EmitterBase {
345
351
  if (base && (base.type.kind === "array" || base.type.kind === "vec")) {
346
352
  if (node.arguments.length !== 1)
347
353
  this.fail(node, "get(index) takes one argument");
348
- const idx = this.index(node.arguments[0], base.type.kind === "array" ? `${base.type.len}ULL` : `(uint64_t)${base.vec.len}`);
349
- return { ...base, lv: `${base.lv}[${idx}]`, type: base.type.elem, path: undefined, vec: undefined };
354
+ const idx = this.index(node.arguments[0], base.type.kind === "array" ? `${base.type.len}ULL` : vecCount(base.vec));
355
+ const lv = base.type.kind === "array" ? `${base.lv}[${idx}]` : `${base.vec.items}[${idx}]`;
356
+ return this.withFixedVec({ ...base, lv, type: base.type.elem, path: undefined, vec: undefined });
350
357
  }
351
358
  }
352
359
  return undefined;
@@ -474,7 +481,19 @@ export class StorageEmitter extends EmitterBase {
474
481
  * Write a struct or array into storage: from an object literal (field by field, every field
475
482
  * required) or by copying another location of the same type.
476
483
  */
484
+ /** A whole struct account copied as a value (`return vault`, `info = vault`): fixed-size structs only. */
485
+ wholeAccount(node) {
486
+ if (!ts.isIdentifier(node) || this.lookup(node.text))
487
+ return undefined;
488
+ const a = this.accounts.get(node.text);
489
+ if (!a || !(a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded") || a.kind.struct.size === undefined || a.kind.struct.vec)
490
+ return undefined;
491
+ this.assertUsable(a, node);
492
+ return { lv: `(*d_${a.name})`, type: { kind: "struct", name: a.kind.struct.name }, writable: false };
493
+ }
477
494
  writeAggregate(target, valueNode) {
495
+ if (ts.isParenthesizedExpression(valueNode))
496
+ return this.writeAggregate(target, valueNode.expression);
478
497
  if (target.type.kind === "option" && target.type.inner.kind === "struct") {
479
498
  if (valueNode.kind === ts.SyntaxKind.NullKeyword) {
480
499
  this.line(`${target.lv}.some = 0;`);
@@ -534,6 +553,35 @@ export class StorageEmitter extends EmitterBase {
534
553
  this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
535
554
  return;
536
555
  }
556
+ if (target.type.kind === "vec" && target.type.cap !== undefined && ts.isCallExpression(valueNode) && this.exportName(valueNode.expression) === "fixedVec") {
557
+ const list = valueNode.arguments[0];
558
+ if (valueNode.arguments.length !== 1 || !ts.isArrayLiteralExpression(list))
559
+ this.fail(valueNode, "fixedVec([…]) takes an array literal");
560
+ return this.writeAggregate(target, list);
561
+ }
562
+ if (target.type.kind === "vec" && target.type.cap !== undefined && ts.isArrayLiteralExpression(valueNode)) {
563
+ // `list = [a, b]`: built in a temporary (the elements may read the old list), the unused slots zeroed.
564
+ const t = target.type;
565
+ const els = valueNode.elements;
566
+ if (els.length > t.cap)
567
+ this.fail(valueNode, `${els.length} elements do not fit in ${typeName(t)}`);
568
+ const tmp = this.tmp("fv");
569
+ this.line(`${fixedVecC(t)} ${tmp};`);
570
+ this.line(`ts_memzero(&${tmp}, sizeof(${tmp}));`);
571
+ this.line(`${tmp}.len = ${els.length};`);
572
+ els.forEach((e, i) => {
573
+ if (ts.isSpreadElement(e))
574
+ this.fail(e, "list the elements one by one");
575
+ const sub = { lv: `${tmp}.items[${i}]`, type: t.elem, writable: true };
576
+ const vt = this.storageValueType(t.elem);
577
+ if (vt)
578
+ this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(e, vt), vt, e)).c) + ";");
579
+ else
580
+ this.writeAggregate(sub, e);
581
+ });
582
+ this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
583
+ return;
584
+ }
537
585
  if (ts.isObjectLiteralExpression(valueNode)) {
538
586
  if (target.type.kind !== "struct")
539
587
  this.fail(valueNode, `expected a ${typeName(target.type)}, not an object literal`);
@@ -569,12 +617,33 @@ export class StorageEmitter extends EmitterBase {
569
617
  this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
570
618
  return;
571
619
  }
572
- const src = this.ref(valueNode);
620
+ if (ts.isConditionalExpression(valueNode)) {
621
+ // `cond ? a : b` (places, literals or calls): each branch writes the target. A branch that reads the target
622
+ // copies it onto itself, which the copies below allow.
623
+ const cond = this.expect(this.expr(valueNode.condition, BOOL), BOOL, valueNode.condition);
624
+ this.line(`if (${cond.c}) {`);
625
+ this.block(() => this.writeAggregate(target, valueNode.whenTrue));
626
+ this.line(`} else {`);
627
+ this.block(() => this.writeAggregate(target, valueNode.whenFalse));
628
+ this.line(`}`);
629
+ return;
630
+ }
631
+ const src = this.ref(valueNode) ?? this.wholeAccount(valueNode);
573
632
  if (!src || !sameType(src.type, target.type))
574
633
  this.fail(valueNode, `expected a ${typeName(target.type)}: an object literal or another ${typeName(target.type)}`);
575
634
  // Same-typed places are either distinct or identical (`a.p = a.p`); ts_copy skips the latter.
576
635
  this.line(`ts_copy(&${target.lv}, &${src.lv}, sizeof(${target.lv}));`);
577
636
  }
637
+ /** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
638
+ fixedVec(lv, elem, cap) {
639
+ return { len: `${lv}.len`, items: `${lv}.items`, fixed: "", elemSize: this.fieldBytes(elem), resizable: false, cap };
640
+ }
641
+ /** A place whose type is a FixedVec gets its Vec view. */
642
+ withFixedVec(r) {
643
+ if (r.type.kind === "vec" && r.type.cap !== undefined)
644
+ r.vec = this.fixedVec(r.lv, r.type.elem, r.type.cap);
645
+ return r;
646
+ }
578
647
  /** `arr.get(i)` reads storage rather than calling anything. */
579
648
  isStorageCall(node) {
580
649
  return ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "get" && !!this.peekArray(node.expression.expression);
@@ -658,8 +727,11 @@ export class StorageEmitter extends EmitterBase {
658
727
  return this.fieldBytes(t.elem) * t.len;
659
728
  case "struct":
660
729
  return this.model.structs.get(t.name).size;
661
- case "string":
662
730
  case "vec":
731
+ if (t.cap !== undefined)
732
+ return 4 + this.fieldBytes(t.elem) * t.cap;
733
+ throw new Error("unreachable: variable-size field has no fixed size");
734
+ case "string":
663
735
  throw new Error("unreachable: variable-size field has no fixed size");
664
736
  }
665
737
  }
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { snakeCase } from "./analyze.js";
3
3
  import { decodeAddress } from "../keys.js";
4
- import { isWritable, optionKey } from "./model.js";
4
+ import { fixedVecName, isWritable, optionKey, typeName, } from "./model.js";
5
5
  const SYSTEM_PROGRAM = "11111111111111111111111111111111";
6
6
  const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
7
7
  const TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
@@ -37,6 +37,19 @@ const fixedEnumName = (name) => `FixedEnum_${name}`;
37
37
  * IDL type of a field or value. `borshOption` picks the encoding of Option: Borsh `option` for
38
38
  * instruction arguments and events, the fixed { is_some, value } struct for account data.
39
39
  */
40
+ /** IDL type of a @layout borsh account's field: Anchor's own (`string`, `{ vec }`, `{ option }`). */
41
+ function borshIdlType(t) {
42
+ if (t.kind === "fstring")
43
+ return "string";
44
+ if (t.kind === "vec")
45
+ return { vec: borshIdlType(t.elem) };
46
+ if (t.kind === "option")
47
+ return { option: idlType(t.inner) };
48
+ // A data enum in a Borsh struct is the Borsh enum itself (tag, then the variant's fields).
49
+ if (t.kind === "dataEnum")
50
+ return { defined: { name: t.name } };
51
+ return idlType(t);
52
+ }
40
53
  function idlType(t, borshOption = false) {
41
54
  switch (t.kind) {
42
55
  case "int":
@@ -62,7 +75,7 @@ function idlType(t, borshOption = false) {
62
75
  // Borsh enum in arguments and events; fixed tag + payload in account data.
63
76
  return borshOption || t.kind === "dataEnumArg" ? { defined: { name: t.name } } : { defined: { name: fixedEnumName(t.name) } };
64
77
  case "vec":
65
- return { vec: idlType(t.elem) };
78
+ return t.cap === undefined ? { vec: idlType(t.elem) } : { defined: { name: fixedVecName(t) } };
66
79
  case "vecArg":
67
80
  return { vec: idlType(t.elem) };
68
81
  case "arrayArg":
@@ -80,7 +93,7 @@ function accountEntry(ix, name, k, programAddress) {
80
93
  entry.writable = true;
81
94
  if (param?.optional)
82
95
  entry.optional = true;
83
- if (k.kind === "signer")
96
+ if (k.kind === "signer" || param?.signsInit)
84
97
  entry.signer = true;
85
98
  if (k.kind === "system")
86
99
  entry.address = SYSTEM_PROGRAM;
@@ -125,7 +138,7 @@ function accountEntry(ix, name, k, programAddress) {
125
138
  export function buildIdl(model, address, version = "0.1.0") {
126
139
  const structs = [...model.structs.values()];
127
140
  const usedStructs = structs.filter((s) => model.accountStructs.has(s.name) || model.events.has(s.name) || s.size !== undefined);
128
- return {
141
+ const idl = {
129
142
  address,
130
143
  metadata: { name: model.name, version, spec: "0.1.0", description: "Built with TypeSOL" },
131
144
  instructions: model.instructions.map((ix) => ({
@@ -148,7 +161,7 @@ export function buildIdl(model, address, version = "0.1.0") {
148
161
  fields: s.fields.map((f) => ({
149
162
  name: snakeCase(f.name),
150
163
  ...docsOf(s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name)),
151
- type: idlType(f.type, model.events.has(s.name) && !model.accountStructs.has(s.name)),
164
+ type: s.layout === "borsh" ? borshIdlType(f.type) : idlType(f.type, model.events.has(s.name) && !model.accountStructs.has(s.name)),
152
165
  })),
153
166
  },
154
167
  })),
@@ -201,6 +214,17 @@ export function buildIdl(model, address, version = "0.1.0") {
201
214
  ],
202
215
  },
203
216
  })),
217
+ ...[...model.fixedVecs].map(([name, t]) => ({
218
+ name,
219
+ docs: [`TypeSOL FixedVec<${typeName(t.elem)}, ${t.cap}>: \`len\` elements in use, room for ${t.cap} (the rest zero or stale).`],
220
+ type: {
221
+ kind: "struct",
222
+ fields: [
223
+ { name: "len", type: "u32" },
224
+ { name: "items", type: { array: [idlType(t.elem), t.cap] } },
225
+ ],
226
+ },
227
+ })),
204
228
  ...[...model.fstrings]
205
229
  .sort((a, b) => a - b)
206
230
  .map((cap) => ({
@@ -216,4 +240,12 @@ export function buildIdl(model, address, version = "0.1.0") {
216
240
  })),
217
241
  ],
218
242
  };
243
+ // FixedString / FixedVec / FixedOption types only @layout borsh accounts declared (they list theirs as string / vec /
244
+ // option) are left out.
245
+ // FixedVecs first: a kept one may name a FixedString.
246
+ for (const prefix of [/^FixedVec\d/, /^FixedOption_/, /^FixedString\d/]) {
247
+ const text = JSON.stringify([idl.instructions, idl.types.filter((t) => !prefix.test(t.name))]);
248
+ idl.types = idl.types.filter((t) => !prefix.test(t.name) || text.includes(`"name":"${t.name}"`));
249
+ }
250
+ return idl;
219
251
  }
@@ -110,9 +110,11 @@ export type FieldType = ScalarType | {
110
110
  name: string;
111
111
  }
112
112
  /** `Vec<T>`: Borsh u32 length + elements. Only as the last field of an account struct. */
113
+ /** `Vec<T>` (last field, grows the account) or, with `cap`, `FixedVec<T, N>`: a u32 length and room for N elements. */
113
114
  | {
114
115
  kind: "vec";
115
116
  elem: FieldType;
117
+ cap?: number;
116
118
  };
117
119
  export interface StructField {
118
120
  name: string;
@@ -136,14 +138,27 @@ export interface StructDef {
136
138
  accountDiscriminator: number[];
137
139
  eventDiscriminator: number[];
138
140
  node: ts.InterfaceDeclaration;
141
+ /**
142
+ * How account data stores it: "fixed" (every field at a fixed offset, the default) or "borsh" (`@layout borsh`:
143
+ * Anchor's bytes, where strings, Vecs and Options take only what they hold).
144
+ */
145
+ layout: "fixed" | "borsh";
146
+ /**
147
+ * A @layout borsh struct with a `string` field or a data enum carrying strings: they are views of the bytes it was
148
+ * read from, so it is an argument, return value or event, never account data.
149
+ */
150
+ viewsOnly?: boolean;
139
151
  }
140
152
  export type TokenProgramKind = "token" | "token2022" | "tokenInterface";
141
153
  export type ParamKind = {
142
154
  kind: "signer";
143
155
  mut: boolean;
144
- } | {
156
+ }
157
+ /** `systemOwned`: a SystemAccount (Anchor's): owned by the System Program (3011). */
158
+ | {
145
159
  kind: "info";
146
160
  mut: boolean;
161
+ systemOwned?: boolean;
147
162
  } | {
148
163
  kind: "account";
149
164
  struct: StructDef;
@@ -168,9 +183,12 @@ export type ParamKind = {
168
183
  kind: "token";
169
184
  mut: boolean;
170
185
  init?: "create" | "ifNeeded";
171
- } | {
186
+ }
187
+ /** `init`: created in the body (`mint.create(payer, seeds | null, decimals, authority, freeze)`). */
188
+ | {
172
189
  kind: "mint";
173
190
  mut: boolean;
191
+ init?: "create" | "ifNeeded";
174
192
  } | {
175
193
  kind: "system";
176
194
  } | {
@@ -253,6 +271,8 @@ export interface AccountParam {
253
271
  optional?: boolean;
254
272
  node: ts.ParameterDeclaration;
255
273
  constraints: Constraints;
274
+ /** Created in the body as a new keypair account (`create(payer, null, …)`), so it signs: the IDL says so. */
275
+ signsInit?: boolean;
256
276
  }
257
277
  export interface ArgParam {
258
278
  name: string;
@@ -280,6 +300,8 @@ export interface Instruction {
280
300
  minArgsSize: number;
281
301
  /** Sent as return data: a scalar in Borsh, or a fixed-size struct in its fixed layout (as the IDL describes it). */
282
302
  returns: ScalarType | {
303
+ kind: "string";
304
+ } | {
283
305
  kind: "struct";
284
306
  name: string;
285
307
  } | undefined;
@@ -291,6 +313,16 @@ export interface Instruction {
291
313
  /** The declaring file's `typesol` imports (local name -> export name). */
292
314
  imports: Map<string, string>;
293
315
  }
316
+ /** A helper with type parameters, before it is instantiated. */
317
+ export interface GenericHelper {
318
+ name: string;
319
+ display: string;
320
+ /** Indexes of parameters with a function type (`f: (x: u64) => u64`): bound per call to a named function. */
321
+ fnParams: number[];
322
+ node: HelperNode;
323
+ typeParams: string[];
324
+ imports: Map<string, string>;
325
+ }
294
326
  export interface HelperParam {
295
327
  name: string;
296
328
  /** `{ kind: "account" }` for account parameters (then `account` says which kind). */
@@ -298,16 +330,36 @@ export interface HelperParam {
298
330
  account?: ParamKind;
299
331
  node: ts.ParameterDeclaration;
300
332
  }
301
- /** A non-exported function (or any function of an imported file): values and accounts in; void, a scalar or a struct out. */
333
+ /** A function written as `function f(…)`, `const f = (…) => …` or `const f = function (…) {…}`. */
334
+ export type HelperNode = ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression;
335
+ /**
336
+ * A non-exported function (or any function of an imported file), or an arrow function declared with const at the top
337
+ * level or inside a function (capturing nothing): values and accounts in; void, a scalar or a struct out.
338
+ */
302
339
  export interface Helper {
340
+ /** The C name (fn_<name>): the function's name, `outer__local` for a local arrow, `name__u64` for an instance. */
303
341
  name: string;
342
+ /** The name in messages: `local`, `max<u64>`. */
343
+ display: string;
304
344
  params: HelperParam[];
305
345
  /** A struct result is written through an out pointer (`TSS_<name> *ret`) that the caller provides. */
306
346
  returns: ScalarType | {
347
+ kind: "string";
348
+ } | {
307
349
  kind: "struct";
308
350
  name: string;
309
351
  } | undefined;
310
- node: ts.FunctionDeclaration;
352
+ node: HelperNode;
353
+ /** The body; an arrow's expression body is one return (or expression) statement. */
354
+ statements: readonly ts.Statement[];
355
+ /** A generic helper's instance: type parameter -> the type it stands for. */
356
+ typeArgs?: Map<string, ScalarType>;
357
+ /** A generic helper's instance: type parameter -> the struct it stands for. */
358
+ structArgs?: Map<string, string>;
359
+ /** An instance's function parameters: name -> the helper passed for it (its calls go there). */
360
+ fnArgs?: Map<string, string>;
361
+ /** Their argument positions, which have no C parameter. */
362
+ fnArgIndexes?: number[];
311
363
  /** The declaring file's `typesol` imports (local name -> export name). */
312
364
  imports: Map<string, string>;
313
365
  }
@@ -352,6 +404,12 @@ export interface ProgramModel {
352
404
  structs: Map<string, StructDef>;
353
405
  instructions: Instruction[];
354
406
  helpers: Map<string, Helper>;
407
+ /** Generic helpers by name: instantiated (into `helpers`) for the types each call uses. */
408
+ generics: Map<string, GenericHelper>;
409
+ /** Declaration (function, or the const of an arrow) -> helper or generic name, for calls to local arrows. */
410
+ helperDecls: Map<ts.Node, string>;
411
+ /** Local closures (they read the enclosing function's variables): their declaration -> name; expanded where called. */
412
+ closureDecls: Map<ts.Node, string>;
355
413
  errors: ErrorDef[];
356
414
  /** Enum name -> member name -> error */
357
415
  errorEnums: Map<string, Map<string, ErrorDef>>;
@@ -374,6 +432,10 @@ export interface ProgramModel {
374
432
  argStructs: Set<string>;
375
433
  /** FixedString capacities in use, for C typedefs and IDL types. */
376
434
  fstrings: Set<number>;
435
+ /** FixedVec<T, N> types in use (IDL name -> type), for IDL types. */
436
+ fixedVecs: Map<string, FieldType & {
437
+ kind: "vec";
438
+ }>;
377
439
  }
378
440
  export declare function isWritable(k: ParamKind): boolean;
379
441
  export declare function typeName(t: ValueType | FieldType): string;
@@ -386,3 +448,7 @@ export declare function optionKey(t: ScalarType | {
386
448
  name: string;
387
449
  }): string;
388
450
  export declare function scalarKey(t: ScalarType): string;
451
+ /** The IDL type name of a FixedVec<T, N>: `FixedVec<N>_<T>`. */
452
+ export declare function fixedVecName(t: FieldType & {
453
+ kind: "vec";
454
+ }): string;
@@ -56,7 +56,7 @@ export function typeName(t) {
56
56
  case "enumRef":
57
57
  return t.name;
58
58
  case "vec":
59
- return `Vec<${typeName(t.elem)}>`;
59
+ return t.cap === undefined ? `Vec<${typeName(t.elem)}>` : `FixedVec<${typeName(t.elem)}, ${t.cap}>`;
60
60
  case "vecArg":
61
61
  return `Vec<${typeName(t.elem)}>`;
62
62
  case "arrayArg":
@@ -92,7 +92,7 @@ export function sameType(a, b) {
92
92
  case "option":
93
93
  return sameType(a.inner, b.inner);
94
94
  case "vec":
95
- return sameType(a.elem, b.elem);
95
+ return a.cap === b.cap && sameType(a.elem, b.elem);
96
96
  case "vecArg":
97
97
  return sameType(a.elem, b.elem);
98
98
  case "arrayArg":
@@ -120,3 +120,9 @@ export function optionKey(t) {
120
120
  export function scalarKey(t) {
121
121
  return t.kind === "int" ? t.name : t.kind === "enum" ? `e_${t.name}` : t.kind === "pubkey" && t.hash ? "hash" : t.kind;
122
122
  }
123
+ /** The IDL type name of a FixedVec<T, N>: `FixedVec<N>_<T>`. */
124
+ export function fixedVecName(t) {
125
+ return `FixedVec${t.cap}_${typeName(t.elem)
126
+ .replace(/[^A-Za-z0-9]+/g, "_")
127
+ .replace(/_+$/, "")}`;
128
+ }
package/dist/portgen.js CHANGED
@@ -4,8 +4,9 @@
4
4
  * It writes the account structs, enums, errors, events and one function per instruction with its accounts and
5
5
  * arguments, bodies left to fill in. Instructions keep their discriminators (an explicit `@discriminator` when the
6
6
  * name would derive another), account structs keep their names (so their discriminators), errors keep their order —
7
- * so existing clients keep working when the layout allows it. Where TypeSOL stores data differently from Anchor's Borsh
8
- * layout (String, Option and a Vec that is not the last field in account data), it says so in a note and a comment.
7
+ * so existing clients keep working. Account structs holding a String, Vec or Option get `@layout borsh`, so they keep
8
+ * Anchor's bytes (existing accounts stay readable); where a layout still differs (a String or Vec nested in a struct, a
9
+ * data enum, a COption) it says so in a note and a comment.
9
10
  */
10
11
  import { createHash } from "node:crypto";
11
12
  import { cNameProblem } from "./compiler/analyze.js";
@@ -83,6 +84,34 @@ export function generatePort(idl) {
83
84
  for (const ix of idl.instructions)
84
85
  for (const g of ix.args)
85
86
  mark(g.type, "arg");
87
+ // Structs passed or returned whole (not inside another type): these can keep Anchor's Borsh bytes with @layout borsh.
88
+ const whole = new Set();
89
+ const nested = new Set();
90
+ const inside = (t) => {
91
+ if (!t || typeof t === "string")
92
+ return;
93
+ const inner = t.vec ?? t.option ?? t.coption ?? t.array?.[0];
94
+ if (inner)
95
+ return inside(inner);
96
+ if (t.defined)
97
+ nested.add(definedName(t.defined));
98
+ };
99
+ for (const ix of idl.instructions) {
100
+ for (const t of [...ix.args.map((g) => g.type), ix.returns]) {
101
+ if (t && typeof t === "object" && t.defined)
102
+ whole.add(definedName(t.defined));
103
+ else if (t && typeof t === "object")
104
+ inside(t.vec ?? t.option ?? t.coption ?? t.array?.[0]);
105
+ }
106
+ }
107
+ for (const def of idl.types ?? []) {
108
+ for (const f of (def.type.fields ?? []))
109
+ if (typeof f === "object" && f && "type" in f)
110
+ inside(f.type);
111
+ for (const v of def.type.variants ?? [])
112
+ for (const f of v.fields ?? [])
113
+ inside(typeof f === "object" && f && "type" in f ? f.type : f);
114
+ }
86
115
  const fixed = (w) => w === "account" || w === "struct" || w === "variant";
87
116
  const describe = (w) => (w === "account" ? "account data" : w === "struct" ? "a struct argument" : "a data-enum variant");
88
117
  const tsType = (t, where, at, last = false) => {
@@ -152,6 +181,57 @@ export function generatePort(idl) {
152
181
  notes.push(`${at}: unsupported IDL type ${JSON.stringify(t)}; left as u64`);
153
182
  return `${use("u64")} /* TODO */`;
154
183
  };
184
+ /** A String, Vec or Option at a struct's top level: Borsh-sized in Anchor. */
185
+ const borshOnly = (t) => t === "string" || t === "bytes" || (typeof t === "object" && (!!t.vec || !!t.option));
186
+ /** A field's type in a `@layout borsh` struct, or undefined when that layout cannot hold it. Notes the capacities. */
187
+ const borshField = (t, at) => {
188
+ const quiet = at === "";
189
+ const fixedOk = (u) => {
190
+ if (typeof u === "string")
191
+ return INTS.has(u) || u === "bool" || u === "pubkey" || u === "publicKey";
192
+ if (u.array)
193
+ return Number.isInteger(Number(u.array[1])) && fixedOk(u.array[0]);
194
+ if (u.defined) {
195
+ const name = definedName(u.defined);
196
+ const d = defs.get(name);
197
+ if (!d || skipped.has(name) || helperDefs.has(name))
198
+ return false;
199
+ if (d.type.kind === "enum")
200
+ return (d.type.variants ?? []).every((v) => !v.fields?.length);
201
+ return (d.type.fields ?? []).every((f) => fixedOk(f.type));
202
+ }
203
+ return false;
204
+ };
205
+ if (t === "string") {
206
+ if (!quiet)
207
+ notes.push(`${at}: set the String's capacity to Anchor's #[max_len] (Borsh bytes are kept: @layout borsh)`);
208
+ return `${use("FixedString")}<32> /* TODO: #[max_len] */`;
209
+ }
210
+ if (t === "bytes")
211
+ return borshField({ vec: "u8" }, at);
212
+ if (typeof t === "object" && t.vec) {
213
+ const elem = t.vec === "string"
214
+ ? `${use("FixedString")}<32> /* TODO: #[max_len] */`
215
+ : fixedOk(t.vec) && !quiet
216
+ ? tsType(t.vec, "account", at)
217
+ : fixedOk(t.vec)
218
+ ? "x"
219
+ : undefined;
220
+ if (elem === undefined)
221
+ return undefined;
222
+ if (!quiet)
223
+ notes.push(`${at}: set the Vec's capacity to Anchor's #[max_len] (Borsh bytes are kept: @layout borsh)`);
224
+ return `${use("FixedVec")}<${elem}, 8> /* TODO: #[max_len] */`;
225
+ }
226
+ if (typeof t === "object" && t.option) {
227
+ if (!fixedOk(t.option))
228
+ return undefined;
229
+ return quiet ? "x" : `${use("Option")}<${tsType(t.option, "account", at)}>`;
230
+ }
231
+ if (!fixedOk(t))
232
+ return undefined;
233
+ return quiet ? "x" : tsType(t, "account", at);
234
+ };
155
235
  const esc = (s) => s.replace(/\*\//g, "* /");
156
236
  const docs = (lines, pad = "") => (lines?.length ? `${pad}/** ${esc(lines.join(" "))} */\n` : "");
157
237
  const out = [];
@@ -180,8 +260,13 @@ export function generatePort(idl) {
180
260
  const where = used.has("account") ? "account" : used.has("arg") ? "struct" : used.has("event") ? "event" : "account";
181
261
  if (def.type.kind === "struct") {
182
262
  const fields = (def.type.fields ?? []);
263
+ // An account struct, or one passed / returned whole, with a String, Vec or Option of its own: `@layout borsh`
264
+ // keeps Anchor's bytes.
265
+ const borshable = accountNames.has(def.name) || (whole.has(def.name) && !nested.has(def.name) && !eventNames.has(def.name));
266
+ const borsh = borshable && !isTuple(fields) && fields.some((f) => borshOnly(f.type)) && fields.every((f) => borshField(f.type, "") !== undefined);
183
267
  const tag = accountNames.has(def.name) ? " (account)" : eventNames.has(def.name) ? ` (event: emit<${typeName(def.name)}>({ … }))` : "";
184
- out.push(docs(def.docs).trimEnd() || `/** ${def.name}${tag} */`);
268
+ const doc = def.docs?.length ? esc(def.docs.join(" ")) : `${def.name}${tag}`;
269
+ out.push(borsh ? `/**\n * ${doc}\n * @layout borsh\n */` : `/** ${doc} */`);
185
270
  out.push(`export interface ${typeName(def.name)} {`);
186
271
  const seen = new Set();
187
272
  fields.forEach((f, i) => {
@@ -189,7 +274,8 @@ export function generatePort(idl) {
189
274
  while (seen.has(n))
190
275
  n = `${n}2`;
191
276
  seen.add(n);
192
- out.push(`${docs(f.docs, " ")} ${n}: ${tsType(f.type, where, `${def.name}.${f.name}`, i === fields.length - 1)};`);
277
+ const ty = borsh ? borshField(f.type, `${def.name}.${f.name}`) : tsType(f.type, where, `${def.name}.${f.name}`, i === fields.length - 1);
278
+ out.push(`${docs(f.docs, " ")} ${n}: ${ty};`);
193
279
  });
194
280
  out.push("}", "");
195
281
  }
package/docs/errors.md CHANGED
@@ -11,22 +11,24 @@ Framework errors use Anchor's numbers where Anchor has the same check, so client
11
11
  | 2009 | not the associated token account (ConstraintAssociated) |
12
12
  | 2012 | not the expected address (ConstraintAddress) |
13
13
  | 2014 / 2015 | token account mint / owner mismatch (ConstraintTokenMint / ConstraintTokenOwner) |
14
- | 2016 / 2017 | mint authority / freeze authority mismatch |
14
+ | 2016 / 2017 / 2018 | mint authority / freeze authority / decimals mismatch (2018: an existing mint `createIfNeeded` found) |
15
15
  | 2040 | the same account passed in two writable slots (ConstraintDuplicateMutableAccount) |
16
16
  | 3000 | account already exists / initialized (AccountDiscriminatorAlreadySet) |
17
17
  | 3002 | discriminator mismatch (AccountDiscriminatorMismatch) |
18
- | 3003 | data too small, not an initialized token account / mint, or a read past the end (AccountDidNotDeserialize) |
18
+ | 3003 | data too small, not an initialized token account / mint, a read past the end, or a `@layout borsh` account whose bytes are not a valid Borsh value (AccountDidNotDeserialize) |
19
+ | 3004 | a `@layout borsh` account too small for the value it would now hold (AccountDidNotSerialize) |
19
20
  | 3005 | not enough accounts (AccountNotEnoughKeys) |
20
21
  | 3006 | account must be writable (AccountNotMutable) |
21
22
  | 3007 | wrong owner program (AccountOwnedByWrongProgram) |
22
23
  | 3008 / 3009 | wrong program address / not executable (InvalidProgramId / InvalidProgramExecutable) |
23
- | 3010 | account must sign (AccountNotSigner) |
24
+ | 3010 | account must sign (AccountNotSigner; also a new keypair account a `create(payer, null, …)` makes) |
25
+ | 3011 | a `SystemAccount` owned by another program (AccountNotSystemOwned) |
24
26
  | 3012 | account not initialized: empty system account where a program account was expected (AccountNotInitialized) |
25
27
  | 3016 | account would grow more than 10 KiB in one instruction, or grow without a payer (AccountReallocExceedsLimit) |
26
28
  | 4102 | cast overflow (InvalidNumericConversion) |
27
29
  | 5100 / 5101 | arithmetic overflow (including shifts) / division by zero |
28
30
  | 5103 | sysvar unavailable |
29
- | 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec) |
31
+ | 5104 | index out of bounds (arrays, Vecs, remaining accounts, `pop()` on an empty Vec, `push` on a full FixedVec) |
30
32
  | 5105 | string longer than the FixedString capacity |
31
33
  | 5106 | missing or mismatched return data |
32
34
  | 5107 | event larger than 1024 bytes |