@typesol/cli 0.16.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.
@@ -1,6 +1,7 @@
1
1
  import ts from "typescript";
2
+ import { calleeName, resolveValueType } from "../analyze.js";
2
3
  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";
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.
@@ -62,6 +63,20 @@ export class ExpressionEmitter extends StorageEmitter {
62
63
  expr(node, hint) {
63
64
  if (ts.isParenthesizedExpression(node))
64
65
  return this.expr(node.expression, hint);
66
+ // `x as T`: an annotation the value must already match (no conversion: use u8(x) etc. for that). Inside a
67
+ // generic helper it names the instance's type: `return (a + b) as T`.
68
+ if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
69
+ if (ts.isAsExpression(node) && ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName) && node.type.typeName.text === "const")
70
+ return this.expr(node.expression, hint);
71
+ const t = resolveValueType(node.type, this.scope);
72
+ if (!t || t.kind === "vecArg" || t.kind === "dataEnumArg")
73
+ this.fail(node.type, "`as` names an integer, boolean, Pubkey, enum, option or string type the value already has");
74
+ const v = this.expr(node.expression, t);
75
+ // Not a conversion: `x as u64 | null` does not make an Option of a u64.
76
+ if (v.literal === undefined && !v.isNull && !sameType(v.type, t))
77
+ 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)`);
78
+ return this.expect(v, t, node.expression);
79
+ }
65
80
  // `x!` on an Option: its value, aborting with 5108 when it is empty (JS would carry the null on).
66
81
  if (ts.isNonNullExpression(node)) {
67
82
  const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
@@ -140,6 +155,8 @@ export class ExpressionEmitter extends StorageEmitter {
140
155
  }
141
156
  if (this.accounts.has(node.text))
142
157
  this.fail(node, `"${node.text}" is an account; use ${node.text}.key for its address`);
158
+ if (calleeName(node, this.model.helperDecls, this.checker) || this.model.helpers.has(node.text))
159
+ this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
143
160
  this.fail(node, `unknown name "${node.text}"`);
144
161
  }
145
162
  if (ts.isPropertyAccessExpression(node))
@@ -202,7 +219,7 @@ export class ExpressionEmitter extends StorageEmitter {
202
219
  if (arr?.type.kind === "array")
203
220
  return { c: `${arr.type.len}ULL`, type: U64 };
204
221
  if (arr?.type.kind === "vec")
205
- return { c: `((uint64_t)${arr.vec.len})`, type: U64 };
222
+ return { c: vecCount(arr.vec), type: U64 };
206
223
  if (ts.isIdentifier(node.expression)) {
207
224
  const l = this.lookup(node.expression.text);
208
225
  if (l?.type.kind === "vecArg")
@@ -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 { 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,8 +40,9 @@ 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}));`);
@@ -87,7 +89,16 @@ export class FunctionEmitter extends StatementEmitter {
87
89
  head.push(` TS_TRY(ts_init_zeroed(${v}, params->program_id, TS_DISC_${k.struct.name}, sizeof(TSS_${k.struct.name})));`);
88
90
  if (k.kind === "initIfNeeded" && k.struct.vec)
89
91
  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") {
92
+ if ((k.kind === "account" || k.kind === "init" || k.kind === "initIfNeeded") && k.struct.layout === "borsh") {
93
+ // Worked on as its fixed-layout struct on the heap, packed back when the instruction returns. The heap is zero at
94
+ // entry and these are its first allocations (before the body uses it), so a string's padding and a list's
95
+ // unused slots start as zero: ts_unpack_* only writes what the Borsh bytes hold.
96
+ const S = k.struct.name;
97
+ head.push(` TSS_${S} *d_${a.name} = NULL;`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&d_${a.name}));`);
98
+ if (k.kind === "account")
99
+ head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}));`);
100
+ }
101
+ else if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
91
102
  head.push(` TSS_${k.struct.name} *d_${a.name} = (TSS_${k.struct.name} *)(${v}->data + 8);`);
92
103
  if (k.struct.vec) {
93
104
  head.push(` uint64_t entry_${a.name} = ${v}->data_len;`, ` (void)entry_${a.name};`);
@@ -285,13 +296,25 @@ export class FunctionEmitter extends StatementEmitter {
285
296
  epilogue() {
286
297
  if (!this.ix)
287
298
  return [];
288
- return this.ix.accounts
289
- .filter((a) => a.constraints.closeTo)
299
+ // @layout borsh accounts go back to their Borsh bytes (unless closed, or never created).
300
+ const packs = this.ix.accounts
301
+ .filter((a) => (a.kind.kind === "account" ? a.kind.mut : a.kind.kind === "init" || a.kind.kind === "initIfNeeded"))
302
+ .filter((a) => a.kind.struct.layout === "borsh")
290
303
  .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;
304
+ const v = `a_${a.name}`;
305
+ 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));`;
294
307
  });
308
+ return [
309
+ ...packs,
310
+ ...this.ix.accounts
311
+ .filter((a) => a.constraints.closeTo)
312
+ .map((a) => {
313
+ const line = `TS_TRY(ts_close(a_${a.name}, a_${a.constraints.closeTo}));`;
314
+ const optional = this.optionalIn(line);
315
+ return optional.length ? `if (${optional.map((n) => `a_${n} != NULL`).join(" && ")}) ${line}` : line;
316
+ }),
317
+ ];
295
318
  }
296
319
  constraintChecks(ix) {
297
320
  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 {
@@ -88,3 +94,9 @@ export interface EmitOptions {
88
94
  /** Log a marker and the remaining compute units before every statement (`typesol build --profile`). */
89
95
  profile?: boolean;
90
96
  }
97
+ /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
98
+ export declare function fixedVecC(t: FieldType & {
99
+ kind: "vec";
100
+ }): string;
101
+ /** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
102
+ 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,11 @@ 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
+ }
180
189
  else if (p.type.kind === "signerSeeds")
181
190
  params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
182
191
  else if (p.type.kind === "pubkey")
@@ -190,3 +199,11 @@ export function helperSignature(h) {
190
199
  params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : `${scalarC(h.returns)} *ret`);
191
200
  return `static uint64_t fn_${h.name}(${params.join(", ")})`;
192
201
  }
202
+ /** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
203
+ export function fixedVecC(t) {
204
+ return `TSF_${fixedVecName(t)}`;
205
+ }
206
+ /** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
207
+ export function vecCount(v) {
208
+ return v.cap === undefined ? `((uint64_t)${v.len})` : `((uint64_t)(${v.len} < ${v.cap} ? ${v.len} : ${v.cap}))`;
209
+ }
@@ -290,6 +290,12 @@ export class StatementEmitter extends CpiEmitter {
290
290
  }
291
291
  if (!ts.isIdentifier(d.name) || !d.initializer)
292
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
+ }
293
299
  const name = d.name.text;
294
300
  if (this.scopes.at(-1).has(name))
295
301
  this.fail(d.name, `"${name}" is already declared`);
@@ -498,14 +504,15 @@ export class StatementEmitter extends CpiEmitter {
498
504
  }
499
505
  // JS evaluates the target (`a.get(i).x`: which element) before the right side. The exception is
500
506
  // `acct.field = acct.create(...)`, which must create the account before the field can be touched.
501
- 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";
502
509
  const createsFirst = isCall && ts.isPropertyAccessExpression(valueNode.expression) && ["create", "createIfNeeded"].includes(valueNode.expression.name.text);
503
510
  let raw = createsFirst ? this.call(valueNode, topLevel) : undefined;
504
511
  if (createsFirst && !raw)
505
512
  this.fail(valueNode, "this call does not produce a value");
506
513
  // A helper's struct result is copied like any struct (the target first, as JS evaluates it first).
507
514
  const structCall = isCall && ts.isIdentifier(valueNode.expression)
508
- ? this.model.helpers.get(valueNode.expression.text)?.returns?.kind === "struct"
515
+ ? this.helperOf(valueNode)?.returns?.kind === "struct"
509
516
  : false;
510
517
  if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
511
518
  this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
@@ -515,6 +522,7 @@ export class StatementEmitter extends CpiEmitter {
515
522
  (targetRef.type.kind === "array" ||
516
523
  targetRef.type.kind === "struct" ||
517
524
  targetRef.type.kind === "dataEnum" ||
525
+ (targetRef.type.kind === "vec" && targetRef.type.cap !== undefined) ||
518
526
  (targetRef.type.kind === "option" && targetRef.type.inner.kind === "struct"))) {
519
527
  if (!targetRef.writable)
520
528
  this.fail(target, this.readOnly(targetRef));
@@ -554,7 +562,7 @@ export class StatementEmitter extends CpiEmitter {
554
562
  }
555
563
  }
556
564
  helperTakesMutAccounts(node) {
557
- const h = ts.isIdentifier(node.expression) ? this.model.helpers.get(node.expression.text) : undefined;
565
+ const h = this.helperOf(node);
558
566
  return !!h?.params.some((p) => p.account && isWritable(p.account));
559
567
  }
560
568
  }
@@ -67,6 +67,10 @@ export declare abstract class StorageEmitter extends EmitterBase {
67
67
  * required) or by copying another location of the same type.
68
68
  */
69
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;
70
74
  /** `arr.get(i)` reads storage rather than calling anything. */
71
75
  protected isStorageCall(node: ts.CallExpression): boolean;
72
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
@@ -245,8 +245,8 @@ export class StorageEmitter extends EmitterBase {
245
245
  this.noOptionalChain(node);
246
246
  // A helper's struct result: a local of the caller's, read and copied like any struct.
247
247
  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)) {
248
+ const h = this.helperOf(node);
249
+ if (h?.returns?.kind === "struct") {
250
250
  const v = this.callHelper(node, h);
251
251
  return { lv: `(*${v.c})`, type: { kind: "struct", name: h.returns.name }, writable: true };
252
252
  }
@@ -273,10 +273,13 @@ export class StorageEmitter extends EmitterBase {
273
273
  return undefined;
274
274
  this.assertUsable(a, node);
275
275
  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")
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")
277
279
  // Growing needs the account's length at instruction entry: only instructions know it.
278
280
  r.vec = {
279
281
  len: `d_${a.name}->${f.name}__len`,
282
+ items: r.lv,
280
283
  fixed: `sizeof(TSS_${a.kind.struct.name})`,
281
284
  elemSize: a.kind.struct.vec.elemSize,
282
285
  resizable: isWritable(a.kind) && !!this.ix,
@@ -307,7 +310,7 @@ export class StorageEmitter extends EmitterBase {
307
310
  // TypeScript proved the variant set; this runtime check is the backstop.
308
311
  const cond = located.map((l) => `${base.lv}.tag != ${l.vi}`).join(" && ");
309
312
  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 };
313
+ return this.withFixedVec({ ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined });
311
314
  }
312
315
  if (base && base.type.kind === "option" && base.type.inner.kind === "struct") {
313
316
  const s = this.model.structs.get(base.type.inner.name);
@@ -315,7 +318,7 @@ export class StorageEmitter extends EmitterBase {
315
318
  if (!f)
316
319
  this.fail(node.name, `${s.name} has no field "${field}"`);
317
320
  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 };
321
+ return this.withFixedVec({ ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined });
319
322
  }
320
323
  if (base && base.type.kind === "struct") {
321
324
  const s = this.model.structs.get(base.type.name);
@@ -324,9 +327,9 @@ export class StorageEmitter extends EmitterBase {
324
327
  this.fail(node.name, `${s.name} has no field "${field}"`);
325
328
  const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
326
329
  // 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;
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);
330
333
  }
331
334
  return undefined;
332
335
  }
@@ -345,8 +348,9 @@ export class StorageEmitter extends EmitterBase {
345
348
  if (base && (base.type.kind === "array" || base.type.kind === "vec")) {
346
349
  if (node.arguments.length !== 1)
347
350
  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 };
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 });
350
354
  }
351
355
  }
352
356
  return undefined;
@@ -475,6 +479,8 @@ export class StorageEmitter extends EmitterBase {
475
479
  * required) or by copying another location of the same type.
476
480
  */
477
481
  writeAggregate(target, valueNode) {
482
+ if (ts.isParenthesizedExpression(valueNode))
483
+ return this.writeAggregate(target, valueNode.expression);
478
484
  if (target.type.kind === "option" && target.type.inner.kind === "struct") {
479
485
  if (valueNode.kind === ts.SyntaxKind.NullKeyword) {
480
486
  this.line(`${target.lv}.some = 0;`);
@@ -534,6 +540,35 @@ export class StorageEmitter extends EmitterBase {
534
540
  this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
535
541
  return;
536
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
+ }
537
572
  if (ts.isObjectLiteralExpression(valueNode)) {
538
573
  if (target.type.kind !== "struct")
539
574
  this.fail(valueNode, `expected a ${typeName(target.type)}, not an object literal`);
@@ -575,6 +610,16 @@ export class StorageEmitter extends EmitterBase {
575
610
  // Same-typed places are either distinct or identical (`a.p = a.p`); ts_copy skips the latter.
576
611
  this.line(`ts_copy(&${target.lv}, &${src.lv}, sizeof(${target.lv}));`);
577
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
+ }
578
623
  /** `arr.get(i)` reads storage rather than calling anything. */
579
624
  isStorageCall(node) {
580
625
  return ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "get" && !!this.peekArray(node.expression.expression);
@@ -658,8 +703,11 @@ export class StorageEmitter extends EmitterBase {
658
703
  return this.fieldBytes(t.elem) * t.len;
659
704
  case "struct":
660
705
  return this.model.structs.get(t.name).size;
661
- case "string":
662
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":
663
711
  throw new Error("unreachable: variable-size field has no fixed size");
664
712
  }
665
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,7 +158,7 @@ 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
  })),
@@ -201,6 +211,17 @@ export function buildIdl(model, address, version = "0.1.0") {
201
211
  ],
202
212
  },
203
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
+ })),
204
225
  ...[...model.fstrings]
205
226
  .sort((a, b) => a - b)
206
227
  .map((cap) => ({
@@ -216,4 +237,12 @@ export function buildIdl(model, address, version = "0.1.0") {
216
237
  })),
217
238
  ],
218
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;
219
248
  }