@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.
@@ -2,7 +2,7 @@ import ts from "typescript";
2
2
  import { type Analysis, type TypeScope } from "../analyze.js";
3
3
  import type { Reporter } from "../diagnostics.js";
4
4
  import { type AccountParam, type Helper, type IdlSeed, type Instruction, type ProgramModel, type ValueType } from "../model.js";
5
- import { type Value, type Local, type EmitOptions } from "./shared.js";
5
+ import { type Value, type Ref, type Local, type Closure, type EmitOptions } from "./shared.js";
6
6
  /**
7
7
  * State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
8
8
  * scopes, evaluation-order snapshots and account lookups.
@@ -24,16 +24,42 @@ export declare abstract class EmitterBase {
24
24
  protected readonly pendingInit: Set<string>;
25
25
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
26
26
  protected readonly bumps: Map<string, string>;
27
+ /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
28
+ protected inlineFrames: {
29
+ label: string;
30
+ result: string;
31
+ type?: ValueType;
32
+ declAt: number;
33
+ pad: string;
34
+ node: ts.Node;
35
+ }[];
36
+ /** Closures being expanded (a closure calling itself cannot be expanded). */
37
+ protected readonly inlining: Set<ts.Node>;
27
38
  /** Enclosing loops, innermost last; each has a `continue` label. */
28
39
  protected readonly loops: string[];
29
40
  protected readonly ix: Instruction | undefined;
30
41
  protected readonly helper: Helper | undefined;
31
42
  protected readonly scope: TypeScope;
32
43
  protected readonly checker: ts.TypeChecker;
44
+ protected readonly analysis: Analysis;
45
+ /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
46
+ private readonly calleeCache;
33
47
  /** `file.ts:line` of the statement being lowered. */
34
48
  protected where: string | undefined;
35
49
  protected readonly profile: boolean;
36
50
  constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
51
+ /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
52
+ protected helperOf(node: ts.CallExpression): Helper | undefined;
53
+ /**
54
+ * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
55
+ * captures nothing). A closure can only run where it is declared, so it cannot be passed.
56
+ */
57
+ private fnArgsOf;
58
+ /** A struct written as a type argument (`pick<Point>(…)`). */
59
+ private structArg;
60
+ private fixedStruct;
61
+ /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
62
+ private typeArgsOf;
37
63
  protected line(s: string): void;
38
64
  /** `file.ts:line` of a node, for messages. */
39
65
  protected location(node: ts.Node): string;
@@ -52,11 +78,17 @@ export declare abstract class EmitterBase {
52
78
  protected block(fn: () => void): void;
53
79
  protected fail(node: ts.Node, message: string): never;
54
80
  protected exportName(node: ts.Expression): string | undefined;
81
+ /**
82
+ * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
83
+ * failing with 3020 otherwise). Returns undefined for anything else.
84
+ */
85
+ protected withoutBang(node: ts.Expression): ts.PropertyAccessExpression | undefined;
55
86
  protected accountParam(node: ts.Expression, what: string): AccountParam;
56
87
  protected assertUsable(a: AccountParam, node: ts.Node): void;
57
88
  protected abstract expect(v: Value, want: ValueType | undefined, node: ts.Node): Value;
58
89
  protected abstract literal(value: bigint, type: ValueType, node: ts.Node): Value;
59
90
  protected abstract expr(node: ts.Expression, hint?: ValueType): Value;
91
+ protected abstract ref(node: ts.Expression): Ref | undefined;
60
92
  protected abstract call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
61
93
  protected abstract isRemaining(node: ts.Expression): boolean;
62
94
  protected abstract handle(node: ts.Expression): string | undefined;
@@ -68,6 +100,7 @@ export declare abstract class EmitterBase {
68
100
  protected abstract recordPda(account: AccountParam, idl: IdlSeed[] | undefined): void;
69
101
  protected abstract create(node: ts.CallExpression, target: AccountParam, topLevel: boolean, ifNeeded?: boolean): Value;
70
102
  protected abstract createAta(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value | undefined;
103
+ protected abstract createToken(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value;
71
104
  protected abstract systemTransfer(node: ts.CallExpression): undefined;
72
105
  protected abstract createAccount(node: ts.CallExpression, system: AccountParam): undefined;
73
106
  protected abstract tokenOp(node: ts.CallExpression, program: AccountParam, method: string): undefined;
@@ -81,4 +114,6 @@ export declare abstract class EmitterBase {
81
114
  program: string;
82
115
  }): undefined;
83
116
  protected abstract epilogue(): string[];
117
+ protected abstract expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
118
+ protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
84
119
  }
@@ -1,4 +1,6 @@
1
1
  import ts from "typescript";
2
+ import { calleeName, resolveScalarType, structNameOf } from "../analyze.js";
3
+ import { isScalar, } from "../model.js";
2
4
  import { isStable, cString, valueC } from "./shared.js";
3
5
  /**
4
6
  * State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
@@ -21,12 +23,19 @@ export class EmitterBase {
21
23
  pendingInit = new Set();
22
24
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
23
25
  bumps = new Map();
26
+ /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
27
+ inlineFrames = [];
28
+ /** Closures being expanded (a closure calling itself cannot be expanded). */
29
+ inlining = new Set();
24
30
  /** Enclosing loops, innermost last; each has a `continue` label. */
25
31
  loops = [];
26
32
  ix;
27
33
  helper;
28
34
  scope;
29
35
  checker;
36
+ analysis;
37
+ /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
38
+ calleeCache = new Map();
30
39
  /** `file.ts:line` of the statement being lowered. */
31
40
  where;
32
41
  profile;
@@ -36,13 +45,21 @@ export class EmitterBase {
36
45
  this.model = analysis.model;
37
46
  // Each function resolves `typesol` names through its own file's imports.
38
47
  this.imports = target.imports;
39
- this.scope = { imports: target.imports, enums: analysis.model.enums, options: analysis.model.options, dataEnums: analysis.model.dataEnums };
48
+ this.scope = {
49
+ imports: target.imports,
50
+ enums: analysis.model.enums,
51
+ options: analysis.model.options,
52
+ dataEnums: analysis.model.dataEnums,
53
+ typeArgs: "accounts" in target ? undefined : target.typeArgs,
54
+ structArgs: "accounts" in target ? undefined : target.structArgs,
55
+ };
40
56
  this.checker = analysis.checker;
57
+ this.analysis = analysis;
41
58
  if ("accounts" in target) {
42
59
  this.ix = target;
43
60
  for (const a of target.accounts) {
44
61
  this.accounts.set(a.name, a);
45
- if (a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || (a.kind.kind === "token" && a.kind.init))
62
+ if (a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || ((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init))
46
63
  this.pendingInit.add(a.name);
47
64
  }
48
65
  for (const a of target.args)
@@ -66,6 +83,102 @@ export class EmitterBase {
66
83
  }
67
84
  }
68
85
  }
86
+ /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
87
+ helperOf(node) {
88
+ if (!ts.isIdentifier(node.expression))
89
+ return undefined;
90
+ const cached = this.calleeCache.get(node);
91
+ if (cached !== undefined)
92
+ return cached ?? undefined;
93
+ const callee = node.expression;
94
+ // A function parameter of this helper instance: the function it was given.
95
+ const passed = this.helper?.fnArgs?.get(callee.text);
96
+ const name = passed ?? calleeName(callee, this.model.helperDecls, this.checker) ?? (this.lookup(callee.text) ? undefined : callee.text);
97
+ const g = name ? this.model.generics.get(name) : undefined;
98
+ let h = name ? this.model.helpers.get(name) : undefined;
99
+ if (g)
100
+ h = this.analysis.instantiate(g, g.typeParams.length ? this.typeArgsOf(node, g) : [], this.fnArgsOf(node, g));
101
+ else if (h && node.typeArguments)
102
+ this.fail(node, `${h.display} takes no type arguments`);
103
+ this.calleeCache.set(node, h ?? null);
104
+ return h;
105
+ }
106
+ /**
107
+ * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
108
+ * captures nothing). A closure can only run where it is declared, so it cannot be passed.
109
+ */
110
+ fnArgsOf(node, g) {
111
+ return g.fnParams.map((i) => {
112
+ const arg = node.arguments[i];
113
+ const pname = g.node.parameters[i].name.text;
114
+ if (!arg || !ts.isIdentifier(arg))
115
+ this.fail(arg ?? node, `pass ${g.display}'s ${pname} a named function (declare it: const f = (…) => …)`);
116
+ const passed = this.helper?.fnArgs?.get(arg.text);
117
+ if (passed)
118
+ return passed;
119
+ if (this.lookup(arg.text)?.closure)
120
+ this.fail(arg, `${arg.text} reads the variables of the function around it, so it can only be called there, not passed to ${g.display}`);
121
+ const name = calleeName(arg, this.model.helperDecls, this.checker) ?? arg.text;
122
+ if (this.model.generics.has(name))
123
+ this.fail(arg, `${arg.text} is generic or takes functions itself: pass a plain function`);
124
+ if (!this.model.helpers.has(name))
125
+ this.fail(arg, `${arg.text} is not a function of this program`);
126
+ return name;
127
+ });
128
+ }
129
+ /** A struct written as a type argument (`pick<Point>(…)`). */
130
+ structArg(t) {
131
+ const name = structNameOf(t, this.scope);
132
+ return name && this.model.structs.has(name) ? this.fixedStruct(name, t) : undefined;
133
+ }
134
+ fixedStruct(name, at) {
135
+ const s = this.model.structs.get(name);
136
+ if (s.size === undefined || s.vec)
137
+ this.fail(at, `${name} has variable-size fields; type parameters stand for fixed-size structs`);
138
+ return { kind: "struct", name };
139
+ }
140
+ /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
141
+ typeArgsOf(node, g) {
142
+ const usage = `write ${g.display}<u64>(…) with the types`;
143
+ if (node.typeArguments) {
144
+ if (node.typeArguments.length !== g.typeParams.length)
145
+ this.fail(node, `${g.display} takes ${g.typeParams.length} type argument(s)`);
146
+ return node.typeArguments.map((t) => resolveScalarType(t, this.scope) ?? this.structArg(t) ?? this.fail(t, "type arguments are integers, boolean, Pubkey, enums or fixed-size structs"));
147
+ }
148
+ return g.typeParams.map((tp) => {
149
+ for (const [i, p] of g.node.parameters.entries()) {
150
+ const arg = node.arguments[i];
151
+ if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
152
+ continue;
153
+ // Type the argument without emitting it: it is lowered again, once, by the call.
154
+ const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
155
+ const probe = this.reporter.guard(() => this.expr(arg));
156
+ this.lines.length = saved.lines;
157
+ this.hoisted.length = saved.hoisted;
158
+ this.temp = saved.temp;
159
+ this.reporter.diagnostics.length = saved.diagnostics;
160
+ if (probe?.type.kind === "structRef")
161
+ return this.fixedStruct(probe.type.struct, arg);
162
+ if (!probe) {
163
+ // A struct place (`p.a`) is not a value: type it as a reference, again without emitting anything.
164
+ const r = this.reporter.guard(() => this.ref(arg));
165
+ this.lines.length = saved.lines;
166
+ this.hoisted.length = saved.hoisted;
167
+ this.temp = saved.temp;
168
+ this.reporter.diagnostics.length = saved.diagnostics;
169
+ if (r?.type.kind === "struct")
170
+ return this.fixedStruct(r.type.name, arg);
171
+ }
172
+ if (!probe || probe.literal !== undefined || probe.isNull)
173
+ continue;
174
+ if (!isScalar(probe.type))
175
+ this.fail(arg, `${tp} stands for an integer, boolean, Pubkey, enum or struct, not ${probe.type.kind}`);
176
+ const t = probe.type;
177
+ return t.kind === "pubkey" ? (t.hash ? { kind: "pubkey", hash: true } : { kind: "pubkey" }) : t;
178
+ }
179
+ return this.fail(node, `cannot tell what ${tp} is from literal arguments; ${usage}`);
180
+ });
181
+ }
69
182
  line(s) {
70
183
  // Runtime failures name the TypeScript line they come from: "TypeSOL: arithmetic overflow (vault.ts:42)".
71
184
  if (this.where)
@@ -136,7 +249,7 @@ export class EmitterBase {
136
249
  if ((ts.isPropertyAccessExpression(node) || ts.isCallExpression(node) || ts.isElementAccessExpression(node)) && node.questionDotToken)
137
250
  this.fail(node, "optional chaining (?.) is not supported; check `x !== null` first");
138
251
  if (ts.isNonNullExpression(node))
139
- this.fail(node, "the ! assertion works on Option values only; check `x !== null` so TypeScript narrows it");
252
+ this.fail(node, "the ! assertion works on Option values and optional accounts only; check `x !== null` so TypeScript narrows it");
140
253
  }
141
254
  lookup(name) {
142
255
  for (let i = this.scopes.length - 1; i >= 0; i--) {
@@ -159,7 +272,25 @@ export class EmitterBase {
159
272
  exportName(node) {
160
273
  return ts.isIdentifier(node) ? this.imports.get(node.text) : undefined;
161
274
  }
275
+ /**
276
+ * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
277
+ * failing with 3020 otherwise). Returns undefined for anything else.
278
+ */
279
+ withoutBang(node) {
280
+ if (!ts.isPropertyAccessExpression(node) || !ts.isNonNullExpression(node.expression))
281
+ return undefined;
282
+ const inner = node.expression.expression;
283
+ if (!ts.isIdentifier(inner) || !this.accounts.get(inner.text)?.optional)
284
+ return undefined;
285
+ const plain = ts.factory.createPropertyAccessExpression(inner, node.name);
286
+ ts.setTextRange(plain, node);
287
+ plain.parent = node.parent;
288
+ return plain;
289
+ }
162
290
  accountParam(node, what) {
291
+ // `acct!` on an optional account: the account (its use checks it was passed).
292
+ if (ts.isNonNullExpression(node) && ts.isIdentifier(node.expression) && this.accounts.get(node.expression.text)?.optional)
293
+ return this.accountParam(node.expression, what);
163
294
  if (!ts.isIdentifier(node))
164
295
  this.fail(node, `${what} must be an account parameter`);
165
296
  const a = this.accounts.get(node.text);
@@ -176,7 +307,7 @@ export class EmitterBase {
176
307
  ? "create"
177
308
  : a.kind.kind === "initIfNeeded"
178
309
  ? "createIfNeeded"
179
- : a.kind.kind === "token" && a.kind.init
310
+ : (a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init
180
311
  ? a.kind.init === "create"
181
312
  ? "create"
182
313
  : "createIfNeeded"
@@ -0,0 +1,13 @@
1
+ import type { FieldType, ProgramModel, StructDef } from "../model.js";
2
+ /**
3
+ * Accounts declared `@layout borsh` are stored as Anchor stores them (Borsh: a String or Vec takes 4 + its length, an
4
+ * empty Option one byte, and the fields after them move). An instruction unpacks such an account into the usual
5
+ * fixed-layout struct on the heap (`ts_unpack_<S>`, checking it as Borsh deserialization would), runs on that, and
6
+ * packs it back when it returns (`ts_pack_<S>`), so every language feature works on it unchanged.
7
+ */
8
+ /** A field type whose Borsh bytes are its fixed-layout bytes: scalars, Pubkeys, unit enums, arrays and structs of them. */
9
+ export declare function borshIdentical(t: FieldType, model: Pick<ProgramModel, "structs">): boolean;
10
+ /** The fewest bytes a Borsh `S` can take (every String and Vec empty, every Option None). */
11
+ export declare function borshMinSize(s: StructDef, model: Pick<ProgramModel, "structs">): number;
12
+ /** C for `ts_unpack_<S>` and `ts_pack_<S>` of every `@layout borsh` struct. */
13
+ export declare function emitBorshCodecs(model: ProgramModel): string[];
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Accounts declared `@layout borsh` are stored as Anchor stores them (Borsh: a String or Vec takes 4 + its length, an
3
+ * empty Option one byte, and the fields after them move). An instruction unpacks such an account into the usual
4
+ * fixed-layout struct on the heap (`ts_unpack_<S>`, checking it as Borsh deserialization would), runs on that, and
5
+ * packs it back when it returns (`ts_pack_<S>`), so every language feature works on it unchanged.
6
+ */
7
+ /** A field type whose Borsh bytes are its fixed-layout bytes: scalars, Pubkeys, unit enums, arrays and structs of them. */
8
+ export function borshIdentical(t, model) {
9
+ switch (t.kind) {
10
+ case "int":
11
+ case "bool":
12
+ case "pubkey":
13
+ case "enum":
14
+ return true;
15
+ case "array":
16
+ return borshIdentical(t.elem, model);
17
+ case "struct": {
18
+ const s = model.structs.get(t.name);
19
+ return !!s && s.layout !== "borsh" && s.fields.every((f) => borshIdentical(f.type, model));
20
+ }
21
+ default:
22
+ return false;
23
+ }
24
+ }
25
+ /** Bytes of a Borsh-identical type. */
26
+ function fixedBytes(t, model) {
27
+ switch (t.kind) {
28
+ case "int":
29
+ return Number(t.name.slice(1)) / 8;
30
+ case "bool":
31
+ case "enum":
32
+ return 1;
33
+ case "pubkey":
34
+ return 32;
35
+ case "array":
36
+ return fixedBytes(t.elem, model) * t.len;
37
+ case "struct":
38
+ return model.structs.get(t.name).size;
39
+ default:
40
+ throw new Error(`unreachable: ${t.kind} has no fixed Borsh size`);
41
+ }
42
+ }
43
+ /** The fewest bytes a Borsh `S` can take (every String and Vec empty, every Option None). */
44
+ export function borshMinSize(s, model) {
45
+ let n = 0;
46
+ for (const f of s.fields) {
47
+ const t = f.type;
48
+ if (t.kind === "fstring" || t.kind === "vec" || t.kind === "string")
49
+ n += 4;
50
+ else if (t.kind === "option" || t.kind === "dataEnum")
51
+ n += 1;
52
+ else
53
+ n += fixedBytes(t, model);
54
+ }
55
+ return n;
56
+ }
57
+ const DESER = `TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: bytes are not a valid Borsh value")`;
58
+ const SER = `TS_FAIL(TS_ERR_DID_NOT_SERIALIZE, "TypeSOL: account data too small for the value")`;
59
+ /** C for `ts_unpack_<S>` and `ts_pack_<S>` of every `@layout borsh` struct. */
60
+ export function emitBorshCodecs(model) {
61
+ const out = [];
62
+ for (const s of model.structs.values()) {
63
+ if (s.layout !== "borsh" || s.size === undefined)
64
+ continue;
65
+ let k = 0;
66
+ const tmp = (p) => `${p}${k++}`;
67
+ // Bools and enum tags inside a Borsh-identical value: invalid ones fail, as Borsh deserialization does.
68
+ const validate = (t, lv, pad) => {
69
+ switch (t.kind) {
70
+ case "bool":
71
+ return [`${pad}if (${lv} > 1) ${DESER};`];
72
+ case "enum":
73
+ return [`${pad}if (${lv} >= ${model.enums.get(t.name).variants.length}) ${DESER};`];
74
+ case "array": {
75
+ const i = tmp("i");
76
+ const inner = validate(t.elem, `${lv}[${i}]`, `${pad} `);
77
+ return inner.length ? [`${pad}for (uint64_t ${i} = 0; ${i} < ${t.len}; ${i}++) {`, ...inner, `${pad}}`] : [];
78
+ }
79
+ case "struct":
80
+ return model.structs.get(t.name).fields.flatMap((f) => validate(f.type, `${lv}.${f.name}`, pad));
81
+ default:
82
+ return [];
83
+ }
84
+ };
85
+ // Bounds: the caller checked `len >= MIN` (the smallest Borsh form). Fixed-size pieces then always fit; only a
86
+ // String / Vec / Option payload is checked, against what is left after the fields that follow it still need.
87
+ const minOf = (t) => t.kind === "fstring" || t.kind === "vec" || t.kind === "string" ? 4 : t.kind === "option" || t.kind === "dataEnum" ? 1 : fixedBytes(t, model);
88
+ const minAfter = s.fields.map((_, i) => s.fields.slice(i + 1).reduce((n, f) => n + minOf(f.type), 0));
89
+ const need = (n, rest, pad) => `${pad}if (${n} > len - at - ${rest}) ${DESER};`;
90
+ const room = (n, rest, pad) => `${pad}if (${n} > cap - at - ${rest}) ${SER};`;
91
+ // A String / Vec / Option field, `rest` bytes of later fields still to come.
92
+ const unpackVar = (t, lv, rest, pad) => {
93
+ switch (t.kind) {
94
+ case "fstring": {
95
+ const l = tmp("n");
96
+ // Strings were checked as UTF-8 when they came in (arguments) and are only stored whole: not checked again.
97
+ return [
98
+ `${pad}uint32_t ${l} = (uint32_t)ts_read_le(src + at, 4);`,
99
+ `${pad}at += 4;`,
100
+ `${pad}if (${l} > ${t.cap}) ${DESER};`,
101
+ need(l, rest, pad),
102
+ `${pad}${lv}.len = ${l};`,
103
+ `${pad}ts_memcpy(${lv}.bytes, src + at, ${l});`,
104
+ `${pad}at += ${l};`,
105
+ ];
106
+ }
107
+ case "vec": {
108
+ const l = tmp("n");
109
+ const head = [
110
+ `${pad}uint32_t ${l} = (uint32_t)ts_read_le(src + at, 4);`,
111
+ `${pad}at += 4;`,
112
+ `${pad}if (${l} > ${t.cap}) ${DESER};`,
113
+ `${pad}${lv}.len = ${l};`,
114
+ ];
115
+ if (borshIdentical(t.elem, model)) {
116
+ const n = fixedBytes(t.elem, model);
117
+ const i = tmp("i");
118
+ const check = validate(t.elem, `${lv}.items[${i}]`, `${pad} `);
119
+ return [
120
+ ...head,
121
+ need(`(uint64_t)${l} * ${n}`, rest, pad),
122
+ `${pad}ts_memcpy(${lv}.items, src + at, (uint64_t)${l} * ${n});`,
123
+ `${pad}at += (uint64_t)${l} * ${n};`,
124
+ ...(check.length ? [`${pad}for (uint64_t ${i} = 0; ${i} < ${l}; ${i}++) {`, ...check, `${pad}}`] : []),
125
+ ];
126
+ }
127
+ // Strings: each element's length prefix is checked as it is reached.
128
+ const i = tmp("i");
129
+ return [
130
+ ...head,
131
+ `${pad}for (uint64_t ${i} = 0; ${i} < ${l}; ${i}++) {`,
132
+ need("4", rest, `${pad} `),
133
+ ...unpackVar(t.elem, `${lv}.items[${i}]`, rest, `${pad} `),
134
+ `${pad}}`,
135
+ ];
136
+ }
137
+ case "option": {
138
+ const tag = tmp("tag");
139
+ const n = fixedBytes(t.inner, model);
140
+ return [
141
+ `${pad}uint8_t ${tag} = src[at++];`,
142
+ `${pad}if (${tag} > 1) ${DESER};`,
143
+ `${pad}${lv}.some = ${tag};`,
144
+ `${pad}if (${tag}) {`,
145
+ need(String(n), rest, `${pad} `),
146
+ `${pad} ts_memcpy(&${lv}.v, src + at, ${n});`,
147
+ `${pad} at += ${n};`,
148
+ ...validate(t.inner, `${lv}.v`, `${pad} `),
149
+ `${pad}}`,
150
+ ];
151
+ }
152
+ case "string": {
153
+ // A view of the bytes it was read from (arguments only), checked as UTF-8 as Borsh String deserialization does.
154
+ const l = tmp("n");
155
+ return [
156
+ `${pad}uint32_t ${l} = (uint32_t)ts_read_le(src + at, 4);`,
157
+ `${pad}at += 4;`,
158
+ need(l, rest, pad),
159
+ `${pad}if (!ts_utf8_valid(src + at, ${l})) ${DESER};`,
160
+ `${pad}${lv} = ts_str(src + at, ${l});`,
161
+ `${pad}at += ${l};`,
162
+ ];
163
+ }
164
+ case "dataEnum": {
165
+ const e = model.dataEnums.get(t.name);
166
+ const tag = tmp("tag");
167
+ const lines = [
168
+ `${pad}uint8_t ${tag} = src[at++];`,
169
+ `${pad}if (${tag} >= ${e.variants.length}) ${DESER};`,
170
+ `${pad}${lv}.tag = ${tag};`,
171
+ `${pad}switch (${tag}) {`,
172
+ ];
173
+ e.variants.forEach((v, i) => {
174
+ lines.push(`${pad}case ${i}: {`);
175
+ for (const f of v.fields) {
176
+ const flv = `${lv}.v.${v.name}.${f.name}`;
177
+ if (f.type.kind === "string") {
178
+ lines.push(`${pad} if (4 > len - at - ${rest}) ${DESER};`, ...unpackVar(f.type, flv, rest, `${pad} `));
179
+ continue;
180
+ }
181
+ const n = fixedBytes(f.type, model);
182
+ lines.push(need(String(n), rest, `${pad} `), `${pad} ts_memcpy(&${flv}, src + at, ${n});`, `${pad} at += ${n};`, ...validate(f.type, flv, `${pad} `));
183
+ }
184
+ lines.push(`${pad} break;`, `${pad}}`);
185
+ });
186
+ lines.push(`${pad}}`);
187
+ return lines;
188
+ }
189
+ default:
190
+ throw new Error(`unreachable: ${t.kind} in a Borsh account`);
191
+ }
192
+ };
193
+ const packVar = (t, lv, rest, pad) => {
194
+ switch (t.kind) {
195
+ case "fstring": {
196
+ const l = tmp("n");
197
+ return [
198
+ `${pad}uint32_t ${l} = ${lv}.len > ${t.cap} ? ${t.cap} : ${lv}.len;`,
199
+ room(`4 + (uint64_t)${l}`, rest, pad),
200
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
201
+ `${pad}ts_memcpy(dst + at + 4, ${lv}.bytes, ${l});`,
202
+ `${pad}at += 4 + (uint64_t)${l};`,
203
+ ];
204
+ }
205
+ case "vec": {
206
+ const l = tmp("n");
207
+ if (borshIdentical(t.elem, model)) {
208
+ const n = fixedBytes(t.elem, model);
209
+ return [
210
+ `${pad}uint32_t ${l} = ${lv}.len > ${t.cap} ? ${t.cap} : ${lv}.len;`,
211
+ room(`4 + (uint64_t)${l} * ${n}`, rest, pad),
212
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
213
+ `${pad}ts_memcpy(dst + at + 4, ${lv}.items, (uint64_t)${l} * ${n});`,
214
+ `${pad}at += 4 + (uint64_t)${l} * ${n};`,
215
+ ];
216
+ }
217
+ const i = tmp("i");
218
+ return [
219
+ `${pad}uint32_t ${l} = ${lv}.len > ${t.cap} ? ${t.cap} : ${lv}.len;`,
220
+ room("4", rest, pad),
221
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
222
+ `${pad}at += 4;`,
223
+ `${pad}for (uint64_t ${i} = 0; ${i} < ${l}; ${i}++) {`,
224
+ ...packVar(t.elem, `${lv}.items[${i}]`, rest, `${pad} `),
225
+ `${pad}}`,
226
+ ];
227
+ }
228
+ case "option": {
229
+ const n = fixedBytes(t.inner, model);
230
+ return [
231
+ room(`(uint64_t)(${lv}.some ? ${1 + n} : 1)`, rest, pad),
232
+ `${pad}dst[at++] = ${lv}.some ? 1 : 0;`,
233
+ `${pad}if (${lv}.some) {`,
234
+ `${pad} ts_memcpy(dst + at, &${lv}.v, ${n});`,
235
+ `${pad} at += ${n};`,
236
+ `${pad}}`,
237
+ ];
238
+ }
239
+ case "string": {
240
+ const l = tmp("n");
241
+ return [
242
+ `${pad}uint32_t ${l} = (uint32_t)${lv}.len;`,
243
+ room(`4 + (uint64_t)${l}`, rest, pad),
244
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
245
+ `${pad}ts_memcpy(dst + at + 4, ${lv}.ptr, ${l});`,
246
+ `${pad}at += 4 + (uint64_t)${l};`,
247
+ ];
248
+ }
249
+ case "dataEnum": {
250
+ const e = model.dataEnums.get(t.name);
251
+ const lines = [room("1", rest, pad), `${pad}dst[at++] = ${lv}.tag;`, `${pad}switch (${lv}.tag) {`];
252
+ e.variants.forEach((v, i) => {
253
+ lines.push(`${pad}case ${i}: {`);
254
+ for (const f of v.fields) {
255
+ const flv = `${lv}.v.${v.name}.${f.name}`;
256
+ if (f.type.kind === "string") {
257
+ lines.push(...packVar(f.type, flv, rest, `${pad} `));
258
+ continue;
259
+ }
260
+ const n = fixedBytes(f.type, model);
261
+ lines.push(room(String(n), rest, `${pad} `), `${pad} ts_memcpy(dst + at, &${flv}, ${n});`, `${pad} at += ${n};`);
262
+ }
263
+ lines.push(`${pad} break;`, `${pad}}`);
264
+ });
265
+ lines.push(`${pad}}`);
266
+ return lines;
267
+ }
268
+ default:
269
+ throw new Error(`unreachable: ${t.kind} in a Borsh account`);
270
+ }
271
+ };
272
+ // Runs of Borsh-identical fields are contiguous in both layouts: one copy each.
273
+ const unpackLines = [];
274
+ const packLines = [];
275
+ for (let i = 0; i < s.fields.length;) {
276
+ const f = s.fields[i];
277
+ if (borshIdentical(f.type, model)) {
278
+ let j = i;
279
+ let bytes = 0;
280
+ while (j < s.fields.length && borshIdentical(s.fields[j].type, model))
281
+ bytes += fixedBytes(s.fields[j++].type, model);
282
+ unpackLines.push(` ts_memcpy(&out->${f.name}, src + at, ${bytes});`, ` at += ${bytes};`);
283
+ for (const g of s.fields.slice(i, j))
284
+ unpackLines.push(...validate(g.type, `out->${g.name}`, " "));
285
+ // A pack can be short of room only after a String / Vec / Option grew; the start always fits (cap >= MIN).
286
+ packLines.push(...(i > 0 ? [room(String(bytes), minAfter[j - 1], " ")] : []), ` ts_memcpy(dst + at, &in->${f.name}, ${bytes});`, ` at += ${bytes};`);
287
+ i = j;
288
+ continue;
289
+ }
290
+ unpackLines.push(...unpackVar(f.type, `out->${f.name}`, minAfter[i], " "));
291
+ packLines.push(...packVar(f.type, `in->${f.name}`, minAfter[i], " "));
292
+ i++;
293
+ }
294
+ const min = borshMinSize(s, model);
295
+ out.push(`/* ${s.name} as Borsh (Anchor's account layout) <-> its fixed-layout struct. */`, `__attribute__((noinline)) static uint64_t ts_unpack_${s.name}(const uint8_t *src, uint64_t len, TSS_${s.name} *out, uint64_t *used) {`, ` if (len < ${min}) ${DESER};`, ` uint64_t at = 0;`, ...unpackLines, ` if (used) *used = at;`, ` return SUCCESS;`, `}`, `__attribute__((noinline)) static uint64_t ts_pack_${s.name}(const TSS_${s.name} *in, uint8_t *dst, uint64_t cap, uint64_t *written) {`, ` if (cap < ${min}) ${SER};`, ` uint64_t at = 0;`, ...packLines, ` if (written) *written = at;`, ` return SUCCESS;`, `}`, "");
296
+ }
297
+ return out;
298
+ }
@@ -2,9 +2,6 @@ import ts from "typescript";
2
2
  import { type ErrorDef, type FieldType, type Helper } from "../model.js";
3
3
  import { type Value, type Ref } from "./shared.js";
4
4
  import { ExpressionEmitter } from "./expressions.js";
5
- /**
6
- * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
7
- */
8
5
  export declare abstract class CallEmitter extends ExpressionEmitter {
9
6
  protected call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
10
7
  protected vecMethod(node: ts.CallExpression, method: string, v: Ref): Value | undefined;
@@ -32,6 +29,8 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
32
29
  protected loadForeign(node: ts.CallExpression): Value;
33
30
  /** `loadAccount<T>(acct)` / `loadAccountMut<T>(acct)`: checked view of a program account. */
34
31
  protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
32
+ /** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
33
+ private helperGrows;
35
34
  protected callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
36
35
  /** One helper argument: a C argument list fragment (accounts, seeds, references) or a value to settle. */
37
36
  protected helperArg(h: Helper, p: Helper["params"][number], n: ts.Expression): Value | string;