@typesol/cli 0.15.0 → 0.16.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 +61 -0
- package/README.md +10 -6
- package/dist/cli.js +147 -20
- package/dist/compiler/analyze.d.ts +4 -0
- package/dist/compiler/analyze.js +51 -9
- package/dist/compiler/emit/base.js +1 -1
- package/dist/compiler/emit/calls.js +7 -0
- package/dist/compiler/emit/cpi.js +188 -9
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +107 -2
- package/dist/compiler/emit/function.js +11 -1
- package/dist/compiler/emit/shared.js +3 -2
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +131 -5
- package/dist/compiler/emit/storage.d.ts +2 -1
- package/dist/compiler/emit/storage.js +24 -7
- package/dist/compiler/idl.js +28 -14
- package/dist/compiler/model.d.ts +16 -3
- 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 +311 -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/language.md +11 -10
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +104 -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 +68 -12
- package/package.json +19 -1
- package/runtime/typesol_rt.h +25 -5
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
|
@@ -371,7 +371,98 @@ export class CpiEmitter extends CallEmitter {
|
|
|
371
371
|
};
|
|
372
372
|
const value = (n, t) => (put) => this.serialize(this.expect(this.expr(n, t), t, n), n, put);
|
|
373
373
|
const optKey = { kind: "option", inner: PUBKEY };
|
|
374
|
+
const U16 = { kind: "int", name: "u16" };
|
|
375
|
+
const I16 = { kind: "int", name: "i16" };
|
|
376
|
+
const byte = (k) => (put) => {
|
|
377
|
+
const t = this.tmp("b");
|
|
378
|
+
this.line(`uint8_t ${t} = ${k};`);
|
|
379
|
+
put(`&${t}`, "1", 1);
|
|
380
|
+
};
|
|
381
|
+
/** Token-2022's OptionalNonZeroPubkey: 32 bytes, all zero for none. */
|
|
382
|
+
const nonZeroKey = (n) => (put) => {
|
|
383
|
+
this.model.options.set("pubkey", PUBKEY);
|
|
384
|
+
const v = this.expect(this.expr(n, optKey), optKey, n);
|
|
385
|
+
const t = this.tmp("nzk");
|
|
386
|
+
this.line(`SolPubkey ${t};`);
|
|
387
|
+
this.line(`{ ${optC(PUBKEY)} o = ${v.c}; if (o.some) ${t} = o.v; else ts_memzero(&${t}, 32); }`);
|
|
388
|
+
put(`&${t}`, "32", 32);
|
|
389
|
+
};
|
|
390
|
+
const token2022Only = () => {
|
|
391
|
+
if (program.kind.kind === "tokenProgram" && program.kind.flavor === "token")
|
|
392
|
+
this.fail(node, `${method} is a Token-2022 extension; use Token2022Program or TokenInterface`);
|
|
393
|
+
};
|
|
394
|
+
const mintOnly = (n, usage) => {
|
|
395
|
+
token2022Only();
|
|
396
|
+
if (a.length !== n)
|
|
397
|
+
this.fail(node, usage);
|
|
398
|
+
return anyAcct(a[0], "the mint", true);
|
|
399
|
+
};
|
|
400
|
+
const none = { name: "NULL", count: 0 };
|
|
374
401
|
switch (method) {
|
|
402
|
+
case "initializeTransferFeeConfig": {
|
|
403
|
+
const mint = mintOnly(5, "initializeTransferFeeConfig(mint, configAuthority | null, withdrawAuthority | null, basisPoints, maximumFee)");
|
|
404
|
+
this.model.options.set("pubkey", PUBKEY);
|
|
405
|
+
// Arguments in source order, as JS evaluates them: the basis points are checked when they are read.
|
|
406
|
+
const bps = (put) => {
|
|
407
|
+
const v = this.expect(this.expr(a[3], U16), U16, a[3]);
|
|
408
|
+
this.line(`if (${v.c} > 10000) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: transfer fee above 10000 basis points");`);
|
|
409
|
+
this.serialize(v, a[3], put);
|
|
410
|
+
};
|
|
411
|
+
simple(26, [byte(0), value(a[1], optKey), value(a[2], optKey), bps, value(a[4], U64)], [[mint, true, false]], none);
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
case "initializeMintCloseAuthority": {
|
|
415
|
+
const mint = mintOnly(2, "initializeMintCloseAuthority(mint, closeAuthority | null)");
|
|
416
|
+
this.model.options.set("pubkey", PUBKEY);
|
|
417
|
+
simple(25, [value(a[1], optKey)], [[mint, true, false]], none);
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
case "initializeNonTransferableMint": {
|
|
421
|
+
const mint = mintOnly(1, "initializeNonTransferableMint(mint)");
|
|
422
|
+
simple(32, [], [[mint, true, false]], none);
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
case "initializePermanentDelegate": {
|
|
426
|
+
const mint = mintOnly(2, "initializePermanentDelegate(mint, delegate)");
|
|
427
|
+
simple(35, [value(a[1], PUBKEY)], [[mint, true, false]], none);
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
case "initializeDefaultAccountState": {
|
|
431
|
+
const mint = mintOnly(2, "initializeDefaultAccountState(mint, frozen)");
|
|
432
|
+
const frozen = this.expect(this.expr(a[1], BOOL), BOOL, a[1]);
|
|
433
|
+
const st = this.tmp("st");
|
|
434
|
+
this.line(`uint8_t ${st} = (${frozen.c}) ? 2 : 1;`); // AccountState::Frozen / Initialized
|
|
435
|
+
simple(28, [byte(0), (put) => put(`&${st}`, "1", 1)], [[mint, true, false]], none);
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
case "initializeMetadataPointer":
|
|
439
|
+
case "initializeGroupPointer":
|
|
440
|
+
case "initializeGroupMemberPointer":
|
|
441
|
+
case "initializeTransferHook": {
|
|
442
|
+
const tag = { initializeMetadataPointer: 39, initializeGroupPointer: 40, initializeGroupMemberPointer: 41, initializeTransferHook: 36 }[method];
|
|
443
|
+
const second = {
|
|
444
|
+
initializeMetadataPointer: "metadataAddress",
|
|
445
|
+
initializeGroupPointer: "groupAddress",
|
|
446
|
+
initializeGroupMemberPointer: "memberAddress",
|
|
447
|
+
initializeTransferHook: "programId",
|
|
448
|
+
}[method];
|
|
449
|
+
const mint = mintOnly(3, `${method}(mint, authority | null, ${second} | null)`);
|
|
450
|
+
simple(tag, [byte(0), nonZeroKey(a[1]), nonZeroKey(a[2])], [[mint, true, false]], none);
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
case "initializeInterestBearingMint": {
|
|
454
|
+
const mint = mintOnly(3, "initializeInterestBearingMint(mint, rateAuthority | null, rate)");
|
|
455
|
+
simple(33, [byte(0), nonZeroKey(a[1]), value(a[2], I16)], [[mint, true, false]], none);
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
case "initializeImmutableOwner": {
|
|
459
|
+
token2022Only();
|
|
460
|
+
if (a.length !== 1)
|
|
461
|
+
this.fail(node, "initializeImmutableOwner(account)");
|
|
462
|
+
const acct = anyAcct(a[0], "the token account", true);
|
|
463
|
+
simple(22, [], [[acct, true, false]], none);
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
375
466
|
case "approve": {
|
|
376
467
|
arity(4);
|
|
377
468
|
const src = want(a[0], "token", true, "the source");
|
|
@@ -448,7 +539,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
448
539
|
return undefined;
|
|
449
540
|
}
|
|
450
541
|
}
|
|
451
|
-
this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3 and
|
|
542
|
+
this.fail(node.expression, `token programs support transfer, transferChecked, mintTo, burn, closeAccount, approve, revoke, setAuthority, freezeAccount, thawAccount, syncNative, initializeMint2, initializeAccount3, initializeMetadata and the Token-2022 extension initializers (initializeTransferFeeConfig, initializeMintCloseAuthority, initializeNonTransferableMint, initializePermanentDelegate, initializeDefaultAccountState, initializeMetadataPointer, initializeGroupPointer, initializeGroupMemberPointer, initializeTransferHook, initializeInterestBearingMint, initializeImmutableOwner), not ${method}`);
|
|
452
543
|
}
|
|
453
544
|
ataOp(node, program, method) {
|
|
454
545
|
if (method !== "create" && method !== "createIdempotent")
|
|
@@ -474,8 +565,12 @@ export class CpiEmitter extends CallEmitter {
|
|
|
474
565
|
if (method !== "createV3")
|
|
475
566
|
this.fail(node.expression, "the Token Metadata program supports createV3");
|
|
476
567
|
const a = node.arguments;
|
|
477
|
-
|
|
478
|
-
|
|
568
|
+
const usage = "createV3(metadata, mint, mintAuthority, payer, updateAuthority, systemProgram, name, symbol, uri, sellerFeeBasisPoints, isMutable, { creators?, collection? }?, signerSeeds?)";
|
|
569
|
+
// An object literal after isMutable carries the optional creators / collection; signerSeeds stays last.
|
|
570
|
+
const extras = a[11] && ts.isObjectLiteralExpression(a[11]) ? a[11] : undefined;
|
|
571
|
+
const seedsArg = extras ? a[12] : a[11];
|
|
572
|
+
if (a.length < 11 || a.length > (extras ? 13 : 12))
|
|
573
|
+
this.fail(node, usage);
|
|
479
574
|
const [metadata, mint, mintAuth, payer, updateAuth, sys] = a.slice(0, 6).map((n, i) => this.accountParam(n, `account #${i + 1}`));
|
|
480
575
|
if (!isWritable(metadata.kind))
|
|
481
576
|
this.fail(a[0], "the metadata account must be Mut<AccountInfo>");
|
|
@@ -483,11 +578,82 @@ export class CpiEmitter extends CallEmitter {
|
|
|
483
578
|
this.fail(a[3], "the payer must be a Mut<Signer>");
|
|
484
579
|
if (sys.kind.kind !== "system")
|
|
485
580
|
this.fail(a[5], "pass the SystemProgram parameter");
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
const
|
|
490
|
-
|
|
581
|
+
// Arguments in source order, as JS evaluates them: a later one that runs statements (a helper changing account
|
|
582
|
+
// text) must not change what an earlier one read.
|
|
583
|
+
const vals = [];
|
|
584
|
+
const inOrder = (n, t) => {
|
|
585
|
+
const mark = this.lines.length;
|
|
586
|
+
const v = this.expect(this.expr(n, t), t, n);
|
|
587
|
+
for (let j = 0; j < vals.length; j++)
|
|
588
|
+
vals[j] = this.settle(vals[j], mark);
|
|
589
|
+
vals.push(v);
|
|
590
|
+
return vals.length - 1;
|
|
591
|
+
};
|
|
592
|
+
const slots = [...a.slice(6, 9).map((n) => inOrder(n, STRING)), inOrder(a[9], { kind: "int", name: "u16" }), inOrder(a[10], BOOL)];
|
|
593
|
+
const settleAll = (mark) => {
|
|
594
|
+
for (let j = 0; j < vals.length; j++)
|
|
595
|
+
vals[j] = this.settle(vals[j], mark);
|
|
596
|
+
};
|
|
597
|
+
const extrasMark = this.lines.length;
|
|
598
|
+
let creators = "NULL";
|
|
599
|
+
let ncreators = "-1";
|
|
600
|
+
let collection = "NULL";
|
|
601
|
+
if (extras) {
|
|
602
|
+
for (const p of extras.properties) {
|
|
603
|
+
if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))
|
|
604
|
+
this.fail(p, "use creators: [...] and collection: key");
|
|
605
|
+
if (p.name.text === "creators") {
|
|
606
|
+
if (!ts.isArrayLiteralExpression(p.initializer))
|
|
607
|
+
this.fail(p.initializer, "creators: [{ address, verified, share }, …] (an array literal)");
|
|
608
|
+
const list = p.initializer.elements;
|
|
609
|
+
if (list.length < 1 || list.length > 5)
|
|
610
|
+
this.fail(p.initializer, "Metaplex takes 1 to 5 creators");
|
|
611
|
+
const buf = this.tmp("creators");
|
|
612
|
+
this.line(`uint8_t ${buf}[${34 * list.length}];`);
|
|
613
|
+
let shares = 0n;
|
|
614
|
+
let constant = true;
|
|
615
|
+
list.forEach((el, i) => {
|
|
616
|
+
if (!ts.isObjectLiteralExpression(el))
|
|
617
|
+
this.fail(el, "a creator is { address, verified, share }");
|
|
618
|
+
const field = (k) => {
|
|
619
|
+
const q = el.properties.find((x) => ts.isPropertyAssignment(x) && ts.isIdentifier(x.name) && x.name.text === k);
|
|
620
|
+
const sh = el.properties.find((x) => ts.isShorthandPropertyAssignment(x) && x.name.text === k);
|
|
621
|
+
if (!q && !sh)
|
|
622
|
+
this.fail(el, `a creator needs ${k}`);
|
|
623
|
+
return q ? q.initializer : sh.name;
|
|
624
|
+
};
|
|
625
|
+
const at = 34 * i;
|
|
626
|
+
const key = this.expect(this.expr(field("address"), PUBKEY), PUBKEY, el);
|
|
627
|
+
this.line(`ts_memcpy(${buf} + ${at}, ${key.c}, 32);`);
|
|
628
|
+
const verified = this.expect(this.expr(field("verified"), BOOL), BOOL, el);
|
|
629
|
+
this.line(`${buf}[${at + 32}] = (${verified.c}) ? 1 : 0;`);
|
|
630
|
+
const shareNode = field("share");
|
|
631
|
+
if (ts.isBigIntLiteral(shareNode))
|
|
632
|
+
shares += BigInt(shareNode.text.slice(0, -1));
|
|
633
|
+
else
|
|
634
|
+
constant = false;
|
|
635
|
+
const share = this.expect(this.expr(shareNode, U8), U8, el);
|
|
636
|
+
this.line(`${buf}[${at + 33}] = (uint8_t)(${share.c});`);
|
|
637
|
+
});
|
|
638
|
+
if (constant && shares !== 100n)
|
|
639
|
+
this.fail(p.initializer, `creator shares add up to ${shares}; Metaplex requires 100`);
|
|
640
|
+
creators = buf;
|
|
641
|
+
ncreators = String(list.length);
|
|
642
|
+
}
|
|
643
|
+
else if (p.name.text === "collection") {
|
|
644
|
+
const key = this.expect(this.expr(p.initializer, PUBKEY), PUBKEY, p.initializer);
|
|
645
|
+
const t = this.tmp("coll");
|
|
646
|
+
this.line(`SolPubkey ${t} = *(${key.c});`);
|
|
647
|
+
collection = `&${t}`;
|
|
648
|
+
}
|
|
649
|
+
else
|
|
650
|
+
this.fail(p.name, `createV3 extras are creators and collection, not ${p.name.text}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const seeds = this.signerSeeds(seedsArg, mintAuth, a[2]);
|
|
654
|
+
settleAll(extrasMark);
|
|
655
|
+
const [name, symbol, uri, fee, mutable] = slots.map((i) => vals[i]);
|
|
656
|
+
this.line(`TS_TRY(ts_mpl_create_v3(params, a_${program.name}, a_${metadata.name}, a_${mint.name}, a_${mintAuth.name}, a_${payer.name}, a_${updateAuth.name}, a_${sys.name}, ${name.c}, ${symbol.c}, ${uri.c}, ${fee.c}, ${mutable.c}, ${creators}, ${ncreators}, ${collection}, ${seeds.name}, ${seeds.count}));`);
|
|
491
657
|
return undefined;
|
|
492
658
|
}
|
|
493
659
|
/**
|
|
@@ -914,9 +1080,22 @@ export class CpiEmitter extends CallEmitter {
|
|
|
914
1080
|
e.variants.forEach((v, i) => {
|
|
915
1081
|
this.line(`case ${i}: {`);
|
|
916
1082
|
for (const f of v.fields) {
|
|
1083
|
+
const at = `${src.lv}.v.${v.name}.${f.name}`;
|
|
1084
|
+
if (f.type.kind === "string") {
|
|
1085
|
+
// Borsh String: u32 length, then the bytes.
|
|
1086
|
+
const n = this.tmp("sl");
|
|
1087
|
+
this.line(`uint32_t ${n} = (uint32_t)${at}.len;`);
|
|
1088
|
+
put(`&${n}`, "4");
|
|
1089
|
+
put(`${at}.ptr`, `${at}.len`);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (f.type.kind === "dataEnum" && this.model.dataEnums.get(f.type.name)?.variable) {
|
|
1093
|
+
this.serializeDataEnumLv(at, f.type.name, put);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
917
1096
|
const n = this.fieldBytes(f.type);
|
|
918
1097
|
// put() with no static size: the payload differs by variant.
|
|
919
|
-
put(`&${
|
|
1098
|
+
put(`&${at}`, String(n));
|
|
920
1099
|
}
|
|
921
1100
|
this.line(`break; }`);
|
|
922
1101
|
});
|
|
@@ -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;
|
|
@@ -62,6 +62,13 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
62
62
|
expr(node, hint) {
|
|
63
63
|
if (ts.isParenthesizedExpression(node))
|
|
64
64
|
return this.expr(node.expression, hint);
|
|
65
|
+
// `x!` on an Option: its value, aborting with 5108 when it is empty (JS would carry the null on).
|
|
66
|
+
if (ts.isNonNullExpression(node)) {
|
|
67
|
+
const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
|
|
68
|
+
if (v.type.kind !== "option")
|
|
69
|
+
this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
|
|
70
|
+
return this.unwrap(v, node);
|
|
71
|
+
}
|
|
65
72
|
this.noOptionalChain(node);
|
|
66
73
|
if (ts.isBigIntLiteral(node)) {
|
|
67
74
|
const value = BigInt(node.text.slice(0, -1).replace(/_/g, ""));
|
|
@@ -406,9 +413,10 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
406
413
|
(ts.isBigIntLiteral(node.whenFalse) || ts.isPrefixUnaryExpression(node.whenFalse));
|
|
407
414
|
if (!type && !bothLiteral && (ts.isBigIntLiteral(node.whenTrue) || node.whenTrue.kind === ts.SyntaxKind.NullKeyword)) {
|
|
408
415
|
// 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 };
|
|
416
|
+
const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
410
417
|
const probe = this.reporter.guard(() => this.expr(node.whenFalse));
|
|
411
418
|
this.lines.length = saved.lines;
|
|
419
|
+
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
412
420
|
this.temp = saved.temp;
|
|
413
421
|
this.reporter.diagnostics.length = saved.diagnostics;
|
|
414
422
|
if (probe && probe.literal === undefined && !probe.isNull)
|
|
@@ -434,11 +442,108 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
434
442
|
this.lines[declAt] = this.lines[declAt].replace(`/* ${t} */`, `${vt.kind === "pubkey" ? "SolPubkey" : valueC(vt)} ${t};`);
|
|
435
443
|
return vt.kind === "pubkey" ? { c: `(&${t})`, type: vt } : { c: t, type: vt };
|
|
436
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* `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
|
|
447
|
+
* Option itself when b can be null.
|
|
448
|
+
*/
|
|
449
|
+
nullish(node, hint) {
|
|
450
|
+
const left = this.expr(node.left, hint?.kind === "option" ? hint : hint ? { kind: "option", inner: hint } : undefined);
|
|
451
|
+
if (left.isNull)
|
|
452
|
+
return this.expect(this.expr(node.right, hint), hint, node.right);
|
|
453
|
+
if (left.type.kind !== "option")
|
|
454
|
+
return left; // never null: JS never evaluates b
|
|
455
|
+
const inner = left.type.inner;
|
|
456
|
+
if (inner.kind === "struct")
|
|
457
|
+
this.fail(node, "?? works on Options of integers, booleans and Pubkeys");
|
|
458
|
+
const nullable = (t) => !!(t.flags & ts.TypeFlags.Null) || (t.isUnion() && t.types.some(nullable));
|
|
459
|
+
const type = hint ?? (nullable(this.checker.getTypeAtLocation(node)) ? left.type : inner);
|
|
460
|
+
const lo = this.tmp("opt");
|
|
461
|
+
this.line(`${optC(inner)} ${lo} = ${left.c};`);
|
|
462
|
+
const t = this.tmp("nn");
|
|
463
|
+
this.line(`${type.kind === "pubkey" ? "SolPubkey" : valueC(type)} ${t};`);
|
|
464
|
+
const set = (v) => this.line(v.type.kind === "pubkey" ? `${t} = *(${v.c});` : `${t} = ${v.c};`);
|
|
465
|
+
this.line(`if (${lo}.some) {`);
|
|
466
|
+
this.block(() => {
|
|
467
|
+
const some = inner.kind === "pubkey"
|
|
468
|
+
? { c: `(&${lo}.v)`, type: inner }
|
|
469
|
+
: inner.kind === "bool"
|
|
470
|
+
? { c: `(${lo}.v != 0)`, type: inner }
|
|
471
|
+
: { c: `${lo}.v`, type: inner };
|
|
472
|
+
set(this.expect(some, type, node.left));
|
|
473
|
+
});
|
|
474
|
+
this.line(`} else {`);
|
|
475
|
+
this.block(() => set(this.expect(this.expr(node.right, type), type, node.right)));
|
|
476
|
+
this.line(`}`);
|
|
477
|
+
return type.kind === "pubkey" ? { c: `(&${t})`, type } : { c: t, type };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* `a ** b` on integers, every multiplication checked (5100 on overflow, like `*`). A literal exponent unrolls by
|
|
481
|
+
* squaring at compile time; otherwise a square-and-multiply loop runs, squaring only while bits remain (a square
|
|
482
|
+
* that overflows then would make the result overflow too). A negative exponent aborts, as JS throws a RangeError.
|
|
483
|
+
*/
|
|
484
|
+
power(node, hint) {
|
|
485
|
+
let base = this.expr(node.left, hint?.kind === "int" ? hint : undefined);
|
|
486
|
+
const mark = this.lines.length;
|
|
487
|
+
const exp = this.expr(node.right);
|
|
488
|
+
// JS reads the base first: if the exponent ran statements (a helper that changes it), use the value read before.
|
|
489
|
+
base = this.settle(base, mark);
|
|
490
|
+
if (base.literal !== undefined)
|
|
491
|
+
base = this.literal(base.literal, hint?.kind === "int" ? hint : U64, node.left);
|
|
492
|
+
if (base.type.kind !== "int")
|
|
493
|
+
this.fail(node.left, "** needs an integer base");
|
|
494
|
+
const type = base.type;
|
|
495
|
+
const ct = INT_INFO[type.name].c;
|
|
496
|
+
const K = ts.SyntaxKind;
|
|
497
|
+
if (exp.literal !== undefined) {
|
|
498
|
+
if (exp.literal < 0n)
|
|
499
|
+
this.fail(node.right, "the exponent of ** cannot be negative");
|
|
500
|
+
if (exp.literal > 255n)
|
|
501
|
+
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)");
|
|
502
|
+
const n = exp.literal;
|
|
503
|
+
if (n === 0n)
|
|
504
|
+
return this.literal(1n, type, node);
|
|
505
|
+
// Bits from high to low: square, then multiply by the base when the bit is set.
|
|
506
|
+
const b = this.tmp("pb");
|
|
507
|
+
this.line(`${ct} ${b} = ${base.c};`);
|
|
508
|
+
let acc = { c: b, type };
|
|
509
|
+
const bits = n.toString(2);
|
|
510
|
+
for (const bit of bits.slice(1)) {
|
|
511
|
+
acc = this.arith(K.AsteriskToken, acc, acc, node);
|
|
512
|
+
if (bit === "1")
|
|
513
|
+
acc = this.arith(K.AsteriskToken, acc, { c: b, type }, node);
|
|
514
|
+
}
|
|
515
|
+
return acc;
|
|
516
|
+
}
|
|
517
|
+
if (exp.type.kind !== "int")
|
|
518
|
+
this.fail(node.right, "the exponent of ** must be an integer");
|
|
519
|
+
const r = this.tmp("pr");
|
|
520
|
+
const sq = this.tmp("ps");
|
|
521
|
+
const e = this.tmp("pe");
|
|
522
|
+
this.line(`${ct} ${r} = 1;`);
|
|
523
|
+
this.line(`${ct} ${sq} = ${base.c};`);
|
|
524
|
+
if (INT_INFO[exp.type.name].signed)
|
|
525
|
+
this.line(`if (${exp.c} < 0) TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: negative exponent");`);
|
|
526
|
+
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});`);
|
|
527
|
+
this.line(`while (${e}) {`);
|
|
528
|
+
this.block(() => {
|
|
529
|
+
this.line(`if (${e} & 1) {`);
|
|
530
|
+
this.block(() => this.line(`${r} = ${this.arith(K.AsteriskToken, { c: r, type }, { c: sq, type }, node).c};`));
|
|
531
|
+
this.line(`}`);
|
|
532
|
+
this.line(`${e} >>= 1;`);
|
|
533
|
+
this.line(`if (${e}) {`);
|
|
534
|
+
this.block(() => this.line(`${sq} = ${this.arith(K.AsteriskToken, { c: sq, type }, { c: sq, type }, node).c};`));
|
|
535
|
+
this.line(`}`);
|
|
536
|
+
});
|
|
537
|
+
this.line(`}`);
|
|
538
|
+
return { c: r, type };
|
|
539
|
+
}
|
|
437
540
|
binary(node, hint) {
|
|
438
541
|
const op = node.operatorToken.kind;
|
|
439
542
|
const K = ts.SyntaxKind;
|
|
440
543
|
if (op === K.AsteriskAsteriskToken)
|
|
441
|
-
this.
|
|
544
|
+
return this.power(node, hint);
|
|
545
|
+
if (op === K.QuestionQuestionToken)
|
|
546
|
+
return this.nullish(node, hint);
|
|
442
547
|
if (op === K.AmpersandAmpersandToken || op === K.BarBarToken) {
|
|
443
548
|
const left = this.expect(this.expr(node.left, BOOL), BOOL, node.left);
|
|
444
549
|
const t = this.tmp();
|
|
@@ -224,8 +224,18 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
224
224
|
e.variants.forEach((variant, i) => {
|
|
225
225
|
head.push(` case ${i}: {`);
|
|
226
226
|
for (const f of variant.fields) {
|
|
227
|
+
if (f.type.kind === "string") {
|
|
228
|
+
// Borsh String: u32 length, then UTF-8 bytes, kept as a view of the instruction data.
|
|
229
|
+
const lv = `${v}.v.${variant.name}.${f.name}`;
|
|
230
|
+
head.push(` if (params->data_len < off + 4) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
231
|
+
head.push(` { uint32_t n = (uint32_t)ts_read_le(params->data + off, 4); off += 4;`);
|
|
232
|
+
head.push(` if (params->data_len < off + n) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
233
|
+
head.push(` ${lv} = ts_str(params->data + off, n); off += n; }`);
|
|
234
|
+
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
|
+
continue;
|
|
236
|
+
}
|
|
227
237
|
if (!isScalar(f.type))
|
|
228
|
-
this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys and
|
|
238
|
+
this.reporter.fail(a.node, `${e.name}.${variant.name}.${f.name}: enum arguments carry integers, booleans, Pubkeys, enums and strings`);
|
|
229
239
|
const size = scalarSize(f.type);
|
|
230
240
|
head.push(` if (params->data_len < off + ${size}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: instruction data too short");`);
|
|
231
241
|
head.push(` ts_memcpy(&${v}.v.${variant.name}.${f.name}, params->data + off, ${size});`, ` off += ${size};`);
|
|
@@ -119,7 +119,8 @@ export function storageDecl(t, name) {
|
|
|
119
119
|
return `uint32_t ${name}__len;
|
|
120
120
|
${storageDecl(t.elem, `${name}[]`)}`;
|
|
121
121
|
case "string":
|
|
122
|
-
|
|
122
|
+
// Only in a variable data enum's payload (arguments, events, locals): a view of the text.
|
|
123
|
+
return `TS_Str ${name}`;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
/** Why an account of kind `have` cannot be passed where a helper takes `want`, or undefined when it can. */
|
|
@@ -186,6 +187,6 @@ export function helperSignature(h) {
|
|
|
186
187
|
params.push(`${valueC(p.type)} p_${p.name}`);
|
|
187
188
|
}
|
|
188
189
|
if (h.returns)
|
|
189
|
-
params.push(`${scalarC(h.returns)} *ret`);
|
|
190
|
+
params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : `${scalarC(h.returns)} *ret`);
|
|
190
191
|
return `static uint64_t fn_${h.name}(${params.join(", ")})`;
|
|
191
192
|
}
|
|
@@ -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)`). */
|