@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +11 -7
  3. package/dist/cli.js +147 -20
  4. package/dist/clientgen.js +37 -0
  5. package/dist/compiler/analyze.d.ts +11 -1
  6. package/dist/compiler/analyze.js +307 -37
  7. package/dist/compiler/emit/base.d.ts +7 -0
  8. package/dist/compiler/emit/base.js +62 -2
  9. package/dist/compiler/emit/borsh.d.ts +13 -0
  10. package/dist/compiler/emit/borsh.js +232 -0
  11. package/dist/compiler/emit/calls.js +50 -15
  12. package/dist/compiler/emit/cpi.d.ts +32 -8
  13. package/dist/compiler/emit/cpi.js +706 -25
  14. package/dist/compiler/emit/expressions.d.ts +11 -0
  15. package/dist/compiler/emit/expressions.js +126 -4
  16. package/dist/compiler/emit/function.js +45 -12
  17. package/dist/compiler/emit/program.js +24 -6
  18. package/dist/compiler/emit/shared.d.ts +12 -0
  19. package/dist/compiler/emit/shared.js +22 -4
  20. package/dist/compiler/emit/statements.d.ts +13 -0
  21. package/dist/compiler/emit/statements.js +141 -7
  22. package/dist/compiler/emit/storage.d.ts +6 -1
  23. package/dist/compiler/emit/storage.js +82 -17
  24. package/dist/compiler/idl.js +61 -18
  25. package/dist/compiler/model.d.ts +56 -4
  26. package/dist/compiler/model.js +8 -2
  27. package/dist/compiler/toolchain.d.ts +2 -0
  28. package/dist/compiler/toolchain.js +27 -1
  29. package/dist/onchain.d.ts +30 -0
  30. package/dist/onchain.js +53 -0
  31. package/dist/portgen.d.ts +87 -0
  32. package/dist/portgen.js +367 -0
  33. package/dist/templates.js +49 -154
  34. package/dist/testing.d.ts +66 -0
  35. package/dist/testing.js +337 -0
  36. package/dist/workspace.js +11 -1
  37. package/docs/architecture.md +3 -0
  38. package/docs/cli.md +16 -2
  39. package/docs/errors.md +3 -2
  40. package/docs/language.md +19 -14
  41. package/docs/mainnet.md +5 -4
  42. package/docs/migrate-from-anchor.md +113 -0
  43. package/docs/test-your-program.md +52 -0
  44. package/docs/testing.md +1 -1
  45. package/docs/troubleshooting.md +7 -0
  46. package/lib/typesol.d.ts +325 -13
  47. package/package.json +19 -1
  48. package/runtime/typesol_rt.h +54 -9
  49. package/ts-plugin/index.js +112 -0
  50. 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: `((uint64_t)${arr.vec.len})`, type: U64 };
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.fail(node.operatorToken, "** is not supported; multiply, or use mulDiv/isqrt");
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.node.body.statements)
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.name} ended without returning a value");` : ` return SUCCESS;`;
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 === "zeroed" || k.kind === "initIfNeeded") {
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 enums`);
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
- return this.ix.accounts
279
- .filter((a) => a.constraints.closeTo)
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 line = `TS_TRY(ts_close(a_${a.name}, a_${a.constraints.closeTo}));`;
282
- const optional = this.optionalIn(line);
283
- return optional.length ? `if (${optional.map((n) => `a_${n} != NULL`).join(" && ")}) ${line}` : line;
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
- for (const h of model.helpers.values()) {
12
- const body = reporter.guard(() => new FunctionEmitter(analysis, h, opts).emitHelper());
13
- if (body)
14
- helperBodies.push(body, "");
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
- throw new Error("unreachable: variable strings are not stored");
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)`). */