@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
@@ -1,4 +1,6 @@
1
1
  import ts from "typescript";
2
+ import { calleeName, resolveScalarType } 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,
@@ -27,6 +29,9 @@ export class EmitterBase {
27
29
  helper;
28
30
  scope;
29
31
  checker;
32
+ analysis;
33
+ /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
34
+ calleeCache = new Map();
30
35
  /** `file.ts:line` of the statement being lowered. */
31
36
  where;
32
37
  profile;
@@ -36,8 +41,15 @@ export class EmitterBase {
36
41
  this.model = analysis.model;
37
42
  // Each function resolves `typesol` names through its own file's imports.
38
43
  this.imports = target.imports;
39
- this.scope = { imports: target.imports, enums: analysis.model.enums, options: analysis.model.options, dataEnums: analysis.model.dataEnums };
44
+ this.scope = {
45
+ imports: target.imports,
46
+ enums: analysis.model.enums,
47
+ options: analysis.model.options,
48
+ dataEnums: analysis.model.dataEnums,
49
+ typeArgs: "accounts" in target ? undefined : target.typeArgs,
50
+ };
40
51
  this.checker = analysis.checker;
52
+ this.analysis = analysis;
41
53
  if ("accounts" in target) {
42
54
  this.ix = target;
43
55
  for (const a of target.accounts) {
@@ -66,6 +78,54 @@ export class EmitterBase {
66
78
  }
67
79
  }
68
80
  }
81
+ /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
82
+ helperOf(node) {
83
+ if (!ts.isIdentifier(node.expression))
84
+ return undefined;
85
+ const cached = this.calleeCache.get(node);
86
+ if (cached !== undefined)
87
+ return cached ?? undefined;
88
+ const callee = node.expression;
89
+ const name = calleeName(callee, this.model.helperDecls, this.checker) ?? (this.lookup(callee.text) ? undefined : callee.text);
90
+ const g = name ? this.model.generics.get(name) : undefined;
91
+ let h = name ? this.model.helpers.get(name) : undefined;
92
+ if (g)
93
+ h = this.analysis.instantiate(g, this.typeArgsOf(node, g));
94
+ else if (h && node.typeArguments)
95
+ this.fail(node, `${h.display} takes no type arguments`);
96
+ this.calleeCache.set(node, h ?? null);
97
+ return h;
98
+ }
99
+ /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
100
+ typeArgsOf(node, g) {
101
+ const usage = `write ${g.display}<u64>(…) with the types`;
102
+ if (node.typeArguments) {
103
+ if (node.typeArguments.length !== g.typeParams.length)
104
+ this.fail(node, `${g.display} takes ${g.typeParams.length} type argument(s)`);
105
+ return node.typeArguments.map((t) => resolveScalarType(t, this.scope) ?? this.fail(t, "type arguments are integers, boolean, Pubkey or enums"));
106
+ }
107
+ return g.typeParams.map((tp) => {
108
+ for (const [i, p] of g.node.parameters.entries()) {
109
+ const arg = node.arguments[i];
110
+ if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
111
+ continue;
112
+ // Type the argument without emitting it: it is lowered again, once, by the call.
113
+ const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
114
+ const probe = this.reporter.guard(() => this.expr(arg));
115
+ this.lines.length = saved.lines;
116
+ this.hoisted.length = saved.hoisted;
117
+ this.temp = saved.temp;
118
+ this.reporter.diagnostics.length = saved.diagnostics;
119
+ if (!probe || probe.literal !== undefined || probe.isNull)
120
+ continue;
121
+ if (!isScalar(probe.type))
122
+ this.fail(arg, `${tp} stands for an integer, boolean, Pubkey or enum, not ${probe.type.kind}`);
123
+ const t = probe.type;
124
+ return t.kind === "pubkey" ? (t.hash ? { kind: "pubkey", hash: true } : { kind: "pubkey" }) : t;
125
+ }
126
+ return this.fail(node, `cannot tell what ${tp} is from literal arguments; ${usage}`);
127
+ });
128
+ }
69
129
  line(s) {
70
130
  // Runtime failures name the TypeScript line they come from: "TypeSOL: arithmetic overflow (vault.ts:42)".
71
131
  if (this.where)
@@ -136,7 +196,7 @@ export class EmitterBase {
136
196
  if ((ts.isPropertyAccessExpression(node) || ts.isCallExpression(node) || ts.isElementAccessExpression(node)) && node.questionDotToken)
137
197
  this.fail(node, "optional chaining (?.) is not supported; check `x !== null` first");
138
198
  if (ts.isNonNullExpression(node))
139
- this.fail(node, "the ! assertion is not supported; check `x !== null` so TypeScript narrows it");
199
+ this.fail(node, "the ! assertion works on Option values only; check `x !== null` so TypeScript narrows it");
140
200
  }
141
201
  lookup(name) {
142
202
  for (let i = this.scopes.length - 1; i >= 0; i--) {
@@ -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,232 @@
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")
49
+ n += 4;
50
+ else if (t.kind === "option")
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: account data is 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" ? 4 : t.kind === "option" ? 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
+ default:
153
+ throw new Error(`unreachable: ${t.kind} in a Borsh account`);
154
+ }
155
+ };
156
+ const packVar = (t, lv, rest, pad) => {
157
+ switch (t.kind) {
158
+ case "fstring": {
159
+ const l = tmp("n");
160
+ return [
161
+ `${pad}uint32_t ${l} = ${lv}.len > ${t.cap} ? ${t.cap} : ${lv}.len;`,
162
+ room(`4 + (uint64_t)${l}`, rest, pad),
163
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
164
+ `${pad}ts_memcpy(dst + at + 4, ${lv}.bytes, ${l});`,
165
+ `${pad}at += 4 + (uint64_t)${l};`,
166
+ ];
167
+ }
168
+ case "vec": {
169
+ const l = tmp("n");
170
+ if (borshIdentical(t.elem, model)) {
171
+ const n = fixedBytes(t.elem, model);
172
+ return [
173
+ `${pad}uint32_t ${l} = ${lv}.len > ${t.cap} ? ${t.cap} : ${lv}.len;`,
174
+ room(`4 + (uint64_t)${l} * ${n}`, rest, pad),
175
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
176
+ `${pad}ts_memcpy(dst + at + 4, ${lv}.items, (uint64_t)${l} * ${n});`,
177
+ `${pad}at += 4 + (uint64_t)${l} * ${n};`,
178
+ ];
179
+ }
180
+ const i = tmp("i");
181
+ return [
182
+ `${pad}uint32_t ${l} = ${lv}.len > ${t.cap} ? ${t.cap} : ${lv}.len;`,
183
+ room("4", rest, pad),
184
+ `${pad}ts_write_le(dst + at, ${l}, 4);`,
185
+ `${pad}at += 4;`,
186
+ `${pad}for (uint64_t ${i} = 0; ${i} < ${l}; ${i}++) {`,
187
+ ...packVar(t.elem, `${lv}.items[${i}]`, rest, `${pad} `),
188
+ `${pad}}`,
189
+ ];
190
+ }
191
+ case "option": {
192
+ const n = fixedBytes(t.inner, model);
193
+ return [
194
+ room(`(uint64_t)(${lv}.some ? ${1 + n} : 1)`, rest, pad),
195
+ `${pad}dst[at++] = ${lv}.some ? 1 : 0;`,
196
+ `${pad}if (${lv}.some) {`,
197
+ `${pad} ts_memcpy(dst + at, &${lv}.v, ${n});`,
198
+ `${pad} at += ${n};`,
199
+ `${pad}}`,
200
+ ];
201
+ }
202
+ default:
203
+ throw new Error(`unreachable: ${t.kind} in a Borsh account`);
204
+ }
205
+ };
206
+ // Runs of Borsh-identical fields are contiguous in both layouts: one copy each.
207
+ const unpackLines = [];
208
+ const packLines = [];
209
+ for (let i = 0; i < s.fields.length;) {
210
+ const f = s.fields[i];
211
+ if (borshIdentical(f.type, model)) {
212
+ let j = i;
213
+ let bytes = 0;
214
+ while (j < s.fields.length && borshIdentical(s.fields[j].type, model))
215
+ bytes += fixedBytes(s.fields[j++].type, model);
216
+ unpackLines.push(` ts_memcpy(&out->${f.name}, src + at, ${bytes});`, ` at += ${bytes};`);
217
+ for (const g of s.fields.slice(i, j))
218
+ unpackLines.push(...validate(g.type, `out->${g.name}`, " "));
219
+ // A pack can be short of room only after a String / Vec / Option grew; the start always fits (cap >= MIN).
220
+ packLines.push(...(i > 0 ? [room(String(bytes), minAfter[j - 1], " ")] : []), ` ts_memcpy(dst + at, &in->${f.name}, ${bytes});`, ` at += ${bytes};`);
221
+ i = j;
222
+ continue;
223
+ }
224
+ unpackLines.push(...unpackVar(f.type, `out->${f.name}`, minAfter[i], " "));
225
+ packLines.push(...packVar(f.type, `in->${f.name}`, minAfter[i], " "));
226
+ i++;
227
+ }
228
+ const min = borshMinSize(s, model);
229
+ 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) {`, ` if (len < ${min}) ${DESER};`, ` uint64_t at = 0;`, ...unpackLines, ` return SUCCESS;`, `}`, `__attribute__((noinline)) static uint64_t ts_pack_${s.name}(const TSS_${s.name} *in, uint8_t *dst, uint64_t cap) {`, ` if (cap < ${min}) ${SER};`, ` uint64_t at = 0;`, ...packLines, ` return SUCCESS;`, `}`, "");
230
+ }
231
+ return out;
232
+ }
@@ -2,7 +2,7 @@ import ts from "typescript";
2
2
  import { resolveScalarType } from "../analyze.js";
3
3
  import { decodeAddress } from "../../keys.js";
4
4
  import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
5
- import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, } from "./shared.js";
5
+ import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
6
6
  import { ExpressionEmitter } from "./expressions.js";
7
7
  /**
8
8
  * Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
@@ -364,13 +364,14 @@ export class CallEmitter extends ExpressionEmitter {
364
364
  this.line(`if (ts_find_pda(${seeds.name}, ${seeds.count}, ${program.c}, &${t}, &${b}) != SUCCESS) TS_FAIL(TS_ERR_SEEDS, "TypeSOL: no program address for these seeds");`);
365
365
  return { c: `(&${t})`, type: PUBKEY };
366
366
  }
367
- case "metadataAddress": {
367
+ case "metadataAddress":
368
+ case "masterEditionAddress": {
368
369
  if (args.length !== 1)
369
- this.fail(node, "metadataAddress(mint) takes the mint");
370
+ this.fail(node, `${builtin}(mint) takes the mint`);
370
371
  const m = this.expect(this.expr(args[0], PUBKEY), PUBKEY, args[0]);
371
372
  const t = this.tmp("md");
372
373
  this.line(`SolPubkey ${t};`);
373
- this.line(`TS_TRY(ts_metadata_address(${m.c}, &${t}));`);
374
+ this.line(`TS_TRY(${builtin === "metadataAddress" ? "ts_metadata_address" : "ts_master_edition_address"}(${m.c}, &${t}));`);
374
375
  return { c: `(&${t})`, type: PUBKEY };
375
376
  }
376
377
  case "initExtraAccountMetas":
@@ -433,8 +434,9 @@ export class CallEmitter extends ExpressionEmitter {
433
434
  }
434
435
  if (builtin)
435
436
  this.fail(callee, `${builtin} cannot be called here`);
436
- if (ts.isIdentifier(callee) && this.model.helpers.has(callee.text))
437
- return this.callHelper(node, this.model.helpers.get(callee.text));
437
+ const helper = this.helperOf(node);
438
+ if (helper)
439
+ return this.callHelper(node, helper);
438
440
  if (ts.isPropertyAccessExpression(callee)) {
439
441
  const method = callee.name.text;
440
442
  // Vec<T> instruction arguments are read-only views.
@@ -520,10 +522,14 @@ export class CallEmitter extends ExpressionEmitter {
520
522
  return this.ataOp(node, target, method);
521
523
  if (target?.kind.kind === "metadataProgram")
522
524
  return this.metadataOp(node, target, method);
523
- if (target?.kind.kind === "coreProgram")
524
- return this.coreOp(node, target, method);
525
- if (target?.kind.kind === "bubblegumProgram")
526
- return this.bubblegumOp(node, target, method);
525
+ // Each Metaplex CPI in its own C block: its data buffer's stack slot is reused by the next one.
526
+ if (target?.kind.kind === "coreProgram" || target?.kind.kind === "bubblegumProgram") {
527
+ const t = target;
528
+ this.line("{");
529
+ this.block(() => (t.kind.kind === "coreProgram" ? this.coreOp(node, t, method) : this.bubblegumOp(node, t, method)));
530
+ this.line("}");
531
+ return undefined;
532
+ }
527
533
  }
528
534
  }
529
535
  if (ts.isPropertyAccessExpression(callee) && ts.isCallExpression(callee.expression) && this.exportName(callee.expression.expression) === "fixedArray")
@@ -536,7 +542,8 @@ export class CallEmitter extends ExpressionEmitter {
536
542
  const args = node.arguments;
537
543
  const vt = v.type;
538
544
  const info = v.vec;
539
- const elemRef = (idx) => ({ ...v, lv: `${v.lv}[${idx}]`, type: vt.elem, vec: undefined, path: undefined });
545
+ const elemRef = (idx) => this.withFixedVec({ ...v, lv: `${info.items}[${idx}]`, type: vt.elem, vec: undefined, path: undefined });
546
+ const count = vecCount(info);
540
547
  /**
541
548
  * Evaluate the value into a temporary first (JS evaluates arguments before the call runs), then return a
542
549
  * writer for the element: `push(v.length)` sees the old length, `set(i, pop())` checks i after the pop.
@@ -564,6 +571,8 @@ export class CallEmitter extends ExpressionEmitter {
564
571
  // Elements removed or replaced here must not be referenced afterwards.
565
572
  const elemPaths = () => (ts.isPropertyAccessExpression(node.expression) ? this.pathsOf(node.expression.expression)?.map((p) => `${p}[]`) : undefined);
566
573
  const resize = (newLen, payer) => {
574
+ if (info.cap !== undefined)
575
+ return; // a FixedVec has its room already
567
576
  if (!info.resizable || !v.account)
568
577
  this.fail(node, "only Vecs of declared Mut<...> / Init<...> accounts can grow or shrink");
569
578
  this.line(`TS_TRY(ts_resize(params, ${payer}, a_${v.account.name}, 8 + ${info.fixed} + (${newLen}) * ${info.elemSize}, entry_${v.account.name}));`);
@@ -577,11 +586,23 @@ export class CallEmitter extends ExpressionEmitter {
577
586
  this.assertNotAliased(elemPaths(), node);
578
587
  const i = this.indexValue(args[0]);
579
588
  const write = stageElem(args[1]);
580
- this.checkIndex(i, `(uint64_t)${info.len}`);
589
+ this.checkIndex(i, count);
581
590
  write(elemRef(i));
582
591
  return undefined;
583
592
  }
584
593
  case "push": {
594
+ if (info.cap !== undefined) {
595
+ if (args.length !== 1)
596
+ this.fail(node, "push(value) on a FixedVec takes the value (it has room for its capacity already)");
597
+ needWritable();
598
+ const write = stageElem(args[0]);
599
+ const n = this.tmp("n");
600
+ this.line(`uint64_t ${n} = ${count};`);
601
+ this.line(`if (${n} >= ${info.cap}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: push() on a full FixedVec");`);
602
+ this.line(`${info.len} = (uint32_t)(${n} + 1);`);
603
+ write(elemRef(n));
604
+ return undefined;
605
+ }
585
606
  if (args.length !== 2)
586
607
  this.fail(node, "push(value, payer) takes the value and a Mut<Signer> that pays for the extra rent");
587
608
  needWritable();
@@ -604,7 +625,7 @@ export class CallEmitter extends ExpressionEmitter {
604
625
  this.assertNotAliased(elemPaths(), node);
605
626
  const st = this.storageValueType(vt.elem);
606
627
  const n = this.tmp("n");
607
- this.line(`uint64_t ${n} = (uint64_t)${info.len};`);
628
+ this.line(`uint64_t ${n} = ${count};`);
608
629
  this.line(`if (${n} == 0) TS_FAIL(TS_ERR_INDEX, "TypeSOL: pop() on an empty Vec");`);
609
630
  // Structs and arrays are dropped (pop() as a statement); read .get(length - 1) first to keep one.
610
631
  let result;
@@ -836,6 +857,8 @@ export class CallEmitter extends ExpressionEmitter {
836
857
  const s = targ && ts.isTypeReferenceNode(targ) && ts.isIdentifier(targ.typeName) ? this.model.structs.get(targ.typeName.text) : undefined;
837
858
  if (!s || s.size === undefined || s.vec)
838
859
  this.fail(node, "loadForeign needs a fixed-size struct type: loadForeign<Pool>(account, owner)");
860
+ if (s.layout === "borsh")
861
+ this.fail(node, `${s.name} is @layout borsh: loadForeign reads fixed layouts only`);
839
862
  if (node.arguments.length !== 2)
840
863
  this.fail(node, "loadForeign<T>(account, owner) takes the account and its owning program's address");
841
864
  const h = this.anyAccount(node.arguments[0], "loadForeign's account");
@@ -857,6 +880,8 @@ export class CallEmitter extends ExpressionEmitter {
857
880
  const s = targ && ts.isTypeReferenceNode(targ) && ts.isIdentifier(targ.typeName) ? this.model.structs.get(targ.typeName.text) : undefined;
858
881
  if (!s || s.size === undefined)
859
882
  this.fail(node, "loadAccount needs an account struct type: loadAccount<MyStruct>(account)");
883
+ if (s.layout === "borsh")
884
+ this.fail(node, `${s.name} is @layout borsh: declare it as an instruction parameter (remaining accounts are read in place)`);
860
885
  if (node.arguments.length !== 1)
861
886
  this.fail(node, "loadAccount<T>(account) takes one remaining account");
862
887
  const h = this.handle(node.arguments[0]);
@@ -876,7 +901,7 @@ export class CallEmitter extends ExpressionEmitter {
876
901
  }
877
902
  callHelper(node, h) {
878
903
  if (node.arguments.length !== h.params.length)
879
- this.fail(node, `${h.name}() takes ${h.params.length} argument(s)`);
904
+ this.fail(node, `${h.display}() takes ${h.params.length} argument(s)`);
880
905
  // Arguments are evaluated left to right: settle earlier ones when a later one runs statements.
881
906
  // A helper that takes accounts may change account data, so account text it gets is copied first.
882
907
  const takesAccounts = h.params.some((p) => p.account);
@@ -898,13 +923,20 @@ export class CallEmitter extends ExpressionEmitter {
898
923
  // A helper that takes a writable struct account may replace anything stored in it (see assertNotAliased).
899
924
  for (const [i, p] of h.params.entries())
900
925
  if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(node.arguments[i]))
901
- this.assertNotAliased([node.arguments[i].text], node, h.name);
926
+ this.assertNotAliased([node.arguments[i].text], node, h.display);
902
927
  const args = ["params", ...pieces.map((x) => (typeof x === "number" ? (values[x].type.kind === "pubkey" ? `*(${values[x].c})` : values[x].c) : x))];
903
928
  if (!h.returns) {
904
929
  this.line(`TS_TRY(fn_${h.name}(${args.join(", ")}));`);
905
930
  return undefined;
906
931
  }
907
932
  const t = this.tmp();
933
+ if (h.returns.kind === "struct") {
934
+ // The result lives in a local the caller owns: a reference to it reads and copies like any struct. Declared at
935
+ // the top of the function, so a reference taken inside a block (a ternary's branch) stays valid after it.
936
+ this.hoisted.push(` TSS_${h.returns.name} ${t};`);
937
+ this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
938
+ return { c: `(&${t})`, type: { kind: "structRef", struct: h.returns.name, writable: true } };
939
+ }
908
940
  this.line(`${scalarC(h.returns)} ${t};`);
909
941
  this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
910
942
  return h.returns.kind === "pubkey" ? { c: `(&${t})`, type: h.returns } : { c: t, type: h.returns };
@@ -916,6 +948,9 @@ export class CallEmitter extends ExpressionEmitter {
916
948
  const why = accountMismatch(acct.kind, p.account);
917
949
  if (why)
918
950
  this.fail(n, `${h.name}() takes ${p.name} as ${why}`);
951
+ // A @layout borsh account goes with the struct it is being worked on as.
952
+ if (p.account.kind === "account" && p.account.struct.layout === "borsh")
953
+ return `a_${acct.name}, d_${acct.name}`;
919
954
  return `a_${acct.name}`;
920
955
  }
921
956
  switch (p.type.kind) {
@@ -2,6 +2,13 @@ import ts from "typescript";
2
2
  import { type AccountParam, type IdlSeed, type ValueType } from "../model.js";
3
3
  import { type Value } from "./shared.js";
4
4
  import { CallEmitter } from "./calls.js";
5
+ /** One piece of a Metaplex instruction's Borsh data: fixed bytes, or a value lowered and serialized in source order. */
6
+ type MplField = {
7
+ bytes: number[];
8
+ } | {
9
+ value: ts.Expression;
10
+ type: ValueType;
11
+ };
5
12
  /**
6
13
  * Other programs: PDAs and seeds, account creation, System/Token/ATA/Metaplex CPIs, generic invoke, Borsh
7
14
  * serialization and events.
@@ -30,10 +37,16 @@ export declare abstract class CpiEmitter extends CallEmitter {
30
37
  protected systemTransfer(node: ts.CallExpression): undefined;
31
38
  /** systemProgram.createAccount(payer, account, lamports, space, owner, signerSeeds?) */
32
39
  protected createAccount(node: ts.CallExpression, system: AccountParam): undefined;
33
- /** Emit a CPI to the program account `program` with these metas (account, writable, signer). */
40
+ /**
41
+ * Emit a CPI to the program account `program` with these metas (account, writable, signer), and then (`tail`) a range
42
+ * of the remaining accounts, read-only (a Merkle proof).
43
+ */
34
44
  protected cpi(program: string, metas: [string, boolean, boolean][], buf: string, len: string, seeds: {
35
45
  name: string;
36
46
  count: number | string;
47
+ }, tail?: {
48
+ from: string;
49
+ to: string;
37
50
  }): void;
38
51
  protected signerSeeds(node: ts.Expression | undefined, authority: AccountParam, at: ts.Node): {
39
52
  name: string;
@@ -47,25 +60,35 @@ export declare abstract class CpiEmitter extends CallEmitter {
47
60
  protected tokenOp(node: ts.CallExpression, program: AccountParam, method: string): undefined;
48
61
  protected ataOp(node: ts.CallExpression, program: AccountParam, method: string): undefined;
49
62
  protected metadataOp(node: ts.CallExpression, program: AccountParam, method: string): undefined;
63
+ /** CreateMasterEditionV3 (17) and Verify (52) with VerificationArgs::CollectionV1. */
64
+ protected metadataCollectionOp(node: ts.CallExpression, program: AccountParam, method: string): undefined;
50
65
  /**
51
66
  * One account of a Metaplex CPI: an account parameter, or `null` for an optional account left out
52
67
  * (Metaplex programs take their own id in its place).
53
68
  */
54
69
  protected mplMeta(n: ts.Expression, program: AccountParam, what: string, writable: boolean, signer: boolean): [string, boolean, boolean];
55
70
  /** Borsh fields into a fresh buffer: raw bytes ("u8:N" style) or lowered values. */
56
- protected mplData(at: ts.Node, max: number, fields: ({
57
- bytes: number[];
58
- } | {
59
- value: ts.Expression;
60
- type: ValueType;
61
- })[]): {
71
+ protected mplData(at: ts.Node, max: number, fields: MplField[]): {
62
72
  buf: string;
63
73
  len: string;
64
74
  };
65
- /** Metaplex Core: createAsset, createCollection, transferAsset, burnAsset (no plugins). */
75
+ /**
76
+ * Borsh of one Metaplex Core plugin literal (`{ kind: "FreezeDelegate", frozen: true, authority?: … }`): the Plugin
77
+ * enum, then (`withAuthority`) the Option<PluginAuthority> that CreateV1's PluginAuthorityPair and AddPluginV1 take.
78
+ */
79
+ protected corePlugin(n: ts.Expression, withAuthority: boolean): MplField[];
80
+ /** CreateV1 / CreateCollectionV1 plugins: Option<Vec<PluginAuthorityPair>>. */
81
+ protected corePluginList(n: ts.Expression | undefined): MplField[];
82
+ /** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
66
83
  protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
84
+ /** PluginType (u8) from a kind string literal: removePlugin(…, "FreezeDelegate"). */
85
+ protected corePluginType(n: ts.Expression): MplField;
67
86
  /** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */
68
87
  protected bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
88
+ /** Bubblegum V2 (trees on mpl-account-compression with mpl-noop; optional Metaplex Core collections). */
89
+ protected bubblegumV2(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
90
+ /** `acct.key` of an account parameter, as an expression to serialize. */
91
+ protected keyOf(n: ts.Expression): ts.Expression;
69
92
  /**
70
93
  * `remainingAccounts()` (or a const bound to it): every remaining account; `rest.slice(a, b?)`: indexes a..b of them
71
94
  * (both checked against the count, 5104). C expressions for the range, relative to the first remaining account.
@@ -99,3 +122,4 @@ export declare abstract class CpiEmitter extends CallEmitter {
99
122
  /** Structs whose IDL description is the fixed layout: account data, arguments, and everything nested in them. */
100
123
  protected fixedLayoutStructs(): Set<string>;
101
124
  }
125
+ export {};