@typesol/cli 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +7 -1
- package/dist/compiler/analyze.js +258 -30
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +61 -1
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +43 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +522 -20
- package/dist/compiler/emit/expressions.js +19 -2
- package/dist/compiler/emit/function.js +34 -11
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +19 -2
- package/dist/compiler/emit/statements.js +11 -3
- package/dist/compiler/emit/storage.d.ts +4 -0
- package/dist/compiler/emit/storage.js +60 -12
- package/dist/compiler/idl.js +33 -4
- package/dist/compiler/model.d.ts +41 -2
- package/dist/compiler/model.js +8 -2
- package/dist/portgen.js +60 -4
- package/docs/errors.md +3 -2
- package/docs/language.md +10 -6
- package/docs/migrate-from-anchor.md +14 -5
- package/docs/test-your-program.md +1 -1
- package/lib/typesol.d.ts +259 -3
- package/package.json +1 -1
- package/runtime/typesol_rt.h +32 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
+
import { calleeName, resolveValueType } from "../analyze.js";
|
|
2
3
|
import { INT_INFO, unsignedC, sameType, typeName } from "../model.js";
|
|
3
|
-
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC } from "./shared.js";
|
|
4
|
+
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
|
|
4
5
|
import { StorageEmitter } from "./storage.js";
|
|
5
6
|
/**
|
|
6
7
|
* Expressions: literals, operators with checked arithmetic, property reads, conditionals, text and casts.
|
|
@@ -62,6 +63,20 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
62
63
|
expr(node, hint) {
|
|
63
64
|
if (ts.isParenthesizedExpression(node))
|
|
64
65
|
return this.expr(node.expression, hint);
|
|
66
|
+
// `x as T`: an annotation the value must already match (no conversion: use u8(x) etc. for that). Inside a
|
|
67
|
+
// generic helper it names the instance's type: `return (a + b) as T`.
|
|
68
|
+
if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
69
|
+
if (ts.isAsExpression(node) && ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName) && node.type.typeName.text === "const")
|
|
70
|
+
return this.expr(node.expression, hint);
|
|
71
|
+
const t = resolveValueType(node.type, this.scope);
|
|
72
|
+
if (!t || t.kind === "vecArg" || t.kind === "dataEnumArg")
|
|
73
|
+
this.fail(node.type, "`as` names an integer, boolean, Pubkey, enum, option or string type the value already has");
|
|
74
|
+
const v = this.expr(node.expression, t);
|
|
75
|
+
// Not a conversion: `x as u64 | null` does not make an Option of a u64.
|
|
76
|
+
if (v.literal === undefined && !v.isNull && !sameType(v.type, t))
|
|
77
|
+
this.fail(node, `this is a ${typeName(v.type)}, not a ${typeName(t)}: \`as\` does not convert (use u64(x) and the like for integers)`);
|
|
78
|
+
return this.expect(v, t, node.expression);
|
|
79
|
+
}
|
|
65
80
|
// `x!` on an Option: its value, aborting with 5108 when it is empty (JS would carry the null on).
|
|
66
81
|
if (ts.isNonNullExpression(node)) {
|
|
67
82
|
const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
|
|
@@ -140,6 +155,8 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
140
155
|
}
|
|
141
156
|
if (this.accounts.has(node.text))
|
|
142
157
|
this.fail(node, `"${node.text}" is an account; use ${node.text}.key for its address`);
|
|
158
|
+
if (calleeName(node, this.model.helperDecls, this.checker) || this.model.helpers.has(node.text))
|
|
159
|
+
this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
|
|
143
160
|
this.fail(node, `unknown name "${node.text}"`);
|
|
144
161
|
}
|
|
145
162
|
if (ts.isPropertyAccessExpression(node))
|
|
@@ -202,7 +219,7 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
202
219
|
if (arr?.type.kind === "array")
|
|
203
220
|
return { c: `${arr.type.len}ULL`, type: U64 };
|
|
204
221
|
if (arr?.type.kind === "vec")
|
|
205
|
-
return { c:
|
|
222
|
+
return { c: vecCount(arr.vec), type: U64 };
|
|
206
223
|
if (ts.isIdentifier(node.expression)) {
|
|
207
224
|
const l = this.lookup(node.expression.text);
|
|
208
225
|
if (l?.type.kind === "vecArg")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { snakeCase } from "../analyze.js";
|
|
3
3
|
import { decodeAddress } from "../../keys.js";
|
|
4
|
-
import { INT_INFO, isScalar, isWritable, scalarSize, typeName } from "../model.js";
|
|
4
|
+
import { INT_INFO, isScalar, isWritable, scalarSize, typeName, } from "../model.js";
|
|
5
|
+
import { borshMinSize } from "./borsh.js";
|
|
5
6
|
import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature } from "./shared.js";
|
|
6
7
|
import { StatementEmitter } from "./statements.js";
|
|
7
8
|
/**
|
|
@@ -14,12 +15,12 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
14
15
|
this.scopes = [new Map()];
|
|
15
16
|
this.line(`(void)params;`);
|
|
16
17
|
for (const p of h.params)
|
|
17
|
-
if (p.account && p.account.kind === "account")
|
|
18
|
+
if (p.account && p.account.kind === "account" && p.account.struct.layout !== "borsh")
|
|
18
19
|
this.line(`TSS_${p.account.struct.name} *d_${p.name} = (TSS_${p.account.struct.name} *)(a_${p.name}->data + 8);`);
|
|
19
|
-
for (const stmt of h.
|
|
20
|
+
for (const stmt of h.statements)
|
|
20
21
|
this.reporter.guard(() => this.statement(stmt));
|
|
21
22
|
// A helper with a result must return one; falling off the end would leave it unset.
|
|
22
|
-
const end = h.returns ? ` TS_FAIL(TS_ERR_NO_RETURN, "TypeSOL: ${h.
|
|
23
|
+
const end = h.returns ? ` TS_FAIL(TS_ERR_NO_RETURN, "TypeSOL: ${h.display} ended without returning a value");` : ` return SUCCESS;`;
|
|
23
24
|
return [`${helperSignature(h)} {`, ...this.hoisted, ...this.lines, end, `}`].join("\n");
|
|
24
25
|
}
|
|
25
26
|
emitInstruction() {
|
|
@@ -39,8 +40,9 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
39
40
|
head.push(` TS_TRY(ts_check_signer(${v}));`);
|
|
40
41
|
if (isWritable(k))
|
|
41
42
|
head.push(` TS_TRY(ts_check_writable(${v}));`);
|
|
43
|
+
// A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
|
|
42
44
|
if (k.kind === "account")
|
|
43
|
-
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, sizeof(TSS_${k.struct.name})));`);
|
|
45
|
+
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : `sizeof(TSS_${k.struct.name})`}));`);
|
|
44
46
|
// A token account created by this instruction is checked where it is created (create/createIfNeeded).
|
|
45
47
|
if (k.kind === "token" && !k.init)
|
|
46
48
|
head.push(` TS_TRY(ts_check_token_account(${v}));`);
|
|
@@ -87,7 +89,16 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
87
89
|
head.push(` TS_TRY(ts_init_zeroed(${v}, params->program_id, TS_DISC_${k.struct.name}, sizeof(TSS_${k.struct.name})));`);
|
|
88
90
|
if (k.kind === "initIfNeeded" && k.struct.vec)
|
|
89
91
|
this.reporter.fail(a.node, "InitIfNeeded accounts cannot end in a Vec yet");
|
|
90
|
-
if (k.kind === "account" || k.kind === "init" || k.kind === "
|
|
92
|
+
if ((k.kind === "account" || k.kind === "init" || k.kind === "initIfNeeded") && k.struct.layout === "borsh") {
|
|
93
|
+
// Worked on as its fixed-layout struct on the heap, packed back when the instruction returns. The heap is zero at
|
|
94
|
+
// entry and these are its first allocations (before the body uses it), so a string's padding and a list's
|
|
95
|
+
// unused slots start as zero: ts_unpack_* only writes what the Borsh bytes hold.
|
|
96
|
+
const S = k.struct.name;
|
|
97
|
+
head.push(` TSS_${S} *d_${a.name} = NULL;`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&d_${a.name}));`);
|
|
98
|
+
if (k.kind === "account")
|
|
99
|
+
head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}));`);
|
|
100
|
+
}
|
|
101
|
+
else if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
|
|
91
102
|
head.push(` TSS_${k.struct.name} *d_${a.name} = (TSS_${k.struct.name} *)(${v}->data + 8);`);
|
|
92
103
|
if (k.struct.vec) {
|
|
93
104
|
head.push(` uint64_t entry_${a.name} = ${v}->data_len;`, ` (void)entry_${a.name};`);
|
|
@@ -285,13 +296,25 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
285
296
|
epilogue() {
|
|
286
297
|
if (!this.ix)
|
|
287
298
|
return [];
|
|
288
|
-
|
|
289
|
-
|
|
299
|
+
// @layout borsh accounts go back to their Borsh bytes (unless closed, or never created).
|
|
300
|
+
const packs = this.ix.accounts
|
|
301
|
+
.filter((a) => (a.kind.kind === "account" ? a.kind.mut : a.kind.kind === "init" || a.kind.kind === "initIfNeeded"))
|
|
302
|
+
.filter((a) => a.kind.struct.layout === "borsh")
|
|
290
303
|
.map((a) => {
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
return
|
|
304
|
+
const v = `a_${a.name}`;
|
|
305
|
+
const S = a.kind.struct.name;
|
|
306
|
+
return `if (${a.optional ? `${v} != NULL && ` : ""}${v}->data_len >= 8 && ts_key_eq(&${v}->owner, params->program_id)) TS_TRY(ts_pack_${S}(d_${a.name}, ${v}->data + 8, ${v}->data_len - 8));`;
|
|
294
307
|
});
|
|
308
|
+
return [
|
|
309
|
+
...packs,
|
|
310
|
+
...this.ix.accounts
|
|
311
|
+
.filter((a) => a.constraints.closeTo)
|
|
312
|
+
.map((a) => {
|
|
313
|
+
const line = `TS_TRY(ts_close(a_${a.name}, a_${a.constraints.closeTo}));`;
|
|
314
|
+
const optional = this.optionalIn(line);
|
|
315
|
+
return optional.length ? `if (${optional.map((n) => `a_${n} != NULL`).join(" && ")}) ${line}` : line;
|
|
316
|
+
}),
|
|
317
|
+
];
|
|
295
318
|
}
|
|
296
319
|
constraintChecks(ix) {
|
|
297
320
|
const out = [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature } from "./shared.js";
|
|
1
|
+
import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature, fixedVecC } from "./shared.js";
|
|
2
2
|
import { FunctionEmitter } from "./function.js";
|
|
3
|
+
import { emitBorshCodecs } from "./borsh.js";
|
|
3
4
|
/**
|
|
4
5
|
* The whole C translation unit: type declarations (structs, options, enums, events), every helper and instruction
|
|
5
6
|
* (lowered by FunctionEmitter), and the entry point that dispatches on the instruction discriminator.
|
|
@@ -8,17 +9,26 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
8
9
|
const { model, reporter } = analysis;
|
|
9
10
|
// Bodies first: they register events, string-literal errors and PDA descriptions.
|
|
10
11
|
const helperBodies = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const emittedHelpers = new Set();
|
|
13
|
+
// Generic helpers' instances appear while bodies are lowered: emit until none is left.
|
|
14
|
+
const emitHelpers = () => {
|
|
15
|
+
for (const h of model.helpers.values()) {
|
|
16
|
+
if (emittedHelpers.has(h.name))
|
|
17
|
+
continue;
|
|
18
|
+
emittedHelpers.add(h.name);
|
|
19
|
+
const body = reporter.guard(() => new FunctionEmitter(analysis, h, opts).emitHelper());
|
|
20
|
+
if (body)
|
|
21
|
+
helperBodies.push(body, "");
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
emitHelpers();
|
|
16
25
|
const ixBodies = [];
|
|
17
26
|
for (const ix of model.instructions) {
|
|
18
27
|
const body = reporter.guard(() => new FunctionEmitter(analysis, ix, opts).emitInstruction());
|
|
19
28
|
if (body)
|
|
20
29
|
ixBodies.push(body, "");
|
|
21
30
|
}
|
|
31
|
+
emitHelpers();
|
|
22
32
|
const out = [];
|
|
23
33
|
out.push(`/* Generated by TypeSOL from ${analysis.sourceFile.fileName.split("/").pop()}. Do not edit. */`);
|
|
24
34
|
// `const HEAP_SIZE_KIB = 64n`: a larger heap, which clients request with ComputeBudget requestHeapFrame.
|
|
@@ -43,6 +53,13 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
43
53
|
emitStruct(model.structs.get(t.name));
|
|
44
54
|
if (t.kind === "array" || t.kind === "vec")
|
|
45
55
|
deps(t.elem);
|
|
56
|
+
if (t.kind === "vec" && t.cap !== undefined) {
|
|
57
|
+
const name = fixedVecC(t);
|
|
58
|
+
if (emitted.has(name))
|
|
59
|
+
return;
|
|
60
|
+
emitted.add(name);
|
|
61
|
+
out.push(`typedef struct __attribute__((packed)) {`, ` uint32_t len;`, ` ${storageDecl(t.elem, `items[${t.cap}]`)};`, `} ${name};`, "");
|
|
62
|
+
}
|
|
46
63
|
if (t.kind === "option" && t.inner.kind === "struct") {
|
|
47
64
|
const key = `opt:${t.inner.name}`;
|
|
48
65
|
if (emitted.has(key))
|
|
@@ -99,6 +116,7 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
99
116
|
for (const k of model.constants.values())
|
|
100
117
|
if (typeof k.value === "object")
|
|
101
118
|
out.push(`static const SolPubkey TSK_${k.name} = {${cBytes(k.value.pubkey)}};`);
|
|
119
|
+
out.push(...emitBorshCodecs(model));
|
|
102
120
|
const helpers = [...model.helpers.values()];
|
|
103
121
|
for (const h of helpers)
|
|
104
122
|
out.push(helperSignature(h) + ";");
|
|
@@ -39,11 +39,17 @@ export interface Ref {
|
|
|
39
39
|
/** Field path from the account root, when it is a plain chain of fields. */
|
|
40
40
|
path?: string[];
|
|
41
41
|
/** For a Vec: the u32 length lvalue, the fixed-part size expression, and whether it may be resized here. */
|
|
42
|
+
/**
|
|
43
|
+
* A Vec place: `len` the u32 length (an lvalue), `items` the element array. A trailing Vec grows the account
|
|
44
|
+
* (`fixed` bytes before it); a FixedVec (`cap`) has room for `cap` elements and never resizes.
|
|
45
|
+
*/
|
|
42
46
|
vec?: {
|
|
43
47
|
len: string;
|
|
48
|
+
items: string;
|
|
44
49
|
fixed: string;
|
|
45
50
|
elemSize: number;
|
|
46
51
|
resizable: boolean;
|
|
52
|
+
cap?: number;
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
55
|
export interface Local {
|
|
@@ -88,3 +94,9 @@ export interface EmitOptions {
|
|
|
88
94
|
/** Log a marker and the remaining compute units before every statement (`typesol build --profile`). */
|
|
89
95
|
profile?: boolean;
|
|
90
96
|
}
|
|
97
|
+
/** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
|
|
98
|
+
export declare function fixedVecC(t: FieldType & {
|
|
99
|
+
kind: "vec";
|
|
100
|
+
}): string;
|
|
101
|
+
/** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
|
|
102
|
+
export declare function vecCount(v: NonNullable<Ref["vec"]>): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INT_INFO, isWritable, optionKey, } from "../model.js";
|
|
1
|
+
import { INT_INFO, fixedVecName, isWritable, optionKey, } from "../model.js";
|
|
2
2
|
// Types, limits and C type helpers shared by the emitter modules.
|
|
3
3
|
/** Maximum accounts the generated entrypoint deserializes. */
|
|
4
4
|
export const MAX_ACCOUNTS = 32;
|
|
@@ -89,6 +89,10 @@ export function fieldC(t) {
|
|
|
89
89
|
case "pubkey":
|
|
90
90
|
case "enum":
|
|
91
91
|
return scalarStorageC(t);
|
|
92
|
+
case "vec":
|
|
93
|
+
if (t.cap !== undefined)
|
|
94
|
+
return fixedVecC(t);
|
|
95
|
+
throw new Error("unreachable: a Vec is never an element");
|
|
92
96
|
default:
|
|
93
97
|
throw new Error(`unreachable: no element C type for ${t.kind}`);
|
|
94
98
|
}
|
|
@@ -115,6 +119,8 @@ export function storageDecl(t, name) {
|
|
|
115
119
|
case "array":
|
|
116
120
|
return storageDecl(t.elem, `${name}[${t.len}]`);
|
|
117
121
|
case "vec":
|
|
122
|
+
if (t.cap !== undefined)
|
|
123
|
+
return `${fixedVecC(t)} ${name}`;
|
|
118
124
|
// Borsh Vec as a trailing flexible array: u32 length, then the elements.
|
|
119
125
|
return `uint32_t ${name}__len;
|
|
120
126
|
${storageDecl(t.elem, `${name}[]`)}`;
|
|
@@ -175,8 +181,11 @@ export function accountMismatch(have, want) {
|
|
|
175
181
|
export function helperSignature(h) {
|
|
176
182
|
const params = ["TsParams *params"];
|
|
177
183
|
for (const p of h.params) {
|
|
178
|
-
if (p.account)
|
|
184
|
+
if (p.account) {
|
|
179
185
|
params.push(`TsAccount *a_${p.name}`);
|
|
186
|
+
if (p.account.kind === "account" && p.account.struct.layout === "borsh")
|
|
187
|
+
params.push(`TSS_${p.account.struct.name} *d_${p.name}`);
|
|
188
|
+
}
|
|
180
189
|
else if (p.type.kind === "signerSeeds")
|
|
181
190
|
params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
|
|
182
191
|
else if (p.type.kind === "pubkey")
|
|
@@ -190,3 +199,11 @@ export function helperSignature(h) {
|
|
|
190
199
|
params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : `${scalarC(h.returns)} *ret`);
|
|
191
200
|
return `static uint64_t fn_${h.name}(${params.join(", ")})`;
|
|
192
201
|
}
|
|
202
|
+
/** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
|
|
203
|
+
export function fixedVecC(t) {
|
|
204
|
+
return `TSF_${fixedVecName(t)}`;
|
|
205
|
+
}
|
|
206
|
+
/** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
|
|
207
|
+
export function vecCount(v) {
|
|
208
|
+
return v.cap === undefined ? `((uint64_t)${v.len})` : `((uint64_t)(${v.len} < ${v.cap} ? ${v.len} : ${v.cap}))`;
|
|
209
|
+
}
|
|
@@ -290,6 +290,12 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
290
290
|
}
|
|
291
291
|
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
292
292
|
this.fail(d, "declare one plain name with an initializer");
|
|
293
|
+
// A local function is a helper of its own (lifted when the program was analyzed): nothing to run here.
|
|
294
|
+
if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
|
|
295
|
+
if (!this.model.helperDecls.has(d))
|
|
296
|
+
this.fail(d, "declare local functions with const: const f = (…) => …");
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
293
299
|
const name = d.name.text;
|
|
294
300
|
if (this.scopes.at(-1).has(name))
|
|
295
301
|
this.fail(d.name, `"${name}" is already declared`);
|
|
@@ -498,14 +504,15 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
498
504
|
}
|
|
499
505
|
// JS evaluates the target (`a.get(i).x`: which element) before the right side. The exception is
|
|
500
506
|
// `acct.field = acct.create(...)`, which must create the account before the field can be touched.
|
|
501
|
-
|
|
507
|
+
// `fixedVec([…])` is a value written in place, not a call.
|
|
508
|
+
const isCall = ts.isCallExpression(valueNode) && !this.isStorageCall(valueNode) && this.exportName(valueNode.expression) !== "fixedVec";
|
|
502
509
|
const createsFirst = isCall && ts.isPropertyAccessExpression(valueNode.expression) && ["create", "createIfNeeded"].includes(valueNode.expression.name.text);
|
|
503
510
|
let raw = createsFirst ? this.call(valueNode, topLevel) : undefined;
|
|
504
511
|
if (createsFirst && !raw)
|
|
505
512
|
this.fail(valueNode, "this call does not produce a value");
|
|
506
513
|
// A helper's struct result is copied like any struct (the target first, as JS evaluates it first).
|
|
507
514
|
const structCall = isCall && ts.isIdentifier(valueNode.expression)
|
|
508
|
-
? this.
|
|
515
|
+
? this.helperOf(valueNode)?.returns?.kind === "struct"
|
|
509
516
|
: false;
|
|
510
517
|
if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
511
518
|
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
@@ -515,6 +522,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
515
522
|
(targetRef.type.kind === "array" ||
|
|
516
523
|
targetRef.type.kind === "struct" ||
|
|
517
524
|
targetRef.type.kind === "dataEnum" ||
|
|
525
|
+
(targetRef.type.kind === "vec" && targetRef.type.cap !== undefined) ||
|
|
518
526
|
(targetRef.type.kind === "option" && targetRef.type.inner.kind === "struct"))) {
|
|
519
527
|
if (!targetRef.writable)
|
|
520
528
|
this.fail(target, this.readOnly(targetRef));
|
|
@@ -554,7 +562,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
554
562
|
}
|
|
555
563
|
}
|
|
556
564
|
helperTakesMutAccounts(node) {
|
|
557
|
-
const h =
|
|
565
|
+
const h = this.helperOf(node);
|
|
558
566
|
return !!h?.params.some((p) => p.account && isWritable(p.account));
|
|
559
567
|
}
|
|
560
568
|
}
|
|
@@ -67,6 +67,10 @@ export declare abstract class StorageEmitter extends EmitterBase {
|
|
|
67
67
|
* required) or by copying another location of the same type.
|
|
68
68
|
*/
|
|
69
69
|
protected writeAggregate(target: Ref, valueNode: ts.Expression): void;
|
|
70
|
+
/** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
|
|
71
|
+
protected fixedVec(lv: string, elem: FieldType, cap: number): NonNullable<Ref["vec"]>;
|
|
72
|
+
/** A place whose type is a FixedVec gets its Vec view. */
|
|
73
|
+
protected withFixedVec(r: Ref): Ref;
|
|
70
74
|
/** `arr.get(i)` reads storage rather than calling anything. */
|
|
71
75
|
protected isStorageCall(node: ts.CallExpression): boolean;
|
|
72
76
|
/** Whether an expression names an array, without emitting code or failing. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveScalarType } from "../analyze.js";
|
|
3
3
|
import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName } from "../model.js";
|
|
4
|
-
import { U64, BOOL, PUBKEY, STRING, optC, fieldC, storageDecl } from "./shared.js";
|
|
4
|
+
import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount } from "./shared.js";
|
|
5
5
|
import { EmitterBase } from "./base.js";
|
|
6
6
|
/**
|
|
7
7
|
* Places in account data and locals: references, reads and writes, Vec/array indexing, aliasing rules and
|
|
@@ -245,8 +245,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
245
245
|
this.noOptionalChain(node);
|
|
246
246
|
// A helper's struct result: a local of the caller's, read and copied like any struct.
|
|
247
247
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
248
|
-
const h = this.
|
|
249
|
-
if (h?.returns?.kind === "struct"
|
|
248
|
+
const h = this.helperOf(node);
|
|
249
|
+
if (h?.returns?.kind === "struct") {
|
|
250
250
|
const v = this.callHelper(node, h);
|
|
251
251
|
return { lv: `(*${v.c})`, type: { kind: "struct", name: h.returns.name }, writable: true };
|
|
252
252
|
}
|
|
@@ -273,10 +273,13 @@ export class StorageEmitter extends EmitterBase {
|
|
|
273
273
|
return undefined;
|
|
274
274
|
this.assertUsable(a, node);
|
|
275
275
|
const r = { lv: `d_${a.name}->${f.name}`, type: f.type, writable: isWritable(a.kind), account: a, path: [f.name] };
|
|
276
|
-
if (f.type.kind === "vec")
|
|
276
|
+
if (f.type.kind === "vec" && f.type.cap !== undefined)
|
|
277
|
+
r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
|
|
278
|
+
else if (f.type.kind === "vec")
|
|
277
279
|
// Growing needs the account's length at instruction entry: only instructions know it.
|
|
278
280
|
r.vec = {
|
|
279
281
|
len: `d_${a.name}->${f.name}__len`,
|
|
282
|
+
items: r.lv,
|
|
280
283
|
fixed: `sizeof(TSS_${a.kind.struct.name})`,
|
|
281
284
|
elemSize: a.kind.struct.vec.elemSize,
|
|
282
285
|
resizable: isWritable(a.kind) && !!this.ix,
|
|
@@ -307,7 +310,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
307
310
|
// TypeScript proved the variant set; this runtime check is the backstop.
|
|
308
311
|
const cond = located.map((l) => `${base.lv}.tag != ${l.vi}`).join(" && ");
|
|
309
312
|
this.line(`if (${cond}) TS_FAIL(TS_ERR_ENUM_VARIANT, "TypeSOL: ${e.name} is not ${names.join("/")}");`);
|
|
310
|
-
return { ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined };
|
|
313
|
+
return this.withFixedVec({ ...base, lv: `${base.lv}.v.${first.name}.${first.f.name}`, type: first.f.type, path: undefined, vec: undefined });
|
|
311
314
|
}
|
|
312
315
|
if (base && base.type.kind === "option" && base.type.inner.kind === "struct") {
|
|
313
316
|
const s = this.model.structs.get(base.type.inner.name);
|
|
@@ -315,7 +318,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
315
318
|
if (!f)
|
|
316
319
|
this.fail(node.name, `${s.name} has no field "${field}"`);
|
|
317
320
|
this.line(`if (!${base.lv}.some) TS_FAIL(TS_ERR_UNWRAP, "TypeSOL: option is empty");`);
|
|
318
|
-
return { ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined };
|
|
321
|
+
return this.withFixedVec({ ...base, lv: `${base.lv}.v.${f.name}`, type: f.type, path: undefined, vec: undefined });
|
|
319
322
|
}
|
|
320
323
|
if (base && base.type.kind === "struct") {
|
|
321
324
|
const s = this.model.structs.get(base.type.name);
|
|
@@ -324,9 +327,9 @@ export class StorageEmitter extends EmitterBase {
|
|
|
324
327
|
this.fail(node.name, `${s.name} has no field "${field}"`);
|
|
325
328
|
const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
|
|
326
329
|
// Vecs of loaded accounts can be read and updated in place but not resized (no entry length known).
|
|
327
|
-
if (f.type.kind === "vec")
|
|
328
|
-
r.vec = { len: `${base.lv}.${f.name}__len`, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
|
|
329
|
-
return r;
|
|
330
|
+
if (f.type.kind === "vec" && f.type.cap === undefined)
|
|
331
|
+
r.vec = { len: `${base.lv}.${f.name}__len`, items: r.lv, fixed: `sizeof(TSS_${s.name})`, elemSize: s.vec.elemSize, resizable: false };
|
|
332
|
+
return this.withFixedVec(r);
|
|
330
333
|
}
|
|
331
334
|
return undefined;
|
|
332
335
|
}
|
|
@@ -345,8 +348,9 @@ export class StorageEmitter extends EmitterBase {
|
|
|
345
348
|
if (base && (base.type.kind === "array" || base.type.kind === "vec")) {
|
|
346
349
|
if (node.arguments.length !== 1)
|
|
347
350
|
this.fail(node, "get(index) takes one argument");
|
|
348
|
-
const idx = this.index(node.arguments[0], base.type.kind === "array" ? `${base.type.len}ULL` :
|
|
349
|
-
|
|
351
|
+
const idx = this.index(node.arguments[0], base.type.kind === "array" ? `${base.type.len}ULL` : vecCount(base.vec));
|
|
352
|
+
const lv = base.type.kind === "array" ? `${base.lv}[${idx}]` : `${base.vec.items}[${idx}]`;
|
|
353
|
+
return this.withFixedVec({ ...base, lv, type: base.type.elem, path: undefined, vec: undefined });
|
|
350
354
|
}
|
|
351
355
|
}
|
|
352
356
|
return undefined;
|
|
@@ -475,6 +479,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
475
479
|
* required) or by copying another location of the same type.
|
|
476
480
|
*/
|
|
477
481
|
writeAggregate(target, valueNode) {
|
|
482
|
+
if (ts.isParenthesizedExpression(valueNode))
|
|
483
|
+
return this.writeAggregate(target, valueNode.expression);
|
|
478
484
|
if (target.type.kind === "option" && target.type.inner.kind === "struct") {
|
|
479
485
|
if (valueNode.kind === ts.SyntaxKind.NullKeyword) {
|
|
480
486
|
this.line(`${target.lv}.some = 0;`);
|
|
@@ -534,6 +540,35 @@ export class StorageEmitter extends EmitterBase {
|
|
|
534
540
|
this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
|
|
535
541
|
return;
|
|
536
542
|
}
|
|
543
|
+
if (target.type.kind === "vec" && target.type.cap !== undefined && ts.isCallExpression(valueNode) && this.exportName(valueNode.expression) === "fixedVec") {
|
|
544
|
+
const list = valueNode.arguments[0];
|
|
545
|
+
if (valueNode.arguments.length !== 1 || !ts.isArrayLiteralExpression(list))
|
|
546
|
+
this.fail(valueNode, "fixedVec([…]) takes an array literal");
|
|
547
|
+
return this.writeAggregate(target, list);
|
|
548
|
+
}
|
|
549
|
+
if (target.type.kind === "vec" && target.type.cap !== undefined && ts.isArrayLiteralExpression(valueNode)) {
|
|
550
|
+
// `list = [a, b]`: built in a temporary (the elements may read the old list), the unused slots zeroed.
|
|
551
|
+
const t = target.type;
|
|
552
|
+
const els = valueNode.elements;
|
|
553
|
+
if (els.length > t.cap)
|
|
554
|
+
this.fail(valueNode, `${els.length} elements do not fit in ${typeName(t)}`);
|
|
555
|
+
const tmp = this.tmp("fv");
|
|
556
|
+
this.line(`${fixedVecC(t)} ${tmp};`);
|
|
557
|
+
this.line(`ts_memzero(&${tmp}, sizeof(${tmp}));`);
|
|
558
|
+
this.line(`${tmp}.len = ${els.length};`);
|
|
559
|
+
els.forEach((e, i) => {
|
|
560
|
+
if (ts.isSpreadElement(e))
|
|
561
|
+
this.fail(e, "list the elements one by one");
|
|
562
|
+
const sub = { lv: `${tmp}.items[${i}]`, type: t.elem, writable: true };
|
|
563
|
+
const vt = this.storageValueType(t.elem);
|
|
564
|
+
if (vt)
|
|
565
|
+
this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(e, vt), vt, e)).c) + ";");
|
|
566
|
+
else
|
|
567
|
+
this.writeAggregate(sub, e);
|
|
568
|
+
});
|
|
569
|
+
this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
537
572
|
if (ts.isObjectLiteralExpression(valueNode)) {
|
|
538
573
|
if (target.type.kind !== "struct")
|
|
539
574
|
this.fail(valueNode, `expected a ${typeName(target.type)}, not an object literal`);
|
|
@@ -575,6 +610,16 @@ export class StorageEmitter extends EmitterBase {
|
|
|
575
610
|
// Same-typed places are either distinct or identical (`a.p = a.p`); ts_copy skips the latter.
|
|
576
611
|
this.line(`ts_copy(&${target.lv}, &${src.lv}, sizeof(${target.lv}));`);
|
|
577
612
|
}
|
|
613
|
+
/** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
|
|
614
|
+
fixedVec(lv, elem, cap) {
|
|
615
|
+
return { len: `${lv}.len`, items: `${lv}.items`, fixed: "", elemSize: this.fieldBytes(elem), resizable: false, cap };
|
|
616
|
+
}
|
|
617
|
+
/** A place whose type is a FixedVec gets its Vec view. */
|
|
618
|
+
withFixedVec(r) {
|
|
619
|
+
if (r.type.kind === "vec" && r.type.cap !== undefined)
|
|
620
|
+
r.vec = this.fixedVec(r.lv, r.type.elem, r.type.cap);
|
|
621
|
+
return r;
|
|
622
|
+
}
|
|
578
623
|
/** `arr.get(i)` reads storage rather than calling anything. */
|
|
579
624
|
isStorageCall(node) {
|
|
580
625
|
return ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "get" && !!this.peekArray(node.expression.expression);
|
|
@@ -658,8 +703,11 @@ export class StorageEmitter extends EmitterBase {
|
|
|
658
703
|
return this.fieldBytes(t.elem) * t.len;
|
|
659
704
|
case "struct":
|
|
660
705
|
return this.model.structs.get(t.name).size;
|
|
661
|
-
case "string":
|
|
662
706
|
case "vec":
|
|
707
|
+
if (t.cap !== undefined)
|
|
708
|
+
return 4 + this.fieldBytes(t.elem) * t.cap;
|
|
709
|
+
throw new Error("unreachable: variable-size field has no fixed size");
|
|
710
|
+
case "string":
|
|
663
711
|
throw new Error("unreachable: variable-size field has no fixed size");
|
|
664
712
|
}
|
|
665
713
|
}
|
package/dist/compiler/idl.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { snakeCase } from "./analyze.js";
|
|
3
3
|
import { decodeAddress } from "../keys.js";
|
|
4
|
-
import { isWritable, optionKey } from "./model.js";
|
|
4
|
+
import { fixedVecName, isWritable, optionKey, typeName, } from "./model.js";
|
|
5
5
|
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
6
6
|
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
7
7
|
const TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
@@ -37,6 +37,16 @@ const fixedEnumName = (name) => `FixedEnum_${name}`;
|
|
|
37
37
|
* IDL type of a field or value. `borshOption` picks the encoding of Option: Borsh `option` for
|
|
38
38
|
* instruction arguments and events, the fixed { is_some, value } struct for account data.
|
|
39
39
|
*/
|
|
40
|
+
/** IDL type of a @layout borsh account's field: Anchor's own (`string`, `{ vec }`, `{ option }`). */
|
|
41
|
+
function borshIdlType(t) {
|
|
42
|
+
if (t.kind === "fstring")
|
|
43
|
+
return "string";
|
|
44
|
+
if (t.kind === "vec")
|
|
45
|
+
return { vec: borshIdlType(t.elem) };
|
|
46
|
+
if (t.kind === "option")
|
|
47
|
+
return { option: idlType(t.inner) };
|
|
48
|
+
return idlType(t);
|
|
49
|
+
}
|
|
40
50
|
function idlType(t, borshOption = false) {
|
|
41
51
|
switch (t.kind) {
|
|
42
52
|
case "int":
|
|
@@ -62,7 +72,7 @@ function idlType(t, borshOption = false) {
|
|
|
62
72
|
// Borsh enum in arguments and events; fixed tag + payload in account data.
|
|
63
73
|
return borshOption || t.kind === "dataEnumArg" ? { defined: { name: t.name } } : { defined: { name: fixedEnumName(t.name) } };
|
|
64
74
|
case "vec":
|
|
65
|
-
return { vec: idlType(t.elem) };
|
|
75
|
+
return t.cap === undefined ? { vec: idlType(t.elem) } : { defined: { name: fixedVecName(t) } };
|
|
66
76
|
case "vecArg":
|
|
67
77
|
return { vec: idlType(t.elem) };
|
|
68
78
|
case "arrayArg":
|
|
@@ -125,7 +135,7 @@ function accountEntry(ix, name, k, programAddress) {
|
|
|
125
135
|
export function buildIdl(model, address, version = "0.1.0") {
|
|
126
136
|
const structs = [...model.structs.values()];
|
|
127
137
|
const usedStructs = structs.filter((s) => model.accountStructs.has(s.name) || model.events.has(s.name) || s.size !== undefined);
|
|
128
|
-
|
|
138
|
+
const idl = {
|
|
129
139
|
address,
|
|
130
140
|
metadata: { name: model.name, version, spec: "0.1.0", description: "Built with TypeSOL" },
|
|
131
141
|
instructions: model.instructions.map((ix) => ({
|
|
@@ -148,7 +158,7 @@ export function buildIdl(model, address, version = "0.1.0") {
|
|
|
148
158
|
fields: s.fields.map((f) => ({
|
|
149
159
|
name: snakeCase(f.name),
|
|
150
160
|
...docsOf(s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name)),
|
|
151
|
-
type: idlType(f.type, model.events.has(s.name) && !model.accountStructs.has(s.name)),
|
|
161
|
+
type: s.layout === "borsh" ? borshIdlType(f.type) : idlType(f.type, model.events.has(s.name) && !model.accountStructs.has(s.name)),
|
|
152
162
|
})),
|
|
153
163
|
},
|
|
154
164
|
})),
|
|
@@ -201,6 +211,17 @@ export function buildIdl(model, address, version = "0.1.0") {
|
|
|
201
211
|
],
|
|
202
212
|
},
|
|
203
213
|
})),
|
|
214
|
+
...[...model.fixedVecs].map(([name, t]) => ({
|
|
215
|
+
name,
|
|
216
|
+
docs: [`TypeSOL FixedVec<${typeName(t.elem)}, ${t.cap}>: \`len\` elements in use, room for ${t.cap} (the rest zero or stale).`],
|
|
217
|
+
type: {
|
|
218
|
+
kind: "struct",
|
|
219
|
+
fields: [
|
|
220
|
+
{ name: "len", type: "u32" },
|
|
221
|
+
{ name: "items", type: { array: [idlType(t.elem), t.cap] } },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
})),
|
|
204
225
|
...[...model.fstrings]
|
|
205
226
|
.sort((a, b) => a - b)
|
|
206
227
|
.map((cap) => ({
|
|
@@ -216,4 +237,12 @@ export function buildIdl(model, address, version = "0.1.0") {
|
|
|
216
237
|
})),
|
|
217
238
|
],
|
|
218
239
|
};
|
|
240
|
+
// FixedString / FixedVec / FixedOption types only @layout borsh accounts declared (they list theirs as string / vec /
|
|
241
|
+
// option) are left out.
|
|
242
|
+
// FixedVecs first: a kept one may name a FixedString.
|
|
243
|
+
for (const prefix of [/^FixedVec\d/, /^FixedOption_/, /^FixedString\d/]) {
|
|
244
|
+
const text = JSON.stringify([idl.instructions, idl.types.filter((t) => !prefix.test(t.name))]);
|
|
245
|
+
idl.types = idl.types.filter((t) => !prefix.test(t.name) || text.includes(`"name":"${t.name}"`));
|
|
246
|
+
}
|
|
247
|
+
return idl;
|
|
219
248
|
}
|