@typesol/cli 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -0
- package/README.md +4 -4
- package/dist/clientgen.js +39 -0
- package/dist/compiler/analyze.d.ts +16 -1
- package/dist/compiler/analyze.js +355 -43
- package/dist/compiler/emit/base.d.ts +36 -1
- package/dist/compiler/emit/base.js +135 -4
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +298 -0
- package/dist/compiler/emit/calls.d.ts +2 -3
- package/dist/compiler/emit/calls.js +135 -29
- package/dist/compiler/emit/cpi.d.ts +62 -8
- package/dist/compiler/emit/cpi.js +1458 -40
- package/dist/compiler/emit/expressions.d.ts +5 -0
- package/dist/compiler/emit/expressions.js +85 -5
- package/dist/compiler/emit/function.js +82 -14
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +20 -0
- package/dist/compiler/emit/shared.js +23 -3
- package/dist/compiler/emit/statements.d.ts +21 -0
- package/dist/compiler/emit/statements.js +380 -19
- package/dist/compiler/emit/storage.d.ts +6 -0
- package/dist/compiler/emit/storage.js +88 -16
- package/dist/compiler/idl.js +37 -5
- package/dist/compiler/model.d.ts +70 -4
- package/dist/compiler/model.js +8 -2
- package/dist/portgen.js +90 -4
- package/docs/errors.md +6 -4
- package/docs/language.md +32 -20
- package/docs/migrate-from-anchor.md +24 -7
- package/docs/test-your-program.md +1 -1
- package/lib/typesol.d.ts +652 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +230 -7
|
@@ -12,6 +12,11 @@ export declare abstract class ExpressionEmitter extends StorageEmitter {
|
|
|
12
12
|
* check is a backstop: an empty option aborts with 5108.
|
|
13
13
|
*/
|
|
14
14
|
protected unwrap(v: Value, node: ts.Node): Value;
|
|
15
|
+
/**
|
|
16
|
+
* `acct?.field` on an optional account, `x.home?.field` on an Option<struct> place: the field as an Option, empty
|
|
17
|
+
* when the account was left out / the option is empty. (A method call through `?.` is not supported.)
|
|
18
|
+
*/
|
|
19
|
+
protected optionalChain(node: ts.PropertyAccessExpression): Value;
|
|
15
20
|
protected literal(value: bigint, type: ValueType, node: ts.Node): Value;
|
|
16
21
|
protected expr(node: ts.Expression, hint?: ValueType): Value;
|
|
17
22
|
protected property(node: ts.PropertyAccessExpression): Value;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { calleeName, resolveValueType } from "../analyze.js";
|
|
3
|
+
import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.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.
|
|
@@ -51,6 +52,52 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
51
52
|
return { c: `(${t}.v != 0)`, type: inner };
|
|
52
53
|
return { c: `${t}.v`, type: inner };
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* `acct?.field` on an optional account, `x.home?.field` on an Option<struct> place: the field as an Option, empty
|
|
57
|
+
* when the account was left out / the option is empty. (A method call through `?.` is not supported.)
|
|
58
|
+
*/
|
|
59
|
+
optionalChain(node) {
|
|
60
|
+
const base = node.expression;
|
|
61
|
+
const optAccount = ts.isIdentifier(base) ? this.accounts.get(base.text) : undefined;
|
|
62
|
+
let present;
|
|
63
|
+
if (optAccount?.optional)
|
|
64
|
+
present = `a_${optAccount.name} != NULL`;
|
|
65
|
+
else {
|
|
66
|
+
const r = this.ref(base);
|
|
67
|
+
if (!r || r.type.kind !== "option" || r.type.inner.kind !== "struct")
|
|
68
|
+
this.fail(node, "?. works on optional accounts and Option<struct> values (other values are never null)");
|
|
69
|
+
present = `${r.lv}.some`;
|
|
70
|
+
}
|
|
71
|
+
// The same access without `?.`, lowered where the base is known to be there.
|
|
72
|
+
const f = ts.factory;
|
|
73
|
+
const plain = f.createPropertyAccessExpression(base, node.name);
|
|
74
|
+
ts.setTextRange(plain, node);
|
|
75
|
+
plain.parent = node.parent;
|
|
76
|
+
const t = this.tmp("oc");
|
|
77
|
+
const declAt = this.lines.length;
|
|
78
|
+
this.line(`/* ${t} */`);
|
|
79
|
+
let inner;
|
|
80
|
+
this.line(`if (${present}) {`);
|
|
81
|
+
this.block(() => {
|
|
82
|
+
const v = this.expr(plain);
|
|
83
|
+
const typed = v.literal !== undefined ? this.literal(v.literal, U64, node) : v;
|
|
84
|
+
if (typed.type.kind === "option") {
|
|
85
|
+
inner = typed.type.inner;
|
|
86
|
+
this.line(`ts_memcpy(&${t}, &${typed.c}, sizeof(${t}));`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (typed.type.kind !== "int" && typed.type.kind !== "bool" && typed.type.kind !== "pubkey" && typed.type.kind !== "enum")
|
|
90
|
+
this.fail(node, `?. gives an Option, so the field must be an integer, boolean, Pubkey or enum, not ${typeName(typed.type)}`);
|
|
91
|
+
inner = typed.type;
|
|
92
|
+
this.line(`${t}.some = 1;`);
|
|
93
|
+
this.line(inner.kind === "pubkey" ? `${t}.v = *(${typed.c});` : `${t}.v = ${typed.c};`);
|
|
94
|
+
});
|
|
95
|
+
this.line(`}`);
|
|
96
|
+
const scalar = inner;
|
|
97
|
+
this.model.options.set(optionKey(scalar), scalar);
|
|
98
|
+
this.lines[declAt] = `${" ".repeat(this.indent)}${optC(scalar)} ${t};\n${" ".repeat(this.indent)}ts_memzero(&${t}, sizeof(${t}));`;
|
|
99
|
+
return { c: t, type: { kind: "option", inner: scalar } };
|
|
100
|
+
}
|
|
54
101
|
literal(value, type, node) {
|
|
55
102
|
if (type.kind !== "int")
|
|
56
103
|
this.fail(node, `expected ${typeName(type)}, got an integer literal`);
|
|
@@ -62,6 +109,20 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
62
109
|
expr(node, hint) {
|
|
63
110
|
if (ts.isParenthesizedExpression(node))
|
|
64
111
|
return this.expr(node.expression, hint);
|
|
112
|
+
// `x as T`: an annotation the value must already match (no conversion: use u8(x) etc. for that). Inside a
|
|
113
|
+
// generic helper it names the instance's type: `return (a + b) as T`.
|
|
114
|
+
if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
115
|
+
if (ts.isAsExpression(node) && ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName) && node.type.typeName.text === "const")
|
|
116
|
+
return this.expr(node.expression, hint);
|
|
117
|
+
const t = resolveValueType(node.type, this.scope);
|
|
118
|
+
if (!t || t.kind === "vecArg" || t.kind === "dataEnumArg")
|
|
119
|
+
this.fail(node.type, "`as` names an integer, boolean, Pubkey, enum, option or string type the value already has");
|
|
120
|
+
const v = this.expr(node.expression, t);
|
|
121
|
+
// Not a conversion: `x as u64 | null` does not make an Option of a u64.
|
|
122
|
+
if (v.literal === undefined && !v.isNull && !sameType(v.type, t))
|
|
123
|
+
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)`);
|
|
124
|
+
return this.expect(v, t, node.expression);
|
|
125
|
+
}
|
|
65
126
|
// `x!` on an Option: its value, aborting with 5108 when it is empty (JS would carry the null on).
|
|
66
127
|
if (ts.isNonNullExpression(node)) {
|
|
67
128
|
const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
|
|
@@ -69,6 +130,11 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
69
130
|
this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
|
|
70
131
|
return this.unwrap(v, node);
|
|
71
132
|
}
|
|
133
|
+
if (ts.isPropertyAccessExpression(node) && node.questionDotToken)
|
|
134
|
+
return this.optionalChain(node);
|
|
135
|
+
const unbanged = this.withoutBang(node);
|
|
136
|
+
if (unbanged)
|
|
137
|
+
return this.expr(unbanged, hint);
|
|
72
138
|
this.noOptionalChain(node);
|
|
73
139
|
if (ts.isBigIntLiteral(node)) {
|
|
74
140
|
const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
|
|
@@ -111,6 +177,8 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
111
177
|
if (ts.isIdentifier(node)) {
|
|
112
178
|
const l = this.lookup(node.text);
|
|
113
179
|
if (l) {
|
|
180
|
+
if (l.closure)
|
|
181
|
+
this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
|
|
114
182
|
if (l.type.kind === "array")
|
|
115
183
|
this.fail(node, `"${node.text}" is an array; use .get(i)`);
|
|
116
184
|
if (l.type.kind === "remaining")
|
|
@@ -140,6 +208,8 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
140
208
|
}
|
|
141
209
|
if (this.accounts.has(node.text))
|
|
142
210
|
this.fail(node, `"${node.text}" is an account; use ${node.text}.key for its address`);
|
|
211
|
+
if (calleeName(node, this.model.helperDecls, this.checker) || this.model.helpers.has(node.text))
|
|
212
|
+
this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
|
|
143
213
|
this.fail(node, `unknown name "${node.text}"`);
|
|
144
214
|
}
|
|
145
215
|
if (ts.isPropertyAccessExpression(node))
|
|
@@ -202,7 +272,7 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
202
272
|
if (arr?.type.kind === "array")
|
|
203
273
|
return { c: `${arr.type.len}ULL`, type: U64 };
|
|
204
274
|
if (arr?.type.kind === "vec")
|
|
205
|
-
return { c:
|
|
275
|
+
return { c: vecCount(arr.vec), type: U64 };
|
|
206
276
|
if (ts.isIdentifier(node.expression)) {
|
|
207
277
|
const l = this.lookup(node.expression.text);
|
|
208
278
|
if (l?.type.kind === "vecArg")
|
|
@@ -231,8 +301,11 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
231
301
|
if (ts.isIdentifier(node.expression)) {
|
|
232
302
|
const a = this.accounts.get(node.expression.text);
|
|
233
303
|
if (a) {
|
|
234
|
-
this.assertUsable(a, node);
|
|
235
304
|
const v = `a_${a.name}`;
|
|
305
|
+
// The address is known before an Init account is created (Anchor seeds use `mint.key()` that way).
|
|
306
|
+
if (field === "key" && !a.optional)
|
|
307
|
+
return { c: `&${v}->key`, type: PUBKEY };
|
|
308
|
+
this.assertUsable(a, node);
|
|
236
309
|
if (field === "key")
|
|
237
310
|
return { c: `&${v}->key`, type: PUBKEY };
|
|
238
311
|
if (field === "lamports")
|
|
@@ -387,7 +460,14 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
387
460
|
this.line(this.textOfInt(typed.type.name, typed.c, t));
|
|
388
461
|
return t;
|
|
389
462
|
}
|
|
390
|
-
|
|
463
|
+
if (typed.type.kind === "pubkey") {
|
|
464
|
+
// A Pubkey (or Hash) as base58, as Rust's `{}` prints it.
|
|
465
|
+
const t = this.tmp("b58");
|
|
466
|
+
this.line(`TS_Str ${t};`);
|
|
467
|
+
this.line(`TS_TRY(ts_base58_32((const uint8_t *)(${typed.c}), &${t}));`);
|
|
468
|
+
return t;
|
|
469
|
+
}
|
|
470
|
+
this.fail(node, `only strings, integers, booleans and Pubkeys can be joined into text, not ${typeName(typed.type)}`);
|
|
391
471
|
}
|
|
392
472
|
/** C statement writing the decimal text of an integer into the TS_Str `out`. */
|
|
393
473
|
textOfInt(name, c, out) {
|
|
@@ -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 { borshIdentical, 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,13 +40,17 @@ 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}));`);
|
|
47
|
-
|
|
49
|
+
// A mint created by this instruction is checked where it is created.
|
|
50
|
+
if (k.kind === "mint" && !k.init)
|
|
48
51
|
head.push(` TS_TRY(ts_check_mint(${v}));`);
|
|
52
|
+
if (k.kind === "info" && k.systemOwned)
|
|
53
|
+
head.push(` if (!ts_key_eq(&${v}->owner, &TS_SYSTEM_PROGRAM_ID)) TS_FAIL(TS_ERR_NOT_SYSTEM_OWNED, "TypeSOL: ${a.name} must be owned by the System Program");`);
|
|
49
54
|
if (k.kind === "system")
|
|
50
55
|
head.push(` TS_TRY(ts_check_address(${v}, &TS_SYSTEM_PROGRAM_ID));`);
|
|
51
56
|
if (k.kind === "instructionsSysvar")
|
|
@@ -87,7 +92,16 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
87
92
|
head.push(` TS_TRY(ts_init_zeroed(${v}, params->program_id, TS_DISC_${k.struct.name}, sizeof(TSS_${k.struct.name})));`);
|
|
88
93
|
if (k.kind === "initIfNeeded" && k.struct.vec)
|
|
89
94
|
this.reporter.fail(a.node, "InitIfNeeded accounts cannot end in a Vec yet");
|
|
90
|
-
if (k.kind === "account" || k.kind === "init" || k.kind === "
|
|
95
|
+
if ((k.kind === "account" || k.kind === "init" || k.kind === "initIfNeeded") && k.struct.layout === "borsh") {
|
|
96
|
+
// Worked on as its fixed-layout struct on the heap, packed back when the instruction returns. The heap is zero at
|
|
97
|
+
// entry and these are its first allocations (before the body uses it), so a string's padding and a list's
|
|
98
|
+
// unused slots start as zero: ts_unpack_* only writes what the Borsh bytes hold.
|
|
99
|
+
const S = k.struct.name;
|
|
100
|
+
head.push(` TSS_${S} *d_${a.name} = NULL;`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&d_${a.name}));`);
|
|
101
|
+
if (k.kind === "account")
|
|
102
|
+
head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}, NULL));`);
|
|
103
|
+
}
|
|
104
|
+
else if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
|
|
91
105
|
head.push(` TSS_${k.struct.name} *d_${a.name} = (TSS_${k.struct.name} *)(${v}->data + 8);`);
|
|
92
106
|
if (k.struct.vec) {
|
|
93
107
|
head.push(` uint64_t entry_${a.name} = ${v}->data_len;`, ` (void)entry_${a.name};`);
|
|
@@ -189,6 +203,17 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
189
203
|
}
|
|
190
204
|
head.push(` }`);
|
|
191
205
|
}
|
|
206
|
+
else if (a.type.kind === "vecArg" && a.type.elem.kind === "string") {
|
|
207
|
+
// Borsh Vec<String>: u32 count, then each string's u32 length and UTF-8 bytes. Kept as views of the
|
|
208
|
+
// instruction data in a heap array of TS_Str (each string at least 4 bytes, so the count is bounded first).
|
|
209
|
+
const v = `arg_${a.name}`;
|
|
210
|
+
const items = `vs_${a.name}`;
|
|
211
|
+
const i = `vi_${a.name}`;
|
|
212
|
+
need("4");
|
|
213
|
+
head.push(` TS_VecArg ${v};`, ` ${v}.len = (uint32_t)ts_read_le(${at}, 4);`, ` off += 4;`);
|
|
214
|
+
need(`(uint64_t)${v}.len * 4`);
|
|
215
|
+
head.push(` TS_Str *${items};`, ` TS_TRY(ts_alloc((uint64_t)${v}.len * sizeof(TS_Str), (uint8_t **)&${items}));`, ` for (uint64_t ${i} = 0; ${i} < ${v}.len; ${i}++) {`, ` if (params->data_len < off + 4) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`, ` uint32_t n = (uint32_t)ts_read_le(params->data + off, 4);`, ` off += 4;`, ` if (params->data_len - off < n) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`, ` if (!ts_utf8_valid(params->data + off, n)) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid UTF-8 in a string argument");`, ` ${items}[${i}] = ts_str(params->data + off, n);`, ` off += n;`, ` }`, ` ${v}.ptr = (const uint8_t *)${items};`);
|
|
216
|
+
}
|
|
192
217
|
else if (a.type.kind === "vecArg") {
|
|
193
218
|
const elem = a.type.elem;
|
|
194
219
|
const size = this.fieldBytes(elem);
|
|
@@ -204,6 +229,21 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
204
229
|
head.push(` for (uint64_t ${i} = 0; ${i} < arg_${a.name}.len; ${i}++) {`, ...checks, ` }`);
|
|
205
230
|
}
|
|
206
231
|
}
|
|
232
|
+
else if (a.type.kind === "structRef" && this.model.structs.get(a.type.struct).layout === "borsh") {
|
|
233
|
+
// A @layout borsh struct: Borsh-decoded into its fixed-layout struct, as its accounts are.
|
|
234
|
+
const S = a.type.struct;
|
|
235
|
+
const v = `arg_${a.name}`;
|
|
236
|
+
const used = `used_${a.name}`;
|
|
237
|
+
head.push(` TSS_${S} *${v};`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&${v}));`, ` uint64_t ${used} = 0;`, ` if (ts_unpack_${S}(${at}, params->data_len - off, ${v}, &${used}) != SUCCESS) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid ${S} argument");`, ` off += ${used};`);
|
|
238
|
+
// Strings from outside are checked as UTF-8 (account data was checked when it was written).
|
|
239
|
+
for (const f of this.model.structs.get(S).fields) {
|
|
240
|
+
const bad = `TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid UTF-8 in a string argument")`;
|
|
241
|
+
if (f.type.kind === "fstring")
|
|
242
|
+
head.push(` if (!ts_utf8_valid(${v}->${f.name}.bytes, ${v}->${f.name}.len)) ${bad};`);
|
|
243
|
+
else if (f.type.kind === "vec" && f.type.elem.kind === "fstring")
|
|
244
|
+
head.push(` for (uint64_t i = 0; i < ${v}->${f.name}.len; i++)`, ` if (!ts_utf8_valid(${v}->${f.name}.items[i].bytes, ${v}->${f.name}.items[i].len)) ${bad};`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
207
247
|
else if (a.type.kind === "structRef" || a.type.kind === "arrayArg") {
|
|
208
248
|
// Fixed layouts are Borsh-identical: copy to the heap, then check bools, enums, options and strings.
|
|
209
249
|
const v = `arg_${a.name}`;
|
|
@@ -234,8 +274,17 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
234
274
|
head.push(` if (!ts_utf8_valid(${lv}.ptr, ${lv}.len)) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: invalid UTF-8 in a string argument");`);
|
|
235
275
|
continue;
|
|
236
276
|
}
|
|
277
|
+
if (!isScalar(f.type) && borshIdentical(f.type, this.model)) {
|
|
278
|
+
// A struct or array whose Borsh bytes are its fixed layout: copied, then checked (bools, enums).
|
|
279
|
+
const lv = `${v}.v.${variant.name}.${f.name}`;
|
|
280
|
+
const size = this.fieldBytes(f.type);
|
|
281
|
+
head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
282
|
+
head.push(` ts_memcpy(&${lv}, params->data + off, ${size});`, ` off += ${size};`);
|
|
283
|
+
head.push(...this.validation(lv, f.type, " "));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
237
286
|
if (!isScalar(f.type))
|
|
238
|
-
this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys, enums and
|
|
287
|
+
this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys, enums, strings and fixed-size structs and arrays of those`);
|
|
239
288
|
const size = scalarSize(f.type);
|
|
240
289
|
head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
241
290
|
head.push(` ts_memcpy(&${v}.v.${variant.name}.${f.name}, params->data + off, ${size});`, ` off += ${size};`);
|
|
@@ -273,7 +322,14 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
273
322
|
this.reporter.guard(() => this.statement(stmt, true));
|
|
274
323
|
for (const name of this.pendingInit) {
|
|
275
324
|
const k = this.accounts.get(name).kind;
|
|
276
|
-
const
|
|
325
|
+
const a = this.accounts.get(name);
|
|
326
|
+
const how = k.kind === "token" && a.constraints.ata
|
|
327
|
+
? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer)`
|
|
328
|
+
: k.kind === "token"
|
|
329
|
+
? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer, seeds | null, mint, authority)`
|
|
330
|
+
: k.kind === "mint"
|
|
331
|
+
? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer, seeds | null, decimals, authority, freezeAuthority | null)`
|
|
332
|
+
: `${name}.create(payer, seeds)`;
|
|
277
333
|
this.reporter.error(this.accounts.get(name).node, `Init account "${name}" is never created; call ${how}`);
|
|
278
334
|
}
|
|
279
335
|
const end = ix.returns
|
|
@@ -285,13 +341,25 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
285
341
|
epilogue() {
|
|
286
342
|
if (!this.ix)
|
|
287
343
|
return [];
|
|
288
|
-
|
|
289
|
-
|
|
344
|
+
// @layout borsh accounts go back to their Borsh bytes (unless closed, or never created).
|
|
345
|
+
const packs = this.ix.accounts
|
|
346
|
+
.filter((a) => (a.kind.kind === "account" ? a.kind.mut : a.kind.kind === "init" || a.kind.kind === "initIfNeeded"))
|
|
347
|
+
.filter((a) => a.kind.struct.layout === "borsh")
|
|
290
348
|
.map((a) => {
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
return
|
|
349
|
+
const v = `a_${a.name}`;
|
|
350
|
+
const S = a.kind.struct.name;
|
|
351
|
+
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, NULL));`;
|
|
294
352
|
});
|
|
353
|
+
return [
|
|
354
|
+
...packs,
|
|
355
|
+
...this.ix.accounts
|
|
356
|
+
.filter((a) => a.constraints.closeTo)
|
|
357
|
+
.map((a) => {
|
|
358
|
+
const line = `TS_TRY(ts_close(a_${a.name}, a_${a.constraints.closeTo}));`;
|
|
359
|
+
const optional = this.optionalIn(line);
|
|
360
|
+
return optional.length ? `if (${optional.map((n) => `a_${n} != NULL`).join(" && ")}) ${line}` : line;
|
|
361
|
+
}),
|
|
362
|
+
];
|
|
295
363
|
}
|
|
296
364
|
constraintChecks(ix) {
|
|
297
365
|
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 {
|
|
@@ -59,6 +65,14 @@ export interface Local {
|
|
|
59
65
|
/** For struct / data-enum references: the storage paths they alias (e.g. "book.asks[]"), and the declaration. */
|
|
60
66
|
paths?: string[];
|
|
61
67
|
decl?: ts.VariableDeclaration;
|
|
68
|
+
/** A local closure (`const f = (…) => …` reading the enclosing function's variables), expanded where it is called. */
|
|
69
|
+
closure?: Closure;
|
|
70
|
+
}
|
|
71
|
+
/** A closure and the scopes it was declared in (its free names resolve there, wherever it is called). */
|
|
72
|
+
export interface Closure {
|
|
73
|
+
node: ts.ArrowFunction | ts.FunctionExpression;
|
|
74
|
+
name: string;
|
|
75
|
+
scopes: Map<string, Local>[];
|
|
62
76
|
}
|
|
63
77
|
export declare const cBytes: (bytes: number[]) => string;
|
|
64
78
|
/** C expressions that nothing in a TypeSOL expression can change: temporaries, locals, arguments, account keys, constants. */
|
|
@@ -88,3 +102,9 @@ export interface EmitOptions {
|
|
|
88
102
|
/** Log a marker and the remaining compute units before every statement (`typesol build --profile`). */
|
|
89
103
|
profile?: boolean;
|
|
90
104
|
}
|
|
105
|
+
/** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
|
|
106
|
+
export declare function fixedVecC(t: FieldType & {
|
|
107
|
+
kind: "vec";
|
|
108
|
+
}): string;
|
|
109
|
+
/** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
|
|
110
|
+
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,14 @@ 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
|
+
// A writable account with a growable Vec: its length at instruction entry, for push / pop / clear.
|
|
189
|
+
if (p.account.kind === "account" && p.account.mut && p.account.struct.vec)
|
|
190
|
+
params.push(`uint64_t entry_${p.name}`);
|
|
191
|
+
}
|
|
180
192
|
else if (p.type.kind === "signerSeeds")
|
|
181
193
|
params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
|
|
182
194
|
else if (p.type.kind === "pubkey")
|
|
@@ -187,6 +199,14 @@ export function helperSignature(h) {
|
|
|
187
199
|
params.push(`${valueC(p.type)} p_${p.name}`);
|
|
188
200
|
}
|
|
189
201
|
if (h.returns)
|
|
190
|
-
params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : `${scalarC(h.returns)} *ret`);
|
|
202
|
+
params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : h.returns.kind === "string" ? `TS_Str *ret` : `${scalarC(h.returns)} *ret`);
|
|
191
203
|
return `static uint64_t fn_${h.name}(${params.join(", ")})`;
|
|
192
204
|
}
|
|
205
|
+
/** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
|
|
206
|
+
export function fixedVecC(t) {
|
|
207
|
+
return `TSF_${fixedVecName(t)}`;
|
|
208
|
+
}
|
|
209
|
+
/** The element count of a Vec place to read: a FixedVec's stored length is clamped to its capacity. */
|
|
210
|
+
export function vecCount(v) {
|
|
211
|
+
return v.cap === undefined ? `((uint64_t)${v.len})` : `((uint64_t)(${v.len} < ${v.cap} ? ${v.len} : ${v.cap}))`;
|
|
212
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
+
import { type Value, type Closure } from "./shared.js";
|
|
2
3
|
import { CpiEmitter } from "./cpi.js";
|
|
3
4
|
/**
|
|
4
5
|
* Statements: variables, assignment, control flow and return.
|
|
@@ -23,6 +24,26 @@ export declare abstract class StatementEmitter extends CpiEmitter {
|
|
|
23
24
|
* inside the loop so its checked-arithmetic temporaries are recomputed every iteration.
|
|
24
25
|
*/
|
|
25
26
|
protected loop(cond: ts.Expression | undefined, body: ts.Statement, step: ts.Expression | undefined): void;
|
|
27
|
+
/**
|
|
28
|
+
* Expand a closure where it is called: the arguments are evaluated here, in order, into the parameters (scalars,
|
|
29
|
+
* Pubkeys and strings by value, structs by reference, as JS passes objects); the body then runs with the names it
|
|
30
|
+
* was declared with, so a block around the call that declares the same name does not change what it reads, and
|
|
31
|
+
* writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
|
|
32
|
+
*/
|
|
33
|
+
protected expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* `xs.forEach / some / every / find / findIndex / reduce / includes / indexOf` on a Vec, FixedArray, FixedVec or Vec
|
|
36
|
+
* argument: an index loop (the length read each step, as `for … of`), the callback (an arrow written at the call)
|
|
37
|
+
* expanded with each element as `xs.get(i)` gives it (structs by reference) and, if it takes one, the index.
|
|
38
|
+
* `find` gives an Option of the element (scalar elements), `findIndex` / `indexOf` an i64 (-1 when absent).
|
|
39
|
+
*/
|
|
40
|
+
protected listMethod(node: ts.CallExpression, method: string): Value | undefined;
|
|
41
|
+
/** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
|
|
42
|
+
protected closureReturn(stmt: ts.ReturnStatement, frame: (typeof this.inlineFrames)[number]): void;
|
|
43
|
+
/** Store a closure's result (fixing its type on the first one) and jump past the rest of its body. */
|
|
44
|
+
protected closureResult(value: Value, at: ts.Expression, frame: (typeof this.inlineFrames)[number]): void;
|
|
45
|
+
/** `do { … } while (cond)`: the body once, then the test; `continue` goes to the test. */
|
|
46
|
+
protected doLoop(stmt: ts.DoStatement): void;
|
|
26
47
|
protected body(stmt: ts.Statement): void;
|
|
27
48
|
protected variable(list: ts.VariableDeclarationList, topLevel?: boolean): void;
|
|
28
49
|
/**
|