@typesol/cli 0.17.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.
- package/CHANGELOG.md +75 -0
- package/README.md +4 -4
- package/dist/clientgen.js +2 -0
- package/dist/compiler/analyze.d.ts +10 -1
- package/dist/compiler/analyze.js +129 -45
- package/dist/compiler/emit/base.d.ts +29 -1
- package/dist/compiler/emit/base.js +79 -8
- package/dist/compiler/emit/borsh.js +71 -5
- package/dist/compiler/emit/calls.d.ts +2 -3
- package/dist/compiler/emit/calls.js +93 -15
- package/dist/compiler/emit/cpi.d.ts +30 -0
- package/dist/compiler/emit/cpi.js +955 -39
- package/dist/compiler/emit/expressions.d.ts +5 -0
- package/dist/compiler/emit/expressions.js +66 -3
- package/dist/compiler/emit/function.js +51 -6
- package/dist/compiler/emit/shared.d.ts +8 -0
- package/dist/compiler/emit/shared.js +4 -1
- package/dist/compiler/emit/statements.d.ts +21 -0
- package/dist/compiler/emit/statements.js +369 -16
- package/dist/compiler/emit/storage.d.ts +2 -0
- package/dist/compiler/emit/storage.js +28 -4
- package/dist/compiler/idl.js +4 -1
- package/dist/compiler/model.d.ts +29 -2
- package/dist/portgen.js +32 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +30 -22
- package/docs/migrate-from-anchor.md +10 -2
- package/lib/typesol.d.ts +396 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +198 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { calleeName, resolveScalarType } from "../analyze.js";
|
|
2
|
+
import { calleeName, resolveScalarType, structNameOf } from "../analyze.js";
|
|
3
3
|
import { isScalar, } from "../model.js";
|
|
4
4
|
import { isStable, cString, valueC } from "./shared.js";
|
|
5
5
|
/**
|
|
@@ -23,6 +23,10 @@ export class EmitterBase {
|
|
|
23
23
|
pendingInit = new Set();
|
|
24
24
|
/** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
|
|
25
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();
|
|
26
30
|
/** Enclosing loops, innermost last; each has a `continue` label. */
|
|
27
31
|
loops = [];
|
|
28
32
|
ix;
|
|
@@ -47,6 +51,7 @@ export class EmitterBase {
|
|
|
47
51
|
options: analysis.model.options,
|
|
48
52
|
dataEnums: analysis.model.dataEnums,
|
|
49
53
|
typeArgs: "accounts" in target ? undefined : target.typeArgs,
|
|
54
|
+
structArgs: "accounts" in target ? undefined : target.structArgs,
|
|
50
55
|
};
|
|
51
56
|
this.checker = analysis.checker;
|
|
52
57
|
this.analysis = analysis;
|
|
@@ -54,7 +59,7 @@ export class EmitterBase {
|
|
|
54
59
|
this.ix = target;
|
|
55
60
|
for (const a of target.accounts) {
|
|
56
61
|
this.accounts.set(a.name, a);
|
|
57
|
-
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))
|
|
58
63
|
this.pendingInit.add(a.name);
|
|
59
64
|
}
|
|
60
65
|
for (const a of target.args)
|
|
@@ -86,23 +91,59 @@ export class EmitterBase {
|
|
|
86
91
|
if (cached !== undefined)
|
|
87
92
|
return cached ?? undefined;
|
|
88
93
|
const callee = node.expression;
|
|
89
|
-
|
|
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);
|
|
90
97
|
const g = name ? this.model.generics.get(name) : undefined;
|
|
91
98
|
let h = name ? this.model.helpers.get(name) : undefined;
|
|
92
99
|
if (g)
|
|
93
|
-
h = this.analysis.instantiate(g, this.typeArgsOf(node, g));
|
|
100
|
+
h = this.analysis.instantiate(g, g.typeParams.length ? this.typeArgsOf(node, g) : [], this.fnArgsOf(node, g));
|
|
94
101
|
else if (h && node.typeArguments)
|
|
95
102
|
this.fail(node, `${h.display} takes no type arguments`);
|
|
96
103
|
this.calleeCache.set(node, h ?? null);
|
|
97
104
|
return h;
|
|
98
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
|
+
}
|
|
99
140
|
/** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
|
|
100
141
|
typeArgsOf(node, g) {
|
|
101
142
|
const usage = `write ${g.display}<u64>(…) with the types`;
|
|
102
143
|
if (node.typeArguments) {
|
|
103
144
|
if (node.typeArguments.length !== g.typeParams.length)
|
|
104
145
|
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
|
|
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"));
|
|
106
147
|
}
|
|
107
148
|
return g.typeParams.map((tp) => {
|
|
108
149
|
for (const [i, p] of g.node.parameters.entries()) {
|
|
@@ -116,10 +157,22 @@ export class EmitterBase {
|
|
|
116
157
|
this.hoisted.length = saved.hoisted;
|
|
117
158
|
this.temp = saved.temp;
|
|
118
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
|
+
}
|
|
119
172
|
if (!probe || probe.literal !== undefined || probe.isNull)
|
|
120
173
|
continue;
|
|
121
174
|
if (!isScalar(probe.type))
|
|
122
|
-
this.fail(arg, `${tp} stands for an integer, boolean, Pubkey or
|
|
175
|
+
this.fail(arg, `${tp} stands for an integer, boolean, Pubkey, enum or struct, not ${probe.type.kind}`);
|
|
123
176
|
const t = probe.type;
|
|
124
177
|
return t.kind === "pubkey" ? (t.hash ? { kind: "pubkey", hash: true } : { kind: "pubkey" }) : t;
|
|
125
178
|
}
|
|
@@ -196,7 +249,7 @@ export class EmitterBase {
|
|
|
196
249
|
if ((ts.isPropertyAccessExpression(node) || ts.isCallExpression(node) || ts.isElementAccessExpression(node)) && node.questionDotToken)
|
|
197
250
|
this.fail(node, "optional chaining (?.) is not supported; check `x !== null` first");
|
|
198
251
|
if (ts.isNonNullExpression(node))
|
|
199
|
-
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");
|
|
200
253
|
}
|
|
201
254
|
lookup(name) {
|
|
202
255
|
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
@@ -219,7 +272,25 @@ export class EmitterBase {
|
|
|
219
272
|
exportName(node) {
|
|
220
273
|
return ts.isIdentifier(node) ? this.imports.get(node.text) : undefined;
|
|
221
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
|
+
}
|
|
222
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);
|
|
223
294
|
if (!ts.isIdentifier(node))
|
|
224
295
|
this.fail(node, `${what} must be an account parameter`);
|
|
225
296
|
const a = this.accounts.get(node.text);
|
|
@@ -236,7 +307,7 @@ export class EmitterBase {
|
|
|
236
307
|
? "create"
|
|
237
308
|
: a.kind.kind === "initIfNeeded"
|
|
238
309
|
? "createIfNeeded"
|
|
239
|
-
: a.kind.kind === "token" && a.kind.init
|
|
310
|
+
: (a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init
|
|
240
311
|
? a.kind.init === "create"
|
|
241
312
|
? "create"
|
|
242
313
|
: "createIfNeeded"
|
|
@@ -45,16 +45,16 @@ export function borshMinSize(s, model) {
|
|
|
45
45
|
let n = 0;
|
|
46
46
|
for (const f of s.fields) {
|
|
47
47
|
const t = f.type;
|
|
48
|
-
if (t.kind === "fstring" || t.kind === "vec")
|
|
48
|
+
if (t.kind === "fstring" || t.kind === "vec" || t.kind === "string")
|
|
49
49
|
n += 4;
|
|
50
|
-
else if (t.kind === "option")
|
|
50
|
+
else if (t.kind === "option" || t.kind === "dataEnum")
|
|
51
51
|
n += 1;
|
|
52
52
|
else
|
|
53
53
|
n += fixedBytes(t, model);
|
|
54
54
|
}
|
|
55
55
|
return n;
|
|
56
56
|
}
|
|
57
|
-
const DESER = `TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL:
|
|
57
|
+
const DESER = `TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: bytes are not a valid Borsh value")`;
|
|
58
58
|
const SER = `TS_FAIL(TS_ERR_DID_NOT_SERIALIZE, "TypeSOL: account data too small for the value")`;
|
|
59
59
|
/** C for `ts_unpack_<S>` and `ts_pack_<S>` of every `@layout borsh` struct. */
|
|
60
60
|
export function emitBorshCodecs(model) {
|
|
@@ -84,7 +84,7 @@ export function emitBorshCodecs(model) {
|
|
|
84
84
|
};
|
|
85
85
|
// Bounds: the caller checked `len >= MIN` (the smallest Borsh form). Fixed-size pieces then always fit; only a
|
|
86
86
|
// String / Vec / Option payload is checked, against what is left after the fields that follow it still need.
|
|
87
|
-
const minOf = (t) =>
|
|
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
88
|
const minAfter = s.fields.map((_, i) => s.fields.slice(i + 1).reduce((n, f) => n + minOf(f.type), 0));
|
|
89
89
|
const need = (n, rest, pad) => `${pad}if (${n} > len - at - ${rest}) ${DESER};`;
|
|
90
90
|
const room = (n, rest, pad) => `${pad}if (${n} > cap - at - ${rest}) ${SER};`;
|
|
@@ -149,6 +149,43 @@ export function emitBorshCodecs(model) {
|
|
|
149
149
|
`${pad}}`,
|
|
150
150
|
];
|
|
151
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
|
+
}
|
|
152
189
|
default:
|
|
153
190
|
throw new Error(`unreachable: ${t.kind} in a Borsh account`);
|
|
154
191
|
}
|
|
@@ -199,6 +236,35 @@ export function emitBorshCodecs(model) {
|
|
|
199
236
|
`${pad}}`,
|
|
200
237
|
];
|
|
201
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
|
+
}
|
|
202
268
|
default:
|
|
203
269
|
throw new Error(`unreachable: ${t.kind} in a Borsh account`);
|
|
204
270
|
}
|
|
@@ -226,7 +292,7 @@ export function emitBorshCodecs(model) {
|
|
|
226
292
|
i++;
|
|
227
293
|
}
|
|
228
294
|
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;`, `}`, "");
|
|
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;`, `}`, "");
|
|
230
296
|
}
|
|
231
297
|
return out;
|
|
232
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { resolveScalarType } from "../analyze.js";
|
|
2
|
+
import { calleeName, 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
5
|
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
|
|
@@ -7,6 +7,8 @@ import { ExpressionEmitter } from "./expressions.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
|
|
9
9
|
*/
|
|
10
|
+
/** List methods taking a callback (or a value) that expand into a loop. */
|
|
11
|
+
const LIST_METHODS = new Set(["forEach", "some", "every", "find", "findIndex", "reduce", "includes", "indexOf"]);
|
|
10
12
|
export class CallEmitter extends ExpressionEmitter {
|
|
11
13
|
call(node, topLevel) {
|
|
12
14
|
const callee = node.expression;
|
|
@@ -126,14 +128,26 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
126
128
|
}
|
|
127
129
|
case "mulDiv":
|
|
128
130
|
case "mulDivCeil": {
|
|
129
|
-
// a * b / c with a 128-bit
|
|
131
|
+
// a * b / c with a double-width intermediate (128-bit for u64, 256-bit for u128), rounding down or up;
|
|
132
|
+
// aborts if the result does not fit. All three are u64, or all u128 (literals take the others' type).
|
|
130
133
|
if (args.length !== 3)
|
|
131
|
-
this.fail(node, `${builtin}(a, b, c) takes three u64 values`);
|
|
132
|
-
const
|
|
134
|
+
this.fail(node, `${builtin}(a, b, c) takes three u64 or three u128 values`);
|
|
135
|
+
const vals = [];
|
|
136
|
+
for (const a of args) {
|
|
137
|
+
const mark = this.lines.length;
|
|
138
|
+
const v = this.expr(a);
|
|
139
|
+
for (let j = 0; j < vals.length; j++)
|
|
140
|
+
vals[j] = this.settle(vals[j], mark);
|
|
141
|
+
vals.push(v);
|
|
142
|
+
}
|
|
143
|
+
const typed = vals.find((v) => v.literal === undefined);
|
|
144
|
+
const wide = typed?.type.kind === "int" && typed.type.name === "u128";
|
|
145
|
+
const ty = wide ? { kind: "int", name: "u128" } : U64;
|
|
146
|
+
const [x, y, z] = vals.map((v, i) => this.expect(v.literal !== undefined ? this.literal(v.literal, ty, args[i]) : v, ty, args[i]));
|
|
133
147
|
const t = this.tmp("md");
|
|
134
|
-
this.line(
|
|
135
|
-
this.line(`TS_TRY(ts_mul_div(${x.c}, ${y.c}, ${z.c}, ${builtin === "mulDivCeil"}, &${t}));`);
|
|
136
|
-
return { c: t, type:
|
|
148
|
+
this.line(`${wide ? "unsigned __int128" : "uint64_t"} ${t};`);
|
|
149
|
+
this.line(`TS_TRY(ts_mul_div${wide ? "_u128" : ""}(${x.c}, ${y.c}, ${z.c}, ${builtin === "mulDivCeil"}, &${t}));`);
|
|
150
|
+
return { c: t, type: ty };
|
|
137
151
|
}
|
|
138
152
|
case "isqrt": {
|
|
139
153
|
if (args.length !== 1)
|
|
@@ -219,7 +233,18 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
219
233
|
this.line(`TS_TRY(ts_t22_transfer_fee(${m}, ${amt.c}, &${t}));`);
|
|
220
234
|
return { c: t, type: U64 };
|
|
221
235
|
}
|
|
236
|
+
case "isTransferring": {
|
|
237
|
+
// TransferHookAccount (15) on a Token-2022 token account: set only while Token-2022 runs a transfer of it.
|
|
238
|
+
if (args.length !== 1)
|
|
239
|
+
this.fail(node, "isTransferring(tokenAccount) takes the source token account");
|
|
240
|
+
const m = this.anyAccount(args[0], "the token account");
|
|
241
|
+
this.line(`TS_TRY(ts_t22_layout(${m}, 2));`);
|
|
242
|
+
const off = this.tmp("eo");
|
|
243
|
+
this.line(`uint64_t ${off}, ${off}l;`);
|
|
244
|
+
return { c: `(ts_t22_ext(${m}, 15, &${off}, &${off}l) && ${off}l >= 1 && ${m}->data[${off}] != 0)`, type: BOOL };
|
|
245
|
+
}
|
|
222
246
|
case "transferHookProgram":
|
|
247
|
+
case "transferHookAuthority":
|
|
223
248
|
case "permanentDelegate":
|
|
224
249
|
case "mintCloseAuthority": {
|
|
225
250
|
// Token-2022 mint extensions holding an optional Pubkey: TransferHook (14, program at +32),
|
|
@@ -228,7 +253,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
228
253
|
this.fail(node, `${builtin}(mint) takes the mint`);
|
|
229
254
|
const m = this.anyAccount(args[0], "the mint");
|
|
230
255
|
this.line(`TS_TRY(ts_t22_layout(${m}, 1));`);
|
|
231
|
-
const [type, field] = builtin === "transferHookProgram" ? [14, 32] : builtin === "permanentDelegate" ? [12, 0] : [3, 0];
|
|
256
|
+
const [type, field] = builtin === "transferHookProgram" ? [14, 32] : builtin === "transferHookAuthority" ? [14, 0] : builtin === "permanentDelegate" ? [12, 0] : [3, 0];
|
|
232
257
|
this.model.options.set("pubkey", PUBKEY);
|
|
233
258
|
const t = this.tmp("ext");
|
|
234
259
|
this.line(`${optC(PUBKEY)} ${t};`);
|
|
@@ -434,11 +459,22 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
434
459
|
}
|
|
435
460
|
if (builtin)
|
|
436
461
|
this.fail(callee, `${builtin} cannot be called here`);
|
|
462
|
+
if (ts.isIdentifier(callee)) {
|
|
463
|
+
const local = this.lookup(callee.text);
|
|
464
|
+
if (local?.closure)
|
|
465
|
+
return this.expandClosure(local.closure, node.arguments, node);
|
|
466
|
+
}
|
|
437
467
|
const helper = this.helperOf(node);
|
|
438
468
|
if (helper)
|
|
439
469
|
return this.callHelper(node, helper);
|
|
440
470
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
441
471
|
const method = callee.name.text;
|
|
472
|
+
// xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
|
|
473
|
+
if (LIST_METHODS.has(method)) {
|
|
474
|
+
const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
|
|
475
|
+
if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(callee.expression))
|
|
476
|
+
return this.listMethod(node, method);
|
|
477
|
+
}
|
|
442
478
|
// Vec<T> instruction arguments are read-only views.
|
|
443
479
|
if (ts.isIdentifier(callee.expression)) {
|
|
444
480
|
const l = this.lookup(callee.expression.text);
|
|
@@ -446,6 +482,10 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
446
482
|
if (method !== "get" || args.length !== 1)
|
|
447
483
|
this.fail(node, "Vec arguments are read-only: use .get(i) and .length");
|
|
448
484
|
const elem = l.type.elem;
|
|
485
|
+
if (elem.kind === "string") {
|
|
486
|
+
const idx = this.index(args[0], `(uint64_t)${l.c}.len`);
|
|
487
|
+
return { c: `(((const TS_Str *)${l.c}.ptr)[${idx}])`, type: STRING };
|
|
488
|
+
}
|
|
449
489
|
if (!isScalar(elem))
|
|
450
490
|
this.fail(node, `read a field of the element: ${callee.expression.text}.get(i).field`);
|
|
451
491
|
const idx = this.index(args[0], `(uint64_t)${l.c}.len`);
|
|
@@ -506,8 +546,10 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
506
546
|
// Program accounts used through their methods (systemProgram.transfer, tokenProgram.*…) may be optional.
|
|
507
547
|
if (target?.optional)
|
|
508
548
|
this.assertUsable(target, callee.expression);
|
|
509
|
-
if (target?.kind.kind === "token"
|
|
510
|
-
|
|
549
|
+
if ((target?.kind.kind === "token" || target?.kind.kind === "mint") &&
|
|
550
|
+
target.kind.init &&
|
|
551
|
+
method === (target.kind.init === "create" ? "create" : "createIfNeeded"))
|
|
552
|
+
return target.kind.kind === "token" && target.constraints.ata ? this.createAta(node, target, topLevel) : this.createToken(node, target, topLevel);
|
|
511
553
|
if (target?.kind.kind === "init" && method === "create")
|
|
512
554
|
return this.create(node, target, topLevel);
|
|
513
555
|
if (target?.kind.kind === "initIfNeeded" && method === "createIfNeeded")
|
|
@@ -574,7 +616,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
574
616
|
if (info.cap !== undefined)
|
|
575
617
|
return; // a FixedVec has its room already
|
|
576
618
|
if (!info.resizable || !v.account)
|
|
577
|
-
this.fail(node, "only Vecs of
|
|
619
|
+
this.fail(node, "only Vecs of Mut<...> / Init<...> accounts can grow or shrink");
|
|
578
620
|
this.line(`TS_TRY(ts_resize(params, ${payer}, a_${v.account.name}, 8 + ${info.fixed} + (${newLen}) * ${info.elemSize}, entry_${v.account.name}));`);
|
|
579
621
|
};
|
|
580
622
|
switch (method) {
|
|
@@ -899,8 +941,37 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
899
941
|
}
|
|
900
942
|
return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
|
|
901
943
|
}
|
|
944
|
+
/** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
|
|
945
|
+
helperGrows(body, seen = new Set()) {
|
|
946
|
+
if (seen.has(body))
|
|
947
|
+
return false;
|
|
948
|
+
seen.add(body);
|
|
949
|
+
let found = false;
|
|
950
|
+
const visit = (n) => {
|
|
951
|
+
if (found)
|
|
952
|
+
return;
|
|
953
|
+
if (ts.isCallExpression(n)) {
|
|
954
|
+
// Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
|
|
955
|
+
if (ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
|
|
956
|
+
found = true;
|
|
957
|
+
else if (ts.isIdentifier(n.expression)) {
|
|
958
|
+
const name = calleeName(n.expression, this.model.helperDecls, this.checker);
|
|
959
|
+
const callee = name ? (this.model.generics.get(name)?.node ?? this.model.helpers.get(name)?.node) : undefined;
|
|
960
|
+
if (callee && this.helperGrows(callee, seen))
|
|
961
|
+
found = true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
ts.forEachChild(n, visit);
|
|
965
|
+
};
|
|
966
|
+
visit(body);
|
|
967
|
+
return found;
|
|
968
|
+
}
|
|
902
969
|
callHelper(node, h) {
|
|
903
|
-
if (
|
|
970
|
+
if (this.ix && !this.ix.accounts.some((a) => a.kind.kind === "system" && !a.optional) && h.params.some((p) => p.account) && this.helperGrows(h.node))
|
|
971
|
+
this.fail(node, `${h.display}() grows a Vec (push): the instruction needs a SystemProgram parameter to top up rent`);
|
|
972
|
+
// Function arguments were bound when the helper was instantiated: they have no C parameter.
|
|
973
|
+
const argNodes = h.fnArgIndexes ? node.arguments.filter((_, i) => !h.fnArgIndexes.includes(i)) : node.arguments;
|
|
974
|
+
if (argNodes.length !== h.params.length)
|
|
904
975
|
this.fail(node, `${h.display}() takes ${h.params.length} argument(s)`);
|
|
905
976
|
// Arguments are evaluated left to right: settle earlier ones when a later one runs statements.
|
|
906
977
|
// A helper that takes accounts may change account data, so account text it gets is copied first.
|
|
@@ -908,7 +979,7 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
908
979
|
const values = [];
|
|
909
980
|
const pieces = [];
|
|
910
981
|
h.params.forEach((p, i) => {
|
|
911
|
-
const n =
|
|
982
|
+
const n = argNodes[i];
|
|
912
983
|
const mark = this.lines.length;
|
|
913
984
|
const raw = this.helperArg(h, p, n);
|
|
914
985
|
for (let j = 0; j < values.length; j++)
|
|
@@ -922,8 +993,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
922
993
|
});
|
|
923
994
|
// A helper that takes a writable struct account may replace anything stored in it (see assertNotAliased).
|
|
924
995
|
for (const [i, p] of h.params.entries())
|
|
925
|
-
if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(
|
|
926
|
-
this.assertNotAliased([
|
|
996
|
+
if (p.account && p.account.kind === "account" && p.account.mut && ts.isIdentifier(argNodes[i]))
|
|
997
|
+
this.assertNotAliased([argNodes[i].text], node, h.display);
|
|
927
998
|
const args = ["params", ...pieces.map((x) => (typeof x === "number" ? (values[x].type.kind === "pubkey" ? `*(${values[x].c})` : values[x].c) : x))];
|
|
928
999
|
if (!h.returns) {
|
|
929
1000
|
this.line(`TS_TRY(fn_${h.name}(${args.join(", ")}));`);
|
|
@@ -937,6 +1008,11 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
937
1008
|
this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
|
|
938
1009
|
return { c: `(&${t})`, type: { kind: "structRef", struct: h.returns.name, writable: true } };
|
|
939
1010
|
}
|
|
1011
|
+
if (h.returns.kind === "string") {
|
|
1012
|
+
this.line(`TS_Str ${t};`);
|
|
1013
|
+
this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
|
|
1014
|
+
return { c: t, type: STRING };
|
|
1015
|
+
}
|
|
940
1016
|
this.line(`${scalarC(h.returns)} ${t};`);
|
|
941
1017
|
this.line(`TS_TRY(fn_${h.name}(${[...args, `&${t}`].join(", ")}));`);
|
|
942
1018
|
return h.returns.kind === "pubkey" ? { c: `(&${t})`, type: h.returns } : { c: t, type: h.returns };
|
|
@@ -951,6 +1027,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
951
1027
|
// A @layout borsh account goes with the struct it is being worked on as.
|
|
952
1028
|
if (p.account.kind === "account" && p.account.struct.layout === "borsh")
|
|
953
1029
|
return `a_${acct.name}, d_${acct.name}`;
|
|
1030
|
+
if (p.account.kind === "account" && p.account.mut && p.account.struct.vec)
|
|
1031
|
+
return `a_${acct.name}, entry_${acct.name}`;
|
|
954
1032
|
return `a_${acct.name}`;
|
|
955
1033
|
}
|
|
956
1034
|
switch (p.type.kind) {
|
|
@@ -26,6 +26,25 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
26
26
|
protected describeSeed(el: ts.Expression): IdlSeed | undefined;
|
|
27
27
|
protected recordPda(account: AccountParam, idl: IdlSeed[] | undefined): void;
|
|
28
28
|
protected create(node: ts.CallExpression, target: AccountParam, topLevel: boolean, ifNeeded?: boolean): Value;
|
|
29
|
+
/**
|
|
30
|
+
* `vault.create(payer, seeds | null, mint, authority)` on `Init<TokenAccount>` and `mint.create(payer, seeds | null,
|
|
31
|
+
* decimals, authority, freezeAuthority | null)` on `Init<Mint>` (Anchor's `init` with `token::` / `mint::`
|
|
32
|
+
* constraints): a PDA of this program (or, with `null`, a new keypair account that signs), created owned by the
|
|
33
|
+
* instruction's token program and initialized. `createIfNeeded` on `InitIfNeeded<…>` does that unless the account
|
|
34
|
+
* exists, and then checks it is that PDA, owned by the token program, and has that mint and owner / those
|
|
35
|
+
* authorities and decimals (Anchor's init_if_needed checks). create returns the bump (0 for a keypair),
|
|
36
|
+
* createIfNeeded whether it created the account.
|
|
37
|
+
*/
|
|
38
|
+
protected createToken(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value;
|
|
39
|
+
/**
|
|
40
|
+
* The extensions in `mint.create(…, { … })` (Anchor's `extensions::…` constraints): their values are evaluated now,
|
|
41
|
+
* in source order; the returned lines (each extension's Token-2022 initializer, mint writable, before
|
|
42
|
+
* InitializeMint2) run after the account is created, sized for exactly these extensions.
|
|
43
|
+
*/
|
|
44
|
+
protected mintExtensions(lit: ts.ObjectLiteralExpression, tp: string, acct: string): {
|
|
45
|
+
space: number;
|
|
46
|
+
lines: string[];
|
|
47
|
+
};
|
|
29
48
|
/**
|
|
30
49
|
* `ata.create(payer)` / `ata.createIfNeeded(payer)` on `Ata<Init|InitIfNeeded<TokenAccount>, "owner", "mint">`
|
|
31
50
|
* (Anchor's `init` / `init_if_needed` with `associated_token::…`). create always calls the ATA program's Create, which
|
|
@@ -47,6 +66,7 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
47
66
|
}, tail?: {
|
|
48
67
|
from: string;
|
|
49
68
|
to: string;
|
|
69
|
+
asPassed?: boolean;
|
|
50
70
|
}): void;
|
|
51
71
|
protected signerSeeds(node: ts.Expression | undefined, authority: AccountParam, at: ts.Node): {
|
|
52
72
|
name: string;
|
|
@@ -87,6 +107,12 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
87
107
|
protected bubblegumOp(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
|
|
88
108
|
/** Bubblegum V2 (trees on mpl-account-compression with mpl-noop; optional Metaplex Core collections). */
|
|
89
109
|
protected bubblegumV2(node: ts.CallExpression, bg: AccountParam, method: string): undefined;
|
|
110
|
+
/** Object literal properties by name, refusing others. */
|
|
111
|
+
protected literalProps(o: ts.ObjectLiteralExpression, allowed: string[], what: string): Map<string, ts.Expression>;
|
|
112
|
+
/** Vec<Creator> (address, verified, share) from a literal list; shares add up to 100 when literal. */
|
|
113
|
+
protected bubblegumCreators(creators: ts.Expression | undefined): MplField[];
|
|
114
|
+
/** UpdateArgs: every field optional (left out or null: unchanged). */
|
|
115
|
+
protected bubblegumUpdateArgs(n: ts.Expression): MplField[];
|
|
90
116
|
/** `acct.key` of an account parameter, as an expression to serialize. */
|
|
91
117
|
protected keyOf(n: ts.Expression): ts.Expression;
|
|
92
118
|
/**
|
|
@@ -117,6 +143,10 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
117
143
|
authority: string;
|
|
118
144
|
program: string;
|
|
119
145
|
}): undefined;
|
|
146
|
+
/** emit<T> of a @layout borsh struct: built like account data, then packed as Anchor serializes it. */
|
|
147
|
+
private emitBorshEvent;
|
|
148
|
+
/** Discriminator + `body`, logged (emit) or self-invoked (emitCpi). */
|
|
149
|
+
private sendEvent;
|
|
120
150
|
/** Structs reachable through the fields of struct `name` (not including itself unless it recurses). */
|
|
121
151
|
protected nestedStructs(name: string): Set<string>;
|
|
122
152
|
/** Structs whose IDL description is the fixed layout: account data, arguments, and everything nested in them. */
|