@typesol/cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/README.md +11 -7
- package/dist/cli.js +147 -20
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +11 -1
- package/dist/compiler/analyze.js +307 -37
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +62 -2
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +50 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +706 -25
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +126 -4
- package/dist/compiler/emit/function.js +45 -12
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +22 -4
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +141 -7
- package/dist/compiler/emit/storage.d.ts +6 -1
- package/dist/compiler/emit/storage.js +82 -17
- package/dist/compiler/idl.js +61 -18
- package/dist/compiler/model.d.ts +56 -4
- package/dist/compiler/model.js +8 -2
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +367 -0
- package/dist/templates.js +49 -154
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.js +11 -1
- package/docs/architecture.md +3 -0
- package/docs/cli.md +16 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +19 -14
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +113 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +7 -0
- package/lib/typesol.d.ts +325 -13
- package/package.json +19 -1
- package/runtime/typesol_rt.h +54 -9
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
|
@@ -23,6 +23,17 @@ export declare abstract class ExpressionEmitter extends StorageEmitter {
|
|
|
23
23
|
protected textOfInt(name: IntName, c: string, out: string): string;
|
|
24
24
|
/** `c ? a : b` on values: only the chosen branch runs (its checks and side effects included). */
|
|
25
25
|
protected conditional(node: ts.ConditionalExpression, hint?: ValueType): Value;
|
|
26
|
+
/**
|
|
27
|
+
* `a ?? b` on an Option: a's value when it has one, else b, which runs only then (as in JS). The result is an
|
|
28
|
+
* Option itself when b can be null.
|
|
29
|
+
*/
|
|
30
|
+
protected nullish(node: ts.BinaryExpression, hint?: ValueType): Value;
|
|
31
|
+
/**
|
|
32
|
+
* `a ** b` on integers, every multiplication checked (5100 on overflow, like `*`). A literal exponent unrolls by
|
|
33
|
+
* squaring at compile time; otherwise a square-and-multiply loop runs, squaring only while bits remain (a square
|
|
34
|
+
* that overflows then would make the result overflow too). A negative exponent aborts, as JS throws a RangeError.
|
|
35
|
+
*/
|
|
36
|
+
protected power(node: ts.BinaryExpression, hint?: ValueType): Value;
|
|
26
37
|
protected binary(node: ts.BinaryExpression, hint?: ValueType): Value;
|
|
27
38
|
/** Checked integer arithmetic into a fresh temporary. */
|
|
28
39
|
protected arith(op: ts.BinaryOperator, left: Value, right: Value, node: ts.Node): Value;
|
|
@@ -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,27 @@ 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
|
+
}
|
|
80
|
+
// `x!` on an Option: its value, aborting with 5108 when it is empty (JS would carry the null on).
|
|
81
|
+
if (ts.isNonNullExpression(node)) {
|
|
82
|
+
const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
|
|
83
|
+
if (v.type.kind !== "option")
|
|
84
|
+
this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
|
|
85
|
+
return this.unwrap(v, node);
|
|
86
|
+
}
|
|
65
87
|
this.noOptionalChain(node);
|
|
66
88
|
if (ts.isBigIntLiteral(node)) {
|
|
67
89
|
const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
|
|
@@ -133,6 +155,8 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
133
155
|
}
|
|
134
156
|
if (this.accounts.has(node.text))
|
|
135
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`);
|
|
136
160
|
this.fail(node, `unknown name "${node.text}"`);
|
|
137
161
|
}
|
|
138
162
|
if (ts.isPropertyAccessExpression(node))
|
|
@@ -195,7 +219,7 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
195
219
|
if (arr?.type.kind === "array")
|
|
196
220
|
return { c: `${arr.type.len}ULL`, type: U64 };
|
|
197
221
|
if (arr?.type.kind === "vec")
|
|
198
|
-
return { c:
|
|
222
|
+
return { c: vecCount(arr.vec), type: U64 };
|
|
199
223
|
if (ts.isIdentifier(node.expression)) {
|
|
200
224
|
const l = this.lookup(node.expression.text);
|
|
201
225
|
if (l?.type.kind === "vecArg")
|
|
@@ -406,9 +430,10 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
406
430
|
(ts.isBigIntLiteral(node.whenFalse) || ts.isPrefixUnaryExpression(node.whenFalse));
|
|
407
431
|
if (!type && !bothLiteral && (ts.isBigIntLiteral(node.whenTrue) || node.whenTrue.kind === ts.SyntaxKind.NullKeyword)) {
|
|
408
432
|
// Type the non-literal branch first, without emitting it, to give the literal its type.
|
|
409
|
-
const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
433
|
+
const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
410
434
|
const probe = this.reporter.guard(() => this.expr(node.whenFalse));
|
|
411
435
|
this.lines.length = saved.lines;
|
|
436
|
+
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
412
437
|
this.temp = saved.temp;
|
|
413
438
|
this.reporter.diagnostics.length = saved.diagnostics;
|
|
414
439
|
if (probe && probe.literal === undefined && !probe.isNull)
|
|
@@ -434,11 +459,108 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
434
459
|
this.lines[declAt] = this.lines[declAt].replace(`/* ${t} */`, `${vt.kind === "pubkey" ? "SolPubkey" : valueC(vt)} ${t};`);
|
|
435
460
|
return vt.kind === "pubkey" ? { c: `(&${t})`, type: vt } : { c: t, type: vt };
|
|
436
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* `a ?? b` on an Option: a's value when it has one, else b, which runs only then (as in JS). The result is an
|
|
464
|
+
* Option itself when b can be null.
|
|
465
|
+
*/
|
|
466
|
+
nullish(node, hint) {
|
|
467
|
+
const left = this.expr(node.left, hint?.kind === "option" ? hint : hint ? { kind: "option", inner: hint } : undefined);
|
|
468
|
+
if (left.isNull)
|
|
469
|
+
return this.expect(this.expr(node.right, hint), hint, node.right);
|
|
470
|
+
if (left.type.kind !== "option")
|
|
471
|
+
return left; // never null: JS never evaluates b
|
|
472
|
+
const inner = left.type.inner;
|
|
473
|
+
if (inner.kind === "struct")
|
|
474
|
+
this.fail(node, "?? works on Options of integers, booleans and Pubkeys");
|
|
475
|
+
const nullable = (t) => !!(t.flags & ts.TypeFlags.Null) || (t.isUnion() && t.types.some(nullable));
|
|
476
|
+
const type = hint ?? (nullable(this.checker.getTypeAtLocation(node)) ? left.type : inner);
|
|
477
|
+
const lo = this.tmp("opt");
|
|
478
|
+
this.line(`${optC(inner)} ${lo} = ${left.c};`);
|
|
479
|
+
const t = this.tmp("nn");
|
|
480
|
+
this.line(`${type.kind === "pubkey" ? "SolPubkey" : valueC(type)} ${t};`);
|
|
481
|
+
const set = (v) => this.line(v.type.kind === "pubkey" ? `${t} = *(${v.c});` : `${t} = ${v.c};`);
|
|
482
|
+
this.line(`if (${lo}.some) {`);
|
|
483
|
+
this.block(() => {
|
|
484
|
+
const some = inner.kind === "pubkey"
|
|
485
|
+
? { c: `(&${lo}.v)`, type: inner }
|
|
486
|
+
: inner.kind === "bool"
|
|
487
|
+
? { c: `(${lo}.v != 0)`, type: inner }
|
|
488
|
+
: { c: `${lo}.v`, type: inner };
|
|
489
|
+
set(this.expect(some, type, node.left));
|
|
490
|
+
});
|
|
491
|
+
this.line(`} else {`);
|
|
492
|
+
this.block(() => set(this.expect(this.expr(node.right, type), type, node.right)));
|
|
493
|
+
this.line(`}`);
|
|
494
|
+
return type.kind === "pubkey" ? { c: `(&${t})`, type } : { c: t, type };
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* `a ** b` on integers, every multiplication checked (5100 on overflow, like `*`). A literal exponent unrolls by
|
|
498
|
+
* squaring at compile time; otherwise a square-and-multiply loop runs, squaring only while bits remain (a square
|
|
499
|
+
* that overflows then would make the result overflow too). A negative exponent aborts, as JS throws a RangeError.
|
|
500
|
+
*/
|
|
501
|
+
power(node, hint) {
|
|
502
|
+
let base = this.expr(node.left, hint?.kind === "int" ? hint : undefined);
|
|
503
|
+
const mark = this.lines.length;
|
|
504
|
+
const exp = this.expr(node.right);
|
|
505
|
+
// JS reads the base first: if the exponent ran statements (a helper that changes it), use the value read before.
|
|
506
|
+
base = this.settle(base, mark);
|
|
507
|
+
if (base.literal !== undefined)
|
|
508
|
+
base = this.literal(base.literal, hint?.kind === "int" ? hint : U64, node.left);
|
|
509
|
+
if (base.type.kind !== "int")
|
|
510
|
+
this.fail(node.left, "** needs an integer base");
|
|
511
|
+
const type = base.type;
|
|
512
|
+
const ct = INT_INFO[type.name].c;
|
|
513
|
+
const K = ts.SyntaxKind;
|
|
514
|
+
if (exp.literal !== undefined) {
|
|
515
|
+
if (exp.literal < 0n)
|
|
516
|
+
this.fail(node.right, "the exponent of ** cannot be negative");
|
|
517
|
+
if (exp.literal > 255n)
|
|
518
|
+
this.fail(node.right, "the exponent of ** must be at most 255 when it is a literal (larger ones always overflow unless the base is 0 or 1)");
|
|
519
|
+
const n = exp.literal;
|
|
520
|
+
if (n === 0n)
|
|
521
|
+
return this.literal(1n, type, node);
|
|
522
|
+
// Bits from high to low: square, then multiply by the base when the bit is set.
|
|
523
|
+
const b = this.tmp("pb");
|
|
524
|
+
this.line(`${ct} ${b} = ${base.c};`);
|
|
525
|
+
let acc = { c: b, type };
|
|
526
|
+
const bits = n.toString(2);
|
|
527
|
+
for (const bit of bits.slice(1)) {
|
|
528
|
+
acc = this.arith(K.AsteriskToken, acc, acc, node);
|
|
529
|
+
if (bit === "1")
|
|
530
|
+
acc = this.arith(K.AsteriskToken, acc, { c: b, type }, node);
|
|
531
|
+
}
|
|
532
|
+
return acc;
|
|
533
|
+
}
|
|
534
|
+
if (exp.type.kind !== "int")
|
|
535
|
+
this.fail(node.right, "the exponent of ** must be an integer");
|
|
536
|
+
const r = this.tmp("pr");
|
|
537
|
+
const sq = this.tmp("ps");
|
|
538
|
+
const e = this.tmp("pe");
|
|
539
|
+
this.line(`${ct} ${r} = 1;`);
|
|
540
|
+
this.line(`${ct} ${sq} = ${base.c};`);
|
|
541
|
+
if (INT_INFO[exp.type.name].signed)
|
|
542
|
+
this.line(`if (${exp.c} < 0) TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: negative exponent");`);
|
|
543
|
+
this.line(`${INT_INFO[exp.type.name].bytes === 16 ? "unsigned __int128" : "uint64_t"} ${e} = (${INT_INFO[exp.type.name].bytes === 16 ? "unsigned __int128" : "uint64_t"})(${exp.c});`);
|
|
544
|
+
this.line(`while (${e}) {`);
|
|
545
|
+
this.block(() => {
|
|
546
|
+
this.line(`if (${e} & 1) {`);
|
|
547
|
+
this.block(() => this.line(`${r} = ${this.arith(K.AsteriskToken, { c: r, type }, { c: sq, type }, node).c};`));
|
|
548
|
+
this.line(`}`);
|
|
549
|
+
this.line(`${e} >>= 1;`);
|
|
550
|
+
this.line(`if (${e}) {`);
|
|
551
|
+
this.block(() => this.line(`${sq} = ${this.arith(K.AsteriskToken, { c: sq, type }, { c: sq, type }, node).c};`));
|
|
552
|
+
this.line(`}`);
|
|
553
|
+
});
|
|
554
|
+
this.line(`}`);
|
|
555
|
+
return { c: r, type };
|
|
556
|
+
}
|
|
437
557
|
binary(node, hint) {
|
|
438
558
|
const op = node.operatorToken.kind;
|
|
439
559
|
const K = ts.SyntaxKind;
|
|
440
560
|
if (op === K.AsteriskAsteriskToken)
|
|
441
|
-
this.
|
|
561
|
+
return this.power(node, hint);
|
|
562
|
+
if (op === K.QuestionQuestionToken)
|
|
563
|
+
return this.nullish(node, hint);
|
|
442
564
|
if (op === K.AmpersandAmpersandToken || op === K.BarBarToken) {
|
|
443
565
|
const left = this.expect(this.expr(node.left, BOOL), BOOL, node.left);
|
|
444
566
|
const t = this.tmp();
|
|
@@ -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};`);
|
|
@@ -224,8 +235,18 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
224
235
|
e.variants.forEach((variant, i) => {
|
|
225
236
|
head.push(` case ${i}: {`);
|
|
226
237
|
for (const f of variant.fields) {
|
|
238
|
+
if (f.type.kind === "string") {
|
|
239
|
+
// Borsh String: u32 length, then UTF-8 bytes, kept as a view of the instruction data.
|
|
240
|
+
const lv = `${v}.v.${variant.name}.${f.name}`;
|
|
241
|
+
head.push(` if (params->data_len < off + 4) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
242
|
+
head.push(` { uint32_t n = (uint32_t)ts_read_le(params->data + off, 4); off += 4;`);
|
|
243
|
+
head.push(` if (params->data_len < off + n) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
244
|
+
head.push(` ${lv} = ts_str(params->data + off, n); off += n; }`);
|
|
245
|
+
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
|
+
continue;
|
|
247
|
+
}
|
|
227
248
|
if (!isScalar(f.type))
|
|
228
|
-
this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys and
|
|
249
|
+
this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys, enums and strings`);
|
|
229
250
|
const size = scalarSize(f.type);
|
|
230
251
|
head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
231
252
|
head.push(` ts_memcpy(&${v}.v.${variant.name}.${f.name}, params->data + off, ${size});`, ` off += ${size};`);
|
|
@@ -275,13 +296,25 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
275
296
|
epilogue() {
|
|
276
297
|
if (!this.ix)
|
|
277
298
|
return [];
|
|
278
|
-
|
|
279
|
-
|
|
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")
|
|
280
303
|
.map((a) => {
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
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));`;
|
|
284
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
|
+
];
|
|
285
318
|
}
|
|
286
319
|
constraintChecks(ix) {
|
|
287
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,11 +119,14 @@ 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}[]`)}`;
|
|
121
127
|
case "string":
|
|
122
|
-
|
|
128
|
+
// Only in a variable data enum's payload (arguments, events, locals): a view of the text.
|
|
129
|
+
return `TS_Str ${name}`;
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
/** Why an account of kind `have` cannot be passed where a helper takes `want`, or undefined when it can. */
|
|
@@ -174,8 +181,11 @@ export function accountMismatch(have, want) {
|
|
|
174
181
|
export function helperSignature(h) {
|
|
175
182
|
const params = ["TsParams *params"];
|
|
176
183
|
for (const p of h.params) {
|
|
177
|
-
if (p.account)
|
|
184
|
+
if (p.account) {
|
|
178
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
|
+
}
|
|
179
189
|
else if (p.type.kind === "signerSeeds")
|
|
180
190
|
params.push(`const SolSignerSeed *p_${p.name}`, `int p_${p.name}__n`);
|
|
181
191
|
else if (p.type.kind === "pubkey")
|
|
@@ -186,6 +196,14 @@ export function helperSignature(h) {
|
|
|
186
196
|
params.push(`${valueC(p.type)} p_${p.name}`);
|
|
187
197
|
}
|
|
188
198
|
if (h.returns)
|
|
189
|
-
params.push(`${scalarC(h.returns)} *ret`);
|
|
199
|
+
params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : `${scalarC(h.returns)} *ret`);
|
|
190
200
|
return `static uint64_t fn_${h.name}(${params.join(", ")})`;
|
|
191
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
|
+
}
|
|
@@ -10,6 +10,14 @@ export declare abstract class StatementEmitter extends CpiEmitter {
|
|
|
10
10
|
/** `switch` on integers, enums and `.kind` of data enums: a C switch, with JS fallthrough/break semantics. */
|
|
11
11
|
protected switchStatement(stmt: ts.SwitchStatement): void;
|
|
12
12
|
protected forLoop(stmt: ts.ForStatement): void;
|
|
13
|
+
/**
|
|
14
|
+
* `for (const x of list)` over a Vec or FixedArray: an index loop that binds `x` as `const x = list.get(i)` would,
|
|
15
|
+
* so elements are values (scalars, strings) or references into the data (structs), with the same aliasing rules.
|
|
16
|
+
* As in JS, the length is read again before every step, so a body that pushes or pops sees the new length.
|
|
17
|
+
*/
|
|
18
|
+
protected forOf(stmt: ts.ForOfStatement): void;
|
|
19
|
+
/** Whether `e` is the remaining accounts (or a const bound to them). */
|
|
20
|
+
protected isRemaining(e: ts.Expression): boolean;
|
|
13
21
|
/**
|
|
14
22
|
* `while (1) { cond; if (!cond) break; body; continue_label: step }`. The condition is lowered
|
|
15
23
|
* inside the loop so its checked-arithmetic temporaries are recomputed every iteration.
|
|
@@ -17,6 +25,11 @@ export declare abstract class StatementEmitter extends CpiEmitter {
|
|
|
17
25
|
protected loop(cond: ts.Expression | undefined, body: ts.Statement, step: ts.Expression | undefined): void;
|
|
18
26
|
protected body(stmt: ts.Statement): void;
|
|
19
27
|
protected variable(list: ts.VariableDeclarationList, topLevel?: boolean): void;
|
|
28
|
+
/**
|
|
29
|
+
* `const { a, b: c } = s`: one declaration per name, `const a = s.a`, in source order. The source must be a name or
|
|
30
|
+
* a field path (no calls), so reading it once per name reads the same thing JS reads once.
|
|
31
|
+
*/
|
|
32
|
+
protected destructure(list: ts.VariableDeclarationList, d: ts.VariableDeclaration, pattern: ts.ObjectBindingPattern, init: ts.Expression, topLevel: boolean): void;
|
|
20
33
|
protected expressionStatement(e: ts.Expression, topLevel: boolean): void;
|
|
21
34
|
protected assign(target: ts.Expression, valueNode: ts.Expression, op?: ts.BinaryOperator, topLevel?: boolean): void;
|
|
22
35
|
/** Whether a place goes through a Vec element (`v.get(i).x`, `v.get(i)`). */
|