@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +10 -6
  3. package/dist/cli.js +147 -20
  4. package/dist/compiler/analyze.d.ts +4 -0
  5. package/dist/compiler/analyze.js +51 -9
  6. package/dist/compiler/emit/base.js +1 -1
  7. package/dist/compiler/emit/calls.js +7 -0
  8. package/dist/compiler/emit/cpi.js +188 -9
  9. package/dist/compiler/emit/expressions.d.ts +11 -0
  10. package/dist/compiler/emit/expressions.js +107 -2
  11. package/dist/compiler/emit/function.js +11 -1
  12. package/dist/compiler/emit/shared.js +3 -2
  13. package/dist/compiler/emit/statements.d.ts +13 -0
  14. package/dist/compiler/emit/statements.js +131 -5
  15. package/dist/compiler/emit/storage.d.ts +2 -1
  16. package/dist/compiler/emit/storage.js +24 -7
  17. package/dist/compiler/idl.js +28 -14
  18. package/dist/compiler/model.d.ts +16 -3
  19. package/dist/compiler/toolchain.d.ts +2 -0
  20. package/dist/compiler/toolchain.js +27 -1
  21. package/dist/onchain.d.ts +30 -0
  22. package/dist/onchain.js +53 -0
  23. package/dist/portgen.d.ts +87 -0
  24. package/dist/portgen.js +311 -0
  25. package/dist/templates.js +49 -154
  26. package/dist/testing.d.ts +66 -0
  27. package/dist/testing.js +337 -0
  28. package/dist/workspace.js +11 -1
  29. package/docs/architecture.md +3 -0
  30. package/docs/cli.md +16 -2
  31. package/docs/language.md +11 -10
  32. package/docs/mainnet.md +5 -4
  33. package/docs/migrate-from-anchor.md +104 -0
  34. package/docs/test-your-program.md +52 -0
  35. package/docs/testing.md +1 -1
  36. package/docs/troubleshooting.md +7 -0
  37. package/lib/typesol.d.ts +68 -12
  38. package/package.json +19 -1
  39. package/runtime/typesol_rt.h +25 -5
  40. package/ts-plugin/index.js +112 -0
  41. 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 initializeMetadata, not ${method}`);
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
- if (a.length !== 11 && a.length !== 12)
478
- this.fail(node, "createV3(metadata, mint, mintAuthority, payer, updateAuthority, systemProgram, name, symbol, uri, sellerFeeBasisPoints, isMutable, signerSeeds?)");
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
- const [name, symbol, uri] = a.slice(6, 9).map((n) => this.expect(this.expr(n, STRING), STRING, n).c);
487
- const fee = this.expect(this.expr(a[9], { kind: "int", name: "u16" }), { kind: "int", name: "u16" }, a[9]);
488
- const mutable = this.expect(this.expr(a[10], BOOL), BOOL, a[10]);
489
- const seeds = this.signerSeeds(a[11], mintAuth, a[2]);
490
- 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}, ${symbol}, ${uri}, ${fee.c}, ${mutable.c}, ${seeds.name}, ${seeds.count}));`);
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(`&${src.lv}.v.${v.name}.${f.name}`, String(n));
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.fail(node.operatorToken, "** is not supported; multiply, or use mulDiv/isqrt");
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 enums`);
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
- throw new Error("unreachable: variable strings are not stored");
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)`). */