@typesol/cli 0.15.0 → 0.17.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +11 -7
  3. package/dist/cli.js +147 -20
  4. package/dist/clientgen.js +37 -0
  5. package/dist/compiler/analyze.d.ts +11 -1
  6. package/dist/compiler/analyze.js +307 -37
  7. package/dist/compiler/emit/base.d.ts +7 -0
  8. package/dist/compiler/emit/base.js +62 -2
  9. package/dist/compiler/emit/borsh.d.ts +13 -0
  10. package/dist/compiler/emit/borsh.js +232 -0
  11. package/dist/compiler/emit/calls.js +50 -15
  12. package/dist/compiler/emit/cpi.d.ts +32 -8
  13. package/dist/compiler/emit/cpi.js +706 -25
  14. package/dist/compiler/emit/expressions.d.ts +11 -0
  15. package/dist/compiler/emit/expressions.js +126 -4
  16. package/dist/compiler/emit/function.js +45 -12
  17. package/dist/compiler/emit/program.js +24 -6
  18. package/dist/compiler/emit/shared.d.ts +12 -0
  19. package/dist/compiler/emit/shared.js +22 -4
  20. package/dist/compiler/emit/statements.d.ts +13 -0
  21. package/dist/compiler/emit/statements.js +141 -7
  22. package/dist/compiler/emit/storage.d.ts +6 -1
  23. package/dist/compiler/emit/storage.js +82 -17
  24. package/dist/compiler/idl.js +61 -18
  25. package/dist/compiler/model.d.ts +56 -4
  26. package/dist/compiler/model.js +8 -2
  27. package/dist/compiler/toolchain.d.ts +2 -0
  28. package/dist/compiler/toolchain.js +27 -1
  29. package/dist/onchain.d.ts +30 -0
  30. package/dist/onchain.js +53 -0
  31. package/dist/portgen.d.ts +87 -0
  32. package/dist/portgen.js +367 -0
  33. package/dist/templates.js +49 -154
  34. package/dist/testing.d.ts +66 -0
  35. package/dist/testing.js +337 -0
  36. package/dist/workspace.js +11 -1
  37. package/docs/architecture.md +3 -0
  38. package/docs/cli.md +16 -2
  39. package/docs/errors.md +3 -2
  40. package/docs/language.md +19 -14
  41. package/docs/mainnet.md +5 -4
  42. package/docs/migrate-from-anchor.md +113 -0
  43. package/docs/test-your-program.md +52 -0
  44. package/docs/testing.md +1 -1
  45. package/docs/troubleshooting.md +7 -0
  46. package/lib/typesol.d.ts +325 -13
  47. package/package.json +19 -1
  48. package/runtime/typesol_rt.h +54 -9
  49. package/ts-plugin/index.js +112 -0
  50. package/ts-plugin/package.json +4 -0
@@ -50,6 +50,8 @@ export class StatementEmitter extends CpiEmitter {
50
50
  return this.switchStatement(stmt);
51
51
  if (ts.isForStatement(stmt))
52
52
  return this.forLoop(stmt);
53
+ if (ts.isForOfStatement(stmt))
54
+ return this.forOf(stmt);
53
55
  if (ts.isWhileStatement(stmt))
54
56
  return this.loop(stmt.expression, stmt.statement, undefined);
55
57
  if (ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt)) {
@@ -63,8 +65,8 @@ export class StatementEmitter extends CpiEmitter {
63
65
  this.line(ts.isBreakStatement(stmt) ? `break;` : `goto ${this.loops.at(-1)};`);
64
66
  return;
65
67
  }
66
- if (ts.isDoStatement(stmt) || ts.isForOfStatement(stmt) || ts.isForInStatement(stmt))
67
- this.fail(stmt, "only for (init; cond; step) and while loops are supported");
68
+ if (ts.isDoStatement(stmt) || ts.isForInStatement(stmt))
69
+ this.fail(stmt, "only for (init; cond; step), for (const x of list) and while loops are supported");
68
70
  this.fail(stmt, `${ts.SyntaxKind[stmt.kind]} is not supported in TypeSOL`);
69
71
  }
70
72
  returnStatement(stmt) {
@@ -77,6 +79,25 @@ export class StatementEmitter extends CpiEmitter {
77
79
  this.line(`return SUCCESS;`);
78
80
  return;
79
81
  }
82
+ if (returns.kind === "struct") {
83
+ if (!stmt.expression)
84
+ this.fail(stmt, `return a ${returns.name}`);
85
+ if (this.helper) {
86
+ // Write the struct through the caller's out pointer (a literal, a place or another helper's result).
87
+ this.writeAggregate({ lv: "(*ret)", type: returns, writable: true }, stmt.expression);
88
+ this.line(`return SUCCESS;`);
89
+ return;
90
+ }
91
+ // Return data: the struct's fixed layout, which the IDL describes field by field.
92
+ const t = this.tmp("ret");
93
+ this.line(`TSS_${returns.name} ${t};`);
94
+ this.writeAggregate({ lv: t, type: returns, writable: true }, stmt.expression);
95
+ this.line(`sol_set_return_data((const uint8_t *)&${t}, sizeof(${t}));`);
96
+ for (const l of this.epilogue())
97
+ this.line(l);
98
+ this.line(`return SUCCESS;`);
99
+ return;
100
+ }
80
101
  if (!stmt.expression)
81
102
  this.fail(stmt, `return a ${typeName(returns)}`);
82
103
  const v = this.expect(this.expr(stmt.expression, returns), returns, stmt.expression);
@@ -170,6 +191,65 @@ export class StatementEmitter extends CpiEmitter {
170
191
  });
171
192
  this.line(`}`);
172
193
  }
194
+ /**
195
+ * `for (const x of list)` over a Vec or FixedArray: an index loop that binds `x` as `const x = list.get(i)` would,
196
+ * so elements are values (scalars, strings) or references into the data (structs), with the same aliasing rules.
197
+ * As in JS, the length is read again before every step, so a body that pushes or pops sees the new length.
198
+ */
199
+ forOf(stmt) {
200
+ if (stmt.awaitModifier)
201
+ this.fail(stmt, "for await is not supported");
202
+ const list = stmt.initializer;
203
+ const d = ts.isVariableDeclarationList(list) && list.declarations.length === 1 ? list.declarations[0] : undefined;
204
+ if (!d || !(list.flags & ts.NodeFlags.Const) || !ts.isIdentifier(d.name) || d.type)
205
+ this.fail(stmt.initializer, "write for (const x of list): one const name, no type annotation");
206
+ if (this.isRemaining(stmt.expression))
207
+ this.fail(stmt.expression, "iterate remaining accounts with for (let i = 0n; i < rest.length; i++) and rest.get(i)");
208
+ // Synthetic `list.length` / `list.get(i)` nodes placed at the loop, so errors and checks point at the source.
209
+ const f = ts.factory;
210
+ const index = `__of${this.temp++}`;
211
+ const place = (n) => {
212
+ ts.setTextRange(n, stmt.expression);
213
+ n.parent = stmt;
214
+ return n;
215
+ };
216
+ const indexId = place(f.createIdentifier(index));
217
+ const length = place(f.createPropertyAccessExpression(stmt.expression, "length"));
218
+ const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(stmt.expression, "get")), undefined, [indexId]));
219
+ // The element's declaration: the source's own name (so aliasing checks find its uses) bound to list.get(i).
220
+ const decl = f.createVariableDeclaration(d.name, undefined, undefined, get);
221
+ ts.setTextRange(decl, d);
222
+ decl.parent = list;
223
+ const declList = { ...list, flags: ts.NodeFlags.Const, declarations: [decl] };
224
+ const ci = this.tmp("of_i");
225
+ const label = this.tmp("ts_continue_");
226
+ this.line(`{`);
227
+ this.block(() => {
228
+ this.line(`uint64_t ${ci} = 0;`);
229
+ this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
230
+ this.line(`while (1) {`);
231
+ this.block(() => {
232
+ const n = this.expect(this.expr(length, U64), U64, stmt.expression);
233
+ this.line(`if (!(${ci} < ${n.c})) break;`);
234
+ this.variable(declList);
235
+ this.loops.push(label);
236
+ this.line(`{`);
237
+ this.block(() => this.body(stmt.statement));
238
+ this.line(`}`);
239
+ this.loops.pop();
240
+ this.line(`${label}:;`);
241
+ this.line(`${ci}++;`);
242
+ });
243
+ this.line(`}`);
244
+ });
245
+ this.line(`}`);
246
+ }
247
+ /** Whether `e` is the remaining accounts (or a const bound to them). */
248
+ isRemaining(e) {
249
+ if (ts.isCallExpression(e) && this.exportName(e.expression) === "remainingAccounts")
250
+ return true;
251
+ return ts.isIdentifier(e) && this.lookup(e.text)?.type.kind === "remaining";
252
+ }
173
253
  /**
174
254
  * `while (1) { cond; if (!cond) break; body; continue_label: step }`. The condition is lowered
175
255
  * inside the loop so its checked-arithmetic temporaries are recomputed every iteration.
@@ -204,8 +284,18 @@ export class StatementEmitter extends CpiEmitter {
204
284
  if (!(list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)))
205
285
  this.fail(list, "use const or let, not var");
206
286
  for (const d of list.declarations) {
287
+ if (ts.isObjectBindingPattern(d.name) && d.initializer) {
288
+ this.destructure(list, d, d.name, d.initializer, topLevel);
289
+ continue;
290
+ }
207
291
  if (!ts.isIdentifier(d.name) || !d.initializer)
208
292
  this.fail(d, "declare one plain name with an initializer");
293
+ // A local function is a helper of its own (lifted when the program was analyzed): nothing to run here.
294
+ if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
295
+ if (!this.model.helperDecls.has(d))
296
+ this.fail(d, "declare local functions with const: const f = (…) => …");
297
+ continue;
298
+ }
209
299
  const name = d.name.text;
210
300
  if (this.scopes.at(-1).has(name))
211
301
  this.fail(d.name, `"${name}" is already declared`);
@@ -312,6 +402,39 @@ export class StatementEmitter extends CpiEmitter {
312
402
  this.scopes.at(-1).set(name, { c, type, mutable });
313
403
  }
314
404
  }
405
+ /**
406
+ * `const { a, b: c } = s`: one declaration per name, `const a = s.a`, in source order. The source must be a name or
407
+ * a field path (no calls), so reading it once per name reads the same thing JS reads once.
408
+ */
409
+ destructure(list, d, pattern, init, topLevel) {
410
+ if (d.type)
411
+ this.fail(d.type, "destructuring takes the types from the source; drop the annotation");
412
+ const plain = (e) => ts.isIdentifier(e) ||
413
+ (ts.isPropertyAccessExpression(e) && !e.questionDotToken && plain(e.expression)) ||
414
+ (ts.isParenthesizedExpression(e) && plain(e.expression));
415
+ if (!plain(init))
416
+ this.fail(init, "destructure a variable or a field (a.b.c); bind a call to a const first");
417
+ const f = ts.factory;
418
+ for (const el of pattern.elements) {
419
+ if (el.dotDotDotToken)
420
+ this.fail(el, "...rest is not supported in destructuring");
421
+ if (el.initializer)
422
+ this.fail(el, "default values are not supported in destructuring");
423
+ if (!ts.isIdentifier(el.name))
424
+ this.fail(el, "nested destructuring is not supported; destructure in two steps");
425
+ const prop = el.propertyName ?? el.name;
426
+ if (!ts.isIdentifier(prop))
427
+ this.fail(el, "use plain field names");
428
+ const access = f.createPropertyAccessExpression(init, prop.text);
429
+ ts.setTextRange(access, el);
430
+ access.parent = d;
431
+ // The element's own name node, so aliasing checks and the type checker find its uses.
432
+ const decl = f.createVariableDeclaration(el.name, undefined, undefined, access);
433
+ ts.setTextRange(decl, el);
434
+ decl.parent = list;
435
+ this.variable({ ...list, declarations: [decl] }, topLevel);
436
+ }
437
+ }
315
438
  expressionStatement(e, topLevel) {
316
439
  if (ts.isBinaryExpression(e)) {
317
440
  const op = e.operatorToken.kind;
@@ -332,6 +455,8 @@ export class StatementEmitter extends CpiEmitter {
332
455
  const base = compound[op];
333
456
  if (base !== undefined)
334
457
  return this.assign(e.left, e.right, base);
458
+ if (op === ts.SyntaxKind.AsteriskAsteriskEqualsToken)
459
+ this.fail(e.operatorToken, "write x = x ** n instead of **=");
335
460
  }
336
461
  if ((ts.isPostfixUnaryExpression(e) || ts.isPrefixUnaryExpression(e)) &&
337
462
  (e.operator === ts.SyntaxKind.PlusPlusToken || e.operator === ts.SyntaxKind.MinusMinusToken)) {
@@ -379,17 +504,25 @@ export class StatementEmitter extends CpiEmitter {
379
504
  }
380
505
  // JS evaluates the target (`a.get(i).x`: which element) before the right side. The exception is
381
506
  // `acct.field = acct.create(...)`, which must create the account before the field can be touched.
382
- const isCall = ts.isCallExpression(valueNode) && !this.isStorageCall(valueNode);
507
+ // `fixedVec([…])` is a value written in place, not a call.
508
+ const isCall = ts.isCallExpression(valueNode) && !this.isStorageCall(valueNode) && this.exportName(valueNode.expression) !== "fixedVec";
383
509
  const createsFirst = isCall && ts.isPropertyAccessExpression(valueNode.expression) && ["create", "createIfNeeded"].includes(valueNode.expression.name.text);
384
510
  let raw = createsFirst ? this.call(valueNode, topLevel) : undefined;
385
511
  if (createsFirst && !raw)
386
512
  this.fail(valueNode, "this call does not produce a value");
513
+ // A helper's struct result is copied like any struct (the target first, as JS evaluates it first).
514
+ const structCall = isCall && ts.isIdentifier(valueNode.expression)
515
+ ? this.helperOf(valueNode)?.returns?.kind === "struct"
516
+ : false;
517
+ if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
518
+ this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
387
519
  // Whole arrays / structs: copy between identical layouts.
388
- const targetRef = isCall ? undefined : this.ref(target);
520
+ const targetRef = isCall && !structCall ? undefined : this.ref(target);
389
521
  if (targetRef &&
390
522
  (targetRef.type.kind === "array" ||
391
523
  targetRef.type.kind === "struct" ||
392
524
  targetRef.type.kind === "dataEnum" ||
525
+ (targetRef.type.kind === "vec" && targetRef.type.cap !== undefined) ||
393
526
  (targetRef.type.kind === "option" && targetRef.type.inner.kind === "struct"))) {
394
527
  if (!targetRef.writable)
395
528
  this.fail(target, this.readOnly(targetRef));
@@ -407,8 +540,9 @@ export class StatementEmitter extends CpiEmitter {
407
540
  this.fail(valueNode, "this call does not produce a value");
408
541
  }
409
542
  let value = this.expect(raw ?? this.expr(valueNode, place.type), place.type, valueNode);
410
- if (ts.isIdentifier(target))
411
- value = this.ownedString(value); // a local keeps its own copy of account text
543
+ // A local, or a string field of a variable enum (a view), keeps its own copy of account text.
544
+ if (ts.isIdentifier(target) || (value.type.kind === "string" && this.peekKind(target) === "string"))
545
+ value = this.ownedString(value);
412
546
  this.line(place.write(value.c) + ";");
413
547
  }
414
548
  /** Whether a place goes through a Vec element (`v.get(i).x`, `v.get(i)`). */
@@ -428,7 +562,7 @@ export class StatementEmitter extends CpiEmitter {
428
562
  }
429
563
  }
430
564
  helperTakesMutAccounts(node) {
431
- const h = ts.isIdentifier(node.expression) ? this.model.helpers.get(node.expression.text) : undefined;
565
+ const h = this.helperOf(node);
432
566
  return !!h?.params.some((p) => p.account && isWritable(p.account));
433
567
  }
434
568
  }
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { type FieldType, type ValueType } from "../model.js";
2
+ import { type FieldType, type Helper, type ValueType } from "../model.js";
3
3
  import { type Value, type Ref } from "./shared.js";
4
4
  import { EmitterBase } from "./base.js";
5
5
  /**
@@ -7,6 +7,7 @@ import { EmitterBase } from "./base.js";
7
7
  * argument validation.
8
8
  */
9
9
  export declare abstract class StorageEmitter extends EmitterBase {
10
+ protected abstract callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
10
11
  /** C lines rejecting malformed fixed-layout data at `lv`: bools, enums, option tags, string lengths, enum tags. */
11
12
  protected validation(lv: string, t: FieldType, pad: string, fail?: string): string[];
12
13
  /** The variants TypeScript has narrowed a data-enum expression to (e.g. inside `if (a.kind === "X")`). */
@@ -66,6 +67,10 @@ export declare abstract class StorageEmitter extends EmitterBase {
66
67
  * required) or by copying another location of the same type.
67
68
  */
68
69
  protected writeAggregate(target: Ref, valueNode: ts.Expression): void;
70
+ /** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
71
+ protected fixedVec(lv: string, elem: FieldType, cap: number): NonNullable<Ref["vec"]>;
72
+ /** A place whose type is a FixedVec gets its Vec view. */
73
+ protected withFixedVec(r: Ref): Ref;
69
74
  /** `arr.get(i)` reads storage rather than calling anything. */
70
75
  protected isStorageCall(node: ts.CallExpression): boolean;
71
76
  /** Whether an expression names an array, without emitting code or failing. */
@@ -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
@@ -73,12 +73,13 @@ export class StorageEmitter extends EmitterBase {
73
73
  return r?.type.kind === "dataEnum" ? r : undefined;
74
74
  }
75
75
  looksLikeDataEnum(node) {
76
- const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
76
+ const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
77
77
  try {
78
78
  return this.reporter.guard(() => this.ref(node))?.type.kind === "dataEnum";
79
79
  }
80
80
  finally {
81
81
  this.lines.length = saved.lines;
82
+ this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
82
83
  this.temp = saved.temp;
83
84
  this.reporter.diagnostics.length = saved.diagnostics;
84
85
  }
@@ -144,7 +145,7 @@ export class StorageEmitter extends EmitterBase {
144
145
  // Loops around `at` that do not also contain the declaration run their earlier statements again later.
145
146
  let loop;
146
147
  for (let n = at.parent; n && n !== decl.parent; n = n.parent)
147
- if ((ts.isForStatement(n) || ts.isWhileStatement(n) || ts.isDoStatement(n)) && !(decl.pos >= n.pos && decl.end <= n.end))
148
+ if ((ts.isForStatement(n) || ts.isForOfStatement(n) || ts.isWhileStatement(n) || ts.isDoStatement(n)) && !(decl.pos >= n.pos && decl.end <= n.end))
148
149
  loop = n;
149
150
  let found = false;
150
151
  const scope = decl.parent.parent.parent; // VariableDeclaration -> List -> Statement -> enclosing block
@@ -207,13 +208,14 @@ export class StorageEmitter extends EmitterBase {
207
208
  return this.looksLikeStructPlace(node.expression);
208
209
  if (ts.isConditionalExpression(node))
209
210
  return this.looksLikeStructPlace(node.whenTrue) && this.looksLikeStructPlace(node.whenFalse);
210
- const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
211
+ const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
211
212
  try {
212
213
  const r = this.reporter.guard(() => this.ref(node));
213
214
  return r?.type.kind === "struct";
214
215
  }
215
216
  finally {
216
217
  this.lines.length = saved.lines;
218
+ this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
217
219
  this.temp = saved.temp;
218
220
  this.reporter.diagnostics.length = saved.diagnostics;
219
221
  }
@@ -241,6 +243,14 @@ export class StorageEmitter extends EmitterBase {
241
243
  if (ts.isParenthesizedExpression(node))
242
244
  return this.ref(node.expression);
243
245
  this.noOptionalChain(node);
246
+ // A helper's struct result: a local of the caller's, read and copied like any struct.
247
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
248
+ const h = this.helperOf(node);
249
+ if (h?.returns?.kind === "struct") {
250
+ const v = this.callHelper(node, h);
251
+ return { lv: `(*${v.c})`, type: { kind: "struct", name: h.returns.name }, writable: true };
252
+ }
253
+ }
244
254
  if (ts.isIdentifier(node)) {
245
255
  const l = this.lookup(node.text);
246
256
  if (l && l.type.kind === "array")
@@ -263,10 +273,13 @@ export class StorageEmitter extends EmitterBase {
263
273
  return undefined;
264
274
  this.assertUsable(a, node);
265
275
  const r = { lv: `d_${a.name}->${f.name}`, type: f.type, writable: isWritable(a.kind), account: a, path: [f.name] };
266
- if (f.type.kind === "vec")
276
+ if (f.type.kind === "vec" && f.type.cap !== undefined)
277
+ r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
278
+ else if (f.type.kind === "vec")
267
279
  // Growing needs the account's length at instruction entry: only instructions know it.
268
280
  r.vec = {
269
281
  len: `d_${a.name}->${f.name}__len`,
282
+ items: r.lv,
270
283
  fixed: `sizeof(TSS_${a.kind.struct.name})`,
271
284
  elemSize: a.kind.struct.vec.elemSize,
272
285
  resizable: isWritable(a.kind) && !!this.ix,
@@ -284,7 +297,9 @@ export class StorageEmitter extends EmitterBase {
284
297
  const vi = e.variants.findIndex((v) => v.name === n);
285
298
  const fields = e.variants[vi]?.fields ?? [];
286
299
  const at = fields.findIndex((x) => x.name === field);
287
- const offset = fields.slice(0, Math.max(at, 0)).reduce((sum, x) => sum + this.fieldBytes(x.type), 0);
300
+ // In the C payload a string is a TS_Str view (16 bytes); everything else has its stored size.
301
+ const cBytes = (t) => (t.kind === "string" ? 16 : this.fieldBytes(t));
302
+ const offset = fields.slice(0, Math.max(at, 0)).reduce((sum, x) => sum + cBytes(x.type), 0);
288
303
  return { vi, name: n, f: fields[at], offset };
289
304
  });
290
305
  if (located.some((l) => !l.f))
@@ -295,7 +310,7 @@ export class StorageEmitter extends EmitterBase {
295
310
  // TypeScript proved the variant set; this runtime check is the backstop.
296
311
  const cond = located.map((l) => `${base.lv}.tag != ${l.vi}`).join(" && ");
297
312
  this.line(`if (${cond}) TS_FAIL(TS_ERR_ENUM_VARIANT, "TypeSOL: ${e.name} is not ${names.join("/")}");`);
298
- return { ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined };
313
+ return this.withFixedVec({ ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined });
299
314
  }
300
315
  if (base && base.type.kind === "option" && base.type.inner.kind === "struct") {
301
316
  const s = this.model.structs.get(base.type.inner.name);
@@ -303,7 +318,7 @@ export class StorageEmitter extends EmitterBase {
303
318
  if (!f)
304
319
  this.fail(node.name, `${s.name} has no field "${field}"`);
305
320
  this.line(`if (!${base.lv}.some) TS_FAIL(TS_ERR_UNWRAP, "TypeSOL: option is empty");`);
306
- return { ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined };
321
+ return this.withFixedVec({ ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined });
307
322
  }
308
323
  if (base && base.type.kind === "struct") {
309
324
  const s = this.model.structs.get(base.type.name);
@@ -312,9 +327,9 @@ export class StorageEmitter extends EmitterBase {
312
327
  this.fail(node.name, `${s.name} has no field "${field}"`);
313
328
  const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
314
329
  // Vecs of loaded accounts can be read and updated in place but not resized (no entry length known).
315
- if (f.type.kind === "vec")
316
- r.vec = { len: `${base.lv}.${f.name}__len`, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
317
- return r;
330
+ if (f.type.kind === "vec" && f.type.cap === undefined)
331
+ r.vec = { len: `${base.lv}.${f.name}__len`, items: r.lv, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
332
+ return this.withFixedVec(r);
318
333
  }
319
334
  return undefined;
320
335
  }
@@ -333,8 +348,9 @@ export class StorageEmitter extends EmitterBase {
333
348
  if (base && (base.type.kind === "array" || base.type.kind === "vec")) {
334
349
  if (node.arguments.length !== 1)
335
350
  this.fail(node, "get(index) takes one argument");
336
- const idx = this.index(node.arguments[0], base.type.kind === "array" ? `${base.type.len}ULL` : `(uint64_t)${base.vec.len}`);
337
- return { ...base, lv: `${base.lv}[${idx}]`, type: base.type.elem, path: undefined, vec: undefined };
351
+ const idx = this.index(node.arguments[0], base.type.kind === "array" ? `${base.type.len}ULL` : vecCount(base.vec));
352
+ const lv = base.type.kind === "array" ? `${base.lv}[${idx}]` : `${base.vec.items}[${idx}]`;
353
+ return this.withFixedVec({ ...base, lv, type: base.type.elem, path: undefined, vec: undefined });
338
354
  }
339
355
  }
340
356
  return undefined;
@@ -412,6 +428,8 @@ export class StorageEmitter extends EmitterBase {
412
428
  return { c: `(&${r.lv})`, type: r.type };
413
429
  case "fstring":
414
430
  return { c: `ts_fstr_view((const uint8_t *)&${r.lv}, ${r.type.cap})`, type: STRING };
431
+ case "string":
432
+ return { c: r.lv, type: STRING }; // a variable enum's text: already a view
415
433
  case "enum":
416
434
  return { c: r.lv, type: r.type };
417
435
  case "option":
@@ -439,6 +457,7 @@ export class StorageEmitter extends EmitterBase {
439
457
  return `${r.lv} = *(${c})`;
440
458
  case "fstring":
441
459
  return `TS_TRY(ts_fstr_set((uint8_t *)&${r.lv}, ${r.type.cap}, ${c}))`;
460
+ case "string":
442
461
  case "enum":
443
462
  case "option":
444
463
  return `${r.lv} = ${c}`;
@@ -449,7 +468,7 @@ export class StorageEmitter extends EmitterBase {
449
468
  storageValueType(t) {
450
469
  if (isScalar(t))
451
470
  return t;
452
- if (t.kind === "fstring")
471
+ if (t.kind === "fstring" || t.kind === "string")
453
472
  return STRING;
454
473
  if (t.kind === "option" && t.inner.kind !== "struct")
455
474
  return t;
@@ -460,6 +479,8 @@ export class StorageEmitter extends EmitterBase {
460
479
  * required) or by copying another location of the same type.
461
480
  */
462
481
  writeAggregate(target, valueNode) {
482
+ if (ts.isParenthesizedExpression(valueNode))
483
+ return this.writeAggregate(target, valueNode.expression);
463
484
  if (target.type.kind === "option" && target.type.inner.kind === "struct") {
464
485
  if (valueNode.kind === ts.SyntaxKind.NullKeyword) {
465
486
  this.line(`${target.lv}.some = 0;`);
@@ -510,14 +531,44 @@ export class StorageEmitter extends EmitterBase {
510
531
  const f = variant.fields.find((x) => x.name === name);
511
532
  const sub = { lv: `${tmp}.v.${variant.name}.${f.name}`, type: f.type, writable: true };
512
533
  const vt = this.storageValueType(f.type);
534
+ // A string kept in a variable enum holds its own copy of account text (the account may change later).
513
535
  if (vt)
514
- this.line(this.writeStorage(sub, this.expect(this.expr(ex, vt), vt, ex).c) + ";");
536
+ this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(ex, vt), vt, ex)).c) + ";");
515
537
  else
516
538
  this.writeAggregate(sub, ex);
517
539
  }
518
540
  this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
519
541
  return;
520
542
  }
543
+ if (target.type.kind === "vec" && target.type.cap !== undefined && ts.isCallExpression(valueNode) && this.exportName(valueNode.expression) === "fixedVec") {
544
+ const list = valueNode.arguments[0];
545
+ if (valueNode.arguments.length !== 1 || !ts.isArrayLiteralExpression(list))
546
+ this.fail(valueNode, "fixedVec([…]) takes an array literal");
547
+ return this.writeAggregate(target, list);
548
+ }
549
+ if (target.type.kind === "vec" && target.type.cap !== undefined && ts.isArrayLiteralExpression(valueNode)) {
550
+ // `list = [a, b]`: built in a temporary (the elements may read the old list), the unused slots zeroed.
551
+ const t = target.type;
552
+ const els = valueNode.elements;
553
+ if (els.length > t.cap)
554
+ this.fail(valueNode, `${els.length} elements do not fit in ${typeName(t)}`);
555
+ const tmp = this.tmp("fv");
556
+ this.line(`${fixedVecC(t)} ${tmp};`);
557
+ this.line(`ts_memzero(&${tmp}, sizeof(${tmp}));`);
558
+ this.line(`${tmp}.len = ${els.length};`);
559
+ els.forEach((e, i) => {
560
+ if (ts.isSpreadElement(e))
561
+ this.fail(e, "list the elements one by one");
562
+ const sub = { lv: `${tmp}.items[${i}]`, type: t.elem, writable: true };
563
+ const vt = this.storageValueType(t.elem);
564
+ if (vt)
565
+ this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(e, vt), vt, e)).c) + ";");
566
+ else
567
+ this.writeAggregate(sub, e);
568
+ });
569
+ this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
570
+ return;
571
+ }
521
572
  if (ts.isObjectLiteralExpression(valueNode)) {
522
573
  if (target.type.kind !== "struct")
523
574
  this.fail(valueNode, `expected a ${typeName(target.type)}, not an object literal`);
@@ -559,6 +610,16 @@ export class StorageEmitter extends EmitterBase {
559
610
  // Same-typed places are either distinct or identical (`a.p = a.p`); ts_copy skips the latter.
560
611
  this.line(`ts_copy(&${target.lv}, &${src.lv}, sizeof(${target.lv}));`);
561
612
  }
613
+ /** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
614
+ fixedVec(lv, elem, cap) {
615
+ return { len: `${lv}.len`, items: `${lv}.items`, fixed: "", elemSize: this.fieldBytes(elem), resizable: false, cap };
616
+ }
617
+ /** A place whose type is a FixedVec gets its Vec view. */
618
+ withFixedVec(r) {
619
+ if (r.type.kind === "vec" && r.type.cap !== undefined)
620
+ r.vec = this.fixedVec(r.lv, r.type.elem, r.type.cap);
621
+ return r;
622
+ }
562
623
  /** `arr.get(i)` reads storage rather than calling anything. */
563
624
  isStorageCall(node) {
564
625
  return ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "get" && !!this.peekArray(node.expression.expression);
@@ -570,12 +631,13 @@ export class StorageEmitter extends EmitterBase {
570
631
  }
571
632
  /** The kind of storage an expression names (struct, array, dataEnum…), without emitting code or failing. */
572
633
  peekKind(node) {
573
- const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
634
+ const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
574
635
  try {
575
636
  return this.reporter.guard(() => this.ref(node))?.type.kind;
576
637
  }
577
638
  finally {
578
639
  this.lines.length = saved.lines;
640
+ this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
579
641
  this.temp = saved.temp;
580
642
  this.reporter.diagnostics.length = saved.diagnostics;
581
643
  }
@@ -641,8 +703,11 @@ export class StorageEmitter extends EmitterBase {
641
703
  return this.fieldBytes(t.elem) * t.len;
642
704
  case "struct":
643
705
  return this.model.structs.get(t.name).size;
644
- case "string":
645
706
  case "vec":
707
+ if (t.cap !== undefined)
708
+ return 4 + this.fieldBytes(t.elem) * t.cap;
709
+ throw new Error("unreachable: variable-size field has no fixed size");
710
+ case "string":
646
711
  throw new Error("unreachable: variable-size field has no fixed size");
647
712
  }
648
713
  }
@@ -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,16 @@ 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
+ return idlType(t);
49
+ }
40
50
  function idlType(t, borshOption = false) {
41
51
  switch (t.kind) {
42
52
  case "int":
@@ -62,7 +72,7 @@ function idlType(t, borshOption = false) {
62
72
  // Borsh enum in arguments and events; fixed tag + payload in account data.
63
73
  return borshOption || t.kind === "dataEnumArg" ? { defined: { name: t.name } } : { defined: { name: fixedEnumName(t.name) } };
64
74
  case "vec":
65
- return { vec: idlType(t.elem) };
75
+ return t.cap === undefined ? { vec: idlType(t.elem) } : { defined: { name: fixedVecName(t) } };
66
76
  case "vecArg":
67
77
  return { vec: idlType(t.elem) };
68
78
  case "arrayArg":
@@ -125,7 +135,7 @@ function accountEntry(ix, name, k, programAddress) {
125
135
  export function buildIdl(model, address, version = "0.1.0") {
126
136
  const structs = [...model.structs.values()];
127
137
  const usedStructs = structs.filter((s) => model.accountStructs.has(s.name) || model.events.has(s.name) || s.size !== undefined);
128
- return {
138
+ const idl = {
129
139
  address,
130
140
  metadata: { name: model.name, version, spec: "0.1.0", description: "Built with TypeSOL" },
131
141
  instructions: model.instructions.map((ix) => ({
@@ -148,33 +158,47 @@ export function buildIdl(model, address, version = "0.1.0") {
148
158
  fields: s.fields.map((f) => ({
149
159
  name: snakeCase(f.name),
150
160
  ...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)),
161
+ type: s.layout === "borsh" ? borshIdlType(f.type) : idlType(f.type, model.events.has(s.name) && !model.accountStructs.has(s.name)),
152
162
  })),
153
163
  },
154
164
  })),
155
165
  ...[...model.enums.values()].map((e) => ({ name: e.name, type: { kind: "enum", variants: e.variants.map((v) => ({ name: v })) } })),
166
+ // A variable enum (it holds strings) only exists in Borsh: no fixed account-data form to describe.
156
167
  ...[...model.dataEnums.values()].flatMap((e) => [
157
168
  {
158
169
  name: e.name,
159
170
  ...docsOf(e.node),
160
171
  type: {
161
172
  kind: "enum",
162
- variants: e.variants.map((v) => v.fields.length ? { name: v.name, fields: v.fields.map((f) => ({ name: snakeCase(f.name), type: idlType(f.type) })) } : { name: v.name }),
163
- },
164
- },
165
- {
166
- name: fixedEnumName(e.name),
167
- docs: [
168
- `TypeSOL ${e.name} in account data: \`tag\` is the variant index, \`payload\` the variant's fields (Borsh layout) padded to the largest variant.`,
169
- ],
170
- type: {
171
- kind: "struct",
172
- fields: [
173
- { name: "tag", type: "u8" },
174
- { name: "payload", type: { array: ["u8", e.payload] } },
175
- ],
173
+ variants: e.variants.map((v) => v.fields.length
174
+ ? {
175
+ name: v.name,
176
+ // A nested variable enum only exists in Borsh; fixed fields keep the fixed forms the bytes use.
177
+ fields: v.fields.map((f) => ({
178
+ name: snakeCase(f.name),
179
+ type: f.type.kind === "dataEnum" && model.dataEnums.get(f.type.name)?.variable ? { defined: { name: f.type.name } } : idlType(f.type),
180
+ })),
181
+ }
182
+ : { name: v.name }),
176
183
  },
177
184
  },
185
+ ...(e.variable
186
+ ? []
187
+ : [
188
+ {
189
+ name: fixedEnumName(e.name),
190
+ docs: [
191
+ `TypeSOL ${e.name} in account data: \`tag\` is the variant index, \`payload\` the variant's fields (Borsh layout) padded to the largest variant.`,
192
+ ],
193
+ type: {
194
+ kind: "struct",
195
+ fields: [
196
+ { name: "tag", type: "u8" },
197
+ { name: "payload", type: { array: ["u8", e.payload] } },
198
+ ],
199
+ },
200
+ },
201
+ ]),
178
202
  ]),
179
203
  ...[...model.options.values()].map((inner) => ({
180
204
  name: fixedOptionName(inner),
@@ -187,6 +211,17 @@ export function buildIdl(model, address, version = "0.1.0") {
187
211
  ],
188
212
  },
189
213
  })),
214
+ ...[...model.fixedVecs].map(([name, t]) => ({
215
+ name,
216
+ docs: [`TypeSOL FixedVec<${typeName(t.elem)}, ${t.cap}>: \`len\` elements in use, room for ${t.cap} (the rest zero or stale).`],
217
+ type: {
218
+ kind: "struct",
219
+ fields: [
220
+ { name: "len", type: "u32" },
221
+ { name: "items", type: { array: [idlType(t.elem), t.cap] } },
222
+ ],
223
+ },
224
+ })),
190
225
  ...[...model.fstrings]
191
226
  .sort((a, b) => a - b)
192
227
  .map((cap) => ({
@@ -202,4 +237,12 @@ export function buildIdl(model, address, version = "0.1.0") {
202
237
  })),
203
238
  ],
204
239
  };
240
+ // FixedString / FixedVec / FixedOption types only @layout borsh accounts declared (they list theirs as string / vec /
241
+ // option) are left out.
242
+ // FixedVecs first: a kept one may name a FixedString.
243
+ for (const prefix of [/^FixedVec\d/, /^FixedOption_/, /^FixedString\d/]) {
244
+ const text = JSON.stringify([idl.instructions, idl.types.filter((t) => !prefix.test(t.name))]);
245
+ idl.types = idl.types.filter((t) => !prefix.test(t.name) || text.includes(`"name":"${t.name}"`));
246
+ }
247
+ return idl;
205
248
  }