@typesol/cli 0.17.0 → 0.19.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 +99 -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 +165 -49
- package/dist/compiler/anchor-errors.d.ts +1 -0
- package/dist/compiler/anchor-errors.js +80 -0
- 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 +101 -15
- package/dist/compiler/emit/cpi.d.ts +31 -0
- package/dist/compiler/emit/cpi.js +1061 -39
- package/dist/compiler/emit/expressions.d.ts +24 -0
- package/dist/compiler/emit/expressions.js +137 -19
- package/dist/compiler/emit/function.js +57 -10
- 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 +386 -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 +31 -24
- package/docs/migrate-from-anchor.md +13 -3
- package/lib/typesol.d.ts +528 -22
- package/package.json +1 -1
- package/runtime/typesol_rt.h +279 -5
|
@@ -12,12 +12,36 @@ 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;
|
|
18
23
|
/** Whether TypeScript sees this expression as a string (decides `+` between concatenation and arithmetic). */
|
|
19
24
|
protected looksLikeString(node: ts.Expression): boolean;
|
|
20
25
|
/** A string view of a string, integer or boolean (for concatenation and templates). */
|
|
26
|
+
/**
|
|
27
|
+
* A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
|
|
28
|
+
* TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
|
|
29
|
+
* first, as JS reads them then.
|
|
30
|
+
*/
|
|
31
|
+
protected templatePieces(node: ts.TemplateExpression): {
|
|
32
|
+
text: string;
|
|
33
|
+
arr?: undefined;
|
|
34
|
+
n?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
text?: undefined;
|
|
37
|
+
arr: string;
|
|
38
|
+
n: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
|
|
42
|
+
* literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
|
|
43
|
+
*/
|
|
44
|
+
protected constantText(e: ts.Expression): string | undefined;
|
|
21
45
|
protected toText(node: ts.Expression): string;
|
|
22
46
|
/** C statement writing the decimal text of an integer into the TS_Str `out`. */
|
|
23
47
|
protected textOfInt(name: IntName, c: string, out: string): string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
+
import { base58 } from "../../keys.js";
|
|
2
3
|
import { calleeName, resolveValueType } from "../analyze.js";
|
|
3
|
-
import { INT_INFO, unsignedC, sameType, typeName } from "../model.js";
|
|
4
|
+
import { INT_INFO, unsignedC, optionKey, sameType, typeName } from "../model.js";
|
|
4
5
|
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cString, intLiteral, scalarStorageC, optC, valueC, vecCount } from "./shared.js";
|
|
5
6
|
import { StorageEmitter } from "./storage.js";
|
|
6
7
|
/**
|
|
@@ -52,6 +53,52 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
52
53
|
return { c: `(${t}.v != 0)`, type: inner };
|
|
53
54
|
return { c: `${t}.v`, type: inner };
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* `acct?.field` on an optional account, `x.home?.field` on an Option<struct> place: the field as an Option, empty
|
|
58
|
+
* when the account was left out / the option is empty. (A method call through `?.` is not supported.)
|
|
59
|
+
*/
|
|
60
|
+
optionalChain(node) {
|
|
61
|
+
const base = node.expression;
|
|
62
|
+
const optAccount = ts.isIdentifier(base) ? this.accounts.get(base.text) : undefined;
|
|
63
|
+
let present;
|
|
64
|
+
if (optAccount?.optional)
|
|
65
|
+
present = `a_${optAccount.name} != NULL`;
|
|
66
|
+
else {
|
|
67
|
+
const r = this.ref(base);
|
|
68
|
+
if (!r || r.type.kind !== "option" || r.type.inner.kind !== "struct")
|
|
69
|
+
this.fail(node, "?. works on optional accounts and Option<struct> values (other values are never null)");
|
|
70
|
+
present = `${r.lv}.some`;
|
|
71
|
+
}
|
|
72
|
+
// The same access without `?.`, lowered where the base is known to be there.
|
|
73
|
+
const f = ts.factory;
|
|
74
|
+
const plain = f.createPropertyAccessExpression(base, node.name);
|
|
75
|
+
ts.setTextRange(plain, node);
|
|
76
|
+
plain.parent = node.parent;
|
|
77
|
+
const t = this.tmp("oc");
|
|
78
|
+
const declAt = this.lines.length;
|
|
79
|
+
this.line(`/* ${t} */`);
|
|
80
|
+
let inner;
|
|
81
|
+
this.line(`if (${present}) {`);
|
|
82
|
+
this.block(() => {
|
|
83
|
+
const v = this.expr(plain);
|
|
84
|
+
const typed = v.literal !== undefined ? this.literal(v.literal, U64, node) : v;
|
|
85
|
+
if (typed.type.kind === "option") {
|
|
86
|
+
inner = typed.type.inner;
|
|
87
|
+
this.line(`ts_memcpy(&${t}, &${typed.c}, sizeof(${t}));`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typed.type.kind !== "int" && typed.type.kind !== "bool" && typed.type.kind !== "pubkey" && typed.type.kind !== "enum")
|
|
91
|
+
this.fail(node, `?. gives an Option, so the field must be an integer, boolean, Pubkey or enum, not ${typeName(typed.type)}`);
|
|
92
|
+
inner = typed.type;
|
|
93
|
+
this.line(`${t}.some = 1;`);
|
|
94
|
+
this.line(inner.kind === "pubkey" ? `${t}.v = *(${typed.c});` : `${t}.v = ${typed.c};`);
|
|
95
|
+
});
|
|
96
|
+
this.line(`}`);
|
|
97
|
+
const scalar = inner;
|
|
98
|
+
this.model.options.set(optionKey(scalar), scalar);
|
|
99
|
+
this.lines[declAt] = `${" ".repeat(this.indent)}${optC(scalar)} ${t};\n${" ".repeat(this.indent)}ts_memzero(&${t}, sizeof(${t}));`;
|
|
100
|
+
return { c: t, type: { kind: "option", inner: scalar } };
|
|
101
|
+
}
|
|
55
102
|
literal(value, type, node) {
|
|
56
103
|
if (type.kind !== "int")
|
|
57
104
|
this.fail(node, `expected ${typeName(type)}, got an integer literal`);
|
|
@@ -84,6 +131,11 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
84
131
|
this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
|
|
85
132
|
return this.unwrap(v, node);
|
|
86
133
|
}
|
|
134
|
+
if (ts.isPropertyAccessExpression(node) && node.questionDotToken)
|
|
135
|
+
return this.optionalChain(node);
|
|
136
|
+
const unbanged = this.withoutBang(node);
|
|
137
|
+
if (unbanged)
|
|
138
|
+
return this.expr(unbanged, hint);
|
|
87
139
|
this.noOptionalChain(node);
|
|
88
140
|
if (ts.isBigIntLiteral(node)) {
|
|
89
141
|
const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
|
|
@@ -100,22 +152,14 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
100
152
|
return { c: `ts_str((const uint8_t *)${cString(node.text)}, ${len})`, type: STRING, stringLiteral: node.text };
|
|
101
153
|
}
|
|
102
154
|
if (ts.isTemplateExpression(node)) {
|
|
103
|
-
// `a${x}b${y}c
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
]) {
|
|
112
|
-
const next = this.tmp("tpl");
|
|
113
|
-
this.line(`TS_Str ${next};`);
|
|
114
|
-
this.line(`TS_TRY(ts_str_concat(${acc}, ${piece}, &${next}));`);
|
|
115
|
-
acc = next;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return { c: acc, type: STRING };
|
|
155
|
+
// `a${x}b${y}c`: one join on the heap (or a literal when every piece is known at compile time).
|
|
156
|
+
const pieces = this.templatePieces(node);
|
|
157
|
+
if (pieces.text !== undefined)
|
|
158
|
+
return { c: `ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})`, type: STRING, stringLiteral: pieces.text };
|
|
159
|
+
const t = this.tmp("tpl");
|
|
160
|
+
this.line(`TS_Str ${t};`);
|
|
161
|
+
this.line(`TS_TRY(ts_str_join(${pieces.arr}, ${pieces.n}, &${t}));`);
|
|
162
|
+
return { c: t, type: STRING };
|
|
119
163
|
}
|
|
120
164
|
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
121
165
|
return { c: "", type: BOOL, isNull: true };
|
|
@@ -126,6 +170,8 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
126
170
|
if (ts.isIdentifier(node)) {
|
|
127
171
|
const l = this.lookup(node.text);
|
|
128
172
|
if (l) {
|
|
173
|
+
if (l.closure)
|
|
174
|
+
this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
|
|
129
175
|
if (l.type.kind === "array")
|
|
130
176
|
this.fail(node, `"${node.text}" is an array; use .get(i)`);
|
|
131
177
|
if (l.type.kind === "remaining")
|
|
@@ -248,8 +294,11 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
248
294
|
if (ts.isIdentifier(node.expression)) {
|
|
249
295
|
const a = this.accounts.get(node.expression.text);
|
|
250
296
|
if (a) {
|
|
251
|
-
this.assertUsable(a, node);
|
|
252
297
|
const v = `a_${a.name}`;
|
|
298
|
+
// The address is known before an Init account is created (Anchor seeds use `mint.key()` that way).
|
|
299
|
+
if (field === "key" && !a.optional)
|
|
300
|
+
return { c: `&${v}->key`, type: PUBKEY };
|
|
301
|
+
this.assertUsable(a, node);
|
|
253
302
|
if (field === "key")
|
|
254
303
|
return { c: `&${v}->key`, type: PUBKEY };
|
|
255
304
|
if (field === "lamports")
|
|
@@ -374,6 +423,68 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
374
423
|
return isText(this.checker.getTypeAtLocation(node));
|
|
375
424
|
}
|
|
376
425
|
/** A string view of a string, integer or boolean (for concatenation and templates). */
|
|
426
|
+
/**
|
|
427
|
+
* A template literal's pieces in order, text known at compile time folded in: all text (`text`), or a C array of
|
|
428
|
+
* TS_Str (`arr`, `n` entries). When a later piece runs statements, earlier pieces that view account text are copied
|
|
429
|
+
* first, as JS reads them then.
|
|
430
|
+
*/
|
|
431
|
+
templatePieces(node) {
|
|
432
|
+
const parts = [];
|
|
433
|
+
const lit = (text) => {
|
|
434
|
+
if (!text)
|
|
435
|
+
return;
|
|
436
|
+
const last = parts.at(-1);
|
|
437
|
+
if (last?.text !== undefined)
|
|
438
|
+
last.text += text;
|
|
439
|
+
else
|
|
440
|
+
parts.push({ text });
|
|
441
|
+
};
|
|
442
|
+
lit(node.head.text);
|
|
443
|
+
for (const span of node.templateSpans) {
|
|
444
|
+
const known = this.constantText(span.expression);
|
|
445
|
+
if (known !== undefined)
|
|
446
|
+
lit(known);
|
|
447
|
+
else {
|
|
448
|
+
const mark = this.lines.length;
|
|
449
|
+
const c = this.toText(span.expression);
|
|
450
|
+
for (const p of parts)
|
|
451
|
+
if (p.c !== undefined)
|
|
452
|
+
p.c = this.settle({ c: p.c, type: STRING }, mark).c;
|
|
453
|
+
parts.push({ c });
|
|
454
|
+
}
|
|
455
|
+
lit(span.literal.text);
|
|
456
|
+
}
|
|
457
|
+
if (parts.every((p) => p.text !== undefined))
|
|
458
|
+
return { text: parts.map((p) => p.text).join("") };
|
|
459
|
+
const strOf = (text) => `ts_str((const uint8_t *)${cString(text)}, ${Buffer.byteLength(text, "utf8")})`;
|
|
460
|
+
const arr = this.tmp("parts");
|
|
461
|
+
this.line(`TS_Str ${arr}[${parts.length}] = {${parts.map((p) => (p.text !== undefined ? strOf(p.text) : p.c)).join(", ")}};`);
|
|
462
|
+
return { arr, n: parts.length };
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
|
|
466
|
+
* literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
|
|
467
|
+
*/
|
|
468
|
+
constantText(e) {
|
|
469
|
+
if (ts.isParenthesizedExpression(e))
|
|
470
|
+
return this.constantText(e.expression);
|
|
471
|
+
if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))
|
|
472
|
+
return e.text;
|
|
473
|
+
if (e.kind === ts.SyntaxKind.TrueKeyword)
|
|
474
|
+
return "true";
|
|
475
|
+
if (e.kind === ts.SyntaxKind.FalseKeyword)
|
|
476
|
+
return "false";
|
|
477
|
+
if (ts.isBigIntLiteral(e))
|
|
478
|
+
return BigInt(e.text.slice(0, -1).replace(/_/g, "")).toString();
|
|
479
|
+
if (ts.isIdentifier(e) && !this.lookup(e.text)) {
|
|
480
|
+
const k = this.model.constants.get(e.text);
|
|
481
|
+
if (k && typeof k.value === "string")
|
|
482
|
+
return k.value;
|
|
483
|
+
if (k && typeof k.value === "object")
|
|
484
|
+
return base58(Uint8Array.from(k.value.pubkey));
|
|
485
|
+
}
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
377
488
|
toText(node) {
|
|
378
489
|
const v = this.expr(node);
|
|
379
490
|
if (v.type.kind === "string")
|
|
@@ -404,7 +515,14 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
404
515
|
this.line(this.textOfInt(typed.type.name, typed.c, t));
|
|
405
516
|
return t;
|
|
406
517
|
}
|
|
407
|
-
|
|
518
|
+
if (typed.type.kind === "pubkey") {
|
|
519
|
+
// A Pubkey (or Hash) as base58, as Rust's `{}` prints it.
|
|
520
|
+
const t = this.tmp("b58");
|
|
521
|
+
this.line(`TS_Str ${t};`);
|
|
522
|
+
this.line(`TS_TRY(ts_base58_32((const uint8_t *)(${typed.c}), &${t}));`);
|
|
523
|
+
return t;
|
|
524
|
+
}
|
|
525
|
+
this.fail(node, `only strings, integers, booleans and Pubkeys can be joined into text, not ${typeName(typed.type)}`);
|
|
408
526
|
}
|
|
409
527
|
/** C statement writing the decimal text of an integer into the TS_Str `out`. */
|
|
410
528
|
textOfInt(name, c, out) {
|
|
@@ -2,7 +2,7 @@ import ts from "typescript";
|
|
|
2
2
|
import { snakeCase } from "../analyze.js";
|
|
3
3
|
import { decodeAddress } from "../../keys.js";
|
|
4
4
|
import { INT_INFO, isScalar, isWritable, scalarSize, typeName, } from "../model.js";
|
|
5
|
-
import { borshMinSize } from "./borsh.js";
|
|
5
|
+
import { borshIdentical, borshMinSize } from "./borsh.js";
|
|
6
6
|
import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature } from "./shared.js";
|
|
7
7
|
import { StatementEmitter } from "./statements.js";
|
|
8
8
|
/**
|
|
@@ -46,8 +46,11 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
46
46
|
// A token account created by this instruction is checked where it is created (create/createIfNeeded).
|
|
47
47
|
if (k.kind === "token" && !k.init)
|
|
48
48
|
head.push(` TS_TRY(ts_check_token_account(${v}));`);
|
|
49
|
-
|
|
49
|
+
// A mint created by this instruction is checked where it is created.
|
|
50
|
+
if (k.kind === "mint" && !k.init)
|
|
50
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");`);
|
|
51
54
|
if (k.kind === "system")
|
|
52
55
|
head.push(` TS_TRY(ts_check_address(${v}, &TS_SYSTEM_PROGRAM_ID));`);
|
|
53
56
|
if (k.kind === "instructionsSysvar")
|
|
@@ -96,7 +99,7 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
96
99
|
const S = k.struct.name;
|
|
97
100
|
head.push(` TSS_${S} *d_${a.name} = NULL;`, ` TS_TRY(ts_alloc(sizeof(TSS_${S}), (uint8_t **)&d_${a.name}));`);
|
|
98
101
|
if (k.kind === "account")
|
|
99
|
-
head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}));`);
|
|
102
|
+
head.push(` TS_TRY(ts_unpack_${S}(${v}->data + 8, ${v}->data_len - 8, d_${a.name}, NULL));`);
|
|
100
103
|
}
|
|
101
104
|
else if (k.kind === "account" || k.kind === "init" || k.kind === "zeroed" || k.kind === "initIfNeeded") {
|
|
102
105
|
head.push(` TSS_${k.struct.name} *d_${a.name} = (TSS_${k.struct.name} *)(${v}->data + 8);`);
|
|
@@ -200,6 +203,17 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
200
203
|
}
|
|
201
204
|
head.push(` }`);
|
|
202
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
|
+
}
|
|
203
217
|
else if (a.type.kind === "vecArg") {
|
|
204
218
|
const elem = a.type.elem;
|
|
205
219
|
const size = this.fieldBytes(elem);
|
|
@@ -215,6 +229,21 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
215
229
|
head.push(` for (uint64_t ${i} = 0; ${i} < arg_${a.name}.len; ${i}++) {`, ...checks, ` }`);
|
|
216
230
|
}
|
|
217
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
|
+
}
|
|
218
247
|
else if (a.type.kind === "structRef" || a.type.kind === "arrayArg") {
|
|
219
248
|
// Fixed layouts are Borsh-identical: copy to the heap, then check bools, enums, options and strings.
|
|
220
249
|
const v = `arg_${a.name}`;
|
|
@@ -245,8 +274,17 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
245
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");`);
|
|
246
275
|
continue;
|
|
247
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
|
+
}
|
|
248
286
|
if (!isScalar(f.type))
|
|
249
|
-
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`);
|
|
250
288
|
const size = scalarSize(f.type);
|
|
251
289
|
head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
252
290
|
head.push(` ts_memcpy(&${v}.v.${variant.name}.${f.name}, params->data + off, ${size});`, ` off += ${size};`);
|
|
@@ -284,7 +322,14 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
284
322
|
this.reporter.guard(() => this.statement(stmt, true));
|
|
285
323
|
for (const name of this.pendingInit) {
|
|
286
324
|
const k = this.accounts.get(name).kind;
|
|
287
|
-
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)`;
|
|
288
333
|
this.reporter.error(this.accounts.get(name).node, `Init account "${name}" is never created; call ${how}`);
|
|
289
334
|
}
|
|
290
335
|
const end = ix.returns
|
|
@@ -303,7 +348,7 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
303
348
|
.map((a) => {
|
|
304
349
|
const v = `a_${a.name}`;
|
|
305
350
|
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));`;
|
|
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));`;
|
|
307
352
|
});
|
|
308
353
|
return [
|
|
309
354
|
...packs,
|
|
@@ -326,15 +371,17 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
326
371
|
const k = a.kind.kind;
|
|
327
372
|
if (c.hasOne.length && k !== "account" && k !== "token" && k !== "mint")
|
|
328
373
|
this.reporter.fail(a.node, "HasOne applies to existing accounts (Account<T> / Mut<T>), TokenAccount and Mint");
|
|
329
|
-
for (const
|
|
374
|
+
for (const entry of c.hasOne) {
|
|
375
|
+
// "field=param" (TokenOf / MintOf) or a field checked against the parameter of the same name (HasOne).
|
|
376
|
+
const [f, p = f] = entry.split("=");
|
|
330
377
|
if (k === "token")
|
|
331
378
|
out.push(f === "mint"
|
|
332
|
-
? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${
|
|
333
|
-
: ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${
|
|
379
|
+
? ` if (!ts_key_eq(ts_token_mint(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_MINT, "TypeSOL: ${a.name} is not a token account of ${p}");`
|
|
380
|
+
: ` if (!ts_key_eq(ts_token_owner(a_${a.name}), &a_${p}->key)) TS_FAIL(TS_ERR_TOKEN_OWNER, "TypeSOL: ${a.name} is not owned by ${p}");`);
|
|
334
381
|
else if (k === "mint") {
|
|
335
382
|
const at = f === "mintAuthority" ? 0 : 46;
|
|
336
383
|
const code = f === "mintAuthority" ? "TS_ERR_MINT_AUTHORITY" : "TS_ERR_FREEZE_AUTHORITY";
|
|
337
|
-
out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${
|
|
384
|
+
out.push(` if (ts_read_le(a_${a.name}->data + ${at}, 4) != 1 || !ts_key_eq((const SolPubkey *)(a_${a.name}->data + ${at + 4}), &a_${p}->key)) TS_FAIL(${code}, "TypeSOL: ${p} is not the ${f} of ${a.name}");`);
|
|
338
385
|
}
|
|
339
386
|
else
|
|
340
387
|
out.push(` if (!ts_key_eq(&d_${a.name}->${f}, &a_${f}->key)) TS_FAIL(TS_ERR_CONSTRAINT, "TypeSOL: ${a.name}.${f} must be the ${f} account");`);
|
|
@@ -65,6 +65,14 @@ export interface Local {
|
|
|
65
65
|
/** For struct / data-enum references: the storage paths they alias (e.g. "book.asks[]"), and the declaration. */
|
|
66
66
|
paths?: string[];
|
|
67
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>[];
|
|
68
76
|
}
|
|
69
77
|
export declare const cBytes: (bytes: number[]) => string;
|
|
70
78
|
/** C expressions that nothing in a TypeSOL expression can change: temporaries, locals, arguments, account keys, constants. */
|
|
@@ -185,6 +185,9 @@ export function helperSignature(h) {
|
|
|
185
185
|
params.push(`TsAccount *a_${p.name}`);
|
|
186
186
|
if (p.account.kind === "account" && p.account.struct.layout === "borsh")
|
|
187
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}`);
|
|
188
191
|
}
|
|
189
192
|
else if (p.type.kind === "signerSeeds")
|
|
190
193
|
params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
|
|
@@ -196,7 +199,7 @@ export function helperSignature(h) {
|
|
|
196
199
|
params.push(`${valueC(p.type)} p_${p.name}`);
|
|
197
200
|
}
|
|
198
201
|
if (h.returns)
|
|
199
|
-
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`);
|
|
200
203
|
return `static uint64_t fn_${h.name}(${params.join(", ")})`;
|
|
201
204
|
}
|
|
202
205
|
/** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
|
|
@@ -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
|
/**
|